diff --git a/app/hackathon_variables.py b/app/hackathon_variables.py index 493b10f8b..c1e5d521b 100644 --- a/app/hackathon_variables.py +++ b/app/hackathon_variables.py @@ -4,61 +4,77 @@ from django.utils import timezone -HACKATHON_NAME = 'HackUPC' +HACKATHON_NAME = "HackUPC" # What's the name for the application -HACKATHON_APPLICATION_NAME = 'My HackUPC' +HACKATHON_APPLICATION_NAME = "My HackUPC" # Hackathon timezone -TIME_ZONE = 'CET' +TIME_ZONE = "CET" # This description will be used on the html and sharing meta tags -HACKATHON_DESCRIPTION = 'Join us for BarcelonaTech\'s hackathon. 36h. May 3 - 5.' +HACKATHON_DESCRIPTION = "Join us for BarcelonaTech's hackathon. 36h. April 24 - 26." # Domain where application is deployed, can be set by env variable -HACKATHON_DOMAIN = os.environ.get('DOMAIN', None) -HEROKU_APP_NAME = os.environ.get('HEROKU_APP_NAME', None) +HACKATHON_DOMAIN = os.environ.get("DOMAIN", None) +HEROKU_APP_NAME = os.environ.get("HEROKU_APP_NAME", None) if HEROKU_APP_NAME and not HACKATHON_DOMAIN: - HACKATHON_DOMAIN = '%s.herokuapp.com' % HEROKU_APP_NAME + HACKATHON_DOMAIN = "%s.herokuapp.com" % HEROKU_APP_NAME elif not HACKATHON_DOMAIN: - HACKATHON_DOMAIN = 'localhost:8000' + HACKATHON_DOMAIN = "localhost:8000" # Hackathon contact email: where should all hackers contact you. It will also be used as a sender for all emails -HACKATHON_CONTACT_EMAIL = 'contact@hackupc.com' +HACKATHON_CONTACT_EMAIL = "contact@hackupc.com" # Hackathon logo url, will be used on all emails -HACKATHON_LOGO_URL = 'https://my.hackupc.com/static/logo.png' +HACKATHON_LOGO_URL = "https://my.hackupc.com/static/logo.png" -HACKATHON_OG_IMAGE = 'https://hackupc.com/ogimage.png?v=2021' +HACKATHON_OG_IMAGE = "https://hackupc.com/ogimage.png?v=2021" # (OPTIONAL) Track visits on your website -HACKATHON_GOOGLE_ANALYTICS = 'UA-69542332-2' +HACKATHON_GOOGLE_ANALYTICS = "UA-69542332-2" # (OPTIONAL) Hackathon Twitter user -HACKATHON_TWITTER_ACCOUNT = 'hackupc' +HACKATHON_TWITTER_ACCOUNT = "hackupc" # (OPTIONAL) Hackathon Facebook page -HACKATHON_FACEBOOK_PAGE = 'hackupc' +HACKATHON_FACEBOOK_PAGE = "hackupc" # (OPTIONAL) Hackathon YouTube channel -HACKATHON_YOUTUBE_PAGE = 'UCiiRorGg59Xd5Sjj9bjIt-g' +HACKATHON_YOUTUBE_PAGE = "UCiiRorGg59Xd5Sjj9bjIt-g" # (OPTIONAL) Hackathon Instagram user -HACKATHON_INSTAGRAM_ACCOUNT = 'hackupc' +HACKATHON_INSTAGRAM_ACCOUNT = "hackupc" # (OPTIONAL) Hackathon Medium user -HACKATHON_MEDIUM_ACCOUNT = 'hackupc' +HACKATHON_MEDIUM_ACCOUNT = "hackupc" # (OPTIONAL) Github Repo for this project (so meta) -HACKATHON_GITHUB_REPO = 'https://github.com/hackupc/myhackupc/' +HACKATHON_GITHUB_REPO = "https://github.com/hackupc/myhackupc/" # (OPTIONAL) Applications deadline -HACKATHON_APP_DEADLINE = timezone.datetime(2030, 4, 24, 23, 59, tzinfo=timezone.pytz.timezone(TIME_ZONE)) -VOLUNTEER_APP_DEADLINE = timezone.datetime(2030, 4, 18, 23, 59, tzinfo=timezone.pytz.timezone(TIME_ZONE)) -MENTOR_APP_DEADLINE = timezone.datetime(2030, 3, 25, 23, 59, tzinfo=timezone.pytz.timezone(TIME_ZONE)) +HACKATHON_APP_DEADLINE = timezone.datetime( + 2026, 4, 1, 23, 59, tzinfo=timezone.pytz.timezone(TIME_ZONE) +) +VOLUNTEER_APP_DEADLINE = timezone.datetime( + 2026, 3, 27, 23, 59, tzinfo=timezone.pytz.timezone(TIME_ZONE) +) +MENTOR_APP_DEADLINE = timezone.datetime( + 2026, 3, 27, 23, 59, tzinfo=timezone.pytz.timezone(TIME_ZONE) +) + # (OPTIONAL) Online checkin activated -ONLINE_CHECKIN = timezone.datetime(2020, 5, 3, 17, 00, tzinfo=timezone.pytz.timezone(TIME_ZONE)) +ONLINE_CHECKIN = timezone.datetime( + 2020, 5, 3, 17, 00, tzinfo=timezone.pytz.timezone(TIME_ZONE) +) # (OPTIONAL) When to arrive at the hackathon -HACKATHON_ARRIVE = '' +HACKATHON_ARRIVE = "" # (OPTIONAL) When to arrive at the hackathon -HACKATHON_LEAVE = '' +HACKATHON_LEAVE = "" + +# (OPTIONAL) When the event ends (to send Devpost link emails) +HACKATHON_EVENT_END = timezone.datetime( + 2026, 4, 26, 20, 00, tzinfo=timezone.pytz.timezone(TIME_ZONE) +) # (OPTIONAL) Hackathon live page -HACKATHON_LIVE_PAGE = 'https://live.hackupc.com' +HACKATHON_LIVE_PAGE = "https://live.hackupc.com" # (OPTIONAL) Regex to automatically match organizers emails and set them as organizers when signing up -REGEX_HACKATHON_ORGANIZER_EMAIL = '^.*@hackupc\.com$' +REGEX_HACKATHON_ORGANIZER_EMAIL = "^.*@hackupc\.com$" # (OPTIONAL) Send 500 errors to email while on production mode -HACKATHON_DEV_EMAILS = ['devs@hackupc.com', ] +HACKATHON_DEV_EMAILS = [ + "devs@hackupc.com", +] # Baggage configuration BAGGAGE_ENABLED = True @@ -67,10 +83,10 @@ # Reimbursement configuration REIMBURSEMENT_ENABLED = True DEFAULT_REIMBURSEMENT_AMOUNT = 100 -CURRENCY = '€' -REIMBURSEMENT_EXPIRY_DATE = timezone.datetime(2025, 5, 2, 17, 00, tzinfo=timezone.pytz.timezone(TIME_ZONE)) +CURRENCY = "€" +REIMBURSEMENT_EXPIRY_DATE = timezone.datetime(2026, 4, 4, 17, 00, tzinfo=timezone.pytz.timezone(TIME_ZONE)) REIMBURSEMENT_REQUIREMENTS = 'You have to submit a project and demo it during the event in order to get reimbursed' -REIMBURSEMENT_DEADLINE = timezone.datetime(2025, 5, 5, 23, 59, tzinfo=timezone.pytz.timezone(TIME_ZONE)) +REIMBURSEMENT_DEADLINE = timezone.datetime(2026, 4, 4, 23, 59, tzinfo=timezone.pytz.timezone(TIME_ZONE)) # (OPTIONAL) Max team members. Defaults to 4 TEAMS_ENABLED = True @@ -82,9 +98,9 @@ # (OPTIONAL) Slack credentials # Highly recommended to create a separate user account to extract the token from SLACK = { - 'team': os.environ.get('SL_TEAM', 'test'), + "team": os.environ.get("SL_TEAM", "test"), # Get it here: https://api.slack.com/custom-integrations/legacy-tokens - 'token': os.environ.get('SL_TOKEN', None) + "token": os.environ.get("SL_TOKEN", None), } # (OPTIONAL) Logged in cookie @@ -95,16 +111,16 @@ # Hardware configuration # Hardware request time length (in minutes) HARDWARE_ENABLED = True -#Hardware request time length (in minutes) +# Hardware request time length (in minutes) HARDWARE_REQUEST_TIME = 15 SLACK_BOT = { - 'id': os.environ.get('SL_BOT_ID', None), - 'token': os.environ.get('SL_BOT_TOKEN', None), - 'channel': os.environ.get('SL_BOT_CHANNEL', None), - 'director1': os.environ.get('SL_BOT_DIRECTOR1', None), - 'director2': os.environ.get('SL_BOT_DIRECTOR2', None) + "id": os.environ.get("SL_BOT_ID", None), + "token": os.environ.get("SL_BOT_TOKEN", None), + "channel": os.environ.get("SL_BOT_CHANNEL", None), + "director1": os.environ.get("SL_BOT_DIRECTOR1", None), + "director2": os.environ.get("SL_BOT_DIRECTOR2", None), } # Enable judging tab JUDGING_ENABLED = False @@ -119,7 +135,7 @@ # Enable blacklist separate pipeline (disabled by default) BLACKLIST_ENABLED = True -SUPPORTED_RESUME_EXTENSIONS = ['.pdf'] +SUPPORTED_RESUME_EXTENSIONS = [".pdf"] # Mentor/Volunteer applications can expire if they are invited, set to False to not MENTOR_EXPIRES = False @@ -129,9 +145,9 @@ HYBRID_HACKATHON = False N_MAX_LIVE_HACKERS = 600 -SERVER_EMAIL = 'HackUPC Team ' +SERVER_EMAIL = "HackUPC Team " -CODE_CONDUCT_LINK = 'https://legal.hackersatupc.org/hackupc/code_of_conduct' -LEGAL_LINK = 'https://legal.hackersatupc.org/hackupc/legal_notice' -PRIVACY_LINK = 'https://legal.hackersatupc.org/hackupc/privacy_and_cookies' -TERMS_LINK = 'https://legal.hackersatupc.org/hackupc/terms_and_conditions' +CODE_CONDUCT_LINK = "https://legal.hackersatupc.org/hackupc/code_of_conduct" +LEGAL_LINK = "https://legal.hackersatupc.org/hackupc/legal_notice" +PRIVACY_LINK = "https://legal.hackersatupc.org/hackupc/privacy_and_cookies" +TERMS_LINK = "https://legal.hackersatupc.org/hackupc/terms_and_conditions" diff --git a/app/templates/base.html b/app/templates/base.html index 1a6d4b2be..de0228562 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -111,10 +111,8 @@ {% endif %} {% if request.user.is_organizer %} - {% if request.user.has_volunteer_access %} -
  • Volunteer
  • - {% endif %} +
  • Volunteer
  • {% if request.user.has_mentor_access %}
  • Mentor
  • diff --git a/applications/templates/include/application_form.html b/applications/templates/include/application_form.html index 950b970c1..3f9d70b99 100644 --- a/applications/templates/include/application_form.html +++ b/applications/templates/include/application_form.html @@ -19,7 +19,7 @@

    ❗ Before filling out the application, read this article about - How To Do a Good Application.. + How To Do a Good Application.

    {% endif %} diff --git a/applications/views.py b/applications/views.py index b61dd5936..63877e8af 100644 --- a/applications/views.py +++ b/applications/views.py @@ -80,6 +80,12 @@ def get(self, request, *args, **kwargs): if msg: msg.send() + if application.user.is_hacker() and application.reimb: + from reimbursement import emails as reimb_emails + reimb_msg = reimb_emails.create_travel_tickets_upload_email( + application.user.reimbursement, request + ) + reimb_msg.send() try: slack.send_slack_invite(request.user.email) # Ignore if we can't send, it's only optional diff --git a/cronjob b/cronjob index eb4a12213..574ab4244 100644 --- a/cronjob +++ b/cronjob @@ -2,3 +2,5 @@ 6 */2 * * * root /usr/local/bin/python /code/manage.py expire_applications # Run expire reimbursements job at 6 minutes past the hour, every 2 hours 36 */2 * * * root /usr/local/bin/python /code/manage.py expire_reimbursements +# Run send devpost emails job every Sunday at 20:00 server va 1 hora abans +0 19 * * 0 root /usr/local/bin/python /code/manage.py send_devpost_emails diff --git a/organizers/templates/other_application_detail.html b/organizers/templates/other_application_detail.html index 8a2bf232b..0978fe018 100644 --- a/organizers/templates/other_application_detail.html +++ b/organizers/templates/other_application_detail.html @@ -20,7 +20,7 @@

    Personal

    {% include 'include/field.html' with desc='Name' value=app.name %} {% 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='Is underage?' 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 %} @@ -35,7 +35,7 @@

    Personal

    {% 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 %} -
    +

    Validation

    @@ -45,16 +45,18 @@

    Validation

    {% csrf_token %} - + {% if user.has_volunteer_access %} + + {% endif %}

    -
    +

    Volunteering

    @@ -188,7 +190,7 @@

    {{ comment.text }}

    {% csrf_token %} + rows="3" required maxlength="500"> @@ -198,28 +200,29 @@

    {{ comment.text }}

    {% block out_panel %} {% if app and not app.user.is_sponsor %} - - + {% if app and app.user.is_volunteer and not user.has_volunteer_access %} + {% else %} + + {% endif %} {% endif %} {% endblock %} diff --git a/organizers/views.py b/organizers/views.py index 01c6a18d0..84e4ff239 100644 --- a/organizers/views.py +++ b/organizers/views.py @@ -61,7 +61,6 @@ IsOrganizerMixin, IsDirectorMixin, HaveDubiousPermissionMixin, - HaveVolunteerPermissionMixin, HaveSponsorPermissionMixin, HaveMentorPermissionMixin, IsBlacklistAdminMixin, @@ -85,7 +84,9 @@ def add_vote(application, user, tech_rat, 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}) + AcceptedResume.objects.update_or_create( + application=application, defaults={"accepted": True} + ) return v @@ -359,12 +360,12 @@ def post(self, request, *args, **kwargs): 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) + 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'): + if request.POST.get("add_comment"): add_comment(application, request.user, comment_text) elif request.POST.get("invite") and request.user.is_director: self.invite_application(application) @@ -376,7 +377,7 @@ def post(self, request, *args, **kwargs): self.waitlist_application(application) elif request.POST.get("slack") and request.user.is_organizer: self.slack_invite(application) - elif request.POST.get('set_dubious') and request.user.is_organizer: + elif request.POST.get("set_dubious") and request.user.is_organizer: application.set_dubious(request.user, dubious_type, dubious_comment_text) elif request.POST.get("set_flagged_cv") and request.user.is_organizer: application.set_flagged_cv() @@ -510,8 +511,8 @@ def post(self, request, *args, **kwargs): tech_vote = request.POST.get("tech_rat", None) pers_vote = request.POST.get("pers_rat", None) comment_text = request.POST.get("comment_text", None) - dubious_type = request.POST.get('dubious_type', None) - dubious_comment_text = request.POST.get('dubious_comment_text', None) + 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") @@ -525,7 +526,9 @@ def post(self, request, *args, **kwargs): "/applications/hacker/review/" + application.uuid_str ) elif request.POST.get("set_dubious"): - application.set_dubious(request.user, dubious_type, dubious_comment_text) + 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: @@ -615,8 +618,8 @@ def post(self, request, *args, **kwargs): tech_vote = request.POST.get("tech_rat", None) pers_vote = request.POST.get("pers_rat", None) comment_text = request.POST.get("comment_text", None) - dubious_type = request.POST.get('dubious_type', None) - dubious_comment_text = request.POST.get('dubious_comment_text', None) + 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") @@ -630,7 +633,9 @@ def post(self, request, *args, **kwargs): "/applications/hacker/review/" + application.uuid_str ) elif request.POST.get("set_dubious"): - application.set_dubious(request.user, dubious_type, dubious_comment_text) + 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: @@ -846,9 +851,7 @@ def get_context_data(self, **kwargs): return context -class VolunteerApplicationsListView( - HaveVolunteerPermissionMixin, _OtherApplicationsListView -): +class VolunteerApplicationsListView(IsOrganizerMixin, _OtherApplicationsListView): table_class = VolunteerListTable filterset_class = VolunteerFilter @@ -912,9 +915,7 @@ def get_current_tabs(self): return mentor_tabs(self.request.user) -class ReviewVolunteerApplicationView( - TabsViewMixin, HaveVolunteerPermissionMixin, TemplateView -): +class ReviewVolunteerApplicationView(IsOrganizerMixin, TabsViewMixin, TemplateView): template_name = "other_application_detail.html" def get_application(self, kwargs): diff --git a/reimbursement/emails.py b/reimbursement/emails.py index 8a2dd1055..a425b4da3 100644 --- a/reimbursement/emails.py +++ b/reimbursement/emails.py @@ -25,15 +25,56 @@ def create_no_reimbursement_email(reimb, request): return emails.render_mail("mails/no_reimbursement", reimb.hacker.email, c) +def create_travel_tickets_upload_email(reimb, request): + app = reimb.hacker.application + c = _get_context(app, reimb, request) + return emails.render_mail( + "mails/travel_tickets_upload", reimb.hacker.email, c, action_required=True + ) + + +def create_ticket_accepted_email(reimb, request): + app = reimb.hacker.application + c = _get_context(app, reimb, request) + return emails.render_mail("mails/ticket_accepted", reimb.hacker.email, c) + + +def create_devpost_upload_email(reimb, request): + app = reimb.hacker.application + c = _get_context(app, reimb, request) + return emails.render_mail( + "mails/devpost_upload", reimb.hacker.email, c, action_required=True + ) + + +def create_project_invalidated_email(reimb, request): + app = reimb.hacker.application + c = _get_context(app, reimb, request) + return emails.render_mail("mails/project_invalidated", reimb.hacker.email, c) + + +def create_devpost_approved_email(reimb, request): + app = reimb.hacker.application + c = _get_context(app, reimb, request) + return emails.render_mail("mails/devpost_approved", reimb.hacker.email, c) + + def _get_context(app, reimb, request): + from django.conf import settings + + confirm_url = reverse("confirm_app", kwargs={"id": app.uuid_str}, request=request) + form_url = reverse("reimbursement_dashboard", request=request) + cancel_url = reverse("cancel_app", kwargs={"id": app.uuid_str}, request=request) + if not request: + base_url = "https://" + settings.HACKATHON_DOMAIN + confirm_url = base_url + confirm_url + form_url = base_url + form_url + cancel_url = base_url + cancel_url + return { "app": app, "reimb": reimb, - "confirm_url": str( - reverse("confirm_app", kwargs={"id": app.uuid_str}, request=request) - ), - "form_url": str(reverse("reimbursement_dashboard", request=request)), - "cancel_url": str( - reverse("cancel_app", kwargs={"id": app.uuid_str}, request=request) - ), + "confirm_url": str(confirm_url), + "form_url": str(form_url), + "cancel_url": str(cancel_url), } diff --git a/reimbursement/management/commands/send_devpost_emails.py b/reimbursement/management/commands/send_devpost_emails.py new file mode 100644 index 000000000..2f24f6bd6 --- /dev/null +++ b/reimbursement/management/commands/send_devpost_emails.py @@ -0,0 +1,39 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from django.conf import settings +from reimbursement.models import Reimbursement, RE_APPROVED +from reimbursement import emails + + +class Command(BaseCommand): + help = 'Sends automatic emails to upload Devpost link to approved reimbursements' + + def handle(self, *args, **options): + # Check if the event has ended + if timezone.now() < settings.HACKATHON_EVENT_END: + self.stdout.write(self.style.WARNING('Event has not ended yet. Skipping Devpost emails.')) + return + + # We target reimbursements that have the receipt approved (RE_APPROVED), + # hasn't uploaded a devpost link yet (devpost=''), + # and we haven't sent the reminder email yet (devpost_email_sent=False). + reimbursements = Reimbursement.objects.filter( + status=RE_APPROVED, + devpost='', + devpost_email_sent=False + ) + + self.stdout.write(f'Found {reimbursements.count()} reimbursements pending Devpost link.') + + sent_count = 0 + for reimb in reimbursements: + try: + msg = emails.create_devpost_upload_email(reimb, None) + msg.send() + reimb.devpost_email_sent = True + reimb.save() + sent_count += 1 + except Exception as e: + self.stderr.write(f'Error sending email to {reimb.hacker.email}: {str(e)}') + + self.stdout.write(self.style.SUCCESS(f'Successfully sent {sent_count} emails.')) diff --git a/reimbursement/migrations/0015_auto_20260219_1731.py b/reimbursement/migrations/0015_auto_20260219_1731.py new file mode 100644 index 000000000..b8ddb4405 --- /dev/null +++ b/reimbursement/migrations/0015_auto_20260219_1731.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.23 on 2026-02-19 17:31 + +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('reimbursement', '0014_alter_reimbursement_expiration_time'), + ] + + operations = [ + migrations.AddField( + model_name='reimbursement', + name='devpost_email_sent', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='reimbursement', + name='expiration_time', + field=models.DateTimeField(default=datetime.datetime(2026, 5, 2, 16, 0, tzinfo=utc)), + ), + migrations.AlterField( + model_name='reimbursement', + name='status', + field=models.CharField(choices=[('W', 'Wait listed'), ('PT', 'Pending receipt submission'), ('PA', 'Pending receipt approval'), ('A', 'Receipt approved'), ('X', 'Expired'), ('FS', 'Friend submission'), ('F', 'Reimbursement Approved'), ('PD', 'Pending demo validation'), ('I', 'Invalid')], default='PT', max_length=2), + ), + ] diff --git a/reimbursement/models.py b/reimbursement/models.py index c89fa6575..0fd238db8 100644 --- a/reimbursement/models.py +++ b/reimbursement/models.py @@ -19,6 +19,7 @@ RE_FINALIZED = "F" RE_EXPIRED = "X" RE_FRIEND_SUBMISSION = "FS" +RE_INVALID = "I" RE_STATUS = [ (RE_WAITLISTED, "Wait listed"), @@ -29,6 +30,7 @@ (RE_FRIEND_SUBMISSION, "Friend submission"), (RE_FINALIZED, "Reimbursement Approved"), (RE_PEND_DEMO_VAL, "Pending demo validation"), + (RE_INVALID, "Invalid"), ] @@ -65,6 +67,7 @@ class Reimbursement(models.Model): public_comment = models.CharField(max_length=300, null=True, blank=True) devpost = models.URLField(blank=True, null=True, default="") + devpost_email_sent = models.BooleanField(default=False) # User controlled paypal_email = models.EmailField(null=True, blank=True) @@ -157,9 +160,10 @@ def validate(self, user): self.reimbursed_by = user self.save() - def invalidate(self, user): + def invalidate(self, user, reason): if self.status == RE_PEND_DEMO_VAL: - self.status = RE_WAITLISTED + self.status = RE_INVALID + self.public_comment = reason self.status_update_date = timezone.now() self.reimbursed_by = user self.save() diff --git a/reimbursement/templates/mails/devpost_approved_message.html b/reimbursement/templates/mails/devpost_approved_message.html new file mode 100644 index 000000000..50497be18 --- /dev/null +++ b/reimbursement/templates/mails/devpost_approved_message.html @@ -0,0 +1,14 @@ +{% extends 'base_email.html' %} +{% block preheader %}Your travel reimbursement has been approved!{% endblock %} + +{% block content %} +

    Congratulations {{ app.user.get_full_name }}!

    +
    +

    Your travel reimbursement request has been approved by our Finance Team. You will receive the reimbursed amount within two months after the event. Please note that minor delays may occur due to the high volume of travel reimbursement requests this year.

    + +

    You have been assigned: {{ h_currency }}{{ reimb.reimbursement_money }}

    + +

    The money will be transferred to your PayPal account. Make sure the PayPal account you provided is correct and active.

    + +{% include 'mails/include/closing.html' with travel="true" %} +{% endblock %} diff --git a/reimbursement/templates/mails/devpost_approved_subject.txt b/reimbursement/templates/mails/devpost_approved_subject.txt new file mode 100644 index 000000000..92c21d31b --- /dev/null +++ b/reimbursement/templates/mails/devpost_approved_subject.txt @@ -0,0 +1 @@ +Travel Reimbursement – Approved! \ No newline at end of file diff --git a/reimbursement/templates/mails/devpost_upload_message.html b/reimbursement/templates/mails/devpost_upload_message.html new file mode 100644 index 000000000..f7d81aca1 --- /dev/null +++ b/reimbursement/templates/mails/devpost_upload_message.html @@ -0,0 +1,19 @@ +{% extends 'base_email.html' %} +{% block preheader %}Upload your Devpost link{% endblock %} + +{% block content %} +

    Hey {{ app.user.get_full_name }},

    +
    +

    HackUPC has officially come to an end, and we hope you enjoyed the event as much as we did!

    +

    The next step to get your travel reimbursement is to upload the link to your project on Devpost. This will allow us + to validate your submission and verify that the project is valid and that it complies with the requirements that + were previously communicated.

    + +

    Upload your link here:

    + +{% include 'mails/include/email_button.html' with text='Upload Devpost link' url=form_url %} + +

    Thank you for your collaboration!!

    + +{% include 'mails/include/closing.html' with travel="true" %} +{% endblock %} \ No newline at end of file diff --git a/reimbursement/templates/mails/devpost_upload_subject.txt b/reimbursement/templates/mails/devpost_upload_subject.txt new file mode 100644 index 000000000..991155d8f --- /dev/null +++ b/reimbursement/templates/mails/devpost_upload_subject.txt @@ -0,0 +1 @@ +Upload your Devpost link diff --git a/reimbursement/templates/mails/project_invalidated_message.html b/reimbursement/templates/mails/project_invalidated_message.html new file mode 100644 index 000000000..73c52b8a7 --- /dev/null +++ b/reimbursement/templates/mails/project_invalidated_message.html @@ -0,0 +1,17 @@ +{% extends 'base_email.html' %} +{% block preheader %}Your travel reimbursement request – Not Approved{% endblock %} + +{% block content %} +

    Dear {{ app.user.get_full_name }},

    +
    +

    After reviewing your travel reimbursement request, we regret to inform you that your application cannot be approved. This may be due to one of the following reasons:

    + + + +

    If you believe this decision has been made in error or would like further clarification, please feel free to contact us.

    +

    Best regards,

    + +{% endblock %} diff --git a/reimbursement/templates/mails/project_invalidated_subject.txt b/reimbursement/templates/mails/project_invalidated_subject.txt new file mode 100644 index 000000000..080f7d800 --- /dev/null +++ b/reimbursement/templates/mails/project_invalidated_subject.txt @@ -0,0 +1 @@ +Travel Reimbursement Request – Not Approved diff --git a/reimbursement/templates/mails/reject_receipt_message.html b/reimbursement/templates/mails/reject_receipt_message.html index b5461b9a0..066c62e08 100644 --- a/reimbursement/templates/mails/reject_receipt_message.html +++ b/reimbursement/templates/mails/reject_receipt_message.html @@ -5,7 +5,7 @@

    Hey {{ app.user.get_full_name }},


    The receipt you submitted is invalid.

    -

    Comment on receipt submitted: {{ reimb.public_comment }}

    +

    Rejected reason: {{ reimb.public_comment }}

    {% include 'mails/include/email_button.html' with text='Submit receipt' url=form_url %}

    diff --git a/reimbursement/templates/mails/ticket_accepted_message.html b/reimbursement/templates/mails/ticket_accepted_message.html new file mode 100644 index 000000000..775dc320e --- /dev/null +++ b/reimbursement/templates/mails/ticket_accepted_message.html @@ -0,0 +1,31 @@ +{% extends 'base_email.html' %} +{% block preheader %}Your ticket has been accepted{% endblock %} + +{% block content %} +

    Hey {{ app.user.get_full_name }},

    +
    +

    If you are receiving this mail is because your travel ticket has been accepted.

    +

    However, the process requires you to follow the below instructions during the event to consider you eligible for a + reimbursement.

    + +

    Regarding the project:

    + + +

    Regarding the attendance:

    + + +

    The money will be transferred to the participant using PayPal in the following 60 days after the event. The paypal + account MUST exist.

    + +{% include 'mails/include/closing.html' with travel="true" %} +{% endblock %} \ No newline at end of file diff --git a/reimbursement/templates/mails/ticket_accepted_subject.txt b/reimbursement/templates/mails/ticket_accepted_subject.txt new file mode 100644 index 000000000..4498c9a44 --- /dev/null +++ b/reimbursement/templates/mails/ticket_accepted_subject.txt @@ -0,0 +1 @@ +[IMPORTANT INFORMATION ] Your ticket has been accepted diff --git a/reimbursement/templates/mails/travel_tickets_upload_message.html b/reimbursement/templates/mails/travel_tickets_upload_message.html new file mode 100644 index 000000000..4f33175fb --- /dev/null +++ b/reimbursement/templates/mails/travel_tickets_upload_message.html @@ -0,0 +1,34 @@ +{% extends 'base_email.html' %} +{% block preheader %}Upload your travel ticket{% endblock %} + +{% block content %} +

    Hey {{ app.user.get_full_name }},

    +
    + +

    Congratulations on being accepted to HackUPC 2026! 🎉

    + +

    We noticed that you selected the travel reimbursement option. To proceed, please upload a valid travel ticket to your MyHackUPC dashboard as soon as possible.

    + +{% include 'mails/include/email_button.html' with text='Upload ticket' url=form_url %} + +

    A ticket will be considered valid only if:

    + + +
    + +

    Reimbursement Policy

    +

    The reimbursed amount will be the minimum of:

    + + +{% include 'mails/include/closing.html' with travel="true" %} +{% endblock %} diff --git a/reimbursement/templates/mails/travel_tickets_upload_subject.txt b/reimbursement/templates/mails/travel_tickets_upload_subject.txt new file mode 100644 index 000000000..b74d979d8 --- /dev/null +++ b/reimbursement/templates/mails/travel_tickets_upload_subject.txt @@ -0,0 +1 @@ +Upload your travel tickets diff --git a/reimbursement/views.py b/reimbursement/views.py index 993e93357..99a63af11 100644 --- a/reimbursement/views.py +++ b/reimbursement/views.py @@ -145,6 +145,7 @@ def post(self, request, *args, **kwargs): if form.is_valid(): form.save(commit=False) reimb.validate(request.user) + emails.create_devpost_approved_email(reimb, request).send() form.save() messages.success(self.request, "Reimbursement successfully validated!") else: @@ -170,7 +171,8 @@ def post(self, request, *args, **kwargs): elif "invalidate" in request.POST: id_ = kwargs.get("id", None) reimb = models.Reimbursement.objects.get(pk=id_) - reimb.invalidate(request.user) + reimb.invalidate(request.user, request.POST.get("invalidate_reason")) + emails.create_project_invalidated_email(reimb, request).send() messages.success(request, "Reimbursement invalidated") else: id_ = request.POST.get("id", None) @@ -183,6 +185,7 @@ def post(self, request, *args, **kwargs): if a_form.is_valid(): a_form.save(commit=False) a_form.instance.accept_receipt(request.user) + emails.create_ticket_accepted_email(a_form.instance, request).send() a_form.save() messages.success(request, "Receipt accepted") else: @@ -247,6 +250,7 @@ def post(self, request, *args, **kwargs): if a_form.is_valid(): a_form.save(commit=False) a_form.instance.accept_receipt(request.user) + emails.create_ticket_accepted_email(a_form.instance, request).send() a_form.save() messages.success(request, "Receipt accepted") else: