diff --git a/app.json b/app.json index c4b2f5df..915f5bc7 100644 --- a/app.json +++ b/app.json @@ -2,9 +2,10 @@ "name": "HackAssistant Registration", "description": "Hackathon registration server", "env": { - "DOMAIN": { - "description": "Domain where app will be running", - "value": ".herokuapp.com" + "DOMAIN": { + "description": "Custom domain where app will be running (ignore if deploying using Heroku default domain)", + "value": "{SET THIS}.herokuapp.com", + "required": false }, "SECRET": { "description": "A secret key for verifying the integrity of signed cookies.", diff --git a/app/hackathon_variables.py b/app/hackathon_variables.py index 3d23e84c..75adf6a3 100644 --- a/app/hackathon_variables.py +++ b/app/hackathon_variables.py @@ -12,7 +12,12 @@ # This description will be used on the html and sharing meta tags HACKATHON_DESCRIPTION = 'Join us for BarcelonaTech\'s hackathon. 700 hackers. 36h. October 11th-13th.' # Domain where application is deployed, can be set by env variable -HACKATHON_DOMAIN = os.environ.get('DOMAIN', 'localhost:8000') +HACKATHON_DOMAIN = os.environ.get('DOMAIN', None) +HEROKU_APP_NAME = os.environ.get('HEROKU_APP_NAME', None) +if HEROKU_APP_NAME and not HACKATHON_DOMAIN: + HACKATHON_DOMAIN = '%s.herokuapp.com' % HEROKU_APP_NAME +elif not HACKATHON_DOMAIN: + HACKATHON_DOMAIN = 'localhost:8000' # Hackathon contact email: where should all hackers contact you. It will also be used as a sender for all emails HACKATHON_CONTACT_EMAIL = 'contact@hackupc.com' # Hackathon logo url, will be used on all emails @@ -106,3 +111,5 @@ # Enable dubious separate pipeline (disabled by default) DUBIOUS_ENABLED = True + +SUPPORTED_RESUME_EXTENSIONS = [] diff --git a/app/settings.py b/app/settings.py index 6819bf72..dfd07ead 100644 --- a/app/settings.py +++ b/app/settings.py @@ -269,8 +269,8 @@ # Maximum file upload size for forms MAX_UPLOAD_SIZE = 5242880 +MAX_VOTES = 10 -MAX_VOTES = 5 +MAX_VOTES_TO_APP = 50 MEALS_TOKEN = os.environ.get('MEALS_TOKEN', None) - diff --git a/applications/forms.py b/applications/forms.py index 74650250..73f12868 100644 --- a/applications/forms.py +++ b/applications/forms.py @@ -238,6 +238,8 @@ def fieldsets(self): class Meta: model = models.Application + extensions = getattr(settings, 'SUPPORTED_RESUME_EXTENSIONS', None) + help_texts = { 'gender': 'This is for demographic purposes. You can skip this question if you want.', 'graduation_year': 'What year have you graduated on or when will you graduate', @@ -249,7 +251,8 @@ class Meta: 'but at least we\'ll try!', 'projects': 'You can talk about about past hackathons, personal projects, awards etc. ' '(we love links) Show us your passion! :D', - 'reimb_amount': 'We try our best to cover costs for all hackers, but our budget is limited.' + 'reimb_amount': 'We try our best to cover costs for all hackers, but our budget is limited', + 'resume': 'Accepted file formats: %s' % (', '.join(extensions) if extensions else 'Any') } widgets = { diff --git a/applications/migrations/0019_auto_20200321_1814.py b/applications/migrations/0019_auto_20200321_1814.py new file mode 100644 index 00000000..da512102 --- /dev/null +++ b/applications/migrations/0019_auto_20200321_1814.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.28 on 2020-03-22 01:14 +from __future__ import unicode_literals + +import applications.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0018_auto_20190725_0437'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='resume', + field=models.FileField(blank=True, null=True, upload_to='resumes', validators=[applications.validators.validate_file_extension]), + ), + migrations.AlterField( + model_name='application', + name='status', + field=models.CharField(choices=[('P', 'Under review'), ('R', 'Wait listed'), ('I', 'Invited'), ('LR', 'Last reminder'), ('C', 'Confirmed'), ('X', 'Cancelled'), ('A', 'Attended'), ('E', 'Expired'), ('D', 'Dubious'), ('IV', 'Invalid')], default='P', max_length=2), + ), + ] diff --git a/applications/models.py b/applications/models.py index 864d6ff9..ba4a9cc7 100644 --- a/applications/models.py +++ b/applications/models.py @@ -11,6 +11,7 @@ from app import utils from user.models import User +from applications.validators import validate_file_extension APP_PENDING = 'P' APP_REJECTED = 'R' @@ -141,7 +142,7 @@ class Application(models.Model): lennyface = models.CharField(max_length=300, default='-.-') # Giv me a resume here! - resume = models.FileField(upload_to='resumes', null=True, blank=True) + resume = models.FileField(upload_to='resumes', null=True, blank=True, validators=[validate_file_extension]) cvs_edition = models.BooleanField(default=False) # University diff --git a/applications/validators.py b/applications/validators.py new file mode 100644 index 00000000..195d9f42 --- /dev/null +++ b/applications/validators.py @@ -0,0 +1,10 @@ +import os +from django.core.exceptions import ValidationError +from django.conf import settings + + +def validate_file_extension(value): + (_, ext) = os.path.splitext(value.name) + valid_extensions = getattr(settings, 'SUPPORTED_RESUME_EXTENSIONS', None) + if valid_extensions and not ext.lower() in valid_extensions: + raise ValidationError('Unsupported file extension.') diff --git a/organizers/templates/application_detail.html b/organizers/templates/application_detail.html index d9636eb1..ef8b9be0 100644 --- a/organizers/templates/application_detail.html +++ b/organizers/templates/application_detail.html @@ -120,8 +120,7 @@

{{ comment.text }}

{% csrf_token %} + rows="3" required maxlength="500"> diff --git a/organizers/views.py b/organizers/views.py index fa70e185..55c986bc 100644 --- a/organizers/views.py +++ b/organizers/views.py @@ -251,10 +251,12 @@ def get_application(self, kwargs): :return: pending aplication that has not been voted by the current user and that has less votes and its older """ + max_votes_to_app = getattr(settings, 'MAX_VOTES_TO_APP', 50) return models.Application.objects \ - .exclude(vote__user_id=self.request.user.id) \ + .exclude(vote__user_id=self.request.user.id, user_id=self.request.user.id) \ .filter(status=APP_PENDING) \ .annotate(count=Count('vote__calculated_vote')) \ + .filter(count__lte=max_votes_to_app) \ .order_by('count', 'submission_date') \ .first() diff --git a/requirements.txt b/requirements.txt index 6a3d3751..e53d2eaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ certifi==2019.9.11 chardet==3.0.4 defusedxml==0.6.0 dj-database-url==0.5.0 -Django==1.11.23 +Django==1.11.28 django-bootstrap3==8.2.3 django-filter==1.0.2 django-form-utils==1.0.3 diff --git a/stats/templates/application_stats.html b/stats/templates/application_stats.html index 81253c72..c242860c 100644 --- a/stats/templates/application_stats.html +++ b/stats/templates/application_stats.html @@ -21,6 +21,18 @@

Gender

+

Origin

+ +
+
+

All

+
+
+
+

Confirmed only

+
+
+

T-Shirts sizes

@@ -189,6 +201,42 @@

Confirmed only

} } }); + c3.generate({ + bindto: '#origin_stats', + data: { + json: data['origin'], + keys: { + x: 'origin', + value: ['applications'] + }, + type: 'bar' + + }, + + axis: { + x: { + type: 'category' + } + } + }); + c3.generate({ + bindto: '#origin_stats_confirmed', + data: { + json: data['origin_confirmed'], + keys: { + x: 'origin', + value: ['applications'] + }, + type: 'bar' + + }, + + axis: { + x: { + type: 'category' + } + } + }); $('#other_diet').html(data['other_diet']); $('#update_date').html(data['update_time']); $('#app_count').html(data['app_count']); diff --git a/stats/views.py b/stats/views.py index b82f9bbc..7b4a5344 100644 --- a/stats/views.py +++ b/stats/views.py @@ -58,6 +58,14 @@ def app_stats_api(request): gender_count = Application.objects.all().values('gender') \ .annotate(applications=Count('gender')) gender_count = map(lambda x: dict(gender_name=GENDER_DICT[x['gender']], **x), gender_count) + + origin_count = Application.objects.all().values('origin') \ + .annotate(applications=Count('origin')) \ + .order_by('applications')[:10] + origin_count_confirmed = Application.objects.filter(status=APP_CONFIRMED).values('origin') \ + .annotate(applications=Count('origin')) \ + .order_by('applications')[:10] + tshirt_dict = dict(a_models.TSHIRT_SIZES) shirt_count = map( lambda x: {'tshirt_size': tshirt_dict.get(x['tshirt_size'], 'Unknown'), 'applications': x['applications']}, @@ -87,6 +95,8 @@ def app_stats_api(request): 'shirt_count_confirmed': list(shirt_count_confirmed), 'timeseries': list(timeseries), 'gender': list(gender_count), + 'origin': list(origin_count), + 'origin_confirmed': list(origin_count_confirmed), 'diet': list(diet_count), 'diet_confirmed': list(diet_count_confirmed), 'other_diet': '
'.join([el['other_diet'] for el in other_diets if el['other_diet']])