From 566ee7cbbf5ad86cc0fe154a4c592c522d97ead2 Mon Sep 17 00:00:00 2001 From: Jan Szejko Date: Thu, 13 Sep 2018 14:51:08 +0200 Subject: [PATCH] teacher confirmations + validate unique student emails --- contact/forms.py | 16 ++- contact/views.py | 2 +- edumed/contact_forms.py | 43 +++++- edumed/templates/contact/olimpiada/form.html | 3 + .../templates/contact/olimpiada/mail_body.txt | 2 + .../0013_auto__add_teacherconfirmation.py | 126 ++++++++++++++++++ wtem/models.py | 58 +++++--- wtem/templates/wtem/teacher_confirmed.html | 15 +++ wtem/urls.py | 1 + wtem/views.py | 11 +- 10 files changed, 250 insertions(+), 27 deletions(-) create mode 100644 wtem/migrations/0013_auto__add_teacherconfirmation.py create mode 100644 wtem/templates/wtem/teacher_confirmed.html diff --git a/contact/forms.py b/contact/forms.py index 0d09334..cd5c918 100644 --- a/contact/forms.py +++ b/contact/forms.py @@ -43,6 +43,15 @@ class ContactForm(forms.Form): contact = NotImplemented data_processing = None + def get_dictionary(self, contact): + site = Site.objects.get_current() + return { + 'form_tag': self.form_tag, + 'site_name': getattr(self, 'site_name', site.name), + 'site_domain': getattr(self, 'site_domain', site.domain), + 'contact': contact, + } + def save(self, request, formsets=None): from .models import Attachment, Contact body = {} @@ -71,12 +80,7 @@ class ContactForm(forms.Form): attachment.save() site = Site.objects.get_current() - dictionary = { - 'form_tag': self.form_tag, - 'site_name': getattr(self, 'site_name', site.name), - 'site_domain': getattr(self, 'site_domain', site.domain), - 'contact': contact, - } + dictionary = self.get_dictionary(contact) context = RequestContext(request) mail_managers_subject = render_to_string([ 'contact/%s/mail_managers_subject.txt' % self.form_tag, diff --git a/contact/views.py b/contact/views.py index 86c8417..0d8c0bc 100644 --- a/contact/views.py +++ b/contact/views.py @@ -49,7 +49,7 @@ def form(request, form_tag, force_enabled=False): return render( request, ['contact/%s/form.html' % form_tag, 'contact/form.html'], - {'form': form, 'formsets': formsets} + {'form': form, 'formsets': formsets, 'formset_errors': any(formset.errors for formset in formsets.values())} ) diff --git a/edumed/contact_forms.py b/edumed/contact_forms.py index 2a5d692..bd72597 100644 --- a/edumed/contact_forms.py +++ b/edumed/contact_forms.py @@ -6,6 +6,8 @@ from django.utils.safestring import mark_safe from contact.forms import ContactForm from django.utils.translation import ugettext_lazy as _ +from wtem.models import TeacherConfirmation, Confirmation + WOJEWODZTWA = ( u'dolnośląskie', u'kujawsko-pomorskie', @@ -51,6 +53,12 @@ class WTEMStudentForm(forms.Form): email = forms.EmailField(label=u'Adres e-mail', max_length=128) form_tag = "student" + def clean_email(self): + email = self.cleaned_data['email'] + if Confirmation.objects.filter(email=email): + raise forms.ValidationError(u'Uczeń z tym adresem już został zgłoszony.') + return email + class NonEmptyBaseFormSet(BaseFormSet): """ @@ -63,13 +71,31 @@ class NonEmptyBaseFormSet(BaseFormSet): forms.ValidationError(u"Proszę podać dane przynajmniej jednej osoby.") +class StudentFormset(forms.formsets.formset_factory(WTEMStudentForm, formset=NonEmptyBaseFormSet)): + def clean(self): + from django.forms.util import ErrorList + super(StudentFormset, self).clean() + + emails = set() + for form in self.forms: + if not form.is_valid(): + continue + if form.cleaned_data: + email = form.cleaned_data['email'] + if email in emails: + errors = form._errors.setdefault('email', ErrorList()) + errors.append(u'Każdy zgłoszony uczeń powinien mieć własny adres email') + else: + emails.add(email) + + class CommissionForm(forms.Form): name = forms.CharField(label=u'Imię i nazwisko Członka Komisji', max_length=128) form_tag = "commission" class OlimpiadaForm(ContactForm): - ends_on = (2017, 11, 17, 0, 5) + ends_on = (2018, 11, 17, 0, 5) disabled_template = 'wtem/disabled_contact_form.html' form_tag = "olimpiada" old_form_tags = ["olimpiada-2016"] @@ -77,13 +103,16 @@ class OlimpiadaForm(ContactForm): submit_label = u"Wyślij zgłoszenie" admin_list = ['nazwisko', 'school'] form_formsets = { - 'student': forms.formsets.formset_factory(WTEMStudentForm, formset=NonEmptyBaseFormSet), + 'student': StudentFormset, 'commission': forms.formsets.formset_factory(CommissionForm), } mailing_field = 'zgoda_newsletter' contact = forms.EmailField(label=u'Adres e-mail Przewodniczącego/Przewodniczącej', max_length=128) przewodniczacy = forms.CharField(label=u'Imię i nazwisko Przewodniczącego/Przewodniczącej', max_length=128) + przewodniczacy_phone = forms.CharField( + label=u'Numer telefonu Przewodniczącego/Przewodniczącej', max_length=128, required=False, + help_text=u'Zadzwonimy tylko w przypadku problemów ze zgłoszeniem.') school = forms.CharField(label=u'Nazwa szkoły', max_length=255) school_address = forms.CharField(label=u'Adres szkoły', widget=forms.Textarea, max_length=1000) school_wojewodztwo = forms.ChoiceField(label=u'Województwo', choices=WOJEWODZTWO_CHOICES) @@ -138,6 +167,16 @@ class OlimpiadaForm(ContactForm): current = {} return toret + def get_dictionary(self, contact): + dictionary = super(OlimpiadaForm, self).get_dictionary(contact) + conf = TeacherConfirmation.objects.filter(contact=contact) + if conf: + confirmation = conf.get() + else: + confirmation = TeacherConfirmation.create(contact=contact) + dictionary['confirmation'] = confirmation + return dictionary + def save(self, request, formsets=None): from wtem.models import Confirmation contact = super(OlimpiadaForm, self).save(request, formsets) diff --git a/edumed/templates/contact/olimpiada/form.html b/edumed/templates/contact/olimpiada/form.html index 3a4f5be..792eb45 100755 --- a/edumed/templates/contact/olimpiada/form.html +++ b/edumed/templates/contact/olimpiada/form.html @@ -20,6 +20,9 @@
+ {% if form.errors or formset_errors %} +
Przy wysyłaniu formularza wystąpił problem. Prosimy o poprawienie błędów poniżej.
+ {% endif %} {% csrf_token %} {% render_honeypot_field %}

Dane Przewodniczącego Komisji Szkolnej i szkoły zgłaszającej Uczestników:

diff --git a/edumed/templates/contact/olimpiada/mail_body.txt b/edumed/templates/contact/olimpiada/mail_body.txt index 6f5546c..1c03cf4 100755 --- a/edumed/templates/contact/olimpiada/mail_body.txt +++ b/edumed/templates/contact/olimpiada/mail_body.txt @@ -2,6 +2,8 @@ Dziękujemy za rejestrację Komisji Szkolnej do Olimpiady Cyfrowej. Do udziału w Olimpiadzie zostały zgłoszone następujące osoby: {% for student in contact.body.student %}* {{ student.first_name }} {{ student.last_name }} {% endfor %} +Prosimy o kliknięcie poniższego linku aby potwierdzić, że wiadomość dotarła: +https://olimpiadacyfrowa.pl{{ confirmation.absolute_url }} Każdy zgłoszony uczeń powinien otrzymać wiadomość z potwierdzeniem rejestracji. Prosimy upewnić się, czy potwierdzenie dotarło do każdego diff --git a/wtem/migrations/0013_auto__add_teacherconfirmation.py b/wtem/migrations/0013_auto__add_teacherconfirmation.py new file mode 100644 index 0000000..7a8bbaa --- /dev/null +++ b/wtem/migrations/0013_auto__add_teacherconfirmation.py @@ -0,0 +1,126 @@ +# -*- 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 'TeacherConfirmation' + db.create_table(u'wtem_teacherconfirmation', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('contact', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contact.Contact'], null=True)), + ('key', self.gf('django.db.models.fields.CharField')(max_length=30)), + ('confirmed', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal(u'wtem', ['TeacherConfirmation']) + + + def backwards(self, orm): + # Deleting model 'TeacherConfirmation' + db.delete_table(u'wtem_teacherconfirmation') + + + 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': '{}'}), + 'opened_link': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'random_seed': ('django.db.models.fields.IntegerField', [], {}) + }, + u'wtem.teacherconfirmation': { + 'Meta': {'ordering': "['contact__contact']", 'object_name': 'TeacherConfirmation'}, + 'confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'contact': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contact.Contact']", 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '30'}) + } + } + + complete_apps = ['wtem'] \ No newline at end of file diff --git a/wtem/models.py b/wtem/models.py index 2b093ae..4139efc 100644 --- a/wtem/models.py +++ b/wtem/models.py @@ -265,14 +265,33 @@ class Assignment(models.Model): return self.user.username + ': ' + ','.join(map(str, self.exercises)) -class Confirmation(models.Model): - first_name = models.CharField(max_length=100) - last_name = models.CharField(max_length=100) - email = models.EmailField(max_length=100, unique=True) +class AbstractConfirmation(models.Model): contact = models.ForeignKey(Contact, null=True) key = models.CharField(max_length=30) confirmed = models.BooleanField(default=False) + class Meta: + abstract = True + + def readable_contact(self): + return '%s <%s>' % (self.contact.body.get('przewodniczacy'), self.contact.contact) + + def school_phone(self): + return '%s, tel. %s' % (self.contact.body.get('school'), self.contact.body.get('school_phone')) + + def age(self): + return timezone.now() - self.contact.created_at + + def readable_age(self): + td = self.age() + return '%s dni, %s godzin' % (td.days, td.seconds/3600) + + +class Confirmation(AbstractConfirmation): + first_name = models.CharField(max_length=100) + last_name = models.CharField(max_length=100) + email = models.EmailField(max_length=100, unique=True) + class Meta: ordering = ['contact__contact'] @@ -292,19 +311,6 @@ class Confirmation(models.Model): def absolute_url(self): return reverse('student_confirmation', args=(self.id, self.key)) - def readable_contact(self): - return '%s <%s>' % (self.contact.body.get('przewodniczacy'), self.contact.contact) - - def school_phone(self): - return '%s, tel. %s' % (self.contact.body.get('school'), self.contact.body.get('school_phone')) - - def age(self): - return timezone.now() - self.contact.created_at - - def readable_age(self): - td = self.age() - return '%s dni, %s godzin' % (td.days, td.seconds/3600) - def send_mail(self): mail_subject = render_to_string('contact/olimpiada/student_mail_subject.html').strip() mail_body = render_to_string( @@ -318,5 +324,23 @@ class Confirmation(models.Model): fail_silently=True) +class TeacherConfirmation(AbstractConfirmation): + + class Meta: + ordering = ['contact__contact'] + + @classmethod + def create(cls, contact=None, key=None): + confirmation = cls( + contact=contact, + key=key if key else make_key(30), + ) + confirmation.save() + return confirmation + + def absolute_url(self): + return reverse('teacher_confirmation', args=(self.id, self.key)) + + def exercise_checked_manually(exercise): return (exercise['type'] in ('open', 'file_upload')) or 'open_part' in exercise diff --git a/wtem/templates/wtem/teacher_confirmed.html b/wtem/templates/wtem/teacher_confirmed.html new file mode 100644 index 0000000..bff9452 --- /dev/null +++ b/wtem/templates/wtem/teacher_confirmed.html @@ -0,0 +1,15 @@ +{% extends 'base_super.html' %} + +{% block title %}Potwierdzenie{% endblock %} + +{% block body %} +

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

+

Potwierdzono zgłoszenie

+ + {% if was_confirmed %} +

Zgłoszenie do Olimpiady Cyfrowej zostało już wcześniej potwierdzone.

+ {% else %} +

Dziękujemy za potwierdzenie zgłoszenia w Olimpiadzie Cyfrowej!

+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/wtem/urls.py b/wtem/urls.py index e498743..1e3f377 100644 --- a/wtem/urls.py +++ b/wtem/urls.py @@ -5,6 +5,7 @@ from . import views urlpatterns = patterns( '', url(r'^potwierdzenie/(?P.*)/(?P.*)/$', views.confirmation, name='student_confirmation'), + url(r'^potwierdzenie-zgloszenia/(?P.*)/(?P.*)/$', views.teacher_confirmation, name='teacher_confirmation'), url(r'^_test/(?P.*)/$', views.form_during), url(r'^(?P[^/]*)/(?P[^/]*)/$', views.form, name='wtem_form'), url(r'^(?P[^/]*)/(?P[^/]*)/start/$', views.start, name='wtem_start'), diff --git a/wtem/views.py b/wtem/views.py index 1dea093..baacf44 100644 --- a/wtem/views.py +++ b/wtem/views.py @@ -14,7 +14,7 @@ from django.utils.cache import patch_cache_control, add_never_cache_headers from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt -from wtem.models import Confirmation +from wtem.models import Confirmation, TeacherConfirmation from .forms import WTEMForm, WTEMSingleForm from .models import Submission, DEBUG_KEY, exercises, CompetitionState @@ -163,3 +163,12 @@ def confirmation(request, id, key): conf.confirmed = True conf.save() return render(request, 'wtem/confirmed.html', {'confirmation': conf, 'was_confirmed': was_confirmed}) + + +def teacher_confirmation(request, id, key): + conf = get_object_or_404(TeacherConfirmation, id=id, key=key) + was_confirmed = conf.confirmed + if not was_confirmed: + conf.confirmed = True + conf.save() + return render(request, 'wtem/teacher_confirmed.html', {'confirmation': conf, 'was_confirmed': was_confirmed}) -- 2.20.1