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 %}
- {% 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/applications/templates/include/application_form.html b/applications/templates/include/application_form.html index 8596d30a7..950b970c1 100644 --- a/applications/templates/include/application_form.html +++ b/applications/templates/include/application_form.html @@ -14,6 +14,16 @@
+ {% if app_type == 'H' %} + {% if not application or application.can_be_edit %} +
+

+ âť— Before filling out the application, read this article about + How To Do a Good Application.. +

+
+ {% endif %} + {% endif %} {% csrf_token %} {% include 'include/bootstrap_form.html' %} * Indicates required field. @@ -72,7 +82,7 @@ } else { description.parent().removeClass('has-error'); } - } + } } description_remaining.innerText = (current + '/' + max_length + ' characters'); } @@ -165,14 +175,14 @@ title[0].addEventListener('click', function () { this.classList.toggle('active'); var fieldset = this.closest('fieldset'); - var content = $(fieldset).children().not(':first'); + var content = $(fieldset).children().not(':first'); content.each(function () { var isVisible = $(this).css('display') === 'block'; $(this).css('display', isVisible ? 'none' : 'block'); }); - }); - */ + }); + */ }); // Handles the sending of the form --> Expands sections with errors - Commented out for further testing @@ -227,6 +237,12 @@ conditional_field(other_gender, gender, function () { return gender.val() === 'X'; }, 1); + + var other_hear_about_us = $('#id_other_hear_about_us'); + var hear_about_us = $('#id_hear_about_us'); + conditional_field(other_hear_about_us, hear_about_us, function () { + return hear_about_us.val() === 'Otros'; + }, 1); var online = $('input[name="online"][value="True"]'); var face_to_face = $('input[name="online"][value="False"]'); @@ -293,20 +309,21 @@ 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){ var element = $(elementStrId); - var max_length = parseInt(element.attr('maxLength')); // to int + var max_length = parseInt(element.attr('maxLength')); // to int element.removeAttr('maxLength'); @@ -322,7 +339,7 @@ }); } - {% if app_type == 'H' %} + {% if app_type == 'H' %} // check and display remaining characters from the textarea with id description and projects setUpCharCounterOn('#description', '#description_char_count'); @@ -376,7 +393,7 @@ }); conditional_field(which_hack, is_not_first_hackathon_mentor, function () { return is_firsttime_mentor.prop("checked"); - }); + }); conditional_field(which_hack, is_first_hackathon_mentor, function () { return is_not_first_hackathon_mentor.prop("checked"); }); @@ -418,8 +435,6 @@ university_mentor.parent().addClass('required'); degree_mentor.parent().addClass('required'); graduation_year.parent().addClass('required'); - - {% endif %} {% if not application %} var timer; @@ -473,6 +488,9 @@ } }); } + {% else %} + $('#id_terms_and_conditions').prop('checked', true); + $('#id_diet_notice').prop('checked', true); {% endif %} } ) 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( 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 }}

+ diff --git a/organizers/tables.py b/organizers/tables.py index 3aa07fd4d..818dc001d 100755 --- a/organizers/tables.py +++ b/organizers/tables.py @@ -4,72 +4,101 @@ from django.conf import settings from django.db.models import Q -from applications.models import HackerApplication, STATUS, VolunteerApplication, MentorApplication, SponsorApplication +from applications.models import ( + HackerApplication, + STATUS, + VolunteerApplication, + MentorApplication, + SponsorApplication, +) from organizers.forms import InviteFilterForm from user.models import User class ApplicationFilter(django_filters.FilterSet): - search = django_filters.CharFilter(method='search_filter', label='Search') - status = django_filters.MultipleChoiceFilter('status', label='Status', choices=STATUS, - widget=forms.CheckboxSelectMultiple) + search = django_filters.CharFilter(method="search_filter", label="Search") + status = django_filters.MultipleChoiceFilter( + "status", label="Status", choices=STATUS, widget=forms.CheckboxSelectMultiple + ) def search_filter(self, queryset, name, value): - return queryset.filter(Q(user__email__icontains=value) | Q(user__name__icontains=value) | - Q(university__icontains=value) | Q(origin__icontains=value)) + return queryset.filter( + Q(user__email__icontains=value) + | Q(user__name__icontains=value) + | Q(university__icontains=value) + | Q(origin__icontains=value) + ) class Meta: model = HackerApplication - fields = ['search', 'status', 'under_age', 'online'] if getattr(settings, 'HYBRID_HACKATHON', False) else \ - ['search', 'status', 'under_age'] + fields = ( + ["search", "status", "under_age", "online"] + if getattr(settings, "HYBRID_HACKATHON", False) + else ["search", "status", "under_age"] + ) class DubiousApplicationFilter(django_filters.FilterSet): - search = django_filters.CharFilter(method='search_filter', label='Search') - contacted = django_filters.ChoiceFilter('contacted', label='Hacker contacted?', - null_label=None, - empty_label=None, - choices=[(True, "Yes"), (False, "No")], - widget=forms.RadioSelect) + search = django_filters.CharFilter(method="search_filter", label="Search") + contacted = django_filters.ChoiceFilter( + "contacted", + label="Hacker contacted?", + null_label=None, + empty_label=None, + choices=[(True, "Yes"), (False, "No")], + widget=forms.RadioSelect, + ) def search_filter(self, queryset, name, value): - return queryset.filter(Q(user__email__icontains=value) | Q(user__name__icontains=value) | - Q(university__icontains=value) | Q(origin__icontains=value)) + return queryset.filter( + Q(user__email__icontains=value) + | Q(user__name__icontains=value) + | Q(university__icontains=value) + | Q(origin__icontains=value) + ) class Meta: model = HackerApplication - fields = ['search', 'contacted'] + fields = ["search", "contacted"] class BlacklistApplicationFilter(django_filters.FilterSet): - search = django_filters.CharFilter(method='search_filter', label='Search') + search = django_filters.CharFilter(method="search_filter", label="Search") def search_filter(self, queryset, name, value): - return queryset.filter(Q(user__email__icontains=value) | Q(user__name__icontains=value) | - Q(university__icontains=value) | Q(origin__icontains=value)) + return queryset.filter( + Q(user__email__icontains=value) + | Q(user__name__icontains=value) + | Q(university__icontains=value) + | Q(origin__icontains=value) + ) class Meta: model = HackerApplication - fields = ['search'] + fields = ["search"] def get_fields_invite_filter(): - fields = ['search', 'first_timer'] - reimbursement = getattr(settings, 'REIMBURSEMENT_ENABLED', False) - hybrid = getattr(settings, 'HYBRID_HACKATHON', False) + fields = ["search", "first_timer"] + reimbursement = getattr(settings, "REIMBURSEMENT_ENABLED", False) + hybrid = getattr(settings, "HYBRID_HACKATHON", False) if reimbursement: - fields.append('reimb') + fields.append("reimb") if hybrid: - fields.append('online') + fields.append("online") return fields class InviteFilter(django_filters.FilterSet): - search = django_filters.CharFilter(method='search_filter', label='Search') + search = django_filters.CharFilter(method="search_filter", label="Search") def search_filter(self, queryset, name, value): - return queryset.filter(Q(user__email__icontains=value) | Q(user__name__icontains=value) | - Q(university__icontains=value) | Q(origin__icontains=value)) + return queryset.filter( + Q(user__email__icontains=value) + | Q(user__name__icontains=value) + | Q(university__icontains=value) + | Q(origin__icontains=value) + ) class Meta: model = HackerApplication @@ -78,189 +107,218 @@ class Meta: def get_application_list_fields(): - fields = ['user.name', 'user.email', 'vote_avg', 'university', 'origin'] - if getattr(settings, 'HYBRID_HACKATHON', False): - fields.append('online') - fields.extend(['votes', 'detail']) + fields = ["user.name", "user.email", "vote_avg", "university", "origin"] + if getattr(settings, "HYBRID_HACKATHON", False): + fields.append("online") + fields.extend(["votes", "detail"]) return fields class ApplicationsListTable(tables.Table): detail = tables.TemplateColumn( "Detail ", - verbose_name='Actions', orderable=False) - origin = tables.Column(accessor='origin', verbose_name='Origin') - votes = tables.Column(accessor='vote_set.count', verbose_name='Votes', orderable=False) + verbose_name="Actions", + orderable=False, + ) + origin = tables.Column(accessor="origin", verbose_name="Origin") + votes = tables.Column( + accessor="vote_set.count", verbose_name="Votes", orderable=False + ) class Meta: model = HackerApplication - attrs = {'class': 'table table-hover'} - template = 'django_tables2/bootstrap-responsive.html' + attrs = {"class": "table table-hover"} + template = "django_tables2/bootstrap-responsive.html" fields = get_application_list_fields() - empty_text = 'No applications available' - order_by = '-vote_avg' + empty_text = "No applications available" + order_by = "-vote_avg" class DubiousListTable(tables.Table): detail = tables.TemplateColumn( "Detail ", - verbose_name='Actions', orderable=False) + verbose_name="Actions", + orderable=False, + ) class Meta: model = HackerApplication - attrs = {'class': 'table table-hover'} - template = 'django_tables2/bootstrap-responsive.html' - fields = ['user.name', 'user.email', 'university', 'origin', 'contacted'] - empty_text = 'No dubious applications' - order_by = 'status_update_date' + attrs = {"class": "table table-hover"} + template = "django_tables2/bootstrap-responsive.html" + fields = ["user.name", "user.email", "university", "origin", "contacted"] + empty_text = "No dubious applications" + order_by = "status_update_date" class BlacklistListTable(tables.Table): detail = tables.TemplateColumn( "Detail ", - verbose_name='Actions', orderable=False) - origin = tables.Column(accessor='origin', verbose_name='Origin') + verbose_name="Actions", + orderable=False, + ) + origin = tables.Column(accessor="origin", verbose_name="Origin") class Meta: model = HackerApplication - attrs = {'class': 'table table-hover'} - template = 'django_tables2/bootstrap-responsive.html' - fields = ['user.name', 'user.email', 'university', 'origin'] - empty_text = 'No blacklisted applications' - order_by = 'status_update_date' + attrs = {"class": "table table-hover"} + template = "django_tables2/bootstrap-responsive.html" + fields = ["user.name", "user.email", "university", "origin"] + empty_text = "No blacklisted applications" + order_by = "status_update_date" def get_admin_application_list_fields(): - fields = ['selected', 'user.name', 'vote_avg'] - if getattr(settings, 'HYBRID_HACKATHON', False): - fields.append('online') - if getattr(settings, 'REIMBURSEMENT_ENABLED', False): - fields.append('reimb_amount') - fields.extend(['university', 'origin']) + fields = ["selected", "user.name", "vote_avg"] + if getattr(settings, "HYBRID_HACKATHON", False): + fields.append("online") + if getattr(settings, "REIMBURSEMENT_ENABLED", False): + fields.append("reimb_amount") + fields.extend(["university", "origin"]) return fields class AdminApplicationsListTable(tables.Table): - selected = tables.CheckBoxColumn(accessor="pk", verbose_name='Select') - counter = tables.TemplateColumn('{{ row_counter|add:1 }}', verbose_name='Position') - review_count = tables.Column(accessor='vote_set.count', verbose_name='# of reviews') + selected = tables.CheckBoxColumn(accessor="pk", verbose_name="Select") + counter = tables.TemplateColumn("{{ row_counter|add:1 }}", verbose_name="Position") + review_count = tables.Column(accessor="vote_set.count", verbose_name="# of reviews") detail = tables.TemplateColumn( "Detail ", - verbose_name='Actions', orderable=False) + verbose_name="Actions", + orderable=False, + ) class Meta: model = HackerApplication - attrs = {'class': 'table table-hover'} - template = 'django_tables2/bootstrap-responsive.html' + attrs = {"class": "table table-hover"} + template = "django_tables2/bootstrap-responsive.html" fields = get_admin_application_list_fields() - empty_text = 'No applications available' - order_by = '-vote_avg' + empty_text = "No applications available" + order_by = "-vote_avg" orderable = False class AdminTeamListTable(tables.Table): - selected = tables.CheckBoxColumn(accessor="user__team__team_code", verbose_name='Select') - team = tables.Column(accessor='user__team__team_code') + selected = tables.CheckBoxColumn( + accessor="user__team__team_code", verbose_name="Select" + ) + team = tables.Column(accessor="user__team__team_code") class Meta: model = HackerApplication - attrs = {'class': 'table table-hover'} - template = 'django_tables2/bootstrap-responsive.html' - fields = ['selected', 'team', 'vote_avg', 'members', 'live_pending', 'invited', 'accepted'] - empty_text = 'No pending teams' - order_by = '-vote_avg' + attrs = {"class": "table table-hover"} + template = "django_tables2/bootstrap-responsive.html" + fields = [ + "selected", + "team", + "vote_avg", + "members", + "live_pending", + "invited", + "accepted", + ] + empty_text = "No pending teams" + order_by = "-vote_avg" class VolunteerFilter(ApplicationFilter): class Meta: model = VolunteerApplication - fields = ['search', 'status', 'valid'] + fields = ["search", "status", "valid"] class VolunteerListTable(tables.Table): detail = tables.TemplateColumn( "Detail ", - verbose_name='Actions', orderable=False) + verbose_name="Actions", + orderable=False, + ) - valid = tables.BooleanColumn(accessor='valid', verbose_name='Valid') + valid = tables.BooleanColumn(accessor="valid", verbose_name="Valid") class Meta: model = VolunteerApplication - attrs = {'class': 'table table-hover'} - template = 'django_tables2/bootstrap-responsive.html' - fields = ['user.name', 'user.email', 'status', 'valid'] - empty_text = 'No Volunteer Application available' - order_by = '-submission_date' + attrs = {"class": "table table-hover"} + template = "django_tables2/bootstrap-responsive.html" + fields = ["user.name", "user.email", "status", "valid"] + empty_text = "No Volunteer Application available" + order_by = "-submission_date" class MentorFilter(ApplicationFilter): class Meta: model = MentorApplication - fields = ['search', 'status', 'valid'] + fields = ["search", "status", "valid"] class MentorListTable(tables.Table): detail = tables.TemplateColumn( "Detail ", - verbose_name='Actions', orderable=False) + verbose_name="Actions", + orderable=False, + ) - valid = tables.BooleanColumn(accessor='valid', verbose_name='Valid') + valid = tables.BooleanColumn(accessor="valid", verbose_name="Valid") class Meta: model = MentorApplication - attrs = {'class': 'table table-hover'} - template = 'django_tables2/bootstrap-responsive.html' - fields = ['user.name', 'user.email', 'status', 'valid'] - empty_text = 'No Mentor Application available' - order_by = '-submission_date' + attrs = {"class": "table table-hover"} + template = "django_tables2/bootstrap-responsive.html" + fields = ["user.name", "user.email", "status", "valid"] + empty_text = "No Mentor Application available" + order_by = "-submission_date" class SponsorFilter(django_filters.FilterSet): - search = django_filters.CharFilter(method='search_filter', label='Search') + search = django_filters.CharFilter(method="search_filter", label="Search") def search_filter(self, queryset, name, value): - return queryset.filter(Q(email__icontains=value) | Q(user__name__icontains=value) | - Q(name__icontains=value)) + return queryset.filter( + Q(email__icontains=value) + | Q(user__name__icontains=value) + | Q(name__icontains=value) + ) class Meta: model = SponsorApplication - fields = ['search'] + fields = ["search"] class SponsorListTableWithNoAction(tables.Table): - company = tables.Column(verbose_name='Company', accessor='user.name') + company = tables.Column(verbose_name="Company", accessor="user.name") class Meta: model = SponsorApplication - attrs = {'class': 'table table-hover'} - template = 'django_tables2/bootstrap-responsive.html' - fields = ['name', 'email', 'status'] - empty_text = 'No Sponsor Application available' - order_by = '-submission_date' + attrs = {"class": "table table-hover"} + template = "django_tables2/bootstrap-responsive.html" + fields = ["name", "email", "status"] + empty_text = "No Sponsor Application available" + order_by = "-submission_date" class SponsorListTable(SponsorListTableWithNoAction): detail = tables.TemplateColumn( "Detail ", - verbose_name='Actions', orderable=False) + verbose_name="Actions", + orderable=False, + ) class SponsorUserFilter(django_filters.FilterSet): - search = django_filters.CharFilter(method='search_filter', label='Search') + search = django_filters.CharFilter(method="search_filter", label="Search") def search_filter(self, queryset, name, value): return queryset.filter(Q(email__icontains=value) | Q(name__icontains=value)) class Meta: model = User - fields = ['search'] + fields = ["search"] class SponsorUserListTable(tables.Table): class Meta: model = User - attrs = {'class': 'table table-hover'} - template = 'django_tables2/bootstrap-responsive.html' - fields = ['name', 'email', 'max_applications', 'current_applications'] - empty_text = 'No Sponsor available' + attrs = {"class": "table table-hover"} + template = "django_tables2/bootstrap-responsive.html" + fields = ["name", "email", "max_applications", "current_applications"] + empty_text = "No Sponsor available" diff --git a/organizers/templates/application_detail.html b/organizers/templates/application_detail.html index baf2a4b32..672a98afd 100644 --- a/organizers/templates/application_detail.html +++ b/organizers/templates/application_detail.html @@ -8,19 +8,43 @@ + + {% endblock %} {% block panel %} {% if app %}

{% 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 %}">
@@ -38,12 +62,26 @@

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 %}
Resume
-
{{ app.resume.name }}
+
+ {{ app.resume.name }} + {% if app.cv_flagged %} +

CV already flagged for review

+ {% else %} + + {% csrf_token %} + + +
+ {% endif %} + {% endif %}
@@ -70,6 +108,27 @@

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='Wrong 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' %} + {% elif app.dubious_type == 'OTHER' %} + {% include 'include/field.html' with desc='Dubious reason' value='Other' %} + {% 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 %} {% endif %} @@ -131,7 +190,7 @@

{{ comment.text }}

- +
@@ -195,10 +279,72 @@

Score

application {% if h_dubious_enabled %} - + + + + + + + + + + + {% endif %} {% if h_blacklist_enabled %} {% if h_dubious_enabled %} - + + + + + + + + + + + {% endif %} {% if h_blacklist_enabled %} + {% else%} + {% endif %} {% endblock %} diff --git a/organizers/templates/invite_list.html b/organizers/templates/invite_list.html index 33add9d1f..9895bea08 100644 --- a/organizers/templates/invite_list.html +++ b/organizers/templates/invite_list.html @@ -6,16 +6,22 @@ {% block table_title %}Invite {% if teams %}teams{% else %}applications{% endif %}{% endblock %} {% block extra_panel %} - {% if h_hybrid %}
-

Live hackers invited or confirmed out of {{ n_live_max_hackers }}

-
-
- {{ n_live_hackers|default:0 }} +
+
+

Total live hackers invited or confirmed:

+

{{ n_live_hackers|default:0 }}

+
+
+

Total invited today (does NOT count confirmed):

+

{{ n_invited_hackers_today|default:0 }}

+
+
+

Total waitlisted hackers:

+

{{ n_waitlisted_hackers|default:0 }}

- {% endif %} {% endblock %} @@ -30,7 +36,27 @@ {% elif h_team_enabled %} Group by teams {% endif %} - < Back - + + Back
-{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/organizers/templates/other_application_detail.html b/organizers/templates/other_application_detail.html index 8a35ce320..8a2bf232b 100644 --- a/organizers/templates/other_application_detail.html +++ b/organizers/templates/other_application_detail.html @@ -21,6 +21,7 @@

Personal

{% elif app.user.is_volunteer %} {% include 'include/field.html' with desc='Name' value=app.user.name %} {% include 'include/field.html' with desc='Is over 18?' value=app.under_age|yesno %} + {% include 'include/field.html' with desc='Studies and course/graduation' value=app.studies_and_course %} {% else %} {% include 'include/field.html' with desc='Name' value=app.user.name %} {% include 'include/field.html' with desc='Under age (-18)' value=app.under_age|yesno:'Yes,No,Maybe' %} @@ -31,7 +32,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 %} @@ -77,6 +77,7 @@

Other questions

{% include 'include/field.html' with desc='Dietary restrictions' value=app.diet %} {% include 'include/field.html' with desc='Other diet' value=app.other_diet %} {% include 'include/field.html' with desc='How you meet us' value=app.hear_about_us %} + {% include 'include/field.html' with desc='Other (how you meet us)' value=app.other_hear_about_us %}

Extra

diff --git a/organizers/urls.py b/organizers/urls.py index 4c418a54b..cb2ac8734 100644 --- a/organizers/urls.py +++ b/organizers/urls.py @@ -3,20 +3,76 @@ from organizers import views urlpatterns = [ - url(r'^hacker/review/$', views.ReviewApplicationView.as_view(), name='review'), - 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 44f8aa06a..01c6a18d0 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,29 +25,67 @@ 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 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 >= 5 and not application.cv_flagged: + AcceptedResume.objects.update_or_create(application=application, defaults={'accepted': True}) return v @@ -60,82 +99,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 +257,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 +287,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 +344,118 @@ 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) + dubious_type = request.POST.get('dubious_type', None) + dubious_comment_text = request.POST.get('dubious_comment_text', 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: - application.set_dubious() - elif request.POST.get('contact_user') and request.user.has_dubious_access: + application.set_dubious(request.user, dubious_type, dubious_comment_text) + 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: - 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 +465,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,40 +489,165 @@ 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) + 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") + ) + try: + if request.POST.get("skip"): + add_vote(application, request.user, None, None) + 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"): + application.set_dubious(request.user, dubious_type, dubious_comment_text) + 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 ( + 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) + # If application has already been voted -> Skip and bring next + # application + except IntegrityError: + pass + return HttpResponseRedirect(reverse("review")) + + def can_vote(self): + return True + + +class ReviewApplicationDetailView(ApplicationDetailView): + def get_current_tabs(self): + return hacker_tabs(self.request.user) - application = models.HackerApplication.objects.get(pk=request.POST.get('app_id')) + def get_back_url(self): + return None + + def get_application(self, kwargs): + """ + Django model to the rescue. This is transformed to an SQL sentence + that does exactly what we need + :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") + .first() + ) + else: + 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() + ) + + 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) + 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") + ) 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) - elif request.POST.get('set_dubious'): - application.set_dubious() - elif request.POST.get('unset_dubious'): + return HttpResponseRedirect( + "/applications/hacker/review/" + application.uuid_str + ) + elif request.POST.get("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: + 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 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) @@ -359,53 +655,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: @@ -419,39 +769,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): @@ -461,24 +827,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 @@ -489,28 +859,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 @@ -519,8 +893,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): @@ -538,184 +912,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, cv_flagged=True, resume__isnull=False + ) + .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 @@ -723,5 +1131,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 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)), + ), + ] 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/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 96452352a..a26171e41 100644 --- a/teams/models.py +++ b/teams/models.py @@ -1,22 +1,23 @@ -import random - +import uuid from django.db import models - from user.models import User -TEAM_ID_LENGTH = 13 +MAX_LENGTH_TEAM_CODE = 36 def generate_team_id(): - s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" - while True: - team_id = "".join(random.sample(s, TEAM_ID_LENGTH)) - if not Team.objects.filter(team_code=team_id).exists(): - return team_id + 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: 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) 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 %}