Skip to content

Commit e978b19

Browse files
authored
Added permission slip to under age (#116)
* Added permission slip to under age * Add CI of python 3.10
1 parent 7ca431b commit e978b19

32 files changed

+589
-32
lines changed

.github/workflows/django.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
max-parallel: 4
1515
matrix:
16-
python-version: ['3.8', '3.9']
16+
python-version: ['3.8', '3.9', '3.10']
1717

1818
steps:
1919
- uses: actions/checkout@v3

Dockerfile

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
FROM python:3.9.13
22
RUN apt-get update
33
RUN apt-get install -y cron && touch /var/log/cron.log
4+
RUN apt-get install texlive-latex-extra -y
45
RUN pip install --upgrade pip
56
WORKDIR /code
67

app/hackathon_variables.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
HACKATHON_NAME = 'HackUPC'
22
HACKATHON_DESCRIPTION = 'Join us for BarcelonaTech\'s hackathon. 36h.'
33
HACKATHON_ORG = 'Hackers@UPC'
4+
HACKATHON_START_DATE = '12/12/2012'
5+
HACKATHON_END_DATE = '14/12/2012'
6+
HACKATHON_LOCATION = 'Barcelona'
47

58
HACKATHON_CONTACT_EMAIL = '[email protected]'
69
HACKATHON_SOCIALS = {'Facebook': ('https://www.facebook.com/hackupc', 'bi-facebook'),
@@ -22,4 +25,8 @@
2225
SUPPORTED_RESUME_EXTENSIONS = ['.pdf']
2326
FRIENDS_MAX_CAPACITY = None
2427

28+
REQUIRE_PERMISSION_SLIP_TO_UNDER_AGE = True
29+
SUPPORTED_PERMISSION_SLIP_EXTENSIONS = ['.pdf']
30+
PARTICIPANT_CAN_UPLOAD_PERMISSION_SLIP = True
31+
2532
ATTRITION_RATE = 1.5

app/settings.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
'django_jwt',
5858
'django_jwt.server',
5959
'django_bootstrap5',
60+
'django_tex',
6061
'compressor',
6162
'colorfield',
6263
'corsheaders',
@@ -102,7 +103,6 @@
102103
'django.template.context_processors.request',
103104
'django.contrib.auth.context_processors.auth',
104105
'django.contrib.messages.context_processors.messages',
105-
'django.template.context_processors.request',
106106
'app.template.app_variables',
107107
'csp.context_processors.nonce',
108108
],
@@ -112,6 +112,20 @@
112112
},
113113
},
114114
},
115+
{
116+
'NAME': 'tex',
117+
'BACKEND': 'django_tex.engine.TeXEngine',
118+
'DIRS': [BASE_DIR / 'app' / 'templates'],
119+
'APP_DIRS': True,
120+
'OPTIONS': {
121+
'context_processors': [
122+
'django.template.context_processors.debug',
123+
'django.template.context_processors.request',
124+
'django.contrib.auth.context_processors.auth',
125+
'app.template.app_variables',
126+
]
127+
},
128+
},
115129
]
116130

117131
WSGI_APPLICATION = 'app.wsgi.application'
@@ -444,6 +458,11 @@
444458
# DateTime formats
445459
USE_L10N = False
446460
DATETIME_FORMAT = 'N j, Y, H:i'
461+
DATE_FORMAT = 'N j, Y'
447462
SHORT_DATETIME_FORMAT = 'Y/m/d H:i'
448463
TIME_FORMAT = 'H:i:s'
449464
SHORT_DATE_FORMAT = 'Y/m/d'
465+
466+
# Latex binary
467+
LATEX_INTERPRETER = 'pdflatex'
468+
LATEX_GRAPHICSPATH = [os.path.join(BASE_DIR, 'latex_graphics')]

app/static/css/main.css

+4
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,7 @@ footer {
151151
.g-recaptcha {
152152
display: inline-block;
153153
}
154+
155+
.a-none:link, .a-none:visited, .a-none:hover, .a-none:active, .a-none {
156+
text-decoration: none !important;
157+
}

app/template.py

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.conf import settings
22
from django.urls import reverse
3+
from django.utils import timezone
34

45
from app.utils import get_theme, is_installed
56
from application.models import ApplicationTypeConfig
@@ -48,6 +49,13 @@ def get_main_nav(request):
4849
return nav
4950

5051

52+
def get_date(text):
53+
try:
54+
return timezone.datetime.strptime(text, '%d/%m/%Y')
55+
except ValueError:
56+
return None
57+
58+
5159
def app_variables(request):
5260
return {
5361
'main_nav': get_main_nav(request),
@@ -64,4 +72,8 @@ def app_variables(request):
6472
'socialaccount_providers': getattr(settings, 'SOCIALACCOUNT_PROVIDERS', {}),
6573
'auth_password_validators': getattr(settings, 'PASSWORD_VALIDATORS', {}),
6674
'tables_export_supported': getattr(settings, 'DJANGO_TABLES2_EXPORT_FORMATS', []),
75+
'participant_can_upload_permission_slip': getattr(settings, 'PARTICIPANT_CAN_UPLOAD_PERMISSION_SLIP', False),
76+
'hack_start_date': get_date(getattr(settings, 'HACKATHON_START_DATE', '')),
77+
'hack_end_date': get_date(getattr(settings, 'HACKATHON_END_DATE', '')),
78+
'hack_location': getattr(settings, 'HACKATHON_LOCATION', ''),
6779
}

app/templates/mails/components/button.html

+1
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@
2222
</tr>
2323
</tbody>
2424
</table>
25+
<p>If the link does not work: <a>{{ url }}</a></p>

app/views.py

+14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from django.shortcuts import redirect, render
22
from django.views import View
3+
from django_tex.shortcuts import render_to_pdf
34

5+
from app.template import app_variables
46
from user.mixins import LoginRequiredMixin
57

68

@@ -40,3 +42,15 @@ def handler_error_403(request, exception=None, **kwargs):
4042

4143
def handler_error_400(request, exception=None, **kwargs):
4244
return render(request=request, template_name='errors/400.html', context={'exception': exception}, status=400)
45+
46+
47+
class LatexTemplateView(View):
48+
template_name = ''
49+
file_name = ''
50+
51+
def get_context_data(self, **kwargs):
52+
return app_variables(self.request)
53+
54+
def get(self, request, *args, **kwargs):
55+
context = self.get_context_data(**kwargs)
56+
return render_to_pdf(request, self.template_name, context, filename=self.file_name)

application/admin.py

+1
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,5 @@ class PromotionalCodeAdmin(admin.ModelAdmin):
4343
admin.site.register(models.ApplicationTypeConfig, ApplicationTypeConfigAdmin)
4444
admin.site.register(models.ApplicationLog)
4545
admin.site.register(models.Edition)
46+
admin.site.register(models.PermissionSlip)
4647
admin.site.register(models.PromotionalCode, PromotionalCodeAdmin)

application/emails.py

+22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.conf import settings
2+
from django.contrib.auth import get_user_model
23
from django.urls import reverse
34

45
from app.emails import Email
@@ -32,3 +33,24 @@ def get_email_expired(application):
3233
'app_contact': getattr(settings, 'HACKATHON_CONTACT_EMAIL', ''),
3334
}
3435
return Email(name='application_expired', context=context, to=application.user.email)
36+
37+
38+
def send_email_permission_slip_upload(request, application):
39+
url = request.build_absolute_uri(reverse('permission_slip', kwargs={'uuid': application.get_uuid}))
40+
context = {
41+
'user': application.user,
42+
'url': url,
43+
}
44+
permission_slip_managers = (get_user_model().objects.with_perm('application.can_review_permission_slip')
45+
.value_list('email', flat=True))
46+
Email(name='permission_slip_upload', context=context, to=permission_slip_managers, request=request).send()
47+
48+
49+
def send_email_permission_slip_review(request, application, permission_slip):
50+
url = request.build_absolute_uri(reverse('permission_slip', kwargs={'uuid': application.get_uuid}))
51+
context = {
52+
'user': application.user,
53+
'permission_slip': permission_slip,
54+
'url': url,
55+
}
56+
Email(name='permission_slip_review', context=context, to=application.user.email, request=request).send()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 4.2.2 on 2023-09-11 12:05
2+
3+
import application.models
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
('application', '0031_alter_applicationtypeconfig_spots'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='PermissionSlip',
19+
fields=[
20+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21+
('status', models.CharField(choices=[('N', 'Missing document'), ('D', 'Not accepted'), ('U', 'On review'), ('A', 'Accepted')], default='N', max_length=2)),
22+
('file', models.FileField(null=True, upload_to=application.models.get_permission_slip_file_name)),
23+
('edition', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='application.edition')),
24+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
25+
],
26+
options={
27+
'permissions': (('can_review_permission_slip', 'Can review permission slip'),),
28+
},
29+
),
30+
]

application/models.py

+62
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ def exclude(self, *args, **kwargs):
187187
kwargs = self.convert_kwargs(kwargs)
188188
return super().exclude(*args, **kwargs)
189189

190+
def invited(self):
191+
return self.filter(status__in=[self.model.STATUS_INVITED, self.model.STATUS_LAST_REMINDER,
192+
self.model.STATUS_CONFIRMED, self.model.STATUS_ATTENDED])
193+
190194

191195
class Application(models.Model):
192196
STATUS_PENDING = 'P'
@@ -343,6 +347,15 @@ def save(self, *args, **kwargs):
343347
self.last_modified = timezone.now()
344348
super().save(*args, **kwargs)
345349

350+
def get_permission_slip(self, raise_404=False):
351+
try:
352+
return PermissionSlip.objects.get(user_id=self.user_id, edition_id=self.edition_id)
353+
except PermissionSlip.DoesNotExist:
354+
if raise_404:
355+
from django.http import Http404
356+
raise Http404
357+
return None
358+
346359
class Meta:
347360
unique_together = ('type', 'user', 'edition')
348361
permissions = (
@@ -437,3 +450,52 @@ def form_data(self, new_data: dict):
437450
data = self.form_data
438451
data.update(new_data)
439452
self.data = json.dumps(data)
453+
454+
455+
def get_permission_slip_file_name(instance, filename):
456+
return '%s/User/permission_slip/%s_%s.%s' % (instance.edition.name, instance.user.get_full_name().replace(' ', '-'),
457+
instance.user.id, filename.split('.')[-1])
458+
459+
460+
class PermissionSlip(models.Model):
461+
STATUS_ACCEPTED = 'A'
462+
STATUS_UPLOADED = 'U'
463+
STATUS_NONE = 'N'
464+
STATUS_DENIED = 'D'
465+
STATUS = (
466+
(STATUS_NONE, _('Missing document')),
467+
(STATUS_DENIED, _('Not accepted')),
468+
(STATUS_UPLOADED, _('On review')),
469+
(STATUS_ACCEPTED, _('Accepted')),
470+
)
471+
STATUS_DESCRIPTION = {
472+
STATUS_NONE: _('Upload the permission slip signed by your parents or legal tutors'),
473+
STATUS_DENIED: _('The document has been reviewed and has some problem'),
474+
STATUS_UPLOADED: _('Document uploaded successfully, now we will review it'),
475+
STATUS_ACCEPTED: _('The document has been accepted!'),
476+
}
477+
STATUS_COLORS = {
478+
STATUS_NONE: 'danger',
479+
STATUS_DENIED: 'warning',
480+
STATUS_UPLOADED: 'info',
481+
STATUS_ACCEPTED: 'success',
482+
}
483+
484+
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
485+
edition = models.ForeignKey(Edition, null=True, on_delete=models.SET_NULL)
486+
status = models.CharField(choices=STATUS, max_length=2, default=STATUS_NONE)
487+
file = models.FileField(upload_to=get_permission_slip_file_name, null=True)
488+
489+
def __str__(self):
490+
return '%s - %s' % (self.edition.name, self.user.get_full_name())
491+
492+
def get_status_color(self):
493+
return self.STATUS_COLORS.get(self.status)
494+
495+
def get_status_description(self):
496+
return self.STATUS_DESCRIPTION.get(self.status)
497+
498+
class Meta:
499+
permissions = (
500+
('can_review_permission_slip', _('Can review permission slip')),
501+
)

application/other_forms.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import os
2+
3+
from django import forms
4+
from django.conf import settings
5+
from django.utils.translation import gettext_lazy as _
6+
7+
from app.mixins import BootstrapFormMixin
8+
from application.models import PermissionSlip
9+
from application.validators import validate_file_extension
10+
11+
EXTENSIONS = getattr(settings, 'SUPPORTED_PERMISSION_SLIP_EXTENSIONS', None)
12+
13+
14+
class PermissionSlipForm(forms.ModelForm, BootstrapFormMixin):
15+
bootstrap_field_info = {'': {'fields': [{'name': 'file', 'space': 12}, {'name': 'terms', 'space': 12}]}}
16+
17+
file = forms.FileField(validators=[validate_file_extension(EXTENSIONS)], required=True,
18+
label=_('Upload your permission slip'),
19+
help_text=_('Accepted file formats: %s' % (', '.join(EXTENSIONS) if EXTENSIONS else 'Any')))
20+
terms = forms.BooleanField(label=_('I understand that the permission slip I am providing will be used for '
21+
'ensuring my safety during the event and for addressing any legal aspects '
22+
'related to my participation in the hackathon. I hereby consent to the '
23+
'use of this document for these purposes.'), required=True)
24+
25+
def __init__(self, *args, **kwargs):
26+
instance = kwargs.get('instance', None)
27+
self._old_file = instance.file if instance is not None else None
28+
super().__init__(*args, **kwargs)
29+
30+
def save(self, commit=True):
31+
instance = super().save(commit=False)
32+
instance.status = instance.STATUS_UPLOADED
33+
old_file = getattr(self, '_old_file', None)
34+
try:
35+
os.remove(old_file.path)
36+
except ValueError:
37+
pass
38+
if commit:
39+
instance.save()
40+
return instance
41+
42+
class Meta:
43+
model = PermissionSlip
44+
fields = ('file', 'terms')

application/templates/application_home/user_applications.html

+22
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ <h5 class="modal-title" id="exampleModalLabel">{% translate 'QR code' %}</h5>
6262
{% endwith %}
6363
{% with applications_confirmed=user_applications_grouped|get_item:'C' %}
6464
{% if applications_confirmed %}
65+
{% if request.user.under_age_document_required and permission_slip_action %}
66+
<div class="mt-2 rounded-3 p-3 bg-contrast">
67+
<h2 class="text-center">{% translate 'Permission slip required for attendance' %}</h2>
68+
<p class="text-center">Action needed to attend to the event!</p>
69+
<div class="row justify-content-around mt-3 mb-3">
70+
<div class="col-12 col-lg-6 d-grid d-md-block">
71+
<a href="{% url 'permission_slip' applications_confirmed.0.get_uuid %}" class="btn btn-info col-12">{% trans 'Manage my permission slip' %}</a>
72+
</div>
73+
</div>
74+
</div>
75+
{% endif %}
6576
<div class="mt-2 rounded-3 p-3 bg-contrast">
6677
<div class="row">
6778
<div class="col-12 col-lg-8">
@@ -91,6 +102,17 @@ <h2>{% translate 'Confirmed applications:' %} {{ applications_confirmed|get_type
91102
{% endif %}
92103
{% endwith %}
93104
{% for app in user_applications_grouped|get_item:'default' %}
105+
{% if request.user.under_age_document_required and permission_slip_action %}
106+
<div class="mt-2 rounded-3 p-3 bg-contrast">
107+
<h2 class="text-center">{% translate 'Permission slip required for attendance' %}</h2>
108+
<p class="text-center">Action needed to attend to the event!</p>
109+
<div class="row justify-content-around mt-3 mb-3">
110+
<div class="col-12 col-lg-6 d-grid d-md-block">
111+
<a href="{% url 'permission_slip' app.get_uuid %}" class="btn btn-info col-12">{% trans 'Manage my permission slip' %}</a>
112+
</div>
113+
</div>
114+
</div>
115+
{% endif %}
94116
<div class="mt-2 rounded-3 p-3 bg-contrast">
95117
<div class="row">
96118
<div class="col-12 col-md-9"><h2>{{ app.type.name }} {% translate 'application' %}</h2></div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{% extends 'mails/base.html' %}
2+
{% block content %}
3+
<p>Hi {{ user.first_name }},</p>
4+
5+
6+
{% if permission_slip.status == permission_slip.STATUS_ACCEPTED %}
7+
<p>Your permission slip has just been reviewed and has been accepted!</p>
8+
{% else %}
9+
<p>Your permission slip has some problem and has been denied. Check that everything on the permission slip is ok and sent it again. Remember it has to be signed by one of your parents or legal tutors</p>
10+
11+
{% include 'mails/components/button.html' with url=url text='Check permission slip' %}
12+
{% endif %}
13+
14+
<p>Best regards,</p>
15+
<p>{{ app_name }}.</p>
16+
{% endblock %}

0 commit comments

Comments
 (0)