Skip to content

Commit

Permalink
Fixed some hint issues and started time unlock puzzles
Browse files Browse the repository at this point in the history
  • Loading branch information
dlareau committed Feb 10, 2020
1 parent 809a0ab commit b185cdf
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 58 deletions.
69 changes: 53 additions & 16 deletions huntserver/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ class HuntAdminForm(forms.ModelForm):
model = models.Hunt

class Meta:
fields = '__all__'
widgets = {
'template': HtmlEditor(attrs={'style': 'width: 90%; height: 400px;'}),
}
Expand All @@ -58,6 +57,9 @@ class Meta:
class HuntAdmin(admin.ModelAdmin):
form = HuntAdminForm
inlines = (HintUnlockPLanInline,)
fields = ['hunt_name', 'hunt_number', 'is_current_hunt', 'team_size', 'location',
('start_date', 'display_start_date'), ('end_date', 'display_end_date'),
'resource_link', 'points_per_minute', 'extra_data', 'template', 'hint_lockout']
list_display = ['hunt_name', 'team_size', 'start_date', 'is_current_hunt']


Expand All @@ -73,6 +75,7 @@ def short_message(self, message):
class PersonAdmin(admin.ModelAdmin):
list_display = ['user_full_name', 'user_username', 'is_shib_acct']
search_fields = ['user__email', 'user__username', 'user__first_name', 'user__last_name']
filter_horizontal = ['teams']

def user_full_name(self, person):
return person.user.first_name + " " + person.user.last_name
Expand Down Expand Up @@ -143,26 +146,60 @@ class ResponseInline(admin.TabularInline):
extra = 1


class PuzzleAdminForm(forms.ModelForm):
reverse_unlocks = forms.ModelMultipleChoiceField(
models.Puzzle.objects.all(),
widget=admin.widgets.FilteredSelectMultiple('Puzzle', False),
required=False,
)

def __init__(self, *args, **kwargs):
super(PuzzleAdminForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.initial['reverse_unlocks'] = self.instance.puzzle_set.values_list('pk', flat=True)
self.fields['reverse_unlocks'].choices = self.instance.hunt.puzzle_set.values_list('pk', 'puzzle_name')

def save(self, *args, **kwargs):
instance = super(PuzzleAdminForm, self).save(*args, **kwargs)
if instance.pk:
instance.puzzle_set.clear()
instance.puzzle_set.add(*self.cleaned_data['reverse_unlocks'])
return instance

class Meta:
model = models.Puzzle
fields = ('hunt', 'puzzle_name', 'puzzle_number', 'puzzle_id', 'answer', 'is_meta',
'doesnt_count', 'is_html_puzzle', 'link', 'resource_link', 'solution_link',
'extra_data', 'num_required_to_unlock', 'unlock_type', 'points_cost', 'points_value')


class PuzzleAdmin(admin.ModelAdmin):
def get_object(self, request, object_id, to_field):
# Hook obj for use in formfield_for_manytomany
self.obj = super(PuzzleAdmin, self).get_object(request, object_id, to_field)
return self.obj
class Media:
js = ("huntserver/admin_change_puzzle.js",)

def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "unlocks" and getattr(self, 'obj', None):
query = models.Puzzle.objects.filter(hunt=self.obj.hunt)
kwargs["queryset"] = query.order_by('puzzle_id')
return super(PuzzleAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
form = PuzzleAdminForm

list_filter = ('hunt',)
fields = ('hunt', 'puzzle_name', 'puzzle_number', 'puzzle_id', 'is_meta',
'doesnt_count', 'is_html_puzzle', 'resource_link', 'link', 'solution_link',
'answer', 'extra_data', 'num_pages', 'num_required_to_unlock')
list_display = ['combined_id', 'puzzle_name', 'hunt', 'is_meta']
list_display_links = ['combined_id', 'puzzle_name']
ordering = ['-hunt', 'puzzle_number']
inlines = (UnlockInline, ResponseInline)
inlines = (ResponseInline,)
radio_fields = {"unlock_type": admin.VERTICAL}
fieldsets = (
(None, {
'fields': ('hunt', 'puzzle_name', 'puzzle_number', 'puzzle_id', 'answer', 'is_meta',
'doesnt_count', 'is_html_puzzle', 'link', 'resource_link', 'solution_link',
'extra_data', 'unlock_type')
}),
('Solve Unlocking', {
'classes': ('formset_border', 'solve_unlocking'),
'fields': ('reverse_unlocks', 'num_required_to_unlock')
}),
('Points Unlocking', {
'classes': ('formset_border', 'points_unlocking'),
'fields': ('points_cost', 'points_value')
}),
)

def combined_id(self, puzzle):
return str(puzzle.puzzle_number) + "-" + puzzle.puzzle_id
Expand Down Expand Up @@ -203,8 +240,8 @@ class TeamAdminForm(forms.ModelForm):

class Meta:
model = models.Team
fields = ['team_name', 'unlocked', 'unlockables', 'hunt', 'location',
'join_code', 'playtester', 'num_available_hints']
fields = ['team_name', 'unlocked', 'hunt', 'location',
'join_code', 'playtester', 'num_available_hints', 'num_unlock_points', 'unlockables']

def __init__(self, *args, **kwargs):
super(TeamAdminForm, self).__init__(*args, **kwargs)
Expand Down
20 changes: 19 additions & 1 deletion huntserver/management/commands/runupdates.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from django.db.models import F
from datetime import datetime

from huntserver.models import Hunt, HintUnlockPlan

Expand All @@ -9,10 +10,17 @@ class Command(BaseCommand):
help = 'Runs all time related updates for the huntserver app'

def handle(self, *args, **options):
# Check hints
curr_hunt = Hunt.objects.get(is_current_hunt=True)
if(not curr_hunt.is_open):
return

last_update_time = curr_hunt.last_update_time.replace(second=0, microsecond=0)
curr_hunt.last_update_time = timezone.now()
curr_hunt.save()
diff_time = timezone.now().replace(second=0, microsecond=0) - last_update_time
diff_minutes = diff_time.seconds / 60

# Check hints
num_min = (timezone.now() - curr_hunt.start_date).seconds / 60
for hup in curr_hunt.hintunlockplan_set.exclude(unlock_type=HintUnlockPlan.SOLVES_UNLOCK):
if((hup.unlock_type == hup.TIMED_UNLOCK and
Expand All @@ -22,3 +30,13 @@ def handle(self, *args, **options):
curr_hunt.team_set.all().update(num_available_hints=F('num_available_hints') + 1)
hup.num_triggered = hup.num_triggered + 1
hup.save()

# Check puzzles
if(diff_minutes >= 1):
new_points = curr_hunt.points_per_minute * diff_minutes
curr_hunt.team_set.all().update(num_unlock_points=F('num_unlock_points') + new_points)
#for team in curr_hunt.team_set.all():
#unlockable_puzzles = curr_hunt.puzzle_set.exclude()
# write function on puzzle that takes team and checks if it is/should be unlocked
# filter out all puzzles that can't possibly be unlocked because time then run the
# unlock function on each of them
35 changes: 35 additions & 0 deletions huntserver/migrations/0045_auto_20200209_0856.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 2.2 on 2020-02-09 13:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('huntserver', '0044_auto_20200207_0936'),
]

operations = [
migrations.AddField(
model_name='puzzle',
name='points_cost',
field=models.IntegerField(default=0, help_text='The number of points needed to unlock this puzzle.'),
preserve_default=False,
),
migrations.AddField(
model_name='puzzle',
name='points_value',
field=models.IntegerField(default=0, help_text='The number of points this puzzle grants upon solving.'),
preserve_default=False,
),
migrations.AddField(
model_name='puzzle',
name='unlock_type',
field=models.CharField(choices=[('TIM', 'Points Based Unlock'), ('SOL', 'Solves Based Unlock'), ('ETH', 'Either Unlocking Method'), ('BTH', 'Both Unlocking Methods')], default='SOL', help_text='The type of puzzle unlocking scheme', max_length=3),
),
migrations.AlterField(
model_name='puzzle',
name='is_meta',
field=models.BooleanField(default=False, help_text='Is this puzzle a meta-puzzle?', verbose_name='Is a metapuzzle'),
),
]
44 changes: 44 additions & 0 deletions huntserver/migrations/0046_auto_20200209_1112.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 2.2 on 2020-02-09 16:12

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
('huntserver', '0045_auto_20200209_0856'),
]

operations = [
migrations.AddField(
model_name='hunt',
name='last_update_time',
field=models.DateTimeField(default=django.utils.timezone.now, help_text='The last time that the periodic update management command was ran'),
),
migrations.AddField(
model_name='hunt',
name='points_per_minute',
field=models.IntegerField(default=0, help_text='The number of points granted per minute during the hunt'),
),
migrations.AddField(
model_name='team',
name='num_unlock_points',
field=models.IntegerField(default=0, help_text='The number of points the team has earned'),
),
migrations.AlterField(
model_name='hunt',
name='hint_lockout',
field=models.IntegerField(default=60, help_text='The number of minutes before a hint can be used on a newly unlocked puzzle'),
),
migrations.AlterField(
model_name='puzzle',
name='unlock_type',
field=models.CharField(choices=[('SOL', 'Solves Based Unlock'), ('POT', 'Points Based Unlock'), ('ETH', 'Either (OR) Unlocking Method'), ('BTH', 'Both (AND) Unlocking Methods')], default='SOL', help_text='The type of puzzle unlocking scheme', max_length=3),
),
migrations.AlterField(
model_name='team',
name='num_available_hints',
field=models.IntegerField(default=0, help_text='The number of hints the team has available to use'),
),
]
84 changes: 60 additions & 24 deletions huntserver/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ class Hunt(models.Model):
default="",
help_text="The template string to be rendered to HTML on the hunt page")
hint_lockout = models.IntegerField(
default=settings.DEFAULT_HINT_LOCKOUT)
default=settings.DEFAULT_HINT_LOCKOUT,
help_text="The number of minutes before a hint can be used on a newly unlocked puzzle")
points_per_minute = models.IntegerField(
default=0,
help_text="The number of points granted per minute during the hunt")
last_update_time = models.DateTimeField(
default=timezone.now,
help_text="The last time that the periodic update management command was ran")

# A bit of custom logic in clean and save to ensure exactly one hunt's
# is_current_hunt is true at any time. It makes sure you can never un-set the
Expand Down Expand Up @@ -122,18 +129,44 @@ def __str__(self):
class Puzzle(models.Model):
""" A class representing a puzzle within a hunt """

puzzle_number = models.IntegerField(
help_text="The number of the puzzle within the hunt, for sorting purposes")
SOLVES_UNLOCK = 'SOL'
POINTS_UNLOCK = 'POT'
EITHER_UNLOCK = 'ETH'
BOTH_UNLOCK = 'BTH'

puzzle_unlock_type_choices = [
(SOLVES_UNLOCK, 'Solves Based Unlock'),
(POINTS_UNLOCK, 'Points Based Unlock'),
(EITHER_UNLOCK, 'Either (OR) Unlocking Method'),
(BOTH_UNLOCK, 'Both (AND) Unlocking Methods'),
]

hunt = models.ForeignKey(
Hunt,
on_delete=models.CASCADE,
help_text="The hunt that this puzzle is a part of")
puzzle_name = models.CharField(
max_length=200,
help_text="The name of the puzzle as it will be seen by hunt participants")
puzzle_number = models.IntegerField(
help_text="The number of the puzzle within the hunt, for sorting purposes")
puzzle_id = models.CharField(
max_length=8,
unique=True, # hex only please
help_text="A 3-5 character hex string that uniquely identifies the puzzle")
answer = models.CharField(
max_length=100,
help_text="The answer to the puzzle, not case sensitive")
is_meta = models.BooleanField(
default=False,
verbose_name="Is a metapuzzle",
help_text="Is this puzzle a meta-puzzle?")
is_html_puzzle = models.BooleanField(
default=False,
help_text="Does this puzzle use an HTML folder as it's source?")
doesnt_count = models.BooleanField(
default=False,
help_text="Should this puzzle not count towards scoring?")
link = models.URLField(
max_length=200,
blank=True,
Expand All @@ -146,6 +179,21 @@ class Puzzle(models.Model):
max_length=200,
blank=True,
help_text="The full link (needs http://) to a publicly accessible PDF of the solution")
extra_data = models.CharField(
max_length=200,
blank=True,
help_text="A misc. field for any extra data to be stored with the puzzle.")
num_pages = models.IntegerField(
help_text="Number of pages in the PDF for this puzzle. Set automatically upon download")

# Unlocking:
unlock_type = models.CharField(
max_length=3,
choices=puzzle_unlock_type_choices,
default=SOLVES_UNLOCK,
blank=False,
help_text="The type of puzzle unlocking scheme"
)
num_required_to_unlock = models.IntegerField(
default=1,
help_text="Number of prerequisite puzzles that need to be solved to unlock this puzzle")
Expand All @@ -154,26 +202,10 @@ class Puzzle(models.Model):
blank=True,
symmetrical=False,
help_text="Puzzles that this puzzle is a possible prerequisite for")
hunt = models.ForeignKey(
Hunt,
on_delete=models.CASCADE,
help_text="The hunt that this puzzle is a part of")
extra_data = models.CharField(
max_length=200,
blank=True,
help_text="A misc. field for any extra data to be stored with the puzzle.")
num_pages = models.IntegerField(
help_text="Number of pages in the PDF for this puzzle. Set automatically upon download")
is_meta = models.BooleanField(
default=False,
verbose_name="Is a metapuzzle",
help_text="Is this puzzle a meta-puzzle?")
is_html_puzzle = models.BooleanField(
default=False,
help_text="Does this puzzle use an HTML folder as it's source?")
doesnt_count = models.BooleanField(
default=False,
help_text="Should this puzzle not count towards scoring?")
points_cost = models.IntegerField(
help_text="The number of points needed to unlock this puzzle.")
points_value = models.IntegerField(
help_text="The number of points this puzzle grants upon solving.")

def serialize_for_ajax(self):
""" Serializes the ID, puzzle_number and puzzle_name fields for ajax transmission """
Expand Down Expand Up @@ -266,7 +298,11 @@ class Team(models.Model):
last_received_message = models.IntegerField(
default=0)
num_available_hints = models.IntegerField(
default=0)
default=0,
help_text="The number of hints the team has available to use")
num_unlock_points = models.IntegerField(
default=0,
help_text="The number of points the team has earned")

@property
def is_playtester_team(self):
Expand Down
9 changes: 6 additions & 3 deletions huntserver/staff_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,14 +583,17 @@ def staff_hints_control(request):
update_value = int(request.POST.get("value"))
team_pk = int(request.POST.get("team_pk"))
team = Team.objects.get(pk=team_pk)
team.num_available_hints = F('num_available_hints') + update_value
team.save()
if(team.num_available_hints + update_value >= 0):
team.num_available_hints = F('num_available_hints') + update_value
team.save()

except ValueError:
pass # Maybe a 4XX or 5XX in the future
else:
return HttpResponse("Incorrect usage of hint control page")

return HttpResponse(json.dumps(list(Team.objects.values_list('pk', 'num_available_hints'))))
hunt = Hunt.objects.get(is_current_hunt=True)
return HttpResponse(json.dumps(list(hunt.team_set.values_list('pk', 'num_available_hints'))))


@staff_member_required
Expand Down
7 changes: 7 additions & 0 deletions huntserver/static/huntserver/admin_addon.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,11 @@ td.original p {
height: 70px;
padding-top: 5px;
padding-bottom: 5px;
}

.formset_border {
border: 1px solid lightgrey;
padding: 15px;
padding-bottom: 0px;
margin-bottom: 15px;
}

0 comments on commit b185cdf

Please sign in to comment.