Skip to content

Commit

Permalink
Moved util functions into model methods
Browse files Browse the repository at this point in the history
  • Loading branch information
dlareau committed Feb 11, 2020
1 parent b185cdf commit e2f03c7
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 102 deletions.
3 changes: 3 additions & 0 deletions huntserver/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
class AnswerForm(forms.Form):
answer = forms.CharField(max_length=100, label='Answer')

def clean_answer(self):
return re.sub(r"[ _\-;:+,.!?]", "", self.cleaned_data.get('answer'))


class SubmissionForm(forms.Form):
response = forms.CharField(max_length=400, label='response', initial="Wrong Answer")
Expand Down
15 changes: 7 additions & 8 deletions huntserver/hunt_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from .models import Puzzle, Hunt, Submission, Message, Unlockable, Prepuzzle, Hint
from .forms import AnswerForm, HintRequestForm
from .utils import respond_to_submission, team_from_user_hunt, dummy_team_from_hunt
from .utils import team_from_user_hunt, dummy_team_from_hunt
from .info_views import current_hunt_info

import logging
Expand Down Expand Up @@ -122,8 +122,6 @@ def prepuzzle(request, prepuzzle_num):

puzzle = Prepuzzle.objects.get(pk=prepuzzle_num)

# Dealing with answer submissions, proper procedure is to create a submission
# object and then rely on utils.respond_to_submission for automatic responses.
if request.method == 'POST':
form = AnswerForm(request.POST)
if form.is_valid():
Expand Down Expand Up @@ -194,17 +192,18 @@ def puzzle_view(request, puzzle_id):
return HttpResponseForbidden()

# Dealing with answer submissions, proper procedure is to create a submission
# object and then rely on utils.respond_to_submission for automatic responses.
# object and then rely on Submission.respond for automatic responses.
if request.method == 'POST':
# Deal with answers from archived hunts
if(puzzle.hunt.is_public):
form = AnswerForm(request.POST)
team = dummy_team_from_hunt(puzzle.hunt)
if form.is_valid():
user_answer = re.sub(r"[ _\-;:+,.!?]", "", form.cleaned_data['answer'])
user_answer = form.cleaned_data['answer']
s = Submission.objects.create(submission_text=user_answer, team=team,
puzzle=puzzle, submission_time=timezone.now())
response = respond_to_submission(s)
s.respond()
response = s.response_text
is_correct = s.is_correct
else:
response = "Invalid Submission"
Expand All @@ -221,10 +220,10 @@ def puzzle_view(request, puzzle_id):
# Normal answer responses for a signed in user in an ongoing hunt
form = AnswerForm(request.POST)
if form.is_valid():
user_answer = re.sub(r"[ _\-;:+,.!?]", "", form.cleaned_data['answer'])
user_answer = form.cleaned_data['answer']
s = Submission.objects.create(submission_text=user_answer, team=team,
puzzle=puzzle, submission_time=timezone.now())
response = respond_to_submission(s)
s.respond()

# Render response to HTML
submission_list = [render_to_string('puzzle_sub_row.html', {'submission': s})]
Expand Down
23 changes: 23 additions & 0 deletions huntserver/migrations/0047_auto_20200210_2201.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.2 on 2020-02-11 03:01

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('huntserver', '0046_auto_20200209_1112'),
]

operations = [
migrations.AlterField(
model_name='puzzle',
name='points_cost',
field=models.IntegerField(default=0, help_text='The number of points needed to unlock this puzzle.'),
),
migrations.AlterField(
model_name='puzzle',
name='points_value',
field=models.IntegerField(default=0, help_text='The number of points this puzzle grants upon solving.'),
),
]
70 changes: 70 additions & 0 deletions huntserver/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import os
import re

import logging
logger = logging.getLogger(__name__)

time_zone = tz.gettz(settings.TIME_ZONE)


Expand Down Expand Up @@ -203,8 +206,10 @@ class Puzzle(models.Model):
symmetrical=False,
help_text="Puzzles that this puzzle is a possible prerequisite for")
points_cost = models.IntegerField(
default=0,
help_text="The number of points needed to unlock this puzzle.")
points_value = models.IntegerField(
default=0,
help_text="The number of points this puzzle grants upon solving.")

def serialize_for_ajax(self):
Expand Down Expand Up @@ -338,6 +343,35 @@ def hints_open_for_puzzle(self, puzzle):
else:
return False

def unlock_puzzles(self):
puzzles = self.hunt.puzzle_set.all().order_by('puzzle_number')
numbers = []

numbers = puzzles.values_list('puzzle_number', flat=True)
# make an array for how many points a team has towards unlocking each puzzle
mapping = [0 for i in range(max(numbers) + 1)]

# go through each solved puzzle and add to the list for each puzzle it unlocks
for puzzle in self.solved.all():
for num in puzzle.unlocks.values_list('puzzle_number', flat=True):
mapping[num] += 1

# See if the number of points is enough to unlock any given puzzle
puzzles = puzzles.difference(self.unlocked.all())
for puzzle in puzzles:
if(puzzle.num_required_to_unlock <= mapping[puzzle.puzzle_number]):
logger.info("Team %s unlocked puzzle %s" % (str(self.team_name),
str(puzzle.puzzle_id)))
Unlock.objects.create(team=self, puzzle=puzzle, time=timezone.now())

def unlock_hints(self):
num_solved = self.solved.count()
plans = self.hunt.hintunlockplan_set
num_hints = plans.filter(unlock_type=HintUnlockPlan.SOLVES_UNLOCK,
unlock_parameter=num_solved).count()
self.num_available_hints = models.F('num_available_hints') + num_hints
self.save()

def __str__(self):
return str(self.size) + " (" + self.location + ") " + self.short_name

Expand Down Expand Up @@ -422,6 +456,42 @@ def save(self, *args, **kwargs):
self.modified_date = timezone.now()
super(Submission, self).save(*args, **kwargs)

def create_solve(self):
Solve.objects.create(puzzle=self.puzzle, team=self.team, submission=self)
logger.info("Team %s correctly solved puzzle %s" % (str(self.team.team_name),
str(self.puzzle.puzzle_id)))

# Automatic submission response system
# Returning an empty string means that huntstaff should respond via the queue
# Order of response importance: Regex, Defaults, Staff response.
def respond(self):
# Compare against correct answer
if(self.is_correct):
# Make sure we don't have duplicate or after hunt submission objects
if(not self.puzzle.hunt.is_public):
if(self.puzzle not in self.team.solved.all()):
self.create_solve()
self.team.unlock_puzzles()
self.team.unlock_hints()

# Check against regexes
for resp in self.puzzle.response_set.all():
if(re.match(resp.regex, self.submission_text, re.IGNORECASE)):
response = resp.text
break
else:
if(self.is_correct):
response = "Correct"
else:
# Current philosphy is to auto-can wrong answers: If it's not right, it's wrong
response = "Wrong Answer."
logger.info("Team %s incorrectly guessed %s for puzzle %s" %
(str(self.team.team_name), str(self.submission_text),
str(self.puzzle.puzzle_id)))

self.response_text = response
self.save()

def __str__(self):
return self.submission_text

Expand Down
4 changes: 2 additions & 2 deletions huntserver/staff_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from .models import Submission, Hunt, Team, Puzzle, Unlock, Solve, Message, Prepuzzle, Hint
from .forms import SubmissionForm, UnlockForm, EmailForm, HintResponseForm
from .utils import unlock_puzzles, download_puzzle, download_zip
from .utils import download_puzzle, download_zip


def add_apps_to_context(context, request):
Expand Down Expand Up @@ -460,7 +460,7 @@ def control(request):
if(request.method == 'POST' and "action" in request.POST):
if(request.POST["action"] == "initial"):
for team in teams:
unlock_puzzles(team)
team.unlock_puzzles()
return redirect('huntserver:hunt_management')
if(request.POST["action"] == "reset"):
for team in teams:
Expand Down
93 changes: 1 addition & 92 deletions huntserver/utils.py
Original file line number Diff line number Diff line change
@@ -1,105 +1,14 @@
from django.conf import settings
from django.shortcuts import get_object_or_404
from .models import Solve, Unlock, Person, Team, HintUnlockPlan
from django.utils import timezone
from .models import Person, Team
from subprocess import call, STDOUT
import os
from PyPDF2 import PdfFileReader
import re
from django.db.models import F

import logging
logger = logging.getLogger(__name__)


# Automatic submission response system
# Takes a submission object and should return a string
# Returning an empty string means that huntstaff should respond via the queue
# Order of response importance: Correct regex, Correct default,
# Incorrect regex, Incorrect (archived), Staff response.
def respond_to_submission(submission):
# Check against regexes
regex_response = ""
for resp in submission.puzzle.response_set.all():
if(re.match(resp.regex, submission.submission_text, re.IGNORECASE)):
regex_response = resp.text
break
# Compare against correct answer
if(submission.puzzle.answer.lower() == submission.submission_text.lower()):
# Make sure we don't have duplicate or after hunt submission objects
if(not submission.puzzle.hunt.is_public):
if(submission.puzzle not in submission.team.solved.all()):
Solve.objects.create(puzzle=submission.puzzle,
team=submission.team,
submission=submission)
unlock_puzzles(submission.team)

# Allocate appropriate hints for a number of solves
team = submission.team
solves = team.solved.all()
plans = team.hunt.hintunlockplan_set
num_hints = plans.filter(unlock_type=HintUnlockPlan.SOLVES_UNLOCK,
unlock_parameter=len(solves)).count()
team.num_available_hints = F('num_available_hints') + num_hints
team.save()
team.refresh_from_db()

logger.info("Team %s correctly solved puzzle %s" %
(str(submission.team.team_name), str(submission.puzzle.puzzle_id)))
if(regex_response != ""):
response = regex_response
else:
response = "Correct!"

else:
if(regex_response != ""):
response = regex_response
else:
response = ""
logger.info("Team %s incorrectly guessed %s for puzzle %s" %
(str(submission.team.team_name), str(submission.submission_text),
str(submission.puzzle.puzzle_id)))

# After the hunt is over, if it's not right it's wrong.
if(submission.puzzle.hunt.is_public):
if(response == ""):
response = "Wrong Answer."

# This turns on "auto-canned-response"
if(response == ""):
response = "Wrong Answer."

submission.response_text = response
submission.save()
return response


# Looks through each puzzle and sees if a team has enough solves to unlock it
# Should be called after anything could add a solve object to a team
# Is also called at the start to release initial puzzles (puzzles with 0 reqs)
# It is implemented using this weird list method to allow for gaps in numbering
def unlock_puzzles(team):
puzzles = team.hunt.puzzle_set.all().order_by('puzzle_number')
numbers = []
# replace with a map?
for puzzle in puzzles:
numbers.append(puzzle.puzzle_number)
# make an array for how many points a team has towards unlocking each puzzle
mapping = [0 for i in range(max(numbers) + 1)]

# go through each solved puzzle and add to the list for each puzzle it unlocks
for puzzle in team.solved.all():
for other in puzzle.unlocks.all():
mapping[other.puzzle_number] = mapping[other.puzzle_number]+1
# See if the number of points is enough to unlock any given puzzle
for puzzle in puzzles:
if(puzzle.num_required_to_unlock <= mapping[puzzle.puzzle_number]):
if(puzzle not in team.unlocked.all()):
logger.info("Team %s unlocked puzzle %s" % (str(team.team_name),
str(puzzle.puzzle_id)))
Unlock.objects.create(team=team, puzzle=puzzle, time=timezone.now())


def download_zip(directory, filename, url):
if(url == ""):
return
Expand Down

0 comments on commit e2f03c7

Please sign in to comment.