From: Jan Szejko Date: Wed, 18 Oct 2017 12:57:50 +0000 (+0200) Subject: set competition state in db + randomized single-question forms X-Git-Url: https://git.mdrn.pl/edumed.git/commitdiff_plain/3f387ec5d75ff85576e87649427cbdc1f14a95b8?hp=b9210264c863674f37f8538ade251b056859532e set competition state in db + randomized single-question forms --- diff --git a/wtem/admin.py b/wtem/admin.py index d581de0..4621f5f 100644 --- a/wtem/admin.py +++ b/wtem/admin.py @@ -10,7 +10,7 @@ from django.http import HttpResponse from django.template.loader import render_to_string from django.utils.safestring import mark_safe -from wtem.models import Confirmation +from wtem.models import Confirmation, CompetitionState from .middleware import get_current_request from .models import Submission, Assignment, Attachment, exercises @@ -253,3 +253,4 @@ class ConfirmationAdmin(admin.ModelAdmin): admin.site.register(Submission, SubmissionAdmin) admin.site.register(Assignment) admin.site.register(Confirmation, ConfirmationAdmin) +admin.site.register(CompetitionState) diff --git a/wtem/forms.py b/wtem/forms.py index ae406a3..15c6503 100644 --- a/wtem/forms.py +++ b/wtem/forms.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import json import re from django import forms @@ -14,9 +15,8 @@ class WTEMForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(WTEMForm, self).__init__(*args, **kwargs) for exercise in exercises: - if exercise['type'] != 'file_upload': - continue - self.fields['attachment_for_' + str(exercise['id'])] = forms.FileField(required=False) + if exercise['type'] == 'file_upload': + self.fields['attachment_for_' + str(exercise['id'])] = forms.FileField(required=False) def save(self, commit=True): submission = super(WTEMForm, self).save(commit=commit) @@ -30,3 +30,38 @@ class WTEMForm(forms.ModelForm): attachment = Attachment(submission=submission, exercise_id=exercise_id, tag=tag) attachment.file = attachment_file attachment.save() + + +class WTEMSingleForm(forms.ModelForm): + answers = forms.CharField() + + class Meta: + model = Submission + fields = [] + + def __init__(self, *args, **kwargs): + super(WTEMSingleForm, self).__init__(*args, **kwargs) + i, exercise = self.instance.current_exercise() + if exercise and exercise['type'] == 'file_upload': + self.fields['attachment'] = forms.FileField(required=False) + + def save(self, commit=True): + submission = self.instance + answers = submission.get_answers() + posted_answers = json.loads(self.cleaned_data['answers']) + assert type(posted_answers) == dict, 'answers not dict' + assert len(posted_answers) == 1, 'answers not single' + exercise_id = posted_answers.keys()[0] + i, exercise = submission.current_exercise() + assert exercise_id == str(exercise['id']), 'wrong exercise id' + for answer in posted_answers.values(): + answers[exercise_id] = answer + submission.answers = json.dumps(answers) + submission.save() + for name, attachment_file in self.files.items(): + m = re.match(r'attachment(?:__(.*))?', name) + tag = m.group(1) or None + attachment, created = Attachment.objects.get_or_create( + submission=submission, exercise_id=exercise_id, tag=tag) + attachment.file = attachment_file + attachment.save() diff --git a/wtem/migrations/0011_auto__add_competitionstate__add_field_submission_random_seed.py b/wtem/migrations/0011_auto__add_competitionstate__add_field_submission_random_seed.py new file mode 100644 index 0000000..8538b7e --- /dev/null +++ b/wtem/migrations/0011_auto__add_competitionstate__add_field_submission_random_seed.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'CompetitionState' + db.create_table(u'wtem_competitionstate', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('state', self.gf('django.db.models.fields.CharField')(max_length=16)), + )) + db.send_create_signal(u'wtem', ['CompetitionState']) + + # Adding field 'Submission.random_seed' + db.add_column(u'wtem_submission', 'random_seed', + self.gf('django.db.models.fields.IntegerField')(default=0), + keep_default=False) + + + def backwards(self, orm): + # Deleting model 'CompetitionState' + db.delete_table(u'wtem_competitionstate') + + # Deleting field 'Submission.random_seed' + db.delete_column(u'wtem_submission', 'random_seed') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contact.contact': { + 'Meta': {'ordering': "('-created_at',)", 'object_name': 'Contact'}, + 'body': ('jsonfield.fields.JSONField', [], {}), + 'contact': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'form_tag': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'wtem.assignment': { + 'Meta': {'object_name': 'Assignment'}, + 'exercises': ('jsonfield.fields.JSONField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + }, + u'wtem.attachment': { + 'Meta': {'object_name': 'Attachment'}, + 'exercise_id': ('django.db.models.fields.IntegerField', [], {}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'submission': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['wtem.Submission']"}), + 'tag': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}) + }, + u'wtem.competitionstate': { + 'Meta': {'object_name': 'CompetitionState'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '16'}) + }, + u'wtem.confirmation': { + 'Meta': {'ordering': "['contact__contact']", 'object_name': 'Confirmation'}, + 'confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'contact': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contact.Contact']", 'null': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '100'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'wtem.submission': { + 'Meta': {'object_name': 'Submission'}, + 'answers': ('django.db.models.fields.CharField', [], {'max_length': '65536', 'null': 'True', 'blank': 'True'}), + 'contact': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contact.Contact']", 'null': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '100'}), + 'end_time': ('django.db.models.fields.CharField', [], {'max_length': '5', 'null': 'True', 'blank': 'True'}), + 'examiners': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'key_sent': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'marks': ('jsonfield.fields.JSONField', [], {'default': '{}'}), + 'random_seed': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['wtem'] \ No newline at end of file diff --git a/wtem/models.py b/wtem/models.py index 7c1f17a..6440e70 100644 --- a/wtem/models.py +++ b/wtem/models.py @@ -24,12 +24,33 @@ f.close() DEBUG_KEY = 'smerfetka159' +def get_exercise_by_id(exercise_id): + return [e for e in exercises if str(e['id']) == str(exercise_id)][0] + + def make_key(length): return ''.join( random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for i in range(length)) +class CompetitionState(models.Model): + """singleton""" + BEFORE = 'before' + DURING = 'during' + AFTER = 'after' + STATE_CHOICES = ( + (BEFORE, u'przed rozpoczęciem'), + (DURING, u'w trakcie'), + (AFTER, u'po zakończeniu'), + ) + state = models.CharField(choices=STATE_CHOICES, max_length=16) + + @classmethod + def get_state(cls): + return cls.objects.get().state + + class Submission(models.Model): contact = models.ForeignKey(Contact, null=True) key = models.CharField(max_length=30, unique=True) @@ -41,6 +62,7 @@ class Submission(models.Model): marks = JSONField(default={}) examiners = models.ManyToManyField(User, null=True, blank=True) end_time = models.CharField(max_length=5, null=True, blank=True) + random_seed = models.IntegerField() def __unicode__(self): return ', '.join((self.last_name, self.first_name, self.email)) @@ -59,12 +81,32 @@ class Submission(models.Model): key=key if key else Submission.generate_key(), first_name=first_name, last_name=last_name, - email=email + email=email, + random_seed=random.randint(-2147483648, 2147483647) ) submission.save() return submission + def competition_link(self): + return reverse('form_single', kwargs={'submission_id': self.id, 'key': self.key}) + + def get_answers(self): + return json.loads(self.answers) if self.answers else {} + + def shuffled_exercise_ids(self): + exercise_ids = [e['id'] for e in exercises] + random.seed(self.random_seed) + random.shuffle(exercise_ids) + return exercise_ids + + def current_exercise(self): + answers = self.get_answers() + for i, id in enumerate(self.shuffled_exercise_ids(), 1): + if str(id) not in answers: + return i, get_exercise_by_id(id) + return None, None + def get_mark(self, user_id, exercise_id): mark = None user_id = str(user_id) @@ -92,8 +134,7 @@ class Submission(models.Model): return marks def get_final_exercise_mark(self, exercise_id): - # exercise = exercises[int(exercise_id)-1] - exercise = [e for e in exercises if str(e['id']) == str(exercise_id)][0] + exercise = get_exercise_by_id(exercise_id) if exercise_checked_manually(exercise): marks_by_examiner = self.get_exercise_marks_by_examiner(exercise_id) if len(marks_by_examiner): diff --git a/wtem/static/wtem/wtem.js b/wtem/static/wtem/wtem.js index e481b92..3e55b01 100644 --- a/wtem/static/wtem/wtem.js +++ b/wtem/static/wtem/wtem.js @@ -2,7 +2,7 @@ $(function() { var to_submit; - $('form').submit(function(e) { + $('form').submit(function() { //e.preventDefault(); to_submit = {}; spinner.show(); @@ -33,7 +33,7 @@ $(function() { if(exercise.get_answers) { to_push.closed_part = exercise.get_answers()[0]; } - open_part = el.find('.open_part') + open_part = el.find('.open_part'); if(open_part.length) { to_push.open_part = open_part.find('textarea').val(); } @@ -55,12 +55,12 @@ $(function() { } push_answer(el, to_push); } - } + }; var sms_handler = function() { var textarea = $(this), label_suffix = textarea.parent().find('.label_suffix'), - left = 140 - textarea.val().length; + left = 140 - textarea.val().length, to_insert = '(pozostało: ' + left + ')'; if(left < 0) { to_insert = '' + to_insert + ''; diff --git a/wtem/templates/wtem/exercises/exercise_no.html b/wtem/templates/wtem/exercises/exercise_no.html index 3caa068..aecdd2b 100644 --- a/wtem/templates/wtem/exercises/exercise_no.html +++ b/wtem/templates/wtem/exercises/exercise_no.html @@ -1,3 +1,3 @@ {% if not exercise.continuation %} -

Zadanie {{exercise.id_show|default:exercise.id}}{# ({{ exercise.max_points }} pkt)#}

+

Zadanie {{ no }}{# {{exercise.id_show|default:exercise.id}} #}{# ({{ exercise.max_points }} pkt)#}

{% endif %} \ No newline at end of file diff --git a/wtem/templates/wtem/single.html b/wtem/templates/wtem/single.html new file mode 100644 index 0000000..2e324e4 --- /dev/null +++ b/wtem/templates/wtem/single.html @@ -0,0 +1,80 @@ +{% extends 'base_super.html' %} +{% load compressed %} +{% load static %} +{% load cache %} + +{% block extra_script %} + {% compressed_js 'wtem' %} +{% endblock %} + + + +{% block body %} + + +

{% include "wtem/title.html" %}

+
Rozwiązania można wysyłać do godziny {{end_time|default:"11:30"}}. Nie czekaj na ostatnią chwilę!
+ +

Witamy w I etapie Olimpiady Cyfrowej. Na rozwiązanie zadań masz czas do godz. {{end_time|default:"11:30"}}. Test składa się z 30 pytań.

+ +

Wszelkie aktualności dotyczące Olimpiady możesz znaleźć tutaj.

+ +

Powodzenia!
+Zespół Olimpiady Cyfrowej, fundacja Nowoczesna Polska

+ +
+ +{% cache 300 wtem exercise.id %} +{% with 'wtem/exercises/'|add:exercise.type|add:'.html' as template_name %} +{% include template_name with exercise=exercise %} +{% endwith %} +{% endcache %} + + +
+ + +

Sprawdź jeszcze raz wszystkie swoje odpowiedzi, a następnie wyślij je do nas, klikając w poniższy przycisk:

+ +
+ + Wysyłanie rozwiązań w toku... + + Spróbuj jeszcze raz, jeśli wysyłanie trwa dłużej niż kilka minut. + +

+ +
Rozwiązania można wysyłać do godziny {{end_time|default:"11:30"}}. Nie czekaj na ostatnią chwilę!
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/wtem/templates/wtem/thanks_single.html b/wtem/templates/wtem/thanks_single.html new file mode 100644 index 0000000..bf2890c --- /dev/null +++ b/wtem/templates/wtem/thanks_single.html @@ -0,0 +1,17 @@ +{% extends 'base_super.html' %} + +{% block body %} + +

Twoje rozwiązania zostały wysłane

+ +

Dziękujemy za udział w I etapie Olimpiady Cyfrowej. +Twoja praca została wysłana i poprawnie przyjęta przez system.

+ +

Do końca listopada otrzymasz e-mail z wynikami I etapu. Informacja o uzyskanych przez Ciebie punktach zostanie również przesłana do osoby, która zgłosiła Twój udział w Olimpiadzie.

+ +

Aktualności związane z Olimpiadą możesz sprawdzać tutaj. W razie dodatkowych pytań możesz kontaktować się z nami pod adresem olimpiada@nowoczesnapolska.org.pl lub numerem telefonu +48 515-502-666.

+ +

Zespół Olimpiady Cyfrowej
+fundacja Nowoczesna Polska

+ +{% endblock %} \ No newline at end of file diff --git a/wtem/urls.py b/wtem/urls.py index 58f39e3..f7a5e99 100644 --- a/wtem/urls.py +++ b/wtem/urls.py @@ -6,5 +6,6 @@ urlpatterns = patterns( '', url(r'^potwierdzenie/(?P.*)/(?P.*)/$', views.confirmation, name='student_confirmation'), url(r'^_test/(?P.*)/$', views.form_during), - url(r'^(?P.*)/$', views.form, name='wtem_form'), + # url(r'^(?P.*)/$', views.form, name='wtem_form'), + url(r'^(?P.*)/(?P.*)/$', views.form_single, name='form_single'), ) diff --git a/wtem/views.py b/wtem/views.py index b5f7548..6366e53 100644 --- a/wtem/views.py +++ b/wtem/views.py @@ -3,14 +3,16 @@ import json from copy import deepcopy from django.conf import settings +from django.core.urlresolvers import reverse from django.http import HttpResponseForbidden +from django.http.response import HttpResponseRedirect from django.shortcuts import render, get_object_or_404 from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt from wtem.models import Confirmation -from .forms import WTEMForm -from .models import Submission, DEBUG_KEY, exercises +from .forms import WTEMForm, WTEMSingleForm +from .models import Submission, DEBUG_KEY, exercises, CompetitionState WTEM_CONTEST_STAGE = getattr(settings, 'WTEM_CONTEST_STAGE', 'before') @@ -71,6 +73,33 @@ def form_during(request, key): raise Exception +@never_cache +@csrf_exempt +def form_single(request, submission_id, key): + if CompetitionState.get_state() != CompetitionState.DURING: + if request.META['REMOTE_ADDR'] not in getattr(settings, 'WTEM_CONTEST_IP_ALLOW', []): + return HttpResponseForbidden('Not allowed') + + submission = Submission.objects.get(id=submission_id) + if submission.key != key: + return render(request, 'wtem/key_not_found.html') + + i, exercise = submission.current_exercise() + + if not exercise: + return render(request, 'wtem/thanks_single.html') + + if request.method == 'GET': + return render(request, 'wtem/single.html', {'exercise': exercise, 'no': i}) + elif request.method == 'POST': + form = WTEMSingleForm(request.POST, request.FILES, instance=submission) + if form.is_valid(): + form.save() + return HttpResponseRedirect(reverse('form_single', kwargs={'submission_id': submission_id, 'key': key})) + else: + raise Exception + + def confirmation(request, id, key): conf = get_object_or_404(Confirmation, id=id, key=key) was_confirmed = conf.confirmed