Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[flake8]
exclude = build,.git,.tox,./django/utils/six.py,./django/conf/app_template/*,./tests/.env,./env,./*/migrations,./venv
exclude = build,.git,.tox,./django/utils/six.py,./django/conf/app_template/*,./tests/.env,./env,./*/migrations,./venv,./thegame/settings.py
ignore = W601,F403,W504,F405
max-line-length = 120
Empty file added challenge/__init__.py
Empty file.
13 changes: 13 additions & 0 deletions challenge/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.contrib import admin
from django.contrib.admin import ModelAdmin

from challenge.forms import ChallengeAdminForm
from challenge.models import Challenge, ChallengeUser


class ChallengeAdmin(ModelAdmin):
form = ChallengeAdminForm


admin.site.register(Challenge, admin_class=ChallengeAdmin)
admin.site.register(ChallengeUser)
5 changes: 5 additions & 0 deletions challenge/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class ChallengeConfig(AppConfig):
name = 'challenge'
23 changes: 23 additions & 0 deletions challenge/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django import forms
from django.forms import ModelForm, Form

from challenge.models import Challenge


class ChallengeAdminForm(ModelForm):
solution_code = forms.CharField(initial='', required=False)

def save(self, commit=True):
solution_code = self.cleaned_data.get('solution_code', '')
if solution_code != '':
self.instance.set_solution(solution_code)
self.instance.check_solution(solution_code)
return super().save(commit)

class Meta:
model = Challenge
exclude = ['solution']


class ChallengeTryForm(Form):
code = forms.CharField(initial='', required=True)
24 changes: 24 additions & 0 deletions challenge/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.2.8 on 2021-10-10 12:04

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Challenge',
fields=[
('name', models.CharField(max_length=100)),
('order', models.IntegerField(primary_key=True, serialize=False)),
('type', models.CharField(choices=[('img', 'Image'), ('file', 'File'), ('mp3', 'Audio'), ('mp4', 'Video')], max_length=10)),
('solution', models.CharField(max_length=100)),
('file', models.FileField(upload_to='')),
],
),
]
41 changes: 41 additions & 0 deletions challenge/migrations/0002_auto_20211010_1451.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 3.2.8 on 2021-10-10 14:51

import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('challenge', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='challenge',
name='description',
field=models.TextField(blank=True, max_length=5000),
),
migrations.AlterField(
model_name='challenge',
name='solution',
field=models.CharField(max_length=1000),
),
migrations.CreateModel(
name='ChallengeUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_try', models.DateTimeField(default=datetime.datetime.now)),
('last_try', models.DateTimeField(auto_now_add=True)),
('attempt_date', models.DateTimeField(default=datetime.datetime.now)),
('success', models.BooleanField(default=False)),
('attempts', models.IntegerField(default=0)),
('total_attempts', models.IntegerField(default=0)),
('challenge', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='challenge.challenge')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)),
],
),
]
Empty file.
18 changes: 18 additions & 0 deletions challenge/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.contrib.auth.mixins import AccessMixin

from challenge.models import ChallengeUser


class ChallengePermissionMixin(AccessMixin):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
c_id = kwargs.get('id', 1)
if c_id != 1:
c_id -= 1
try:
if not ChallengeUser.objects.get(challenge_id=c_id, user=request.user).success:
return self.handle_no_permission()
except ChallengeUser.DoesNotExist:
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
45 changes: 45 additions & 0 deletions challenge/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from datetime import datetime

from django.contrib.auth.hashers import make_password, check_password
from django.db import models


class Challenge(models.Model):
TYPE_IMAGE = 'img'
TYPE_FILE = 'file'
TYPE_AUDIO = 'mp3'
TYPE_VIDEO = 'mp4'
TYPES = (
(TYPE_IMAGE, 'Image'),
(TYPE_FILE, 'File'),
(TYPE_AUDIO, 'Audio'),
(TYPE_VIDEO, 'Video'),
)

name = models.CharField(max_length=100)
description = models.TextField(max_length=5000, blank=True)
order = models.IntegerField(primary_key=True)
type = models.CharField(choices=TYPES, max_length=10)
solution = models.CharField(max_length=1000)
file = models.FileField()

def __str__(self):
return '%s - %s' % (self.order, self.name)

# solution will be hard encrypted for security reasons
def set_solution(self, solution):
self.solution = make_password(solution)

def check_solution(self, solution):
return check_password(solution, self.solution)


class ChallengeUser(models.Model):
user = models.ForeignKey('user.User', on_delete=models.DO_NOTHING)
challenge = models.ForeignKey('challenge.Challenge', on_delete=models.DO_NOTHING)
first_try = models.DateTimeField(default=datetime.now)
last_try = models.DateTimeField(auto_now_add=True)
attempt_date = models.DateTimeField(default=datetime.now)
success = models.BooleanField(default=False)
attempts = models.IntegerField(default=0)
total_attempts = models.IntegerField(default=0)
32 changes: 32 additions & 0 deletions challenge/templates/challenge.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block head_title %}{{ challenge.name }}{% endblock %}
{% block content %}
<h1 style="text-align: center">{{ challenge.name }}</h1>
<p>{{ challenge.description }}</p>
<div style="display:flex">
<div style="margin: 0 auto;">
{% if challenge.type == challenge.TYPE_VIDEO %}
<video width="320" height="240" controls style="margin: 0 auto;">
<source src="{{ challenge.file.url }}" type="video/mp4">
Your browser does not support the video tag.
</video>
{% elif challenge.type == challenge.TYPE_AUDIO %}
<audio controls>
<source src="{{ challenge.file.url }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
{% elif challenge.type == challenge.TYPE_FILE %}
<a href="{{ challenge.file.url }}" class="btn btn-block btn-default" target="_blank">Download files</a>
{% elif challenge.type == challenge.TYPE_IMAGE %}
<img src="{{ challenge.file.url }}" width="100%">
{% endif %}
</div>
</div>
<form method="post" style="margin-top: 20px">
{% csrf_token %}
{% bootstrap_form form %}
<button class="btn btn-block btn-primary">Submit</button>
</form>

{% endblock %}
8 changes: 8 additions & 0 deletions challenge/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.urls import path

from challenge.views import HomeView, ChallengeView

urlpatterns = [
path('', HomeView.as_view(), name='home'),
path('challenge/<int:c_id>/', ChallengeView.as_view(), name='challenge'),
]
70 changes: 70 additions & 0 deletions challenge/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from datetime import datetime, timedelta

import pytz
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
from django.views import View
from django.views.generic import TemplateView

from challenge.forms import ChallengeTryForm
from challenge.mixins import ChallengePermissionMixin
from challenge.models import Challenge, ChallengeUser


class HomeView(LoginRequiredMixin, View):
def get(self, request):
challenge = ChallengeUser.objects.filter(user=request.user).order_by('-challenge_id')
next_id = 1
try:
next_id = challenge[0].challenge_id + int(challenge[0].success)
except IndexError:
pass
return redirect(reverse('challenge', args=[next_id]))


class ChallengeView(ChallengePermissionMixin, TemplateView):
template_name = 'challenge.html'

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
c_id = kwargs.get('c_id')
challenge = get_object_or_404(Challenge, order=c_id)
context.update({
'challenge': challenge,
'form': ChallengeTryForm(),
})
return context

def post(self, request, **kwargs):
form = ChallengeTryForm(request.POST)
c_id = kwargs.get('c_id', 1)
context = self.get_context_data(**kwargs)
if form.is_valid():
code = form.cleaned_data.get('code')
try:
challenge_try = ChallengeUser.objects.get(user=self.request.user, challenge_id=c_id)
except ChallengeUser.DoesNotExist:
challenge_try = ChallengeUser(user=self.request.user, challenge_id=c_id)
now = pytz.utc.localize(datetime.now())
if challenge_try.attempt_date.tzinfo is None:
challenge_try.attempt_date = pytz.utc.localize(challenge_try.attempt_date)
challenge_try.attempts += 1
if challenge_try.attempt_date < (now - timedelta(minutes=5)):
challenge_try.attempt_date = now
challenge_try.attempts = 1
elif challenge_try.attempts > settings.ATTEMPTS_PER_5_MINUTES:
form.add_error(None, 'Too many attempts! Only %s attempts per 5 minutes permitted' %
settings.ATTEMPTS_PER_5_MINUTES)
context.update({'form': form})
return render(request, template_name=self.template_name, context=context)
challenge_try.total_attempts += 1
if challenge_try.challenge.check_solution(code):
challenge_try.success = True
challenge_try.save()
return redirect('home')
form.add_error('code', 'Invalid code')
challenge_try.save()
context.update({'form': form})
return render(request, template_name=self.template_name, context=context)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Django==3.2.8
django-cas-ng==4.2.1
flake8==3.9.2
django-bootstrap3==15.0.0
6 changes: 5 additions & 1 deletion thegame/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
import os
from thegame.the_game_settings import *

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
Expand Down Expand Up @@ -37,7 +38,9 @@
'django.contrib.messages',
'django.contrib.staticfiles',
'django_cas_ng',
'bootstrap3',
'user',
'challenge',
]

MIDDLEWARE = [
Expand All @@ -61,14 +64,15 @@
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': os.path.join(BASE_DIR, 'template'),
'DIRS': ['thegame/templates', ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'thegame.utils.get_substitutions_templates'
],
},
},
Expand Down
Loading