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/forms/base.py b/applications/forms/base.py
index 04ee47b2c..4659731cf 100644
--- a/applications/forms/base.py
+++ b/applications/forms/base.py
@@ -30,50 +30,67 @@ 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",
+ "dubious_type",
+ "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 +102,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 +115,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 +143,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 +164,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 +239,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 +250,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 +260,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/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"}
diff --git a/applications/forms/hacker.py b/applications/forms/hacker.py
index 2fbbd9dda..06ef672ee 100644
--- a/applications/forms/hacker.py
+++ b/applications/forms/hacker.py
@@ -2,11 +2,11 @@
from .base import _BaseApplicationForm
-
class HackerApplicationForm(_BaseApplicationForm):
bootstrap_field_info = {
"🎓 Education Info": {
"fields": [
+ {"name": "kind_studies", "space": 12},
{"name": "university", "space": 12},
{"name": "degree", "space": 12},
{"name": "graduation_year", "space": 12},
@@ -59,11 +59,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,8 +132,24 @@ def clean_projects(self):
)
return data
+ def clean_kind_studies(self):
+ data = self.cleaned_data["kind_studies"]
+ if not data or data == "":
+ raise forms.ValidationError("Please select your current studies.")
+ return data
+
first_timer = common_first_timer()
+
+ kind_studies = forms.ChoiceField(
+ required=True,
+ label='What kind of studies are you currently pursuing?',
+ choices=([('', '- Select an option -')] + models.constants.KIND_STUDIES),
+ widget=forms.Select(
+ attrs={'class': 'form-control'}
+ )
+ )
+
university = common_university()
@@ -138,7 +157,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 +210,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 +258,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 +297,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/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(
diff --git a/applications/forms/volunteer.py b/applications/forms/volunteer.py
index a173190e4..73d399864 100644
--- a/applications/forms/volunteer.py
+++ b/applications/forms/volunteer.py
@@ -30,12 +30,17 @@ 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,
)
+ studies_and_course = forms.CharField(
+ required=True,
+ label="¿Qué estudias y en qué curso estás / en qué año te graduaste?",
+ widget=forms.Textarea(attrs={"rows": 1, "cols": 40}),
+ )
night_shifts = forms.TypedChoiceField(
required=True,
label="ÂżEstarias de acuerdo en seguir ayudando pasado medianoche?",
@@ -81,11 +86,12 @@ 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},
+ {"name": "studies_and_course", "space": 12},
{"name": "hear_about_us", "space": 12},
+ {"name": "other_hear_about_us", "space": 12},
{"name": "origin", "space": 12},
],
"description": "Hola voluntari@, necesitamos un poco de informaciĂłn antes de empezar :)",
@@ -98,7 +104,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": [
@@ -236,22 +242,21 @@ def clean_hear_about_us(self):
"origin": forms.TextInput(attrs={"autocomplete": "off"}),
"languages": forms.CheckboxSelectMultiple(),
"friends": forms.Textarea(attrs={"rows": 2, "cols": 40}),
+ "studies_and_course": 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),
+ "other_hear_about_us": forms.TextInput(attrs={"autocomplete": "off"}),
"tshirt_size": forms.Select(),
"diet": forms.Select(),
}
labels = {
- "pronouns": "¿Cuáles son tus pronombres?",
"gender": " ¿Con qué género te identificas?",
"other_gender": "Me quiero describir",
+ "studies_and_course": "¿Qué estudias y en qué curso estás / en qué año te graduaste?",
"graduation_year": "What year will you graduate?",
"tshirt_size": "¿Cuál es tu talla de camiseta?",
"diet": "Restricciones alimentarias",
@@ -265,6 +270,7 @@ def clean_hear_about_us(self):
"cool_skill": "¿Qué habilidad interesante o dato curioso tienes? ¡Sorpréndenos! 🎉",
"friends": "¿Estás aplicando con otr@s amig@s? Escribe sus nombres completos",
"hear_about_us": "ÂżCĂłmo escuchaste sobre nosotros por primera vez?",
+ "other_hear_about_us": "Especifica cĂłmo nos conociste:",
"volunteer_motivation": "¿Por qué quieres asistir como voluntari@ a HackUPC?",
}
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),
+ ),
+ ]
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/migrations/0060_volunteerapplication_studies_and_course.py b/applications/migrations/0060_volunteerapplication_studies_and_course.py
new file mode 100644
index 000000000..39e9e7397
--- /dev/null
+++ b/applications/migrations/0060_volunteerapplication_studies_and_course.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.23 on 2025-12-11 15:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('applications', '0059_remove_volunteerapplication_pronouns'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='volunteerapplication',
+ name='studies_and_course',
+ field=models.CharField(blank=True, default='', max_length=500),
+ ),
+ ]
diff --git a/applications/migrations/0061_volunteerapplication_other_hear_about_us.py b/applications/migrations/0061_volunteerapplication_other_hear_about_us.py
new file mode 100644
index 000000000..a32e25f01
--- /dev/null
+++ b/applications/migrations/0061_volunteerapplication_other_hear_about_us.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.23 on 2025-12-21 22:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('applications', '0060_volunteerapplication_studies_and_course'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='volunteerapplication',
+ name='other_hear_about_us',
+ field=models.CharField(blank=True, max_length=500, null=True),
+ ),
+ ]
diff --git a/applications/migrations/0062_auto_20251228_1817.py b/applications/migrations/0062_auto_20251228_1817.py
new file mode 100644
index 000000000..d9cae9d5a
--- /dev/null
+++ b/applications/migrations/0062_auto_20251228_1817.py
@@ -0,0 +1,60 @@
+# Generated by Django 3.2.23 on 2025-12-28 18:17
+
+import django.core.validators
+from django.db import migrations, models
+import multiselectfield.db.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('applications', '0061_volunteerapplication_other_hear_about_us'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='hackerapplication',
+ name='kind_studies',
+ field=models.CharField(choices=[('SECONDARY', 'Secondary Education - Baccalaureate'), ('VOCATIONAL', 'Vocational Training (FP)'), ('BACHELOR', 'Bachelor’s Degree'), ('MASTER', 'Master’s Degree'), ('OTHER', 'Other')], default='NA', max_length=300),
+ ),
+ migrations.AlterField(
+ model_name='hackerapplication',
+ name='graduation_year',
+ field=models.IntegerField(choices=[(2025, '2025'), (2026, '2026'), (2027, '2027'), (2028, '2028'), (2029, '2029'), (2030, '2030'), (2031, '2031'), (2032, '2032')], default=2026),
+ ),
+ 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, with optional spaces.", regex='^\\+?1?\\d{1,4}(\\s?\\d{1,4}){2,8}$')]),
+ ),
+ migrations.AlterField(
+ model_name='mentorapplication',
+ name='graduation_year',
+ field=models.IntegerField(choices=[(2025, '2025'), (2026, '2026'), (2027, '2027'), (2028, '2028'), (2029, '2029'), (2030, '2030'), (2031, '2031'), (2032, '2032')], default=2026),
+ ),
+ 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, with optional spaces.", regex='^\\+?1?\\d{1,4}(\\s?\\d{1,4}){2,8}$')]),
+ ),
+ migrations.AlterField(
+ model_name='mentorapplication',
+ name='which_hack',
+ field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[(0, 'HackUPC2019 o anterior'), (1, 'HackUPC 2021'), (2, 'HackUPC 2022'), (3, 'HackUPC 2023'), (4, 'HackUPC 2024'), (5, 'HackUPC 2025')], max_length=11, null=True),
+ ),
+ migrations.AlterField(
+ model_name='volunteerapplication',
+ name='graduation_year',
+ field=models.IntegerField(choices=[(2025, '2025'), (2026, '2026'), (2027, '2027'), (2028, '2028'), (2029, '2029'), (2030, '2030'), (2031, '2031'), (2032, '2032')], default=2026),
+ ),
+ 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, with optional spaces.", regex='^\\+?1?\\d{1,4}(\\s?\\d{1,4}){2,8}$')]),
+ ),
+ migrations.AlterField(
+ model_name='volunteerapplication',
+ name='which_hack',
+ field=multiselectfield.db.fields.MultiSelectField(choices=[(0, 'HackUPC2019 o anterior'), (1, 'HackUPC 2021'), (2, 'HackUPC 2022'), (3, 'HackUPC 2023'), (4, 'HackUPC 2024'), (5, 'HackUPC 2025')], max_length=11),
+ ),
+ ]
diff --git a/applications/migrations/0063_auto_20251228_1841.py b/applications/migrations/0063_auto_20251228_1841.py
new file mode 100644
index 000000000..e5672efb6
--- /dev/null
+++ b/applications/migrations/0063_auto_20251228_1841.py
@@ -0,0 +1,31 @@
+# Generated by Django 3.2.23 on 2025-12-28 18:41
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('applications', '0062_auto_20251228_1817'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='hackerapplication',
+ name='dubious_comment',
+ field=models.TextField(blank=True, max_length=500, null=True),
+ ),
+ 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'), ('OTHER', 'Other')], 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/base.py b/applications/models/base.py
index cc3ee721a..68d66f34d 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{1,4}(\s?\d{1,4}){2,8}$",
+ message="Phone number must be entered in the format: \
+ '+#########'. Up to 16 digits allowed, with optional spaces.",
+ )
+ ],
+ )
# 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/constants.py b/applications/models/constants.py
index dded778cb..bb4affc02 100644
--- a/applications/models/constants.py
+++ b/applications/models/constants.py
@@ -91,11 +91,38 @@
(2, "Sunday")
]
+ST_SECONDARY = 'SECONDARY'
+ST_VOCATIONAL = 'VOCATIONAL'
+ST_BACHELOR = 'BACHELOR'
+ST_MASTER = 'MASTER'
+ST_OTHER = 'OTHER'
+
+KIND_STUDIES = [
+ (ST_SECONDARY, 'Secondary Education - Baccalaureate'), (ST_VOCATIONAL, 'Vocational Training (FP)'),
+ (ST_BACHELOR, 'Bachelor’s Degree'), (ST_MASTER, 'Master’s Degree'), (ST_OTHER, 'Other')
+]
+
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 = ['2019 o anterior', ' 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)]
+
+DUBIOUS_NONE = 'OK'
+DUBIOUS_CV = 'INVALID_CV'
+DUBIOUS_GRADUATION_YEAR = 'LATE_GRAD'
+DUBIOUS_NOT_STUDENT = 'NOT_STUDENT'
+DUBIOUS_SCHOOL = 'INVALID_SCHOOL'
+DUBIOUS_OTHER = 'OTHER'
+
+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'),
+ (DUBIOUS_OTHER, 'Other')
+]
diff --git a/applications/models/hacker.py b/applications/models/hacker.py
index 6592ef63a..f2170a5ba 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,9 +11,10 @@ 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
+ # Studies
+ kind_studies = models.CharField(max_length=300, choices=KIND_STUDIES, default=NO_ANSWER)
graduation_year = models.IntegerField(choices=YEARS, default=DEFAULT_YEAR)
university = models.CharField(max_length=300)
degree = models.CharField(max_length=300)
@@ -36,27 +35,44 @@ 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
+ 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)
- 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)
cvs_edition = models.BooleanField(default=False)
+ cv_flagged = models.BooleanField(default=False)
resume = models.FileField(
upload_to=resume_path_hackers,
@@ -67,28 +83,49 @@ 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):
+ """
+ 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):
+ 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'):
+ if hasattr(self, "acceptedresume"):
self.acceptedresume.delete()
self.save()
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_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 set_contacted(self, user):
if not self.contacted:
self.contacted = True
@@ -96,15 +133,25 @@ 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')
+ 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()
+ team = getattr(self.user, 'team', None)
+ if team:
+ team.delete()
self.save()
def set_blacklist(self):
@@ -123,12 +170,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/applications/models/volunteer.py b/applications/models/volunteer.py
index ad55e7518..c5f0b32d6 100644
--- a/applications/models/volunteer.py
+++ b/applications/models/volunteer.py
@@ -83,6 +83,7 @@ class VolunteerApplication(BaseApplication):
#About us
hear_about_us = models.CharField(max_length=300, choices=HEARABOUTUS_ES, default="")
+ other_hear_about_us = models.CharField(max_length=500, blank=True, null=True)
# University
graduation_year = models.IntegerField(choices=YEARS, default=DEFAULT_YEAR)
@@ -100,8 +101,8 @@ 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')
+ studies_and_course = models.CharField(max_length=500, blank=True, default='')
volunteer_motivation = models.CharField(max_length=500)
valid = models.BooleanField(default=True)
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 %}
- 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 %}Live hackers invited or confirmed out of {{ n_live_max_hackers }}
-You can delete your account with all the personal data from your user and applications. This action cannot be reverted.
- Delete account -