From f7e72a30094c2b02b7659bab724fdd61d1aee068 Mon Sep 17 00:00:00 2001 From: Travis Hance Date: Sun, 10 Jan 2016 22:20:26 -0800 Subject: [PATCH 1/2] Integrate collaborative crosswords I added an API endpoint to the crossword app (which is currently running on heroku). This solving software uses the API to make crosswords (similar to how it makes googles sheets). The URLs to the crossword sheets are stored in a new table. Users can manually add crossword sheets (as many per puzzle as desired) with a button in the puzzle info panel. The crossword sheets go in the same frame as the the google sheet. The user can tab the google sheet and the crossword sheet(s). Also, the view defaults to the crossword sheet when there is at least one. I think that's a reasonable assumption, although it could potentially be annoying, since there is no way to delete a crossword sheet once it is made. There is also a button to delete crossword sheets. --- puzzles/lacrosse_town_crossword.py | 30 +++++++++ puzzles/management/commands/initial_config.py | 2 +- puzzles/models.py | 8 +++ puzzles/static/css/base.css | 23 +++++++ puzzles/views.py | 42 ++++++++++++- solving/urls.py | 2 + templates/puzzles/puzzle-frames.html | 7 ++- templates/puzzles/puzzle-info.html | 61 +++++++++++++++++++ 8 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 puzzles/lacrosse_town_crossword.py diff --git a/puzzles/lacrosse_town_crossword.py b/puzzles/lacrosse_town_crossword.py new file mode 100644 index 0000000..804df2f --- /dev/null +++ b/puzzles/lacrosse_town_crossword.py @@ -0,0 +1,30 @@ +import requests +import json +from django.conf import settings +from puzzles.models import Puzzle, LacrosseTownCrossword + +# returns URL +def create_lacrosse_town_crossword(): + params = { + "type": "new", + } + result = requests.post( + settings.LACROSSE_TOWN_CROSSWORD_DOMAIN + "/api", + data={"params": json.dumps(params)}) + + result = json.loads(result.text) + + if not result["success"]: + raise Exception("crossword api did not return success") + if not result["url"]: + raise Exception("crossword api did not return a URL") + + return result["url"] + +def add_crossword_for_puzzle(puzzle): + url = create_lacrosse_town_crossword() + + LacrosseTownCrossword.objects.create( + puzzle=puzzle, + url=url, + is_deleted=False) diff --git a/puzzles/management/commands/initial_config.py b/puzzles/management/commands/initial_config.py index aa1fc64..6f8a869 100644 --- a/puzzles/management/commands/initial_config.py +++ b/puzzles/management/commands/initial_config.py @@ -45,7 +45,7 @@ def handle(self, *args, **options): testing_taglist.tags = [Tag.objects.get(name='testing')] testing_taglist.save() - default_locations = ['Cambridge', 'remote'] + default_locations = ['Cambridge', 'remote', 'unknown'] Location.objects.all().delete() for location_name in default_locations: Location(name=location_name).save() diff --git a/puzzles/models.py b/puzzles/models.py index ee0d421..0a7f0ff 100644 --- a/puzzles/models.py +++ b/puzzles/models.py @@ -166,6 +166,14 @@ class Location(OrderedModel): def __unicode__(self): return self.name +class LacrosseTownCrossword(OrderedModel): + puzzle = models.ForeignKey('Puzzle') + url = models.CharField(max_length=200) + is_deleted = models.BooleanField() + + def __unicode__(self): + return self.url + class UserProfile(models.Model): user = models.OneToOneField(User) diff --git a/puzzles/static/css/base.css b/puzzles/static/css/base.css index ece9888..3b81ff3 100644 --- a/puzzles/static/css/base.css +++ b/puzzles/static/css/base.css @@ -441,6 +441,29 @@ div#answers a#callinanswer:hover { padding-left: 10px; } +.puzzle-blurb div#add_crossword { + display: inline-block; + padding-left: 10px; +} + +#delete_crossword_form { + padding-left: 200px; +} + +.delete_crossword_form_hidden { + display: none; +} + +input[type=button].collaboration_link_button_sel { + background-color: #66ffff; + border-color: #66ffff; + box-shadow: inset; +} + +input[type=button].collaboration_link_button { + cursor: pointer; +} + span.puzzle-title { font-weight: bold; } diff --git a/puzzles/views.py b/puzzles/views.py index abdf3e3..0ae9407 100644 --- a/puzzles/views.py +++ b/puzzles/views.py @@ -12,10 +12,11 @@ from django.db import IntegrityError from django import forms -from models import Status, Priority, Tag, QueuedAnswer, PuzzleWrongAnswer, Puzzle, TagList, UploadedFile, Location, Config +from models import Status, Priority, Tag, QueuedAnswer, PuzzleWrongAnswer, Puzzle, TagList, UploadedFile, Location, Config, LacrosseTownCrossword from forms import UploadForm, AnswerForm from django.contrib.auth.models import User from django.conf import settings +from lacrosse_town_crossword import add_crossword_for_puzzle def get_motd(): try: @@ -81,6 +82,22 @@ def puzzle_info(request, puzzle_id): queued_answers = puzzle.queuedanswer_set.order_by('-id') wrong_answers = puzzle.puzzlewronganswer_set.order_by('-id') uploaded_files = puzzle.uploadedfile_set.order_by('id') + + crosswords = LacrosseTownCrossword.objects.all().filter(puzzle=puzzle, is_deleted=False) + collaboration_links = ([ + {"selected": False, "text": "Spreadsheet", + "url": reverse("puzzles.views.puzzle_spreadsheet", args=[puzzle_id]), + "id": ""}, + ] + + [ {"selected": False, "text": "Crossword Sheet", "url": crossword.url, + "id": str(crossword.id)} + for crossword in crosswords ]) + if len(collaboration_links) > 1: + # by default, select the first crossword, if there is at least one + collaboration_links[1]["selected"] = True + else: + collaboration_links[0]["selected"] = True + return render_to_response("puzzles/puzzle-info.html", puzzle_context(request, { 'puzzle': puzzle, 'statuses': statuses, @@ -91,7 +108,9 @@ def puzzle_info(request, puzzle_id): 'queued_answers': queued_answers, 'wrong_answers': wrong_answers, 'uploaded_files': uploaded_files, - 'refresh': 30 + 'refresh': 30, + 'collaboration_links': collaboration_links, + 'hide_collaboration_links': len(collaboration_links) == 1, })) @login_required @@ -137,6 +156,25 @@ def puzzle_add_solver(request, puzzle_id): puzzle.save() return redirect(request.POST['continue']) +@login_required +def puzzle_add_crossword(request, puzzle_id): + puzzle = Puzzle.objects.get(id=puzzle_id) + add_crossword_for_puzzle(puzzle) + return redirect(request.POST['continue']) + +@login_required +def puzzle_delete_crossword(request, puzzle_id): + # soft delete: just set the `is_deleted` field to True + crossword_id = int(request.POST['crossword_id']) + crossword = LacrosseTownCrossword.objects.get(id=crossword_id) + if crossword.puzzle.id != int(puzzle_id): + raise Exception( + "puzzle id did not match: crossword_id=%d, puzzle_id=%d, puzzle_id from request=%s" % + (crossword_id, crossword.puzzle.id, puzzle_id)) + crossword.is_deleted = True + crossword.save() + return redirect(request.POST['continue']) + def handle_puzzle_upload(puzzle, name, file): if file.name == '' or file.name[0] == '.' or '/' in file.name: raise ValueError diff --git a/solving/urls.py b/solving/urls.py index a8b7a9b..919c3e7 100644 --- a/solving/urls.py +++ b/solving/urls.py @@ -18,6 +18,8 @@ url(r'^puzzle/set_priority/(\d+)/$', 'puzzles.views.puzzle_set_priority'), url(r'^puzzle/remove_solver/(\d+)/$', 'puzzles.views.puzzle_remove_solver'), url(r'^puzzle/add_solver/(\d+)/$', 'puzzles.views.puzzle_add_solver'), + url(r'^puzzle/add_crossword/(\d+)/$', 'puzzles.views.puzzle_add_crossword'), + url(r'^puzzle/delete_crossword/(\d+)/$', 'puzzles.views.puzzle_delete_crossword'), url(r'^puzzle/upload/(\d+)/$', 'puzzles.views.puzzle_upload'), url(r'^puzzle/call_in_answer/(\d+)/$', 'puzzles.views.puzzle_call_in_answer'), diff --git a/templates/puzzles/puzzle-frames.html b/templates/puzzles/puzzle-frames.html index 3638204..70aa5f8 100644 --- a/templates/puzzles/puzzle-frames.html +++ b/templates/puzzles/puzzle-frames.html @@ -3,11 +3,16 @@ {{ title }} + - + diff --git a/templates/puzzles/puzzle-info.html b/templates/puzzles/puzzle-info.html index fd0dbb5..051ed48 100644 --- a/templates/puzzles/puzzle-info.html +++ b/templates/puzzles/puzzle-info.html @@ -39,6 +39,13 @@ +
+
{% csrf_token %} + + +
+ +
Attach file {% if uploaded_files %}Attached files: @@ -123,5 +130,59 @@
+ + + +
{% endblock %} From 5a61bd0306d2c4af39162a61651283132a20bdbb Mon Sep 17 00:00:00 2001 From: Travis Hance Date: Tue, 12 Jan 2016 20:10:49 -0800 Subject: [PATCH 2/2] set LACROSSE_TOWN_CROSSWORD_DOMAIN in local_settings_template.py --- solving/local_settings_template.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/solving/local_settings_template.py b/solving/local_settings_template.py index 02e8e01..5d04593 100644 --- a/solving/local_settings_template.py +++ b/solving/local_settings_template.py @@ -46,3 +46,5 @@ import random ZULIP_HOSTNAME_BALANCING = lambda: 'https://e%d.%s' % ( random.randint(1, 100), ZULIP_SERVER_HOSTNAME) + +LACROSSE_TOWN_CROSSWORD_DOMAIN = 'http://enigmatic-mountain-8851.herokuapp.com'