From c5a271ba3b7a54e80d3c3a20b1887a85eae38134 Mon Sep 17 00:00:00 2001 From: Gerard Madrid Date: Sun, 11 May 2025 17:01:06 +0200 Subject: [PATCH 01/50] added dubious type and dubious by in DB and review --- .../migrations/0058_auto_20250511_1425.py | 27 +++++++++++++++++++ applications/models/constants.py | 14 ++++++++++ applications/models/hacker.py | 6 ++++- organizers/templates/application_detail.html | 15 +++++++++++ organizers/views.py | 4 +-- 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 applications/migrations/0058_auto_20250511_1425.py diff --git a/applications/migrations/0058_auto_20250511_1425.py b/applications/migrations/0058_auto_20250511_1425.py new file mode 100644 index 000000000..1e7b7907e --- /dev/null +++ b/applications/migrations/0058_auto_20250511_1425.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.23 on 2025-05-11 14:25 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('applications', '0057_auto_20250203_2145'), + ] + + operations = [ + migrations.AddField( + model_name='hackerapplication', + name='dubious_type', + field=models.CharField(choices=[('OK', 'Not dubious'), ('INVALID_CV', 'Invalid CV'), ('LATE_GRAD', 'Invalid graduation year'), ('NOT_STUDENT', 'Not a student'), ('INVALID_SCHOOL', 'Invalid school')], default='OK', max_length=300), + ), + migrations.AddField( + model_name='hackerapplication', + name='dubioused_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dubioused_by', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/applications/models/constants.py b/applications/models/constants.py index dded778cb..592e94cd9 100644 --- a/applications/models/constants.py +++ b/applications/models/constants.py @@ -99,3 +99,17 @@ DEFAULT_YEAR = datetime.now().year + 1 ENGLISH_LEVEL = [(i, str(i)) for i in range(1, 5 + 1)] + +DUBIOUS_NONE = 'OK' +DUBIOUS_CV = 'INVALID_CV' +DUBIOUS_GRADUATION_YEAR = 'LATE_GRAD' +DUBIOUS_NOT_STUDENT = 'NOT_STUDENT' +DUBIOUS_SCHOOL = 'INVALID_SCHOOL' + +DUBIOUS_TYPES = [ + (DUBIOUS_NONE, 'Not dubious'), + (DUBIOUS_CV, 'Invalid CV'), + (DUBIOUS_GRADUATION_YEAR, 'Invalid graduation year'), + (DUBIOUS_NOT_STUDENT, 'Not a student'), + (DUBIOUS_SCHOOL, 'Invalid school') +] diff --git a/applications/models/hacker.py b/applications/models/hacker.py index 6592ef63a..06b9b9949 100644 --- a/applications/models/hacker.py +++ b/applications/models/hacker.py @@ -36,6 +36,9 @@ class HackerApplication( projects = models.TextField(max_length=500, blank=True, null=True) # META + dubious_type = models.CharField(max_length=300, choices=DUBIOUS_TYPES, default=DUBIOUS_NONE) # Type of dubious application + dubioused_by = models.ForeignKey(User, related_name='dubioused_by', blank=True, null=True, + on_delete=models.SET_NULL) # User who marked this application as dubious contacted = models.BooleanField(default=False) # If a dubious application has been contacted yet contacted_by = models.ForeignKey(User, related_name='contacted_by', blank=True, null=True, on_delete=models.SET_NULL) @@ -75,10 +78,11 @@ def invalidate(self): self.status = APP_INVALID self.save() - def set_dubious(self): + def set_dubious(self, user): self.status = APP_DUBIOUS self.contacted = False self.status_update_date = timezone.now() + self.dubioused_by = user self.vote_set.all().delete() if hasattr(self, 'acceptedresume'): self.acceptedresume.delete() diff --git a/organizers/templates/application_detail.html b/organizers/templates/application_detail.html index baf2a4b32..284088eeb 100644 --- a/organizers/templates/application_detail.html +++ b/organizers/templates/application_detail.html @@ -70,6 +70,21 @@

Background

Dubious info

+ {% include 'include/field.html' with desc='Sent to dubious by' value=app.dubioused_by %} + + {% if app.dubious_type == 'OK' %} + {% include 'include/field.html' with desc='Dubious reason' value='No reason selected' %} + {% elif app.dubious_type == 'INVALID_CV' %} + {% include 'include/field.html' with desc='Dubious reason' value='Invalid CV' %} + {% elif app.dubious_type == 'LATE_GRAD' %} + {% include 'include/field.html' with desc='Dubious reason' value='Late Graduation Date' %} + {% elif app.dubious_type == 'NOT_STUDENT' %} + {% include 'include/field.html' with desc='Dubious reason' value='Not a Student' %} + {% elif app.dubious_type == 'INVALID_SCHOOL' %} + {% include 'include/field.html' with desc='Dubious reason' value='Invalid School' %} + {% endif %} + +
{% include 'include/field.html' with desc='Contacted' value=app.contacted|yesno:'Yes,No,Maybe' %} {% include 'include/field.html' with desc='Contacted by' value=app.contacted_by %} {% endif %} diff --git a/organizers/views.py b/organizers/views.py index 44f8aa06a..65373d05b 100644 --- a/organizers/views.py +++ b/organizers/views.py @@ -237,7 +237,7 @@ def post(self, request, *args, **kwargs): elif request.POST.get('slack') and request.user.is_organizer: self.slack_invite(application) elif request.POST.get('set_dubious') and request.user.is_organizer: - application.set_dubious() + application.set_dubious(request.user) elif request.POST.get('contact_user') and request.user.has_dubious_access: application.set_contacted(request.user) elif request.POST.get('unset_dubious') and request.user.has_dubious_access: @@ -344,7 +344,7 @@ def post(self, request, *args, **kwargs): elif request.POST.get('add_comment'): add_comment(application, request.user, comment_text) elif request.POST.get('set_dubious'): - application.set_dubious() + application.set_dubious(request.user) elif request.POST.get('unset_dubious'): application.unset_dubious() elif request.POST.get('set_blacklist') and request.user.is_organizer: From 531787b7dafde0a16813052f8c68f4dd72c2493d Mon Sep 17 00:00:00 2001 From: Gerard Madrid Date: Sun, 11 May 2025 17:40:15 +0200 Subject: [PATCH 02/50] comment and dubious type working and showing --- .../migrations/0059_auto_20250511_1520.py | 19 ++++ applications/models/hacker.py | 8 +- organizers/templates/application_detail.html | 93 +++++++++++++++++-- organizers/views.py | 9 +- 4 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 applications/migrations/0059_auto_20250511_1520.py diff --git a/applications/migrations/0059_auto_20250511_1520.py b/applications/migrations/0059_auto_20250511_1520.py new file mode 100644 index 000000000..743bcf526 --- /dev/null +++ b/applications/migrations/0059_auto_20250511_1520.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.23 on 2025-05-11 15:20 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0058_auto_20250511_1425'), + ] + + operations = [ + migrations.AddField( + model_name='hackerapplication', + name='dubious_comment', + field=models.TextField(blank=True, max_length=500, null=True), + ), + ] diff --git a/applications/models/hacker.py b/applications/models/hacker.py index 06b9b9949..4fa6812f5 100644 --- a/applications/models/hacker.py +++ b/applications/models/hacker.py @@ -39,6 +39,7 @@ class HackerApplication( dubious_type = models.CharField(max_length=300, choices=DUBIOUS_TYPES, default=DUBIOUS_NONE) # Type of dubious application dubioused_by = models.ForeignKey(User, related_name='dubioused_by', blank=True, null=True, on_delete=models.SET_NULL) # User who marked this application as dubious + dubious_comment = models.TextField(max_length=500, blank=True, null=True) # Comment for dubious application contacted = models.BooleanField(default=False) # If a dubious application has been contacted yet contacted_by = models.ForeignKey(User, related_name='contacted_by', blank=True, null=True, on_delete=models.SET_NULL) @@ -78,11 +79,13 @@ def invalidate(self): self.status = APP_INVALID self.save() - def set_dubious(self, user): + def set_dubious(self, user, dubious_type, dubious_comment_text): self.status = APP_DUBIOUS self.contacted = False self.status_update_date = timezone.now() self.dubioused_by = user + self.dubious_type = dubious_type + self.dubious_comment = dubious_comment_text self.vote_set.all().delete() if hasattr(self, 'acceptedresume'): self.acceptedresume.delete() @@ -91,6 +94,9 @@ def set_dubious(self, user): def unset_dubious(self): self.status = APP_PENDING self.status_update_date = timezone.now() + self.dubioused_by = None + self.dubious_type = DUBIOUS_NONE + self.dubious_comment = None self.save() def set_contacted(self, user): diff --git a/organizers/templates/application_detail.html b/organizers/templates/application_detail.html index 284088eeb..9f9dea87f 100644 --- a/organizers/templates/application_detail.html +++ b/organizers/templates/application_detail.html @@ -84,6 +84,10 @@

Dubious info

{% include 'include/field.html' with desc='Dubious reason' value='Invalid School' %} {% endif %} + {% if app.dubious_comment %} + {% include 'include/field.html' with desc='Dubious Comment' value=app.dubious_comment %} + {% endif %} +
{% include 'include/field.html' with desc='Contacted' value=app.contacted|yesno:'Yes,No,Maybe' %} {% include 'include/field.html' with desc='Contacted by' value=app.contacted_by %} @@ -166,6 +170,21 @@

{{ comment.text }}

} }); + @@ -210,10 +229,41 @@

Score

application {% if h_dubious_enabled %} - + + + + + {% endif %} {% if h_blacklist_enabled %} {% if h_dubious_enabled %} - + + + + + + + + + + {% endif %} {% if h_blacklist_enabled %} {% if h_dubious_enabled %} @@ -383,30 +391,36 @@
-
+
- +
Invalid CV
-
+
- +
Wrong Graduation
-
+
- +
Not a Student
-
+
- +
Invalid School
+
+
+ +
Other
+
+
From c39acc3b4bff0b719b63f9442e75083593f3d43d Mon Sep 17 00:00:00 2001 From: AdriMM26 Date: Mon, 4 Aug 2025 21:10:37 +0200 Subject: [PATCH 08/50] Fix teamID error --- teams/models.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/teams/models.py b/teams/models.py index 96452352a..f9223d561 100644 --- a/teams/models.py +++ b/teams/models.py @@ -1,19 +1,18 @@ import random - -from django.db import models - +import string +from django.db import models, IntegrityError, transaction from user.models import User TEAM_ID_LENGTH = 13 - +MAX_ATTEMPTS = 33 def generate_team_id(): - s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" - while True: - team_id = "".join(random.sample(s, TEAM_ID_LENGTH)) + s = string.ascii_letters + string.digits # 62 caràcters únics + for _ in range(MAX_ATTEMPTS): + team_id = "".join(random.choices(s, k=TEAM_ID_LENGTH)) if not Team.objects.filter(team_code=team_id).exists(): return team_id - + raise Exception("No s'ha pogut generar un team_code únic després de diversos intents.") class Team(models.Model): team_code = models.CharField(default=generate_team_id, max_length=TEAM_ID_LENGTH) From a0c59824632cf76801900d6c576eb0a47d904978 Mon Sep 17 00:00:00 2001 From: AdriMM26 Date: Mon, 4 Aug 2025 21:14:30 +0200 Subject: [PATCH 09/50] Flake8 check --- teams/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/teams/models.py b/teams/models.py index f9223d561..fd402972c 100644 --- a/teams/models.py +++ b/teams/models.py @@ -1,11 +1,12 @@ import random import string -from django.db import models, IntegrityError, transaction +from django.db import models from user.models import User TEAM_ID_LENGTH = 13 MAX_ATTEMPTS = 33 + def generate_team_id(): s = string.ascii_letters + string.digits # 62 caràcters únics for _ in range(MAX_ATTEMPTS): @@ -14,6 +15,7 @@ def generate_team_id(): return team_id raise Exception("No s'ha pogut generar un team_code únic després de diversos intents.") + class Team(models.Model): team_code = models.CharField(default=generate_team_id, max_length=TEAM_ID_LENGTH) user = models.OneToOneField(User, on_delete=models.CASCADE) From c8d298ab6164bf7511b624e7691ddd40924aa666 Mon Sep 17 00:00:00 2001 From: "enrique.andujar" Date: Wed, 3 Sep 2025 13:22:55 +0200 Subject: [PATCH 10/50] Added spacing between buttons and dubious button for HX when seeing a detail application --- app/static/css/custom-bootstrap.css | 2 +- applications/templates/application.html | 14 ++-- organizers/templates/application_detail.html | 87 ++++++++++++++++++-- 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/app/static/css/custom-bootstrap.css b/app/static/css/custom-bootstrap.css index 3b84de3c5..c68890f41 100644 --- a/app/static/css/custom-bootstrap.css +++ b/app/static/css/custom-bootstrap.css @@ -817,7 +817,7 @@ a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none} .btn-group-lg>.btn,.btn-lg{line-height:1.3333333;border-radius:8px} .btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:8px} .btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:8px} -.btn-block{display:block;width:100%} +.btn-block{display:block;width:100%;margin-top:5px} .btn-block+.btn-block{margin-top:5px} input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%} .fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear} diff --git a/applications/templates/application.html b/applications/templates/application.html index 31ca5a8f0..e08d49786 100644 --- a/applications/templates/application.html +++ b/applications/templates/application.html @@ -24,16 +24,16 @@ {% endif %}
- {% if application.can_be_edit %} - - -

- Be careful, you can only edit the application until 2 hours after {{application.submission_date}}

+ {% if application.can_be_edit %} + + +

+ Be careful, you can only edit the application until 2 hours after {{application.submission_date}}

{% else %} -

Your application has been reviewed already. Editing has been disabled to make sure all reviewers get the +

Your application has been reviewed already. Editing has been disabled to make sure all reviewers get the same data. If you would like to change something important, please email us at {{ h_contact_email|urlize }}.

- {% endif %} + {% endif %}
{% include 'include/application_form.html' %} diff --git a/organizers/templates/application_detail.html b/organizers/templates/application_detail.html index 352661262..c73e37632 100644 --- a/organizers/templates/application_detail.html +++ b/organizers/templates/application_detail.html @@ -38,13 +38,13 @@

{% if vote %}Review applications{% else %}{{ app.user.name }}'s application{% endif %} {% if vote %} - + ({{ apps_left_to_vote }} left) {% endif %}

+ href="{% url 'app_detail' app.uuid_str %}">
@@ -62,7 +62,7 @@

Personal

{% include 'include/field.html' with desc='Email' value=app.user.email %} {% include 'include/field.html' with desc='Travel reimbursement?' value=app.reimb|yesno:'Yes,No,Maybe' %} {% include 'include/field.html' with desc='Money needed' value=app.reimb_amount %} - {% include 'include/field.html' with desc='Origin' value=app.origin %} + {% include 'include/field.html' with desc='Origin' value=app.origin %} {% endif %} {% include 'include/field.html' with desc='Under age (-18)' value=app.under_age|yesno:'Yes,No,Maybe' %} {% if app.resume %} @@ -176,7 +176,7 @@

{{ comment.text }}

- +{% endblock %} \ No newline at end of file diff --git a/organizers/urls.py b/organizers/urls.py index 06f6ca3a7..cb2ac8734 100644 --- a/organizers/urls.py +++ b/organizers/urls.py @@ -3,21 +3,76 @@ from organizers import views urlpatterns = [ - url(r'^hacker/review/$', views.ReviewApplicationView.as_view(), name='review'), - url(r'^hacker/review/(?P[\w-]+)$', views.ReviewApplicationDetailView.as_view(), name='review_detail'), - url(r'^hacker/review_resume/$', views.ReviewResume.as_view(), name='review_resume'), - url(r'^hacker/(?P[\w-]+)$', views.ApplicationDetailView.as_view(), name="app_detail"), - url(r'^hacker/resume/(?P[\w-]+)$', views.VisualizeResume.as_view(), name="app_resume"), - url(r'^hacker/all/$', views.ApplicationsListView.as_view(), name="app_list"), - url(r'^hacker/all/invite/$', views.InviteListView.as_view(), name="invite_list"), - url(r'^hacker/all/invite/teams/$', views.InviteTeamListView.as_view(), name="invite_teams_list"), - url(r'^hacker/dubious/$', views.DubiousApplicationsListView.as_view(), name="dubious"), - url(r'^volunteer/all/$', views.VolunteerApplicationsListView.as_view(), name="volunteer_list"), - url(r'^volunteer/(?P[\w-]+)$', views.ReviewVolunteerApplicationView.as_view(), name="volunteer_detail"), - url(r'^sponsor/all/$', views.SponsorApplicationsListView.as_view(), name="sponsor_list"), - url(r'^sponsor/(?P[\w-]+)$', views.ReviewSponsorApplicationView.as_view(), name="sponsor_detail"), - url(r'^mentor/all/$', views.MentorApplicationsListView.as_view(), name="mentor_list"), - url(r'^mentor/(?P[\w-]+)$', views.ReviewMentorApplicationView.as_view(), name="mentor_detail"), - url(r'^user/sponsor/all/$', views.SponsorUserListView.as_view(), name="sponsor_user_list"), - url(r'^hacker/blacklist/$', views.BlacklistApplicationsListView.as_view(), name="blacklist"), + url(r"^hacker/review/$", views.ReviewApplicationView.as_view(), name="review"), + url( + r"^hacker/review/(?P[\w-]+)$", + views.ReviewApplicationDetailView.as_view(), + name="review_detail", + ), + url(r"^hacker/review_resume/$", views.ReviewResume.as_view(), name="review_resume"), + url( + r"^hacker/(?P[\w-]+)$", + views.ApplicationDetailView.as_view(), + name="app_detail", + ), + url( + r"^hacker/resume/(?P[\w-]+)$", + views.VisualizeResume.as_view(), + name="app_resume", + ), + url(r"^hacker/all/$", views.ApplicationsListView.as_view(), name="app_list"), + url(r"^hacker/all/invite/$", views.InviteListView.as_view(), name="invite_list"), + url( + r"^hacker/all/invite/teams/$", + views.InviteTeamListView.as_view(), + name="invite_teams_list", + ), + url( + r"^hacker/all/waitlisted/$", + views.WaitlistedApplicationsListView.as_view(), + name="waitlisted", + ), + url( + r"^hacker/dubious/$", + views.DubiousApplicationsListView.as_view(), + name="dubious", + ), + url( + r"^volunteer/all/$", + views.VolunteerApplicationsListView.as_view(), + name="volunteer_list", + ), + url( + r"^volunteer/(?P[\w-]+)$", + views.ReviewVolunteerApplicationView.as_view(), + name="volunteer_detail", + ), + url( + r"^sponsor/all/$", + views.SponsorApplicationsListView.as_view(), + name="sponsor_list", + ), + url( + r"^sponsor/(?P[\w-]+)$", + views.ReviewSponsorApplicationView.as_view(), + name="sponsor_detail", + ), + url( + r"^mentor/all/$", views.MentorApplicationsListView.as_view(), name="mentor_list" + ), + url( + r"^mentor/(?P[\w-]+)$", + views.ReviewMentorApplicationView.as_view(), + name="mentor_detail", + ), + url( + r"^user/sponsor/all/$", + views.SponsorUserListView.as_view(), + name="sponsor_user_list", + ), + url( + r"^hacker/blacklist/$", + views.BlacklistApplicationsListView.as_view(), + name="blacklist", + ), ] diff --git a/organizers/views.py b/organizers/views.py index fb7c87c78..7458e5e81 100644 --- a/organizers/views.py +++ b/organizers/views.py @@ -12,6 +12,7 @@ from django.http import Http404, HttpResponseRedirect, HttpResponse from django.shortcuts import redirect from django.urls import reverse +from django.views import View from django.views.generic import TemplateView from django_filters.views import FilterView from django_tables2 import SingleTableMixin @@ -24,19 +25,50 @@ from app.slack import SlackInvitationException from applications import emails from applications.emails import send_batch_emails -from applications.models import APP_PENDING, APP_DUBIOUS, APP_BLACKLISTED, APP_INVITED, APP_LAST_REMIDER, \ - APP_CONFIRMED, AcceptedResume, APP_ATTENDED, APP_REJECTED +from applications.models import ( + APP_PENDING, + APP_DUBIOUS, + APP_BLACKLISTED, + APP_INVITED, + APP_LAST_REMIDER, + APP_CONFIRMED, + AcceptedResume, + APP_ATTENDED, + APP_REJECTED, +) from organizers import models -from organizers.tables import ApplicationsListTable, ApplicationFilter, AdminApplicationsListTable, \ - AdminTeamListTable, InviteFilter, DubiousListTable, DubiousApplicationFilter, VolunteerFilter, \ - VolunteerListTable, MentorListTable, MentorFilter, SponsorListTable, SponsorFilter, SponsorUserListTable, \ - SponsorUserFilter, BlacklistListTable, BlacklistApplicationFilter +from organizers.tables import ( + ApplicationsListTable, + ApplicationFilter, + AdminApplicationsListTable, + AdminTeamListTable, + InviteFilter, + DubiousListTable, + DubiousApplicationFilter, + VolunteerFilter, + VolunteerListTable, + MentorListTable, + MentorFilter, + SponsorListTable, + SponsorFilter, + SponsorUserListTable, + SponsorUserFilter, + BlacklistListTable, + BlacklistApplicationFilter, +) from teams.models import Team -from user.mixins import IsOrganizerMixin, IsDirectorMixin, HaveDubiousPermissionMixin, HaveVolunteerPermissionMixin, \ - HaveSponsorPermissionMixin, HaveMentorPermissionMixin, IsBlacklistAdminMixin +from user.mixins import ( + IsOrganizerMixin, + IsDirectorMixin, + HaveDubiousPermissionMixin, + HaveVolunteerPermissionMixin, + HaveSponsorPermissionMixin, + HaveMentorPermissionMixin, + IsBlacklistAdminMixin, +) from user.models import User, USR_SPONSOR -if getattr(settings, 'REIMBURSEMENT_ENABLED', False): +if getattr(settings, "REIMBURSEMENT_ENABLED", False): from reimbursement.models import Reimbursement, RE_PEND_APPROVAL @@ -60,82 +92,157 @@ def add_comment(application, user, text): def hacker_tabs(user): - new_app = models.HackerApplication.objects.exclude(vote__user_id=user.id)\ - .filter(status=APP_PENDING, submission_date__lte=timezone.now() - timedelta(hours=2)) - t = [('Application', reverse('app_list'), False), ('Review', reverse('review'), 'new' if new_app else '')] - if user.has_dubious_access and getattr(settings, 'DUBIOUS_ENABLED', False): - t.append(('Dubious', reverse('dubious'), - 'new' if models.HackerApplication.objects.filter(status=APP_DUBIOUS, - contacted=False).count() else '')) - if user.has_blacklist_access and getattr(settings, 'BLACKLIST_ENABLED', False): - t.append(('Blacklist', reverse('blacklist'), - 'new' if models.HackerApplication.objects.filter(status=APP_BLACKLISTED, contacted=False).count() - else '')) - t.append(('Check-in', reverse('check_in_list'), False)) + new_app = models.HackerApplication.objects.exclude(vote__user_id=user.id).filter( + status=APP_PENDING, submission_date__lte=timezone.now() - timedelta(hours=2) + ) + t = [ + ("Application", reverse("app_list"), False), + ("Review", reverse("review"), "new" if new_app else ""), + ] + if user.has_dubious_access and getattr(settings, "DUBIOUS_ENABLED", False): + t.append( + ( + "Dubious", + reverse("dubious"), + ( + "new" + if models.HackerApplication.objects.filter( + status=APP_DUBIOUS, contacted=False + ).count() + else "" + ), + ) + ) + if user.has_blacklist_access and getattr(settings, "BLACKLIST_ENABLED", False): + t.append( + ( + "Blacklist", + reverse("blacklist"), + ( + "new" + if models.HackerApplication.objects.filter( + status=APP_BLACKLISTED, contacted=False + ).count() + else "" + ), + ) + ) + t.append(("Check-in", reverse("check_in_list"), False)) if user.has_reimbursement_access: - t.extend([('Reimbursements', reverse('reimbursement_list'), False), - ('Receipts', reverse('receipt_review'), 'new' if Reimbursement.objects.filter( - status=RE_PEND_APPROVAL).count() else False), ]) + t.extend( + [ + ("Reimbursements", reverse("reimbursement_list"), False), + ( + "Receipts", + reverse("receipt_review"), + ( + "new" + if Reimbursement.objects.filter(status=RE_PEND_APPROVAL).count() + else False + ), + ), + ] + ) if user.has_sponsor_access: - new_resume = models.HackerApplication.objects.filter(acceptedresume__isnull=True, cvs_edition=True)\ - .exclude(status__in=[APP_DUBIOUS, APP_BLACKLISTED]).first() - t.append(('Review resume', reverse('review_resume'), 'new' if new_resume else '')) + new_resume = ( + models.HackerApplication.objects.filter( + acceptedresume__isnull=True, cvs_edition=True + ) + .exclude(status__in=[APP_DUBIOUS, APP_BLACKLISTED]) + .first() + ) + t.append( + ("Review resume", reverse("review_resume"), "new" if new_resume else "") + ) return t def sponsor_tabs(user): - return [('Users', reverse('sponsor_user_list'), False), ('Application', reverse('sponsor_list'), False), - ('Check-in', reverse('check_in_sponsor_list'), False)] + return [ + ("Users", reverse("sponsor_user_list"), False), + ("Application", reverse("sponsor_list"), False), + ("Check-in", reverse("check_in_sponsor_list"), False), + ] def volunteer_tabs(user): - return [('Application', reverse('volunteer_list'), False), ('Check-in', reverse('check_in_volunteer_list'), False)] + return [ + ("Application", reverse("volunteer_list"), False), + ("Check-in", reverse("check_in_volunteer_list"), False), + ] def mentor_tabs(user): - return [('Application', reverse('mentor_list'), False), ('Check-in', reverse('check_in_mentor_list'), False)] + return [ + ("Application", reverse("mentor_list"), False), + ("Check-in", reverse("check_in_mentor_list"), False), + ] -class ApplicationsListView(TabsViewMixin, IsOrganizerMixin, ExportMixin, SingleTableMixin, FilterView): - template_name = 'applications_list.html' +class ApplicationsListView( + TabsViewMixin, IsOrganizerMixin, ExportMixin, SingleTableMixin, FilterView +): + template_name = "applications_list.html" table_class = ApplicationsListTable filterset_class = ApplicationFilter - table_pagination = {'per_page': 100} - exclude_columns = ('detail', 'status', 'vote_avg') - export_name = 'applications' + table_pagination = {"per_page": 100} + exclude_columns = ("detail", "status", "vote_avg") + export_name = "applications" def get(self, request, *args, **kwargs): - request.session['edit_app_back'] = 'app_list' + request.session["edit_app_back"] = "app_list" return super().get(request, *args, **kwargs) def get_current_tabs(self): return hacker_tabs(self.request.user) def get_queryset(self): - return models.HackerApplication.annotate_vote(models.HackerApplication.objects.all()) + return models.HackerApplication.annotate_vote( + models.HackerApplication.objects.all() + ) def get_context_data(self, **kwargs): context = super(ApplicationsListView, self).get_context_data(**kwargs) - context['otherApplication'] = False + context["otherApplication"] = False list_email = "" - for u in context.get('object_list').values_list('user__email', flat=True): + for u in context.get("object_list").values_list("user__email", flat=True): list_email += "%s, " % u - context['emails'] = list_email + context["emails"] = list_email return context class InviteListView(TabsViewMixin, IsDirectorMixin, SingleTableMixin, FilterView): - template_name = 'invite_list.html' + template_name = "invite_list.html" table_class = AdminApplicationsListTable filterset_class = InviteFilter - table_pagination = {'per_page': 100} + table_pagination = {"per_page": 100} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - n_live_hackers = models.HackerApplication.objects.filter(status__in=[APP_INVITED, APP_LAST_REMIDER, - APP_CONFIRMED], online=False).count() - context.update({'n_live_hackers': n_live_hackers, - 'n_live_per_hackers': n_live_hackers * 100 / getattr(settings, 'N_MAX_LIVE_HACKERS', 0)}) + n_invited_hackers_today = models.HackerApplication.objects.filter( + status=APP_INVITED, + online=False, + status_update_date__date=timezone.now().date(), + ).count() + + n_waitlisted_hackers = models.HackerApplication.objects.filter( + status=APP_REJECTED, online=False + ).count() + n_live_hackers = models.HackerApplication.objects.filter( + status__in=[APP_INVITED, APP_LAST_REMIDER, APP_CONFIRMED], online=False + ).count() + + context.update( + { + "n_live_hackers": n_live_hackers, + "n_live_per_hackers": n_live_hackers + * 100 + / getattr(settings, "N_MAX_LIVE_HACKERS", 0), + "n_waitlisted_hackers": n_waitlisted_hackers, + "n_invited_hackers_today": n_invited_hackers_today, + } + ) + return context def get_current_tabs(self): @@ -143,16 +250,22 @@ def get_current_tabs(self): def get_queryset(self): return models.HackerApplication.annotate_vote( - models.HackerApplication.objects.filter(status__in=[APP_PENDING, APP_REJECTED])).order_by('-vote_avg') + models.HackerApplication.objects.filter( + status__in=[APP_PENDING, APP_REJECTED] + ) + ).order_by("-vote_avg") def post(self, request, *args, **kwargs): - ids = request.POST.getlist('selected') + ids = request.POST.getlist("selected") apps = models.HackerApplication.objects.filter(pk__in=ids).all() mails = [] errors = 0 for app in apps: try: - app.invite(request.user, online=request.POST.get('force_online', 'false') == 'true') + app.invite( + request.user, + online=request.POST.get("force_online", "false") == "true", + ) m = emails.create_invite_email(app, request) if m: mails.append(m) @@ -167,41 +280,56 @@ def post(self, request, *args, **kwargs): errorMsg = "%s applications not invited" % errors messages.error(request, errorMsg) - return HttpResponseRedirect(reverse('invite_list')) + return HttpResponseRedirect(reverse("invite_list")) class ApplicationDetailView(TabsViewMixin, IsOrganizerMixin, TemplateView): - template_name = 'application_detail.html' + template_name = "application_detail.html" def get_back_url(self): - back = self.request.session.get('edit_app_back', 'app_list') + back = self.request.session.get("edit_app_back", "app_list") return reverse(back) def get_context_data(self, **kwargs): context = super(ApplicationDetailView, self).get_context_data(**kwargs) application = self.get_application(kwargs) - context['app'] = application - context['vote'] = self.can_vote() - context['max_vote'] = dict(models.VOTES) - if (self.can_vote()): - context['apps_left_to_vote'] = \ - models.HackerApplication.objects.exclude(vote__user_id=self.request.user.id)\ - .filter(status=APP_PENDING, submission_date__lte=timezone.now() - timedelta(hours=2))\ + context["app"] = application + context["vote"] = self.can_vote() + context["max_vote"] = dict(models.VOTES) + if self.can_vote(): + context["apps_left_to_vote"] = ( + models.HackerApplication.objects.exclude( + vote__user_id=self.request.user.id + ) + .filter( + status=APP_PENDING, + submission_date__lte=timezone.now() - timedelta(hours=2), + ) .count() - - context['comments'] = models.ApplicationComment.objects.filter(hacker=application) - if application and getattr(application.user, 'team', False) and settings.TEAMS_ENABLED: - context['teammates'] = Team.objects.filter(team_code=application.user.team.team_code) \ - .values('user__name', 'user__email', 'user') - - for mate in context['teammates']: - if application.user.id == mate['user']: - mate['is_me'] = True + ) + + context["comments"] = models.ApplicationComment.objects.filter( + hacker=application + ).order_by("created_at") + if ( + application + and getattr(application.user, "team", False) + and settings.TEAMS_ENABLED + ): + context["teammates"] = Team.objects.filter( + team_code=application.user.team.team_code + ).values("user__name", "user__email", "user") + + for mate in context["teammates"]: + if application.user.id == mate["user"]: + mate["is_me"] = True continue - mate_app = models.HackerApplication.objects.filter(user=mate['user']).first() + mate_app = models.HackerApplication.objects.filter( + user=mate["user"] + ).first() if mate_app: - mate['app_uuid_str'] = mate_app.uuid_str + mate["app_uuid_str"] = mate_app.uuid_str return context @@ -209,84 +337,111 @@ def can_vote(self): return False def get_application(self, kwargs): - application_id = kwargs.get('id', None) + application_id = kwargs.get("id", None) if not application_id: raise Http404 - application = models.HackerApplication.objects.filter(uuid=application_id).first() + application = models.HackerApplication.objects.filter( + uuid=application_id + ).first() if not application: raise Http404 return application def post(self, request, *args, **kwargs): - id_ = request.POST.get('app_id') + id_ = request.POST.get("app_id") application = models.HackerApplication.objects.get(pk=id_) - comment_text = request.POST.get('comment_text', None) - motive_of_ban = request.POST.get('motive_of_ban', None) - if request.POST.get('add_comment'): + comment_text = request.POST.get("comment_text", None) + motive_of_ban = request.POST.get("motive_of_ban", None) + if request.POST.get("add_comment"): add_comment(application, request.user, comment_text) - elif request.POST.get('invite') and request.user.is_director: + elif request.POST.get("invite") and request.user.is_director: self.invite_application(application) - elif request.POST.get('confirm') and request.user.is_director: + elif request.POST.get("confirm") and request.user.is_director: self.confirm_application(application) - elif request.POST.get('cancel') and request.user.is_director: + elif request.POST.get("cancel") and request.user.is_director: self.cancel_application(application) - elif request.POST.get('waitlist') and request.user.is_director: + elif request.POST.get("waitlist") and request.user.is_director: self.waitlist_application(application) - elif request.POST.get('slack') and request.user.is_organizer: + elif request.POST.get("slack") and request.user.is_organizer: self.slack_invite(application) - elif request.POST.get('set_dubious') and request.user.is_organizer: + elif request.POST.get("set_dubious") and request.user.is_organizer: application.set_dubious() - elif request.POST.get('contact_user') and request.user.has_dubious_access: + elif request.POST.get("contact_user") and request.user.has_dubious_access: application.set_contacted(request.user) - elif request.POST.get('unset_dubious') and request.user.has_dubious_access: - add_comment(application, request.user, - "Dubious review result: No problems, hacker allowed to participate in hackathon!") + elif request.POST.get("unset_dubious") and request.user.has_dubious_access: + add_comment( + application, + request.user, + "Dubious review result: No problems, hacker allowed to participate in hackathon!", + ) application.unset_dubious() - elif request.POST.get('invalidate') and request.user.has_dubious_access: - add_comment(application, request.user, - "Dubious review result: Hacker is not allowed to participate in hackathon.") + elif request.POST.get("invalidate") and request.user.has_dubious_access: + add_comment( + application, + request.user, + "Dubious review result: Hacker is not allowed to participate in hackathon.", + ) application.invalidate() - elif request.POST.get('set_blacklist') and request.user.is_organizer: + elif request.POST.get("set_blacklist") and request.user.is_organizer: application.set_blacklist() - elif request.POST.get('unset_blacklist') and request.user.has_blacklist_access: - add_comment(application, request.user, - "Blacklist review result: No problems, hacker allowed to participate in hackathon!") + elif request.POST.get("unset_blacklist") and request.user.has_blacklist_access: + add_comment( + application, + request.user, + "Blacklist review result: No problems, hacker allowed to participate in hackathon!", + ) application.unset_blacklist() - elif request.POST.get('confirm_blacklist') and request.user.has_blacklist_access: - add_comment(application, request.user, - "Blacklist review result: Hacker is not allowed to participate in hackathon. " + - "Motive of ban: " + motive_of_ban) + elif ( + request.POST.get("confirm_blacklist") and request.user.has_blacklist_access + ): + add_comment( + application, + request.user, + "Blacklist review result: Hacker is not allowed to participate in hackathon. " + + "Motive of ban: " + + motive_of_ban, + ) application.confirm_blacklist(request.user, motive_of_ban) - return HttpResponseRedirect(reverse('app_detail', kwargs={'id': application.uuid_str})) + return HttpResponseRedirect( + reverse("app_detail", kwargs={"id": application.uuid_str}) + ) def waitlist_application(self, application): try: application.reject() - messages.success(self.request, "%s application wait listed" % application.user.email) + messages.success( + self.request, "%s application wait listed" % application.user.email + ) except ValidationError as e: messages.error(self.request, e.message) def slack_invite(self, application): try: slack.send_slack_invite(application.user.email) - messages.success(self.request, "Slack invite sent to %s" % application.user.email) + messages.success( + self.request, "Slack invite sent to %s" % application.user.email + ) except SlackInvitationException as e: messages.error(self.request, "Slack error: %s" % str(e)) def cancel_application(self, application): try: application.cancel() - messages.success(self.request, "%s application cancelled" % application.user.email) + messages.success( + self.request, "%s application cancelled" % application.user.email + ) except ValidationError as e: messages.error(self.request, e.message) def confirm_application(self, application): try: application.confirm() - messages.success(self.request, "Ticket to %s successfully sent" % application.user.email) + messages.success( + self.request, "Ticket to %s successfully sent" % application.user.email + ) m = emails.create_confirmation_email(application, self.request) if m: m.send() @@ -296,7 +451,9 @@ def confirm_application(self, application): def invite_application(self, application): try: application.invite(self.request.user) - messages.success(self.request, "Invite to %s successfully sent" % application.user.email) + messages.success( + self.request, "Invite to %s successfully sent" % application.user.email + ) m = emails.create_invite_email(application, self.request) if m: m.send() @@ -318,41 +475,54 @@ 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.HackerApplication.objects \ - .exclude(Q(vote__user_id=self.request.user.id) | Q(user_id=self.request.user.id)) \ - .filter(status=APP_PENDING) \ - .filter(submission_date__lte=timezone.now() - timedelta(hours=2)) \ - .annotate(count=Count('vote__calculated_vote')) \ - .filter(count__lte=max_votes_to_app) \ - .order_by('count', 'submission_date') \ + max_votes_to_app = getattr(settings, "MAX_VOTES_TO_APP", 50) + return ( + models.HackerApplication.objects.exclude( + Q(vote__user_id=self.request.user.id) | Q(user_id=self.request.user.id) + ) + .filter(status=APP_PENDING) + .filter(submission_date__lte=timezone.now() - timedelta(hours=2)) + .annotate(count=Count("vote__calculated_vote")) + .filter(count__lte=max_votes_to_app) + .order_by("count", "submission_date") .first() + ) def get(self, request, *args, **kwargs): r = super(ReviewApplicationView, self).get(request, *args, **kwargs) return r def post(self, request, *args, **kwargs): - tech_vote = request.POST.get('tech_rat', None) - pers_vote = request.POST.get('pers_rat', None) - comment_text = request.POST.get('comment_text', None) + tech_vote = request.POST.get("tech_rat", None) + pers_vote = request.POST.get("pers_rat", None) + comment_text = request.POST.get("comment_text", None) - application = models.HackerApplication.objects.get(pk=request.POST.get('app_id')) + application = models.HackerApplication.objects.get( + pk=request.POST.get("app_id") + ) try: - if request.POST.get('skip'): + if request.POST.get("skip"): add_vote(application, request.user, None, None) - elif request.POST.get('add_comment'): + elif request.POST.get("add_comment"): add_comment(application, request.user, comment_text) - return HttpResponseRedirect('/applications/hacker/review/'+ application.uuid_str) - elif request.POST.get('set_dubious'): + return HttpResponseRedirect( + "/applications/hacker/review/" + application.uuid_str + ) + elif request.POST.get("set_dubious"): application.set_dubious() - elif request.POST.get('unset_dubious'): + elif request.POST.get("unset_dubious"): application.unset_dubious() - elif request.POST.get('set_blacklist') and request.user.is_organizer: + elif request.POST.get("set_blacklist") and request.user.is_organizer: application.set_blacklist() - elif request.POST.get('unset_blacklist') and request.user.has_blacklist_access: - add_comment(application, request.user, - "Blacklist review result: No problems, hacker allowed to participate in hackathon!") + elif ( + request.POST.get("unset_blacklist") + and request.user.has_blacklist_access + ): + add_comment( + application, + request.user, + "Blacklist review result: No problems, hacker allowed to participate in hackathon!", + ) application.unset_blacklist() else: add_vote(application, request.user, tech_vote, pers_vote) @@ -360,7 +530,7 @@ def post(self, request, *args, **kwargs): # application except IntegrityError: pass - return HttpResponseRedirect(reverse('review')) + return HttpResponseRedirect(reverse("review")) def can_vote(self): return True @@ -380,55 +550,78 @@ 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 """ - if 'id' in kwargs: - if models.HackerApplication.objects.filter(uuid=kwargs['id']).first().status != APP_PENDING: - max_votes_to_app = getattr(settings, 'MAX_VOTES_TO_APP', 50) - return models.HackerApplication.objects \ - .exclude(Q(vote__user_id=self.request.user.id) | Q(user_id=self.request.user.id)) \ - .filter(status=APP_PENDING) \ - .filter(submission_date__lte=timezone.now() - timedelta(hours=2)) \ - .annotate(count=Count('vote__calculated_vote')) \ - .filter(count__lte=max_votes_to_app) \ - .order_by('count', 'submission_date') \ + if "id" in kwargs: + if ( + models.HackerApplication.objects.filter(uuid=kwargs["id"]) .first() + .status + != APP_PENDING + ): + max_votes_to_app = getattr(settings, "MAX_VOTES_TO_APP", 50) + return ( + models.HackerApplication.objects.exclude( + Q(vote__user_id=self.request.user.id) + | Q(user_id=self.request.user.id) + ) + .filter(status=APP_PENDING) + .filter(submission_date__lte=timezone.now() - timedelta(hours=2)) + .annotate(count=Count("vote__calculated_vote")) + .filter(count__lte=max_votes_to_app) + .order_by("count", "submission_date") + .first() + ) else: - return models.HackerApplication.objects.get(uuid=kwargs['id']) + return models.HackerApplication.objects.get(uuid=kwargs["id"]) else: - max_votes_to_app = getattr(settings, 'MAX_VOTES_TO_APP', 50) - return models.HackerApplication.objects \ - .exclude(Q(vote__user_id=self.request.user.id) | Q(user_id=self.request.user.id)) \ - .filter(status=APP_PENDING) \ - .filter(submission_date__lte=timezone.now() - timedelta(hours=2)) \ - .annotate(count=Count('vote__calculated_vote')) \ - .filter(count__lte=max_votes_to_app) \ - .order_by('count', 'submission_date') \ - .first() + max_votes_to_app = getattr(settings, "MAX_VOTES_TO_APP", 50) + return ( + models.HackerApplication.objects.exclude( + Q(vote__user_id=self.request.user.id) + | Q(user_id=self.request.user.id) + ) + .filter(status=APP_PENDING) + .filter(submission_date__lte=timezone.now() - timedelta(hours=2)) + .annotate(count=Count("vote__calculated_vote")) + .filter(count__lte=max_votes_to_app) + .order_by("count", "submission_date") + .first() + ) def get(self, request, *args, **kwargs): r = super(ReviewApplicationDetailView, self).get(request, *args, **kwargs) return r def post(self, request, *args, **kwargs): - tech_vote = request.POST.get('tech_rat', None) - pers_vote = request.POST.get('pers_rat', None) - comment_text = request.POST.get('comment_text', None) + tech_vote = request.POST.get("tech_rat", None) + pers_vote = request.POST.get("pers_rat", None) + comment_text = request.POST.get("comment_text", None) - application = models.HackerApplication.objects.get(pk=request.POST.get('app_id')) + application = models.HackerApplication.objects.get( + pk=request.POST.get("app_id") + ) try: - if request.POST.get('skip'): + if request.POST.get("skip"): add_vote(application, request.user, None, None) - elif request.POST.get('add_comment'): + elif request.POST.get("add_comment"): add_comment(application, request.user, comment_text) - return HttpResponseRedirect('/applications/hacker/review/'+ application.uuid_str) - elif request.POST.get('set_dubious'): + return HttpResponseRedirect( + "/applications/hacker/review/" + application.uuid_str + ) + elif request.POST.get("set_dubious"): application.set_dubious() - elif request.POST.get('unset_dubious'): + elif request.POST.get("unset_dubious"): application.unset_dubious() - elif request.POST.get('set_blacklist') and request.user.is_organizer: + elif request.POST.get("set_blacklist") and request.user.is_organizer: application.set_blacklist() - elif request.POST.get('unset_blacklist') and request.user.has_blacklist_access: - add_comment(application, request.user, - "Blacklist review result: No problems, hacker allowed to participate in hackathon!") + elif ( + request.POST.get("unset_blacklist") + and request.user.has_blacklist_access + ): + add_comment( + application, + request.user, + "Blacklist review result: No problems, hacker allowed to participate in hackathon!", + ) application.unset_blacklist() else: add_vote(application, request.user, tech_vote, pers_vote) @@ -436,52 +629,107 @@ def post(self, request, *args, **kwargs): # application except IntegrityError: pass - return HttpResponseRedirect(reverse('review')) + return HttpResponseRedirect(reverse("review")) def can_vote(self): return True -class InviteTeamListView(TabsViewMixin, IsDirectorMixin, SingleTableMixin, TemplateView): - template_name = 'invite_list.html' + +class InviteTeamListView( + TabsViewMixin, IsDirectorMixin, SingleTableMixin, TemplateView +): + template_name = "invite_list.html" table_class = AdminTeamListTable - table_pagination = {'per_page': 100} + table_pagination = {"per_page": 100} def get_current_tabs(self): return hacker_tabs(self.request.user) def get_queryset(self): - return models.HackerApplication.objects.filter(status__in=[APP_PENDING, APP_CONFIRMED, APP_LAST_REMIDER, - APP_INVITED, APP_REJECTED]) \ - .exclude(user__team__team_code__isnull=True).values('user__team__team_code') \ - .annotate(vote_avg=Avg('vote__calculated_vote'), - members=Count('user', distinct=True), - invited=Count(Concat('status', 'user__id', output_field=CharField()), - filter=Q(status__in=[APP_INVITED, APP_LAST_REMIDER]), distinct=True), - accepted=Count(Concat('status', 'user__id', output_field=CharField()), - filter=Q(status=APP_CONFIRMED), distinct=True), - live_pending=Count(Concat('status', 'user__id', output_field=CharField()), - filter=Q(status__in=[APP_PENDING, APP_REJECTED], online=False), - distinct=True))\ - .exclude(members=F('accepted')).order_by('-vote_avg') + hackersList = ( + models.HackerApplication.objects.filter( + status__in=[ + APP_PENDING, + APP_CONFIRMED, + APP_LAST_REMIDER, + APP_INVITED, + APP_REJECTED, + ] + ) + .exclude(user__team__team_code__isnull=True) + .values("user__team__team_code") + .annotate( + vote_avg=Avg("vote__calculated_vote"), + members=Count("user", distinct=True), + invited=Count( + Concat("status", "user__id", output_field=CharField()), + filter=Q(status__in=[APP_INVITED, APP_LAST_REMIDER]), + distinct=True, + ), + accepted=Count( + Concat("status", "user__id", output_field=CharField()), + filter=Q(status=APP_CONFIRMED), + distinct=True, + ), + live_pending=Count( + Concat("status", "user__id", output_field=CharField()), + filter=Q(status__in=[APP_PENDING, APP_REJECTED], online=False), + distinct=True, + ), + ) + .exclude(members=F("accepted")) + .exclude(Q(live_pending=0) | Q(live_pending__gt=F("members") / 2)) + .order_by("-vote_avg") + ) + + return hackersList def get_context_data(self, **kwargs): context = super(InviteTeamListView, self).get_context_data(**kwargs) - context.update({'teams': True}) - n_live_hackers = models.HackerApplication.objects.filter(status__in=[APP_INVITED, APP_LAST_REMIDER, - APP_CONFIRMED], online=False).count() - context.update({'n_live_hackers': n_live_hackers, - 'n_live_per_hackers': n_live_hackers * 100 / getattr(settings, 'N_MAX_LIVE_HACKERS', 0)}) + context.update({"teams": True}) + + n_live_hackers = models.HackerApplication.objects.filter( + status__in=[APP_INVITED, APP_LAST_REMIDER, APP_CONFIRMED], online=False + ).count() + + n_invited_hackers_today = models.HackerApplication.objects.filter( + status__in=[APP_INVITED], + online=False, + status_update_date__date=timezone.now().date(), + ).count() + + n_waitlisted_hackers = models.HackerApplication.objects.filter( + status__in=[APP_REJECTED], online=False + ).count() + + context.update( + { + "n_live_hackers": n_live_hackers, + "n_live_per_hackers": n_live_hackers + * 100 + / getattr(settings, "N_MAX_LIVE_HACKERS", 0), + "n_invited_hackers_today": n_invited_hackers_today, + "n_waitlisted_hackers": n_waitlisted_hackers, + } + ) return context def post(self, request, *args, **kwargs): - ids = request.POST.getlist('selected') - apps = models.HackerApplication.objects.filter(user__team__team_code__in=ids)\ - .exclude(status__in=[APP_DUBIOUS, APP_BLACKLISTED]).annotate(count=Count('vote')).filter(count__gte=5) + ids = request.POST.getlist("selected") + apps = ( + models.HackerApplication.objects.filter(user__team__team_code__in=ids) + .exclude(status__in=[APP_DUBIOUS, APP_BLACKLISTED]) + .annotate(count=Count("vote")) + .filter(count__gte=5) + ) mails = [] errors = 0 for app in apps: try: - app.invite(request.user, online=request.POST.get('force_online', 'false') == 'true') + app.invite( + request.user, + online=request.POST.get("force_online", "false") == "true", + ) m = emails.create_invite_email(app, request) mails.append(m) except ValidationError: @@ -495,39 +743,55 @@ def post(self, request, *args, **kwargs): errorMsg = "%s applications not invited" % errors messages.error(request, errorMsg) - return HttpResponseRedirect(reverse('invite_teams_list')) + return HttpResponseRedirect(reverse("invite_teams_list")) + + +class WaitlistedApplicationsListView( + IsDirectorMixin, ExportMixin, SingleTableMixin, View +): + # This view is to send all hacker applications left under_review to waitlisted + def post(self, request, *args, **kwargs): + models.HackerApplication.objects.filter(status=APP_PENDING).update( + status=APP_REJECTED + ) + return HttpResponse(status=200) -class DubiousApplicationsListView(TabsViewMixin, HaveDubiousPermissionMixin, ExportMixin, SingleTableMixin, - FilterView): - template_name = 'dubious_list.html' +class DubiousApplicationsListView( + TabsViewMixin, HaveDubiousPermissionMixin, ExportMixin, SingleTableMixin, FilterView +): + template_name = "dubious_list.html" table_class = DubiousListTable filterset_class = DubiousApplicationFilter - table_pagination = {'per_page': 100} - exclude_columns = ('status', 'vote_avg') - export_name = 'dubious_applications' + table_pagination = {"per_page": 100} + exclude_columns = ("status", "vote_avg") + export_name = "dubious_applications" def get(self, request, *args, **kwargs): - request.session['edit_app_back'] = 'dubious' + request.session["edit_app_back"] = "dubious" return super().get(request, *args, **kwargs) def get_current_tabs(self): return hacker_tabs(self.request.user) def get_queryset(self): - return models.HackerApplication.objects.filter(status=APP_DUBIOUS).order_by('-status_update_date') + return models.HackerApplication.objects.filter(status=APP_DUBIOUS).order_by( + "-status_update_date" + ) -class BlacklistApplicationsListView(TabsViewMixin, IsBlacklistAdminMixin, ExportMixin, SingleTableMixin, FilterView): - template_name = 'blacklist_list.html' +class BlacklistApplicationsListView( + TabsViewMixin, IsBlacklistAdminMixin, ExportMixin, SingleTableMixin, FilterView +): + template_name = "blacklist_list.html" table_class = BlacklistListTable filterset_class = BlacklistApplicationFilter - table_pagination = {'per_page': 100} - exclude_columns = ('status', 'vote_avg') - export_name = 'blacklist_applications' + table_pagination = {"per_page": 100} + exclude_columns = ("status", "vote_avg") + export_name = "blacklist_applications" def get(self, request, *args, **kwargs): - request.session['edit_app_back'] = 'blacklist' + request.session["edit_app_back"] = "blacklist" return super().get(request, *args, **kwargs) def get_current_tabs(self): @@ -537,24 +801,28 @@ def get_queryset(self): return models.HackerApplication.objects.filter(status=APP_BLACKLISTED) -class _OtherApplicationsListView(TabsViewMixin, ExportMixin, SingleTableMixin, FilterView): - template_name = 'applications_list.html' - table_pagination = {'per_page': 100} - exclude_columns = ('detail', 'status') - export_name = 'applications' - email_field = 'user__email' +class _OtherApplicationsListView( + TabsViewMixin, ExportMixin, SingleTableMixin, FilterView +): + template_name = "applications_list.html" + table_pagination = {"per_page": 100} + exclude_columns = ("detail", "status") + export_name = "applications" + email_field = "user__email" def get_context_data(self, **kwargs): context = super(_OtherApplicationsListView, self).get_context_data(**kwargs) - context['otherApplication'] = True + context["otherApplication"] = True list_email = "" - for u in context.get('object_list').values_list(self.email_field, flat=True): + for u in context.get("object_list").values_list(self.email_field, flat=True): list_email += "%s, " % u - context['emails'] = list_email + context["emails"] = list_email return context -class VolunteerApplicationsListView(HaveVolunteerPermissionMixin, _OtherApplicationsListView): +class VolunteerApplicationsListView( + HaveVolunteerPermissionMixin, _OtherApplicationsListView +): table_class = VolunteerListTable filterset_class = VolunteerFilter @@ -565,28 +833,32 @@ def get_current_tabs(self): return volunteer_tabs(self.request.user) -class SponsorApplicationsListView(HaveSponsorPermissionMixin, _OtherApplicationsListView): +class SponsorApplicationsListView( + HaveSponsorPermissionMixin, _OtherApplicationsListView +): table_class = SponsorListTable filterset_class = SponsorFilter - email_field = 'email' + email_field = "email" def get_queryset(self): return models.SponsorApplication.objects.all() def get_context_data(self, **kwargs): context = super(SponsorApplicationsListView, self).get_context_data(**kwargs) - context['otherApplication'] = True + context["otherApplication"] = True return context def get_current_tabs(self): return sponsor_tabs(self.request.user) -class SponsorUserListView(HaveSponsorPermissionMixin, TabsViewMixin, ExportMixin, SingleTableMixin, FilterView): - template_name = 'applications_list.html' - table_pagination = {'per_page': 100} - exclude_columns = ('detail', 'status') - export_name = 'applications' +class SponsorUserListView( + HaveSponsorPermissionMixin, TabsViewMixin, ExportMixin, SingleTableMixin, FilterView +): + template_name = "applications_list.html" + table_pagination = {"per_page": 100} + exclude_columns = ("detail", "status") + export_name = "applications" table_class = SponsorUserListTable filterset_class = SponsorUserFilter @@ -595,8 +867,8 @@ def get_current_tabs(self): def get_context_data(self, **kwargs): context = super(SponsorUserListView, self).get_context_data(**kwargs) - context['otherApplication'] = True - context['createUser'] = True + context["otherApplication"] = True + context["createUser"] = True return context def get_queryset(self): @@ -614,184 +886,218 @@ def get_current_tabs(self): return mentor_tabs(self.request.user) -class ReviewVolunteerApplicationView(TabsViewMixin, HaveVolunteerPermissionMixin, TemplateView): - template_name = 'other_application_detail.html' +class ReviewVolunteerApplicationView( + TabsViewMixin, HaveVolunteerPermissionMixin, TemplateView +): + template_name = "other_application_detail.html" def get_application(self, kwargs): - application_id = kwargs.get('id', None) + application_id = kwargs.get("id", None) if not application_id: raise Http404 - application = models.VolunteerApplication.objects.filter(uuid=application_id).first() + application = models.VolunteerApplication.objects.filter( + uuid=application_id + ).first() if not application: raise Http404 return application def post(self, request, *args, **kwargs): - id_ = request.POST.get('app_id') - comment_text = request.POST.get('comment_text', None) + id_ = request.POST.get("app_id") + comment_text = request.POST.get("comment_text", None) application = models.VolunteerApplication.objects.get(pk=id_) - if request.POST.get('invite') and request.user.is_organizer: + if request.POST.get("invite") and request.user.is_organizer: application.invite(request.user) application.save() m = emails.create_invite_email(application, self.request) if m: m.send() - messages.success(request, 'Volunteer invited!') - elif request.POST.get('reject') and request.user.is_organizer: + messages.success(request, "Volunteer invited!") + elif request.POST.get("reject") and request.user.is_organizer: application.reject() application.save() - elif request.POST.get('cancel_invite') and request.user.is_organizer: + elif request.POST.get("cancel_invite") and request.user.is_organizer: application.move_to_pending() - messages.success(request, 'Volunteer invite canceled') - elif request.POST.get('add_comment'): + messages.success(request, "Volunteer invite canceled") + elif request.POST.get("add_comment"): add_comment(application, request.user, comment_text) - messages.success(request, 'Comment added') - elif request.POST.get('change_valid') and request.user.is_organizer: + messages.success(request, "Comment added") + elif request.POST.get("change_valid") and request.user.is_organizer: application.valid = not application.valid application.save() - messages.success(request, 'Volunteer valid status changed') + messages.success(request, "Volunteer valid status changed") - return HttpResponseRedirect(reverse('volunteer_detail', kwargs={'id': application.uuid_str})) + return HttpResponseRedirect( + reverse("volunteer_detail", kwargs={"id": application.uuid_str}) + ) def get_back_url(self): - return reverse('volunteer_list') + return reverse("volunteer_list") def get_context_data(self, **kwargs): context = super(ReviewVolunteerApplicationView, self).get_context_data(**kwargs) application = self.get_application(kwargs) - context['app'] = application - context['comments'] = models.ApplicationComment.objects.filter(volunteer=application) + context["app"] = application + context["comments"] = models.ApplicationComment.objects.filter( + volunteer=application + ).order_by("created_at") return context -class ReviewSponsorApplicationView(TabsViewMixin, HaveSponsorPermissionMixin, TemplateView): - template_name = 'other_application_detail.html' +class ReviewSponsorApplicationView( + TabsViewMixin, HaveSponsorPermissionMixin, TemplateView +): + template_name = "other_application_detail.html" def get_application(self, kwargs): - application_id = kwargs.get('id', None) + application_id = kwargs.get("id", None) if not application_id: raise Http404 - application = models.SponsorApplication.objects.filter(uuid=application_id).first() + application = models.SponsorApplication.objects.filter( + uuid=application_id + ).first() if not application: raise Http404 return application def get_back_url(self): - return reverse('sponsor_list') + return reverse("sponsor_list") def post(self, request, *args, **kwargs): - id_ = request.POST.get('app_id') - comment_text = request.POST.get('comment_text', None) + id_ = request.POST.get("app_id") + comment_text = request.POST.get("comment_text", None) application = models.SponsorApplication.objects.get(pk=id_) - if request.POST.get('add_comment'): + if request.POST.get("add_comment"): add_comment(application, request.user, comment_text) - messages.success(request, 'Comment added') + messages.success(request, "Comment added") - return HttpResponseRedirect(reverse('sponsor_detail', kwargs={'id': application.uuid_str})) + return HttpResponseRedirect( + reverse("sponsor_detail", kwargs={"id": application.uuid_str}) + ) def get_context_data(self, **kwargs): context = super(ReviewSponsorApplicationView, self).get_context_data(**kwargs) application = self.get_application(kwargs) - context['app'] = application - context['comments'] = models.ApplicationComment.objects.filter(sponsor=application) + context["app"] = application + context["comments"] = models.ApplicationComment.objects.filter( + sponsor=application + ).order_by("created_at") return context -class ReviewMentorApplicationView(TabsViewMixin, HaveMentorPermissionMixin, TemplateView): - template_name = 'other_application_detail.html' +class ReviewMentorApplicationView( + TabsViewMixin, HaveMentorPermissionMixin, TemplateView +): + template_name = "other_application_detail.html" def get_application(self, kwargs): - application_id = kwargs.get('id', None) + application_id = kwargs.get("id", None) if not application_id: raise Http404 - application = models.MentorApplication.objects.filter(uuid=application_id).first() + application = models.MentorApplication.objects.filter( + uuid=application_id + ).first() if not application: raise Http404 return application def post(self, request, *args, **kwargs): - id_ = request.POST.get('app_id') + id_ = request.POST.get("app_id") application = models.MentorApplication.objects.get(pk=id_) - comment_text = request.POST.get('comment_text', None) - if request.POST.get('invite') and request.user.is_organizer: + comment_text = request.POST.get("comment_text", None) + if request.POST.get("invite") and request.user.is_organizer: application.invite(request.user) application.save() m = emails.create_invite_email(application, self.request) if m: m.send() - messages.success(request, 'Mentor invited!') - elif request.POST.get('cancel_invite') and request.user.is_organizer: + messages.success(request, "Mentor invited!") + elif request.POST.get("cancel_invite") and request.user.is_organizer: application.move_to_pending() - messages.success(request, 'Mentor invite canceled') - elif request.POST.get('add_comment'): + messages.success(request, "Mentor invite canceled") + elif request.POST.get("add_comment"): add_comment(application, request.user, comment_text) - messages.success(request, 'comment added') - elif request.POST.get('change_valid') and request.user.is_organizer: + messages.success(request, "comment added") + elif request.POST.get("change_valid") and request.user.is_organizer: application.valid = not application.valid application.save() - messages.success(request, 'Mentor valid status changed') + messages.success(request, "Mentor valid status changed") - return HttpResponseRedirect(reverse('mentor_detail', kwargs={'id': application.uuid_str})) + return HttpResponseRedirect( + reverse("mentor_detail", kwargs={"id": application.uuid_str}) + ) def get_back_url(self): - return reverse('mentor_list') + return reverse("mentor_list") def get_context_data(self, **kwargs): context = super(ReviewMentorApplicationView, self).get_context_data(**kwargs) application = self.get_application(kwargs) - context['app'] = application - context['comments'] = models.ApplicationComment.objects.filter(mentor=application) + context["app"] = application + context["comments"] = models.ApplicationComment.objects.filter( + mentor=application + ).order_by("created_at") return context class ReviewResume(TabsViewMixin, HaveSponsorPermissionMixin, TemplateView): - template_name = 'review_resume.html' + template_name = "review_resume.html" def get_current_tabs(self): return hacker_tabs(self.request.user) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - app = models.HackerApplication.objects.filter(acceptedresume__isnull=True, cvs_edition=True)\ - .exclude(status__in=[APP_DUBIOUS, APP_BLACKLISTED]).first() - context.update({'app': app}) + app = ( + models.HackerApplication.objects.filter( + acceptedresume__isnull=True, cvs_edition=True + ) + .exclude(status__in=[APP_DUBIOUS, APP_BLACKLISTED]) + .first() + ) + context.update({"app": app}) return context def post(self, request, *args, **kwargs): - app_id = request.POST.get('app_id') - accepted = request.POST.get('accepted') - AcceptedResume(application_id=app_id, accepted=(accepted == 'true')).save() - return redirect(reverse('review_resume')) + app_id = request.POST.get("app_id") + accepted = request.POST.get("accepted") + AcceptedResume(application_id=app_id, accepted=(accepted == "true")).save() + return redirect(reverse("review_resume")) def get(self, request, *args, **kwargs): - file = request.GET.get('files', False) + file = request.GET.get("files", False) if file: s = BytesIO() - accepted_resumes = AcceptedResume.objects.filter(accepted=True, application__status__in=[ - APP_CONFIRMED, APP_ATTENDED]).select_related('application') + accepted_resumes = AcceptedResume.objects.filter( + accepted=True, application__status__in=[APP_CONFIRMED, APP_ATTENDED] + ).select_related("application") with ZipFile(s, "w") as zip_file: for accepted_resume in accepted_resumes: file_path = accepted_resume.application.resume.path _, fname = os.path.split(file_path) zip_path = os.path.join("resumes", fname) zip_file.write(file_path, zip_path) - resp = HttpResponse(s.getvalue(), content_type="application/x-zip-compressed") - resp['Content-Disposition'] = 'attachment; filename=resumes.zip' + resp = HttpResponse( + s.getvalue(), content_type="application/x-zip-compressed" + ) + resp["Content-Disposition"] = "attachment; filename=resumes.zip" return resp return super().get(request, *args, **kwargs) class VisualizeResume(IsOrganizerMixin, TemplateView): - template_name = 'pdf_view.html' + template_name = "pdf_view.html" def get_application(self, kwargs): - application_id = kwargs.get('id', None) + application_id = kwargs.get("id", None) if not application_id: raise Http404 - application = models.HackerApplication.objects.filter(uuid=application_id).first() + application = models.HackerApplication.objects.filter( + uuid=application_id + ).first() if not application: raise Http404 return application @@ -799,5 +1105,5 @@ def get_application(self, kwargs): def get_context_data(self, **kwargs): context = super(VisualizeResume, self).get_context_data(**kwargs) application = self.get_application(kwargs) - context['app'] = application + context["app"] = application return context From b7ea778689c91ea20384c8eee51979bc0591955e Mon Sep 17 00:00:00 2001 From: "enrique.andujar" Date: Wed, 3 Sep 2025 18:58:48 +0200 Subject: [PATCH 14/50] Left-over files --- applications/forms/base.py | 190 ++++++++++++++++++++-------------- applications/models/base.py | 118 ++++++++++++++------- applications/models/hacker.py | 72 +++++++++---- manage.py | 1 + 4 files changed, 241 insertions(+), 140 deletions(-) diff --git a/applications/forms/base.py b/applications/forms/base.py index 04ee47b2c..c79835cd0 100644 --- a/applications/forms/base.py +++ b/applications/forms/base.py @@ -30,50 +30,66 @@ def _widget_render_wrapper(name, value, attrs=None): def get_exclude_fields(): - discord = getattr(settings, 'DISCORD_HACKATHON', False) - exclude = ['user', 'uuid', 'invited_by', 'submission_date', 'status_update_date', 'status', 'contacted_by', - 'blacklisted_by'] + discord = getattr(settings, "DISCORD_HACKATHON", False) + exclude = [ + "user", + "uuid", + "invited_by", + "submission_date", + "status_update_date", + "status", + "contacted_by", + "blacklisted_by", + ] + if discord: - exclude.extend(['diet', 'other_diet', 'diet_notice']) + exclude.extend(["diet", "other_diet", "diet_notice"]) return exclude class _BaseApplicationForm(OverwriteOnlyModelFormMixin, BootstrapFormMixin, ModelForm): - diet = forms.ChoiceField(label='Dietary requirements', choices=models.DIETS, required=True) - phone_number = forms.CharField(required=False, widget=forms.TextInput( - attrs={'class': 'form-control', 'placeholder': '+#########'}), - label='Phone number (Optional)', - help_text='This field is not mandatory.' + diet = forms.ChoiceField( + label="Dietary requirements", choices=models.DIETS, required=True + ) + phone_number = forms.CharField( + required=False, + widget=forms.TextInput( + attrs={"class": "form-control", "placeholder": "+#########"} + ), + label="Phone number (Optional)", + help_text="This field is not mandatory.", ) under_age = forms.TypedChoiceField( required=True, - label='How old will you be at time of the event?', + label="How old will you be at time of the event?", initial=False, - coerce=lambda x: x == 'True', - choices=((False, '18 or over'), (True, 'Between 14 (included) and 18')), - widget=forms.RadioSelect + coerce=lambda x: x == "True", + choices=((False, "18 or over"), (True, "Between 14 (included) and 18")), + widget=forms.RadioSelect, ) terms_and_conditions = forms.BooleanField( required=True, label='I\'ve read, understand and accept %s ' - 'Terms & Conditions and %s ' - 'Privacy and Cookies Policy. *' % ( - settings.HACKATHON_NAME, settings.HACKATHON_NAME - ) + 'Terms & Conditions and %s ' + 'Privacy and Cookies Policy. *' + % (settings.HACKATHON_NAME, settings.HACKATHON_NAME), ) diet_notice = forms.BooleanField( required=False, label='I authorize "Hackers at UPC" to use my food allergies and intolerances information to ' - 'manage the catering service only. *' + 'manage the catering service only. *', ) - email_subscribe = forms.BooleanField(required=False, label='Subscribe to our Marketing list in order to inform ' - 'you about our next events.') + email_subscribe = forms.BooleanField( + required=False, + label="Subscribe to our Marketing list in order to inform " + "you about our next events.", + ) def clean_terms_and_conditions(self): - cc = self.cleaned_data.get('terms_and_conditions', False) + cc = self.cleaned_data.get("terms_and_conditions", False) # Check that if it's the first submission hackers checks terms and conditions checkbox # self.instance.pk is None if there's no Application existing before # https://stackoverflow.com/questions/9704067/test-if-django-modelform-has-instance @@ -85,12 +101,12 @@ def clean_terms_and_conditions(self): return cc def clean_diet_notice(self): - diet = self.cleaned_data.get('diet', 'None') - diet_notice = self.cleaned_data.get('diet_notice', False) + diet = self.cleaned_data.get("diet", "None") + diet_notice = self.cleaned_data.get("diet_notice", False) # Check that if it's the first submission hackers checks terms and conditions checkbox # self.instance.pk is None if there's no Application existing before # https://stackoverflow.com/questions/9704067/test-if-django-modelform-has-instance - if diet != 'None' and not diet_notice and not self.instance.pk: + if diet != "None" and not diet_notice and not self.instance.pk: raise forms.ValidationError( "In order to apply and attend you have to accept us to use your personal data related to your food " "allergies and intolerances only in order to manage the catering service." @@ -98,21 +114,25 @@ def clean_diet_notice(self): return diet_notice def clean_other_diet(self): - data = self.cleaned_data.get('other_diet', '') - diet = self.cleaned_data.get('diet', 'None') - if diet == 'Others' and not data: - raise forms.ValidationError("Please tell us your specific dietary requirements") + data = self.cleaned_data.get("other_diet", "") + diet = self.cleaned_data.get("diet", "None") + if diet == "Others" and not data: + raise forms.ValidationError( + "Please tell us your specific dietary requirements" + ) return data def clean_other_gender(self): - gender = self.cleaned_data.get('gender') - other_gender = self.cleaned_data.get('other_gender', None) + gender = self.cleaned_data.get("gender") + other_gender = self.cleaned_data.get("other_gender", None) if gender == "X" and not other_gender: - raise forms.ValidationError("Please enter this field or select 'Prefer not to answer'") + raise forms.ValidationError( + "Please enter this field or select 'Prefer not to answer'" + ) return other_gender def clean_origin(self): - origin = self.cleaned_data['origin'] + origin = self.cleaned_data["origin"] # read from json file on local machine # actual file path @@ -122,12 +142,14 @@ def clean_origin(self): STATIC_ROOT = os.path.join(dir_path, "../static") # open relative file - with open(os.path.join(STATIC_ROOT,'cities.json')) as f: + with open(os.path.join(STATIC_ROOT, "cities.json")) as f: countries = json.load(f) # check if is part of the list if origin not in countries: - raise forms.ValidationError("Please select one of the dropdown options and don't forget to add commas") + raise forms.ValidationError( + "Please select one of the dropdown options and don't forget to add commas" + ) return origin def __getitem__(self, name): @@ -141,67 +163,71 @@ class Meta: class ConfirmationInvitationForm(BootstrapFormMixin, forms.ModelForm): bootstrap_field_info = { - '': { - 'fields': [{'name': 'tshirt_size', 'space': 4}, {'name': 'diet', 'space': 4}, - {'name': 'other_diet', 'space': 4}, - {'name': 'reimb', 'space': 12}, - {'name': 'terms_and_conditions', 'space': 12}, - {'name': 'mlh_required_terms', 'space': 12}, - {'name': 'mlh_required_privacy', 'space': 12}, {'name': 'mlh_subscribe', 'space': 12}, - {'name': 'diet_notice', 'space': 12} - ], + "": { + "fields": [ + {"name": "tshirt_size", "space": 4}, + {"name": "diet", "space": 4}, + {"name": "other_diet", "space": 4}, + {"name": "reimb", "space": 12}, + {"name": "terms_and_conditions", "space": 12}, + {"name": "mlh_required_terms", "space": 12}, + {"name": "mlh_required_privacy", "space": 12}, + {"name": "mlh_subscribe", "space": 12}, + {"name": "diet_notice", "space": 12}, + ], }, } - diet = forms.ChoiceField(label='Dietary requirements', choices=models.DIETS, required=True) + diet = forms.ChoiceField( + label="Dietary requirements", choices=models.DIETS, required=True + ) reimb = forms.TypedChoiceField( required=False, - label='Do you need a travel reimbursement to attend?', - coerce=lambda x: x == 'True', - choices=((False, 'No'), (True, 'Yes')), + label="Do you need a travel reimbursement to attend?", + coerce=lambda x: x == "True", + choices=((False, "No"), (True, "Yes")), initial=False, widget=forms.RadioSelect(), - help_text='We only provide travel reimbursement if you attend from outside of Catalonia, ' - 'you can find more info in our website\'s FAQs.' + help_text="We only provide travel reimbursement if you attend from outside of Catalonia, " + 'you can find more info in our website\'s FAQs.', ) mlh_required_terms = forms.BooleanField( label='I have read and agree to the MLH Code of ' - 'Conduct. *' + 'Conduct. *' ) mlh_subscribe = forms.BooleanField( required=False, label="I authorize MLH to send me an email where I can further opt into the MLH Hacker, Events, or " - "Organizer Newsletters and other communications from MLH." + "Organizer Newsletters and other communications from MLH.", ) diet_notice = forms.BooleanField( required=False, label='I authorize "Hackers at UPC" to use my food allergies and intolerances information to ' - 'manage the catering service only. *' + 'manage the catering service only. *', ) mlh_required_privacy = forms.BooleanField( label="I authorize you to share my application/registration information with Major League Hacking for " - "event administration, ranking, and MLH administration in-line with the MLH " - ". I further agree to the terms of both the MLH Contest " - "Terms and Conditions " - "and the MLH Privacy Policy. " - " *" + "event administration, ranking, and MLH administration in-line with the MLH " + '. I further agree to the terms of both the MLH Contest ' + 'Terms and Conditions ' + 'and the MLH Privacy Policy. ' + ' *' ) terms_and_conditions = forms.BooleanField( label='I\'ve read, understand and accept %s ' - 'Terms & Conditions and %s ' - 'Privacy and Cookies Policy. *' % ( - settings.HACKATHON_NAME, settings.HACKATHON_NAME - ) + 'Terms & Conditions and %s ' + 'Privacy and Cookies Policy. *' + % (settings.HACKATHON_NAME, settings.HACKATHON_NAME) ) def clean_mlh_required_terms(self): - cc = self.cleaned_data.get('mlh_required_terms', False) + cc = self.cleaned_data.get("mlh_required_terms", False) # Check that if it's the first submission hackers checks terms and conditions checkbox # self.instance.pk is None if there's no Application existing before # https://stackoverflow.com/questions/9704067/test-if-django-modelform-has-instance @@ -212,7 +238,7 @@ def clean_mlh_required_terms(self): return cc def clean_mlh_required_privacy(self): - cc = self.cleaned_data.get('mlh_required_privacy', False) + cc = self.cleaned_data.get("mlh_required_privacy", False) # Check that if it's the first submission hackers checks terms and conditions checkbox # self.instance.pk is None if there's no Application existing before # https://stackoverflow.com/questions/9704067/test-if-django-modelform-has-instance @@ -223,7 +249,7 @@ def clean_mlh_required_privacy(self): return cc def clean_mlh_optional_communications(self): - cc = self.cleaned_data.get('mlh_optional_communications', False) + cc = self.cleaned_data.get("mlh_optional_communications", False) # Check that if it's the first submission hackers checks terms and conditions checkbox # self.instance.pk is None if there's no Application existing before # https://stackoverflow.com/questions/9704067/test-if-django-modelform-has-instance @@ -233,41 +259,45 @@ def clean_mlh_optional_communications(self): ) return cc - def clean_diet_notice(self): - diet = self.cleaned_data.get('diet', 'None') - diet_notice = self.cleaned_data.get('diet_notice', False) + diet = self.cleaned_data.get("diet", "None") + diet_notice = self.cleaned_data.get("diet_notice", False) # Check that if it's the first submission hackers checks terms and conditions checkbox # self.instance.pk is None if there's no Application existing before # https://stackoverflow.com/questions/9704067/test-if-django-modelform-has-instance - if diet != 'None' and not diet_notice and not self.instance.pk: + if diet != "None" and not diet_notice and not self.instance.pk: raise forms.ValidationError( "In order to apply and attend you have to accept us to use your personal data related to your food " "allergies and intolerances only in order to manage the catering service." ) return diet_notice + def clean_other_diet(self): - data = self.cleaned_data.get('other_diet', '') - diet = self.cleaned_data.get('diet', 'None') - if diet == 'Others' and not data: - raise forms.ValidationError("Please tell us your specific dietary requirements") + data = self.cleaned_data.get("other_diet", "") + diet = self.cleaned_data.get("diet", "None") + if diet == "Others" and not data: + raise forms.ValidationError( + "Please tell us your specific dietary requirements" + ) return data def clean_reimb(self): - reimb = self.cleaned_data.get('reimb', False) - deadline = getattr(settings, 'REIMBURSEMENT_DEADLINE', False) + reimb = self.cleaned_data.get("reimb", False) + deadline = getattr(settings, "REIMBURSEMENT_DEADLINE", False) if reimb and deadline and deadline <= timezone.now(): - raise forms.ValidationError("Reimbursement applications are now closed. Trying to hack us?") + raise forms.ValidationError( + "Reimbursement applications are now closed. Trying to hack us?" + ) return reimb class Meta: model = models.HackerApplication - fields = ['diet', 'other_diet', 'reimb', 'reimb_amount', 'tshirt_size'] + fields = ["diet", "other_diet", "reimb", "reimb_amount", "tshirt_size"] help_texts = { - 'other_diet': 'If you have any special dietary requirements, please write write them here. ' - 'We want to make sure we have food for you!', + "other_diet": "If you have any special dietary requirements, please write write them here. " + "We want to make sure we have food for you!", } labels = { - 'tshirt_size': 'What\'s your t-shirt size?', - 'diet': 'Dietary requirements', + "tshirt_size": "What's your t-shirt size?", + "diet": "Dietary requirements", } diff --git a/applications/models/base.py b/applications/models/base.py index cc3ee721a..5dd56d74a 100644 --- a/applications/models/base.py +++ b/applications/models/base.py @@ -16,18 +16,21 @@ from app import utils, hackathon_variables from user.models import User, BlacklistUser from user import models as userModels -from applications.validators import validate_file_extension, validate_file_extension_size +from applications.validators import ( + validate_file_extension, + validate_file_extension_size, +) from .constants import * def resume_path_hackers(instance, filename): (_, ext) = os.path.splitext(filename) - return 'resumes_hackers/{}_{}{}'.format(instance.user.name, instance.uuid, ext) + return "resumes_hackers/{}_{}{}".format(instance.user.name, instance.uuid, ext) def resume_path_mentors(instance, filename): (_, ext) = os.path.splitext(filename) - return 'resumes_mentors/{}_{}{}'.format(instance.user.name, instance.uuid, ext) + return "resumes_mentors/{}_{}{}".format(instance.user.name, instance.uuid, ext) class BaseApplication(models.Model): @@ -35,9 +38,19 @@ class Meta: abstract = True uuid = models.UUIDField(default=uuid.uuid4, editable=False) - user = models.OneToOneField(User, related_name='%(class)s_application', primary_key=True, on_delete=models.CASCADE) - invited_by = models.ForeignKey(User, related_name='%(class)s_invited_applications', blank=True, null=True, - on_delete=models.SET_NULL) + user = models.OneToOneField( + User, + related_name="%(class)s_application", + primary_key=True, + on_delete=models.CASCADE, + ) + invited_by = models.ForeignKey( + User, + related_name="%(class)s_invited_applications", + blank=True, + null=True, + on_delete=models.SET_NULL, + ) # When was the application submitted submission_date = models.DateTimeField(default=timezone.now) @@ -46,9 +59,7 @@ class Meta: status_update_date = models.DateTimeField(blank=True, null=True) # Application status - status = models.CharField(choices=STATUS, - default=APP_PENDING, - max_length=2) + status = models.CharField(choices=STATUS, default=APP_PENDING, max_length=2) # ABOUT YOU # Population analysis, optional @@ -58,34 +69,44 @@ class Meta: # Personal data (asking here because we don't want to ask birthday) under_age = models.BooleanField() - phone_number = models.CharField(blank=True, null=True, max_length=16, - validators=[RegexValidator(regex=r'^\+?1?\d{9,15}$', - message="Phone number must be entered in the format: \ - '+#########'. Up to 16 digits allowed.")]) + phone_number = models.CharField( + blank=True, + null=True, + max_length=16, + validators=[ + RegexValidator( + regex=r"^\+?1?\d{9,15}$", + message="Phone number must be entered in the format: \ + '+#########'. Up to 16 digits allowed.", + ) + ], + ) # Info for swag and food diet = models.CharField(max_length=300, choices=DIETS, default=D_NONE) other_diet = models.CharField(max_length=600, blank=True, null=True) - tshirt_size = models.CharField(max_length=300, default=DEFAULT_TSHIRT_SIZE, choices=TSHIRT_SIZES) + tshirt_size = models.CharField( + max_length=300, default=DEFAULT_TSHIRT_SIZE, choices=TSHIRT_SIZES + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) try: - dict = args[0]['dict'] + dict = args[0]["dict"] except Exception: dict = None if dict is not None: for key in dict: - atr = getattr(self, key, 'NOT_EXIST') - if atr != 'NOT_EXIST': + atr = getattr(self, key, "NOT_EXIST") + if atr != "NOT_EXIST": setattr(self, key, dict[key]) def get_diet_color(self): colors = { - D_NONE: 'white', - D_VEGETARIAN: '#7ABE6F', + D_NONE: "white", + D_VEGETARIAN: "#7ABE6F", } - return colors.get(self.diet, '#42A2CB') + return colors.get(self.diet, "#42A2CB") def __str__(self): return self.user.email @@ -121,7 +142,7 @@ def needs_action(self): def is_pending(self): return self.status == APP_PENDING - def can_be_edit(self, app_type='H'): + def can_be_edit(self, app_type="H"): return self.status == APP_PENDING def is_invited(self): @@ -146,17 +167,32 @@ def is_dubious(self): return self.status == APP_DUBIOUS def can_be_cancelled(self): - return self.status == APP_CONFIRMED or self.status == APP_INVITED or self.status == APP_LAST_REMIDER + return ( + self.status == APP_CONFIRMED + or self.status == APP_INVITED + or self.status == APP_LAST_REMIDER + ) def can_confirm(self): return self.status in [APP_INVITED, APP_LAST_REMIDER] def can_be_invited(self): - return self.status in [APP_INVITED, APP_LAST_REMIDER, APP_CANCELLED, APP_PENDING, APP_EXPIRED, APP_REJECTED, - APP_INVALID] + return self.status in [ + APP_INVITED, + APP_LAST_REMIDER, + APP_CANCELLED, + APP_PENDING, + APP_EXPIRED, + APP_REJECTED, + APP_INVALID, + ] def can_join_team(self): - return self.user.type == userModels.USR_HACKER and self.status in [APP_PENDING, APP_LAST_REMIDER, APP_DUBIOUS] + return self.user.type == userModels.USR_HACKER and self.status in [ + APP_PENDING, + APP_LAST_REMIDER, + APP_DUBIOUS, + ] def check_in(self): self.status = APP_ATTENDED @@ -170,8 +206,9 @@ def move_to_pending(self): def reject(self): if self.status == APP_ATTENDED: - raise ValidationError('Application has already attended. ' - 'Current status: %s' % self.status) + raise ValidationError( + "Application has already attended. " "Current status: %s" % self.status + ) self.status = APP_REJECTED self.status_update_date = timezone.now() self.save() @@ -179,8 +216,10 @@ def reject(self): def invite(self, user, online=False): # We can re-invite someone invited if self.status in [APP_CONFIRMED, APP_ATTENDED]: - raise ValidationError('Application has already answered invite. ' - 'Current status: %s' % self.status) + raise ValidationError( + "Application has already answered invite. " + "Current status: %s" % self.status + ) self.status = APP_INVITED if not self.invited_by: self.invited_by = user @@ -193,9 +232,9 @@ def invite(self, user, online=False): def confirm(self): if self.status == APP_CANCELLED: - raise ValidationError('This invite has been cancelled.') + raise ValidationError("This invite has been cancelled.") elif self.status == APP_EXPIRED: - raise ValidationError('Unfortunately your invite has expired.') + raise ValidationError("Unfortunately your invite has expired.") elif self.status in [APP_INVITED, APP_LAST_REMIDER]: self.status = APP_CONFIRMED self.status_update_date = timezone.now() @@ -203,25 +242,28 @@ def confirm(self): elif self.status in [APP_CONFIRMED, APP_ATTENDED]: return None else: - raise ValidationError('Unfortunately his application hasn\'t been ' - 'invited [yet]') + raise ValidationError( + "Unfortunately his application hasn't been " "invited [yet]" + ) def cancel(self): if not self.can_be_cancelled(): - raise ValidationError('Application can\'t be cancelled. Current ' - 'status: %s' % self.status) + raise ValidationError( + "Application can't be cancelled. Current " "status: %s" % self.status + ) if self.status != APP_CANCELLED: self.status = APP_CANCELLED self.status_update_date = timezone.now() self.save() - reimb = getattr(self.user, 'reimbursement', None) + reimb = getattr(self.user, "reimbursement", None) if reimb: reimb.delete() def last_reminder(self): if self.status != APP_INVITED: - raise ValidationError('Reminder can\'t be sent to non-pending ' - 'applications') + raise ValidationError( + "Reminder can't be sent to non-pending " "applications" + ) self.status_update_date = timezone.now() self.status = APP_LAST_REMIDER self.save() diff --git a/applications/models/hacker.py b/applications/models/hacker.py index 6592ef63a..4e9ee0969 100644 --- a/applications/models/hacker.py +++ b/applications/models/hacker.py @@ -3,9 +3,7 @@ from datetime import timedelta -class HackerApplication( - BaseApplication -): +class HackerApplication(BaseApplication): # Where is this person coming from? origin = models.CharField(max_length=300) @@ -13,7 +11,7 @@ class HackerApplication( first_timer = models.BooleanField(default=False) # Random lenny face - lennyface = models.CharField(max_length=20, default='-.-') + lennyface = models.CharField(max_length=20, default="-.-") # University graduation_year = models.IntegerField(choices=YEARS, default=DEFAULT_YEAR) @@ -36,22 +34,41 @@ class HackerApplication( projects = models.TextField(max_length=500, blank=True, null=True) # META - contacted = models.BooleanField(default=False) # If a dubious application has been contacted yet - contacted_by = models.ForeignKey(User, related_name='contacted_by', blank=True, null=True, - on_delete=models.SET_NULL) + contacted = models.BooleanField( + default=False + ) # If a dubious application has been contacted yet + contacted_by = models.ForeignKey( + User, + related_name="contacted_by", + blank=True, + null=True, + on_delete=models.SET_NULL, + ) - reviewed = models.BooleanField(default=False) # If a blacklisted application has been reviewed yet - blacklisted_by = models.ForeignKey(User, related_name='blacklisted_by', blank=True, null=True, - on_delete=models.SET_NULL) + reviewed = models.BooleanField( + default=False + ) # If a blacklisted application has been reviewed yet + blacklisted_by = models.ForeignKey( + User, + related_name="blacklisted_by", + blank=True, + null=True, + on_delete=models.SET_NULL, + ) # Why do you want to come to X? description = models.TextField(max_length=500) # Reimbursement reimb = models.BooleanField(default=False) - reimb_amount = models.FloatField(blank=True, null=True, validators=[ - MinValueValidator(0, "Negative? Really? Please put a positive value"), - MaxValueValidator(200.0, "Do not exceed the maximum amount of 200")]) + reimb_amount = models.FloatField( + blank=True, + null=True, + validators=[ + MinValueValidator(0, "Negative? Really? Please put a positive value"), + MaxValueValidator(200.0, "Do not exceed the maximum amount of 200"), + ], + ) # Info for hardware hardware = models.CharField(max_length=300, null=True, blank=True) @@ -67,11 +84,13 @@ class HackerApplication( @classmethod def annotate_vote(cls, qs): - return qs.annotate(vote_avg=Avg('vote__calculated_vote')) + return qs.annotate(vote_avg=Avg("vote__calculated_vote")) def invalidate(self): if self.status != APP_DUBIOUS: - raise ValidationError('Applications can only be marked as invalid if they are dubious first') + raise ValidationError( + "Applications can only be marked as invalid if they are dubious first" + ) self.status = APP_INVALID self.save() @@ -80,7 +99,7 @@ def set_dubious(self): self.contacted = False self.status_update_date = timezone.now() self.vote_set.all().delete() - if hasattr(self, 'acceptedresume'): + if hasattr(self, "acceptedresume"): self.acceptedresume.delete() self.save() @@ -97,13 +116,16 @@ def set_contacted(self, user): def confirm_blacklist(self, user, motive_of_ban): if self.status != APP_BLACKLISTED: - raise ValidationError('Applications can only be confirmed as blacklisted if they are blacklisted first') + raise ValidationError( + "Applications can only be confirmed as blacklisted if they are blacklisted first" + ) self.status = APP_INVALID self.set_blacklisted_by(user) blacklist_user = BlacklistUser.objects.filter(email=self.user.email).first() if not blacklist_user: blacklist_user = BlacklistUser.objects.create_blacklist_user( - self.user, motive_of_ban) + self.user, motive_of_ban + ) blacklist_user.save() self.save() @@ -123,12 +145,18 @@ def set_blacklisted_by(self, user): def is_blacklisted(self): return self.status == APP_BLACKLISTED - + def can_be_edit(self, app_type="H"): - return self.status in [APP_PENDING, APP_DUBIOUS, APP_INVITED] and not self.vote_set.exists() and not \ - utils.is_app_closed(app_type) and self.submission_date + timedelta(hours=2) > timezone.now() + return ( + self.status in [APP_PENDING, APP_DUBIOUS, APP_INVITED] + and not self.vote_set.exists() + and not utils.is_app_closed(app_type) + and self.submission_date + timedelta(hours=2) > timezone.now() + ) class AcceptedResume(models.Model): - application = models.OneToOneField(HackerApplication, primary_key=True, on_delete=models.CASCADE) + application = models.OneToOneField( + HackerApplication, primary_key=True, on_delete=models.CASCADE + ) accepted = models.BooleanField() diff --git a/manage.py b/manage.py index 57cdfdf41..606fe0322 100755 --- a/manage.py +++ b/manage.py @@ -12,6 +12,7 @@ # exceptions on Python 2. try: import django + django except ImportError: raise ImportError( From 3b07a03f1e127e8ce2e4d5fbb0ccc15a4bb57101 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Thu, 4 Sep 2025 18:08:22 -0400 Subject: [PATCH 15/50] delete team --- applications/models/hacker.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/applications/models/hacker.py b/applications/models/hacker.py index 6592ef63a..9556b2fa7 100644 --- a/applications/models/hacker.py +++ b/applications/models/hacker.py @@ -70,9 +70,16 @@ def annotate_vote(cls, qs): return qs.annotate(vote_avg=Avg('vote__calculated_vote')) def invalidate(self): + """ + Marks the application as invalid, but only if its current status is "dubious". + Also, if the user has a team, it deletes it. + """ if self.status != APP_DUBIOUS: raise ValidationError('Applications can only be marked as invalid if they are dubious first') - self.status = APP_INVALID + self.status = APP_INVALID + team = getattr(self.user, 'team', None) + if team: + team.delete() self.save() def set_dubious(self): From c0fea5db1af428bb3e30aac496ea130e3f7df1f0 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Thu, 11 Sep 2025 18:28:10 -0400 Subject: [PATCH 16/50] Move "Delete Account" button --- user/templates/profile.html | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/user/templates/profile.html b/user/templates/profile.html index ad55927cb..d88a4940f 100644 --- a/user/templates/profile.html +++ b/user/templates/profile.html @@ -22,11 +22,22 @@

Personal data

{% endfor %} -

Delete account

-
-

You can delete your account with all the personal data from your user and applications. This action cannot be reverted.

- Delete account -
+ +
+ + Advanced options + +
+

Delete account

+
+ + You can delete your account with all the personal data from your user and applications. + This action cannot be reverted. + + Delete account +
+
+ {% endblock %} {% block out_panel %} From 99b9c4c4a58b10f5252ce323b36ac4ae2f20e8eb Mon Sep 17 00:00:00 2001 From: "enrique.andujar" Date: Fri, 26 Sep 2025 19:54:11 +0200 Subject: [PATCH 17/50] Comments --- teams/forms.py | 48 ++++++++++++++++++++++++++++++++++------------- teams/views.py | 51 +++++++++++++++++++++++++++++++------------------- 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/teams/forms.py b/teams/forms.py index 861b80c9f..c5706c9e0 100644 --- a/teams/forms.py +++ b/teams/forms.py @@ -6,26 +6,48 @@ class JoinTeamForm(forms.ModelForm): + """ + Form for joining an existing team. + Validates that the team exists and that it is not full. + If the team does not exist, it raises a validation error. + If the team is full, it raises a validation error. + """ + def clean_team_code(self): - team_code = self.cleaned_data['team_code'] + team_code = self.cleaned_data["team_code"] teammates = Team.objects.filter(team_code=team_code).count() if teammates == 0: - raise forms.ValidationError("No team exists with the current code. Did you want to create a team instead?") - max_teammates = getattr(settings, 'HACKATHON_MAX_TEAMMATES', 4) + raise forms.ValidationError( + "No team exists with the current code. Did you want to create a team instead?" + ) + max_teammates = getattr(settings, "HACKATHON_MAX_TEAMMATES", 4) if teammates == max_teammates: - raise forms.ValidationError("Full team. Max teammates is %d" % max_teammates) - if Team.objects.filter(team_code=team_code).exclude( - user__hackerapplication_application__status__in=[APP_PENDING, APP_LAST_REMIDER, APP_DUBIOUS]).exists(): - raise forms.ValidationError("Some members of this team are accepted or cancelled. You can't join their " - "team here but don't worry you still can join them on the event.") + raise forms.ValidationError( + "Full team. Max teammates is %d" % max_teammates + ) + if ( + Team.objects.filter(team_code=team_code) + .exclude( + user__hackerapplication_application__status__in=[ + APP_PENDING, + APP_LAST_REMIDER, + APP_DUBIOUS, + ] + ) + .exists() + ): + raise forms.ValidationError( + "Some members of this team are accepted or cancelled. You can't join their " + "team here but don't worry you still can join them on the event." + ) return team_code class Meta: model = Team - exclude = ['user', ] - labels = { - 'team_code': 'Your team code' - } + exclude = [ + "user", + ] + labels = {"team_code": "Your team code"} help_texts = { - 'team_code': 'Paste here the team code that your teammate has sent you' + "team_code": "Paste here the team code that your teammate has sent you" } diff --git a/teams/views.py b/teams/views.py index 9e7b1f892..7be4c858b 100644 --- a/teams/views.py +++ b/teams/views.py @@ -10,41 +10,54 @@ class HackerTeam(IsHackerMixin, TabsView): - template_name = 'team.html' + """ + View for hackers to manage their team. + Hackers can create a new team, join an existing team or leave their current team, + by making a POST request with theappropriate action. + """ + + template_name = "team.html" def get_current_tabs(self): return hacker_tabs(self.request.user) def get_context_data(self, **kwargs): c = super(HackerTeam, self).get_context_data(**kwargs) - team = getattr(self.request.user, 'team', None) - app = getattr(self.request.user, 'hackerapplication_application', None) + team = getattr(self.request.user, "team", None) + app = getattr(self.request.user, "hackerapplication_application", None) teammates = [] if team: - teammates = models.Team.objects.filter(team_code=team.team_code) \ - .values('user__name', 'user__email', 'user__hackerapplication_application') - teammates = list(map(lambda x: - {'name': x['user__name'], 'email': x['user__email'], - 'app': x['user__hackerapplication_application']}, - teammates)) + teammates = models.Team.objects.filter(team_code=team.team_code).values( + "user__name", "user__email", "user__hackerapplication_application" + ) + teammates = list( + map( + lambda x: { + "name": x["user__name"], + "email": x["user__email"], + "app": x["user__hackerapplication_application"], + }, + teammates, + ) + ) instance = models.Team() - instance.team_code = '' + instance.team_code = "" form = forms.JoinTeamForm(instance=instance) - c.update({'team': team, 'teammates': teammates, 'app': app, 'form': form}) + c.update({"team": team, "teammates": teammates, "app": app, "form": form}) return c def post(self, request, *args, **kwargs): - if request.POST.get('create', None): + if request.POST.get("create", None): team = models.Team() team.user = request.user team.save() - return HttpResponseRedirect(reverse('teams')) - if request.POST.get('leave', None): - team = getattr(request.user, 'team', None) + return HttpResponseRedirect(reverse("teams")) + if request.POST.get("leave", None): + team = getattr(request.user, "team", None) if team: team.delete() - return HttpResponseRedirect(reverse('teams')) + return HttpResponseRedirect(reverse("teams")) else: form = forms.JoinTeamForm(request.POST, request.FILES) if form.is_valid(): @@ -52,10 +65,10 @@ def post(self, request, *args, **kwargs): team.user = request.user team.save() - messages.success(request, 'Team joined successfully!') + messages.success(request, "Team joined successfully!") - return HttpResponseRedirect(reverse('teams')) + return HttpResponseRedirect(reverse("teams")) else: c = self.get_context_data() - c.update({'form': form}) + c.update({"form": form}) return render(request, self.template_name, c) From d1a909aeb6a809ae4fca46f8ce423f69bca3ceae Mon Sep 17 00:00:00 2001 From: "enrique.andujar" Date: Fri, 26 Sep 2025 19:54:48 +0200 Subject: [PATCH 18/50] Changed team codes to uuids --- teams/migrations/0002_alter_team_team_code.py | 19 +++++++++++++++++ teams/models.py | 21 ++++++++++--------- 2 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 teams/migrations/0002_alter_team_team_code.py diff --git a/teams/migrations/0002_alter_team_team_code.py b/teams/migrations/0002_alter_team_team_code.py new file mode 100644 index 000000000..7f526a1e2 --- /dev/null +++ b/teams/migrations/0002_alter_team_team_code.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.23 on 2025-09-26 17:46 + +from django.db import migrations, models +import teams.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0001_teams'), + ] + + operations = [ + migrations.AlterField( + model_name='team', + name='team_code', + field=models.CharField(default=teams.models.generate_team_id, max_length=36), + ), + ] diff --git a/teams/models.py b/teams/models.py index fd402972c..9c217a465 100644 --- a/teams/models.py +++ b/teams/models.py @@ -1,23 +1,24 @@ import random -import string +import string, uuid from django.db import models from user.models import User -TEAM_ID_LENGTH = 13 -MAX_ATTEMPTS = 33 +MAX_LENGTH_TEAM_CODE = 36 def generate_team_id(): - s = string.ascii_letters + string.digits # 62 caràcters únics - for _ in range(MAX_ATTEMPTS): - team_id = "".join(random.choices(s, k=TEAM_ID_LENGTH)) - if not Team.objects.filter(team_code=team_id).exists(): - return team_id - raise Exception("No s'ha pogut generar un team_code únic després de diversos intents.") + return str(uuid.uuid4()) class Team(models.Model): - team_code = models.CharField(default=generate_team_id, max_length=TEAM_ID_LENGTH) + """ + This model represents a team of hackers. For each hacker in a team, there is one entry + in this table with the same team_code. The user field is a foreign key to the user + """ + + team_code = models.CharField( + default=generate_team_id, max_length=MAX_LENGTH_TEAM_CODE + ) user = models.OneToOneField(User, on_delete=models.CASCADE) class Meta: From e99e4ace2bd03adcf4bf9e2648a1a6cfee1993f8 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Tue, 30 Sep 2025 19:34:26 -0400 Subject: [PATCH 19/50] fixed errors --- organizers/templates/application_detail.html | 6 ------ organizers/views.py | 4 +++- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/organizers/templates/application_detail.html b/organizers/templates/application_detail.html index c73e37632..451e340e7 100644 --- a/organizers/templates/application_detail.html +++ b/organizers/templates/application_detail.html @@ -338,12 +338,6 @@ Blacklist Application {% endif %} - {% if h_blacklist_enabled %} - - {% endif %}
diff --git a/organizers/views.py b/organizers/views.py index aed78d616..197d3be9b 100644 --- a/organizers/views.py +++ b/organizers/views.py @@ -514,7 +514,7 @@ def post(self, request, *args, **kwargs): "/applications/hacker/review/" + application.uuid_str ) elif request.POST.get("set_dubious"): - application.set_dubious() + application.set_dubious(request.user, dubious_type, dubious_comment_text) elif request.POST.get("unset_dubious"): application.unset_dubious() elif request.POST.get("set_blacklist") and request.user.is_organizer: @@ -600,6 +600,8 @@ def post(self, request, *args, **kwargs): tech_vote = request.POST.get("tech_rat", None) pers_vote = request.POST.get("pers_rat", None) comment_text = request.POST.get("comment_text", None) + dubious_type = request.POST.get('dubious_type', None) + dubious_comment_text = request.POST.get('dubious_comment_text', None) application = models.HackerApplication.objects.get( pk=request.POST.get("app_id") From 11a9b9f6c346a1a719326cb96cafac6c3937e80a Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Sat, 4 Oct 2025 19:45:48 -0400 Subject: [PATCH 20/50] Qr-time --- meals/templates/meal_checkin.html | 58 ++++++++++++------------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/meals/templates/meal_checkin.html b/meals/templates/meal_checkin.html index 26e2a5c64..cd3054348 100644 --- a/meals/templates/meal_checkin.html +++ b/meals/templates/meal_checkin.html @@ -45,33 +45,36 @@

{{ meal_name }}

+
From 097dc90bed9205593748dfde449018392656ff5e Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Tue, 7 Oct 2025 19:56:42 -0400 Subject: [PATCH 21/50] blacklist delete team --- applications/models/hacker.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/applications/models/hacker.py b/applications/models/hacker.py index 9556b2fa7..468905660 100644 --- a/applications/models/hacker.py +++ b/applications/models/hacker.py @@ -103,6 +103,10 @@ def set_contacted(self, user): self.save() def confirm_blacklist(self, user, motive_of_ban): + """ + Confirms the application as blacklisted, but only if its current status is "APP_BLACKLISTED". + Also, if the user has a team, it deletes it. + """ if self.status != APP_BLACKLISTED: raise ValidationError('Applications can only be confirmed as blacklisted if they are blacklisted first') self.status = APP_INVALID @@ -112,6 +116,9 @@ def confirm_blacklist(self, user, motive_of_ban): blacklist_user = BlacklistUser.objects.create_blacklist_user( self.user, motive_of_ban) blacklist_user.save() + team = getattr(self.user, 'team', None) + if team: + team.delete() self.save() def set_blacklist(self): From 79bfdd1ef702e33615ca71f9ec43c283e06ebd3e Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Sun, 12 Oct 2025 18:54:49 -0400 Subject: [PATCH 22/50] Add: cv_flagged and functions to set and unset csv_flagged --- applications/models/hacker.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/applications/models/hacker.py b/applications/models/hacker.py index 4e9ee0969..79533721b 100644 --- a/applications/models/hacker.py +++ b/applications/models/hacker.py @@ -74,6 +74,7 @@ class HackerApplication(BaseApplication): hardware = models.CharField(max_length=300, null=True, blank=True) cvs_edition = models.BooleanField(default=False) + cv_flagged = models.BooleanField(default=False) resume = models.FileField( upload_to=resume_path_hackers, @@ -107,6 +108,15 @@ def unset_dubious(self): self.status = APP_PENDING self.status_update_date = timezone.now() self.save() + + def set_flagged_cv(self): + self.cv_flagged = True + self.save() + + def unset_flagged_cv(self): + if self.cv_flagged: + self.cv_flagged = False + self.save() def set_contacted(self, user): if not self.contacted: From e7462cbb1c0ed2467da17898e9a30f3501651e88 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Sun, 12 Oct 2025 18:55:29 -0400 Subject: [PATCH 23/50] Add: Button for flag CV --- organizers/templates/application_detail.html | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/organizers/templates/application_detail.html b/organizers/templates/application_detail.html index b0f4a345c..bd137fe92 100644 --- a/organizers/templates/application_detail.html +++ b/organizers/templates/application_detail.html @@ -43,7 +43,21 @@

Personal

{% include 'include/field.html' with desc='Under age (-18)' value=app.under_age|yesno:'Yes,No,Maybe' %} {% if app.resume %}
Resume
-
{{ app.resume.name }}
+
+ {{ app.resume.name }} + {% if app.cv_flagged %} + CV already flagged + {% else %} +
+ {% csrf_token %} + + +
+ {% endif %} +
{% endif %}
From 7bfcc8705e268e76dad63e229efe2607350fbcd5 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Sun, 12 Oct 2025 19:00:18 -0400 Subject: [PATCH 24/50] Add: Post set_flagged_cv and update filter get_context_data in ReviewResume --- organizers/views.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/organizers/views.py b/organizers/views.py index 7458e5e81..dd4d5e1d5 100644 --- a/organizers/views.py +++ b/organizers/views.py @@ -73,12 +73,20 @@ def add_vote(application, user, tech_rat, pers_rat): + """ + Save the vote of the application and + if the number of votes is >= 5 and the CV is not flagged, create an AcceptedResume + """ v = models.Vote() v.user = user v.application = application v.tech = tech_rat v.personal = pers_rat v.save() + + votes_count = application.vote_set.count() + if votes_count >= 1 and not application.cv_flagged: + AcceptedResume.objects.update_or_create(application=application, defaults={'accepted': True}) return v @@ -368,6 +376,10 @@ def post(self, request, *args, **kwargs): self.slack_invite(application) elif request.POST.get("set_dubious") and request.user.is_organizer: application.set_dubious() + elif request.POST.get("set_flagged_cv") and request.user.is_organizer: + application.set_flagged_cv() + elif request.POST.get("unset_flagged_cv") and request.user.is_organizer: + application.unset_flagged_cv() elif request.POST.get("contact_user") and request.user.has_dubious_access: application.set_contacted(request.user) elif request.POST.get("unset_dubious") and request.user.has_dubious_access: @@ -512,6 +524,10 @@ def post(self, request, *args, **kwargs): application.set_dubious() elif request.POST.get("unset_dubious"): application.unset_dubious() + elif request.POST.get("set_flagged_cv") and request.user.is_organizer: + application.set_flagged_cv() + elif request.POST.get("unset_flagged_cv") and request.user.is_organizer: + application.unset_flagged_cv() elif request.POST.get("set_blacklist") and request.user.is_organizer: application.set_blacklist() elif ( @@ -611,6 +627,10 @@ def post(self, request, *args, **kwargs): application.set_dubious() elif request.POST.get("unset_dubious"): application.unset_dubious() + elif request.POST.get("set_flagged_cv") and request.user.is_organizer: + application.set_flagged_cv() + elif request.POST.get("unset_flagged_cv") and request.user.is_organizer: + application.unset_flagged_cv() elif request.POST.get("set_blacklist") and request.user.is_organizer: application.set_blacklist() elif ( @@ -1053,7 +1073,7 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) app = ( models.HackerApplication.objects.filter( - acceptedresume__isnull=True, cvs_edition=True + acceptedresume__isnull=True, cv_flagged=True, resume__isnull=False ) .exclude(status__in=[APP_DUBIOUS, APP_BLACKLISTED]) .first() From e8637d3a712f2df800964da52603e9579548a454 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Sun, 12 Oct 2025 19:01:25 -0400 Subject: [PATCH 25/50] migration hacker --- .../migrations/0058_auto_20251012_1733.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 applications/migrations/0058_auto_20251012_1733.py diff --git a/applications/migrations/0058_auto_20251012_1733.py b/applications/migrations/0058_auto_20251012_1733.py new file mode 100644 index 000000000..1ad43062d --- /dev/null +++ b/applications/migrations/0058_auto_20251012_1733.py @@ -0,0 +1,84 @@ +# Generated by Django 3.2.23 on 2025-10-12 17:33 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0057_auto_20250203_2145'), + ] + + operations = [ + migrations.AddField( + model_name='hackerapplication', + name='cv_flagged', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='hackerapplication', + name='diet', + field=models.CharField(choices=[('', '- Select a diet -'), ('None', 'No requirements'), ('Vegetarian', 'Vegetarian'), ('Vegan', 'Vegan'), ('Gluten-free', 'Gluten-free'), ('Others', 'Others')], default='None', max_length=300), + ), + migrations.AlterField( + model_name='hackerapplication', + name='lennyface', + field=models.CharField(default='-.-', max_length=20), + ), + migrations.AlterField( + model_name='hackerapplication', + name='phone_number', + field=models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(message="Phone number must be entered in the format: '+#########'. Up to 16 digits allowed.", regex='^\\+?1?\\d{9,15}$')]), + ), + migrations.AlterField( + model_name='hackerapplication', + name='tshirt_size', + field=models.CharField(choices=[('', '- Select a t-shirt size -'), ('XS', 'Unisex - XS'), ('S', 'Unisex - S'), ('M', 'Unisex - M'), ('L', 'Unisex - L'), ('XL', 'Unisex - XL'), ('XXL', 'Unisex - XXL'), ('XXXL', 'Unisex - XXXL')], default='', max_length=300), + ), + migrations.AlterField( + model_name='mentorapplication', + name='diet', + field=models.CharField(choices=[('', '- Select a diet -'), ('None', 'No requirements'), ('Vegetarian', 'Vegetarian'), ('Vegan', 'Vegan'), ('Gluten-free', 'Gluten-free'), ('Others', 'Others')], default='None', max_length=300), + ), + migrations.AlterField( + model_name='mentorapplication', + name='lennyface', + field=models.CharField(default='-.-', max_length=20), + ), + migrations.AlterField( + model_name='mentorapplication', + name='phone_number', + field=models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(message="Phone number must be entered in the format: '+#########'. Up to 16 digits allowed.", regex='^\\+?1?\\d{9,15}$')]), + ), + migrations.AlterField( + model_name='mentorapplication', + name='tshirt_size', + field=models.CharField(choices=[('', '- Select a t-shirt size -'), ('XS', 'Unisex - XS'), ('S', 'Unisex - S'), ('M', 'Unisex - M'), ('L', 'Unisex - L'), ('XL', 'Unisex - XL'), ('XXL', 'Unisex - XXL'), ('XXXL', 'Unisex - XXXL')], default='', max_length=300), + ), + migrations.AlterField( + model_name='sponsorapplication', + name='diet', + field=models.CharField(choices=[('', '- Select a diet -'), ('None', 'No requirements'), ('Vegetarian', 'Vegetarian'), ('Vegan', 'Vegan'), ('Gluten-free', 'Gluten-free'), ('Others', 'Others')], default='None', max_length=300), + ), + migrations.AlterField( + model_name='sponsorapplication', + name='tshirt_size', + field=models.CharField(choices=[('', '- Select a t-shirt size -'), ('XS', 'Unisex - XS'), ('S', 'Unisex - S'), ('M', 'Unisex - M'), ('L', 'Unisex - L'), ('XL', 'Unisex - XL'), ('XXL', 'Unisex - XXL'), ('XXXL', 'Unisex - XXXL')], default='', max_length=300), + ), + migrations.AlterField( + model_name='volunteerapplication', + name='lennyface', + field=models.CharField(default='-.-', max_length=20), + ), + migrations.AlterField( + model_name='volunteerapplication', + name='phone_number', + field=models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(message="Phone number must be entered in the format: '+#########'. Up to 16 digits allowed.", regex='^\\+?1?\\d{9,15}$')]), + ), + migrations.AlterField( + model_name='volunteerapplication', + name='tshirt_size', + field=models.CharField(choices=[('', '- Select a t-shirt size -'), ('XS', 'Unisex - XS'), ('S', 'Unisex - S'), ('M', 'Unisex - M'), ('L', 'Unisex - L'), ('XL', 'Unisex - XL'), ('XXL', 'Unisex - XXL'), ('XXXL', 'Unisex - XXXL')], default='', max_length=300), + ), + ] From 99f25455a0efbcec599ddea023e10dd8dc7834f9 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Sun, 12 Oct 2025 19:05:39 -0400 Subject: [PATCH 26/50] lint --- organizers/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/organizers/views.py b/organizers/views.py index dd4d5e1d5..7718a7c3f 100644 --- a/organizers/views.py +++ b/organizers/views.py @@ -83,7 +83,6 @@ def add_vote(application, user, tech_rat, pers_rat): v.tech = tech_rat v.personal = pers_rat v.save() - votes_count = application.vote_set.count() if votes_count >= 1 and not application.cv_flagged: AcceptedResume.objects.update_or_create(application=application, defaults={'accepted': True}) From d396a73f91e7f117c7051a34e4e6f989a5c52a97 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Sun, 12 Oct 2025 19:24:12 -0400 Subject: [PATCH 27/50] lint --- teams/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/teams/models.py b/teams/models.py index 9c217a465..a26171e41 100644 --- a/teams/models.py +++ b/teams/models.py @@ -1,5 +1,4 @@ -import random -import string, uuid +import uuid from django.db import models from user.models import User From 3d5b0705ad8a956afa72167149836390b154a800 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Wed, 22 Oct 2025 20:55:06 -0400 Subject: [PATCH 28/50] funciona correctament --- applications/models/hacker.py | 9 ++++----- organizers/templates/application_detail.html | 6 +++--- organizers/views.py | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/applications/models/hacker.py b/applications/models/hacker.py index 79533721b..92e7218d3 100644 --- a/applications/models/hacker.py +++ b/applications/models/hacker.py @@ -110,14 +110,13 @@ def unset_dubious(self): self.save() def set_flagged_cv(self): + """Sets the CV as flagged for review. If there was an accepted + resume, deletes it so it can be reviewed.""" self.cv_flagged = True + if hasattr(self, 'acceptedresume'): + self.acceptedresume.delete() self.save() - def unset_flagged_cv(self): - if self.cv_flagged: - self.cv_flagged = False - self.save() - def set_contacted(self, user): if not self.contacted: self.contacted = True diff --git a/organizers/templates/application_detail.html b/organizers/templates/application_detail.html index bd137fe92..117c254f9 100644 --- a/organizers/templates/application_detail.html +++ b/organizers/templates/application_detail.html @@ -46,14 +46,14 @@

Personal

{{ app.resume.name }} {% if app.cv_flagged %} - CV already flagged +

CV already flagged for review

{% else %}
{% csrf_token %} -
{% endif %} diff --git a/organizers/views.py b/organizers/views.py index 7718a7c3f..996c78f27 100644 --- a/organizers/views.py +++ b/organizers/views.py @@ -84,7 +84,7 @@ def add_vote(application, user, tech_rat, pers_rat): v.personal = pers_rat v.save() votes_count = application.vote_set.count() - if votes_count >= 1 and not application.cv_flagged: + if votes_count >= 5 and not application.cv_flagged: AcceptedResume.objects.update_or_create(application=application, defaults={'accepted': True}) return v From 22a9d58401088e16ab960e6297ef017da6560843 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Fri, 24 Oct 2025 10:18:01 -0400 Subject: [PATCH 29/50] add: migration --- ...014_alter_reimbursement_expiration_time.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 reimbursement/migrations/0014_alter_reimbursement_expiration_time.py diff --git a/reimbursement/migrations/0014_alter_reimbursement_expiration_time.py b/reimbursement/migrations/0014_alter_reimbursement_expiration_time.py new file mode 100644 index 000000000..7664825d7 --- /dev/null +++ b/reimbursement/migrations/0014_alter_reimbursement_expiration_time.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.23 on 2025-10-24 14:16 + +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('reimbursement', '0013_alter_reimbursement_devpost'), + ] + + operations = [ + migrations.AlterField( + model_name='reimbursement', + name='expiration_time', + field=models.DateTimeField(default=datetime.datetime(2025, 5, 2, 16, 0, tzinfo=utc)), + ), + ] From ccaea782a2b033b4280c3b81fe498959df941f91 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Mon, 8 Dec 2025 20:30:52 -0500 Subject: [PATCH 30/50] change graduate year --- applications/models/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/models/constants.py b/applications/models/constants.py index dded778cb..958defcf0 100644 --- a/applications/models/constants.py +++ b/applications/models/constants.py @@ -92,10 +92,10 @@ ] HACK_NAME = getattr(hackathon_variables, 'HACKATHON_NAME', "HackAssistant") -EXTRA_NAME = [' 2016 Fall', ' 2016 Winter', ' 2017 Fall', ' 2017 Winter', ' 2018', ' 2019', ' 2021', ' 2022', ' 2023', ' 2024'] +EXTRA_NAME = [' 2016 Fall', ' 2016 Winter', ' 2017 Fall', ' 2017 Winter', ' 2018', ' 2019', ' 2021', ' 2022', ' 2023', ' 2024', ' 2025'] PREVIOUS_HACKS = [(i, HACK_NAME + EXTRA_NAME[i]) for i in range(0, len(EXTRA_NAME))] -YEARS = [(int(size), size) for size in ('2024 2025 2026 2027 2028 2029 2030 2031'.split(' '))] +YEARS = [(int(size), size) for size in ('2025 2026 2027 2028 2029 2030 2031 2032'.split(' '))] DEFAULT_YEAR = datetime.now().year + 1 ENGLISH_LEVEL = [(i, str(i)) for i in range(1, 5 + 1)] From d97cb30f94c20f817aab51231cd1643d23225d91 Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Mon, 8 Dec 2025 23:14:08 -0500 Subject: [PATCH 31/50] canvi pregunta --- applications/forms/common_fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/forms/common_fields.py b/applications/forms/common_fields.py index b0b963474..8d1f3f28a 100644 --- a/applications/forms/common_fields.py +++ b/applications/forms/common_fields.py @@ -16,7 +16,7 @@ def common_university(): return forms.CharField( required=True, max_length= 70, - label="What university do you study at?", + label="What center do you study at?", help_text="Current or most recent school you attended.", widget=forms.TextInput( attrs={"class": "typeahead-schools", "autocomplete": "off"} From 27461170e44e5da4322f91727aa107b2426b3b3d Mon Sep 17 00:00:00 2001 From: Yaoyao Date: Tue, 9 Dec 2025 19:46:00 +0100 Subject: [PATCH 32/50] removed pronoun question in volunteerapplication --- applications/forms/volunteer.py | 5 ----- ...0059_remove_volunteerapplication_pronouns.py | 17 +++++++++++++++++ applications/models/volunteer.py | 1 - .../templates/other_application_detail.html | 1 - 4 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 applications/migrations/0059_remove_volunteerapplication_pronouns.py diff --git a/applications/forms/volunteer.py b/applications/forms/volunteer.py index a173190e4..bf3f887fa 100644 --- a/applications/forms/volunteer.py +++ b/applications/forms/volunteer.py @@ -81,7 +81,6 @@ def __init__(self, *args, **kwargs): bootstrap_field_info = { "👤 Información Personal": { "fields": [ - {"name": "pronouns", "space": 12}, {"name": "gender", "space": 12}, {"name": "other_gender", "space": 12}, {"name": "under_age", "space": 12}, @@ -238,9 +237,6 @@ def clean_hear_about_us(self): "friends": forms.Textarea(attrs={"rows": 2, "cols": 40}), "weakness": forms.Textarea(attrs={"rows": 2, "cols": 40}), "quality": forms.Textarea(attrs={"rows": 2, "cols": 40}), - "pronouns": forms.TextInput( - attrs={"autocomplete": "off", "placeholder": "their/them"} - ), "graduation_year": forms.HiddenInput(), "phone_number": forms.HiddenInput(), "hear_about_us": CustomSelect(choices=models.HEARABOUTUS_ES), @@ -249,7 +245,6 @@ def clean_hear_about_us(self): } labels = { - "pronouns": "¿Cuáles son tus pronombres?", "gender": " ¿Con qué género te identificas?", "other_gender": "Me quiero describir", "graduation_year": "What year will you graduate?", diff --git a/applications/migrations/0059_remove_volunteerapplication_pronouns.py b/applications/migrations/0059_remove_volunteerapplication_pronouns.py new file mode 100644 index 000000000..43b13f71b --- /dev/null +++ b/applications/migrations/0059_remove_volunteerapplication_pronouns.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.23 on 2025-12-09 18:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0058_auto_20251012_1733'), + ] + + operations = [ + migrations.RemoveField( + model_name='volunteerapplication', + name='pronouns', + ), + ] diff --git a/applications/models/volunteer.py b/applications/models/volunteer.py index ad55e7518..8426518a5 100644 --- a/applications/models/volunteer.py +++ b/applications/models/volunteer.py @@ -100,7 +100,6 @@ class VolunteerApplication(BaseApplication): weakness = models.CharField(max_length=150, null=False) friends = models.CharField(max_length=100, null=True, blank=True) - pronouns = models.CharField(max_length=100, null=True, blank=True) night_shifts = MultiSelectField(choices=NIGHT_SHIFT_ES, default='No') volunteer_motivation = models.CharField(max_length=500) valid = models.BooleanField(default=True) diff --git a/organizers/templates/other_application_detail.html b/organizers/templates/other_application_detail.html index 8a35ce320..fa45f5a8d 100644 --- a/organizers/templates/other_application_detail.html +++ b/organizers/templates/other_application_detail.html @@ -31,7 +31,6 @@

Personal

{% include 'include/field.html' with desc='Status' value=app.get_status_display %} {% include 'include/field.html' with desc='Email' value=app.user.email %} {% if app.user.is_volunteer %} - {% include 'include/field.html' with desc='Pronouns' value=app.pronouns %} {% include 'include/field.html' with desc='Gender' value=app.get_gender_display %} {% include 'include/field.html' with desc='Other gender' value=app.other_gender %} {% include 'include/field.html' with desc='In BCN Apr-May' value=app.lennyface|yesno %} From c6207dcc5bc748ea43f037a70bd2b6f8f8026cb5 Mon Sep 17 00:00:00 2001 From: Else Date: Tue, 9 Dec 2025 20:28:07 +0100 Subject: [PATCH 33/50] mentor modifications --- applications/forms/mentor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/forms/mentor.py b/applications/forms/mentor.py index 51e5e52cc..4cb552ac9 100644 --- a/applications/forms/mentor.py +++ b/applications/forms/mentor.py @@ -102,7 +102,7 @@ def clean_projects(self): company = forms.CharField( required=False, help_text="Backend developer, DevOps…", - label="What is your current role?", + label="What is your current role or most recent role?", ) #university = forms.CharField( From cf79d4360dbfaf123666315f9d814ca6a3bc988b Mon Sep 17 00:00:00 2001 From: Yaoyao Date: Tue, 9 Dec 2025 22:48:26 +0100 Subject: [PATCH 34/50] =?UTF-8?q?modified=20and=20corrected=20under=5Fage?= =?UTF-8?q?=20question=20+=20=C2=BF=C2=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- applications/forms/volunteer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/applications/forms/volunteer.py b/applications/forms/volunteer.py index a173190e4..1d41d5729 100644 --- a/applications/forms/volunteer.py +++ b/applications/forms/volunteer.py @@ -30,10 +30,10 @@ class VolunteerApplicationForm(_BaseApplicationForm): ) under_age = forms.TypedChoiceField( required=True, - label="¿Tienes o tendrás la mayoría de edad antes de la fecha del evento?", - initial=True, + label="¿Serás mayor de edad en la fecha del evento?", + initial=False, coerce=lambda x: x == "True", - choices=((True, "Sí"),(False, "No")), + choices=((False, "Sí"),(True, "No")), widget=forms.RadioSelect, ) night_shifts = forms.TypedChoiceField( @@ -98,7 +98,7 @@ def __init__(self, *args, **kwargs): {"name": "attendance", "space": 12}, {"name": "volunteer_motivation", "space": 12}, ], - "description": "Has participado en eventos similares? Cuéntanos más!" + "description": "¿Has participado en eventos similares? ¡Cuéntanos más!" }, "❓ Otras Preguntas": { "fields": [ From 64901857eb0c71586692568574c7045c7761004a Mon Sep 17 00:00:00 2001 From: "enrique.andujar" Date: Wed, 10 Dec 2025 22:43:41 +0100 Subject: [PATCH 35/50] Year changed to 2026 --- applications/forms/hacker.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/applications/forms/hacker.py b/applications/forms/hacker.py index 2fbbd9dda..a87ecfe90 100644 --- a/applications/forms/hacker.py +++ b/applications/forms/hacker.py @@ -2,7 +2,6 @@ from .base import _BaseApplicationForm - class HackerApplicationForm(_BaseApplicationForm): bootstrap_field_info = { "🎓 Education Info": { @@ -59,11 +58,13 @@ class HackerApplicationForm(_BaseApplicationForm): }, } - # make phone mandatory, override the base form - phone_number = forms.CharField(required=True, widget=forms.TextInput( - attrs={'class': 'form-control', 'placeholder': '+#########'}), - label='Phone number', + phone_number = forms.CharField( + required=True, + widget=forms.TextInput( + attrs={"class": "form-control", "placeholder": "+#########"} + ), + label="Phone number", ) github = social_media_field("github", "https://github.com/biene") @@ -129,7 +130,6 @@ def clean_projects(self): ) return data - first_timer = common_first_timer() university = common_university() @@ -138,7 +138,7 @@ def clean_projects(self): cvs_edition = forms.BooleanField( required=False, - label='I authorize "Hackers at UPC" to share my CV with HackUPC 2025 Sponsors.', + label='I authorize "Hackers at UPC" to share my CV with HackUPC 2026 Sponsors.', ) def __init__( @@ -191,7 +191,9 @@ def get_bootstrap_field_info(self): personal_info_fields = fields["👤 Personal Info"]["fields"] logistics_info_fields = fields["🚚 Logistics Info"]["fields"] hackathons_fields = fields["🏆 Hackathons"]["fields"] - show_us_what_youve_built_fields = fields["💻 Show us what you've built"]["fields"] + show_us_what_youve_built_fields = fields["💻 Show us what you've built"][ + "fields" + ] polices_fields = fields["📜 HackUPC Policies"]["fields"] personal_info_fields.append({"name": "online", "space": 12}) if not hybrid: @@ -237,17 +239,15 @@ class Meta(_BaseApplicationForm.Meta): "other_diet": "Please fill here in your dietary requirements. We want to make sure we have food for you!", "lennyface": 'tip: you can chose from here ' " http://textsmili.es/", - "description": "
" + "description": "
" "Be original! Using AI to answer this question might penalize your application.", - "projects": - "Tell us about your personal projects, awards, or any work that you are proud of.
" - "", + "projects": "Tell us about your personal projects, awards, or any work that you are proud of.
" + "", "resume": "Accepted file formats: %s" % (", ".join(extensions) if extensions else "Any"), "origin": "If you don’t see your city, choose the closest one!
Please type following this schema: city, province, country", } - class CustomSelect(forms.Select): def create_option( self, name, value, label, selected, index, subindex=None, attrs=None @@ -278,8 +278,10 @@ def clean_discover(self): widgets = { "origin": forms.TextInput(attrs={"autocomplete": "off"}), - "description": forms.Textarea(attrs={"rows": 3, "cols": 40, 'id': 'description'}), - "projects": forms.Textarea(attrs={"rows": 3, "cols": 40, 'id': 'projects'}), + "description": forms.Textarea( + attrs={"rows": 3, "cols": 40, "id": "description"} + ), + "projects": forms.Textarea(attrs={"rows": 3, "cols": 40, "id": "projects"}), "discover": CustomSelect(choices=discover_choices), "tshirt_size": forms.Select(), "diet": forms.Select(), From ab61450fc6bc57228b1559d4240aa50f81117e53 Mon Sep 17 00:00:00 2001 From: "enrique.andujar" Date: Wed, 10 Dec 2025 23:10:05 +0100 Subject: [PATCH 36/50] Question does no longer disapear --- .../templates/include/application_form.html | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/applications/templates/include/application_form.html b/applications/templates/include/application_form.html index a40ea637a..405bb2743 100644 --- a/applications/templates/include/application_form.html +++ b/applications/templates/include/application_form.html @@ -293,15 +293,16 @@ var experienced = $('input[name="first_timer"][value="False"]'); var projects = $('#projects'); - conditional_field(projects, is_firsttime, function () { - return experienced.prop("checked"); - }, 1); - conditional_field(projects, experienced, function () { - return experienced.prop("checked"); - }, 1); - - // Making projects look like required - projects.parent().addClass('required'); + + + // Refresh projects field when first_timer changes + $('input[name="first_timer"]').change(function() { + if ($(this).val() === "False") { + projects.parent().addClass('required'); + } else { + projects.parent().removeClass('required'); + } + }); function setUpCharCounterOn(elementStrId, elementCharCounterStrId){ From aaaff26f05966db16eb59a24aea0e411d89e6a2b Mon Sep 17 00:00:00 2001 From: "enrique.andujar" Date: Wed, 10 Dec 2025 23:35:43 +0100 Subject: [PATCH 37/50] Added helper text, and modified regex to permit spaces --- applications/forms/hacker.py | 31 +++++++++++++++++-------------- applications/models/base.py | 4 ++-- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/applications/forms/hacker.py b/applications/forms/hacker.py index 2fbbd9dda..e208e7a74 100644 --- a/applications/forms/hacker.py +++ b/applications/forms/hacker.py @@ -2,7 +2,6 @@ from .base import _BaseApplicationForm - class HackerApplicationForm(_BaseApplicationForm): bootstrap_field_info = { "🎓 Education Info": { @@ -59,11 +58,14 @@ class HackerApplicationForm(_BaseApplicationForm): }, } - # make phone mandatory, override the base form - phone_number = forms.CharField(required=True, widget=forms.TextInput( - attrs={'class': 'form-control', 'placeholder': '+#########'}), - label='Phone number', + phone_number = forms.CharField( + required=True, + widget=forms.TextInput( + attrs={"class": "form-control", "placeholder": "+#########"} + ), + label="Phone number", + help_text="Don't worry, we won't call you or use it to contact you.", ) github = social_media_field("github", "https://github.com/biene") @@ -129,7 +131,6 @@ def clean_projects(self): ) return data - first_timer = common_first_timer() university = common_university() @@ -191,7 +192,9 @@ def get_bootstrap_field_info(self): personal_info_fields = fields["👤 Personal Info"]["fields"] logistics_info_fields = fields["🚚 Logistics Info"]["fields"] hackathons_fields = fields["🏆 Hackathons"]["fields"] - show_us_what_youve_built_fields = fields["💻 Show us what you've built"]["fields"] + show_us_what_youve_built_fields = fields["💻 Show us what you've built"][ + "fields" + ] polices_fields = fields["📜 HackUPC Policies"]["fields"] personal_info_fields.append({"name": "online", "space": 12}) if not hybrid: @@ -237,17 +240,15 @@ class Meta(_BaseApplicationForm.Meta): "other_diet": "Please fill here in your dietary requirements. We want to make sure we have food for you!", "lennyface": 'tip: you can chose from here ' " http://textsmili.es/", - "description": "
" + "description": "
" "Be original! Using AI to answer this question might penalize your application.", - "projects": - "Tell us about your personal projects, awards, or any work that you are proud of.
" - "", + "projects": "Tell us about your personal projects, awards, or any work that you are proud of.
" + "", "resume": "Accepted file formats: %s" % (", ".join(extensions) if extensions else "Any"), "origin": "If you don’t see your city, choose the closest one!
Please type following this schema: city, province, country", } - class CustomSelect(forms.Select): def create_option( self, name, value, label, selected, index, subindex=None, attrs=None @@ -278,8 +279,10 @@ def clean_discover(self): widgets = { "origin": forms.TextInput(attrs={"autocomplete": "off"}), - "description": forms.Textarea(attrs={"rows": 3, "cols": 40, 'id': 'description'}), - "projects": forms.Textarea(attrs={"rows": 3, "cols": 40, 'id': 'projects'}), + "description": forms.Textarea( + attrs={"rows": 3, "cols": 40, "id": "description"} + ), + "projects": forms.Textarea(attrs={"rows": 3, "cols": 40, "id": "projects"}), "discover": CustomSelect(choices=discover_choices), "tshirt_size": forms.Select(), "diet": forms.Select(), diff --git a/applications/models/base.py b/applications/models/base.py index 5dd56d74a..68d66f34d 100644 --- a/applications/models/base.py +++ b/applications/models/base.py @@ -75,9 +75,9 @@ class Meta: max_length=16, validators=[ RegexValidator( - regex=r"^\+?1?\d{9,15}$", + regex=r"^\+?1?\d{1,4}(\s?\d{1,4}){2,8}$", message="Phone number must be entered in the format: \ - '+#########'. Up to 16 digits allowed.", + '+#########'. Up to 16 digits allowed, with optional spaces.", ) ], ) From 35cb7ce58a0597a9b718582b6f2dab11c66dc9da Mon Sep 17 00:00:00 2001 From: polmf <99polmf@gmail.com> Date: Wed, 10 Dec 2025 19:54:18 -0500 Subject: [PATCH 38/50] add disclaimer --- applications/templates/include/application_form.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/applications/templates/include/application_form.html b/applications/templates/include/application_form.html index a40ea637a..b4365ea84 100644 --- a/applications/templates/include/application_form.html +++ b/applications/templates/include/application_form.html @@ -14,6 +14,14 @@