[\w-]+)/$',
diff --git a/applications/views/__init__.py b/applications/views/__init__.py
new file mode 100644
index 000000000..4b0049121
--- /dev/null
+++ b/applications/views/__init__.py
@@ -0,0 +1,23 @@
+from applications.views.application import (
+ VIEW_APPLICATION_TYPE,
+ VIEW_APPLICATION_FORM_TYPE,
+ check_application_exists,
+ get_deadline,
+ user_is_in_blacklist,
+ ConfirmApplication,
+ CancelApplication,
+ ApplicationDashboard,
+ ApplicationEditView,
+)
+from applications.views.sponsor import SponsorApplicationView, SponsorDashboard
+from applications.views.mentor import ConvertHackerToMentor
+from applications.views.draft import save_draft
+
+__all__ = [
+ 'VIEW_APPLICATION_TYPE', 'VIEW_APPLICATION_FORM_TYPE',
+ 'check_application_exists', 'get_deadline', 'user_is_in_blacklist',
+ 'ConfirmApplication', 'CancelApplication', 'ApplicationDashboard', 'ApplicationEditView',
+ 'SponsorApplicationView', 'SponsorDashboard',
+ 'ConvertHackerToMentor',
+ 'save_draft',
+]
diff --git a/applications/views.py b/applications/views/application.py
similarity index 69%
rename from applications/views.py
rename to applications/views/application.py
index b6b0c8786..0e21ea518 100644
--- a/applications/views.py
+++ b/applications/views/application.py
@@ -1,4 +1,3 @@
-# Create your views here.
from __future__ import print_function
import logging
@@ -8,32 +7,24 @@
from django.contrib import messages
from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.exceptions import ValidationError
-from django.db import IntegrityError
-from django.http import Http404, HttpResponseRedirect, JsonResponse
+from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone
-from django.utils.encoding import force_text
-from django.utils.http import urlsafe_base64_decode
from django.views import View
-from django.views.generic import TemplateView
from app import slack
from app.hackathon_variables import MAX_REIMBURSEMENTS_UNTIL_WAITLIST
from app.slack import SlackInvitationException
-from app.utils import reverse, hacker_tabs
+from app.utils import reverse, hacker_tabs, generateGTicketUrl, isset
from app.views import TabsView
from applications import models, emails, forms
-from organizers.tables import SponsorFilter, SponsorListTableWithNoAction
-from organizers.views import _OtherApplicationsListView
from reimbursement.models import RE_WAITLISTED, Reimbursement
-from user.mixins import IsHackerMixin, is_hacker, IsSponsorMixin, DashboardMixin
+from user.mixins import IsHackerMixin, DashboardMixin
from user import models as userModels
from reimbursement import emails as reimb_emails
from django.conf import settings
-from app.utils import generateGTicketUrl, isset
-
VIEW_APPLICATION_TYPE = {
userModels.USR_HACKER: models.HackerApplication,
userModels.USR_VOLUNTEER: models.VolunteerApplication,
@@ -165,14 +156,14 @@ def get_deadline(application):
return deadline
-class HackerDashboard(DashboardMixin, TabsView):
+class ApplicationDashboard(DashboardMixin, TabsView):
template_name = "dashboard.html"
def get_current_tabs(self):
return hacker_tabs(self.request.user)
def get_context_data(self, **kwargs):
- context = super(HackerDashboard, self).get_context_data(**kwargs)
+ context = super(ApplicationDashboard, self).get_context_data(**kwargs)
Application = VIEW_APPLICATION_TYPE.get(
self.request.user.type, models.HackerApplication
)
@@ -295,14 +286,14 @@ def confirm_application(self, request, application):
return render(request, self.template_name, c)
-class HackerApplication(IsHackerMixin, TabsView):
+class ApplicationEditView(IsHackerMixin, TabsView):
template_name = "application.html"
def get_current_tabs(self):
return hacker_tabs(self.request.user)
def get_context_data(self, **kwargs):
- context = super(HackerApplication, self).get_context_data(**kwargs)
+ context = super(ApplicationEditView, self).get_context_data(**kwargs)
Application = VIEW_APPLICATION_TYPE.get(
self.request.user.type, models.HackerApplication
@@ -360,136 +351,6 @@ def post(self, request, *args, **kwargs):
return render(request, self.template_name, c)
-class SponsorApplicationView(TemplateView):
- template_name = "dashboard.html"
-
- def get_context_data(self, **kwargs):
- context = super(SponsorApplicationView, self).get_context_data(**kwargs)
- form = forms.SponsorForm()
- context.update({"form": form, "is_sponsor": True})
- try:
- uid = force_text(urlsafe_base64_decode(self.kwargs.get("uid", None)))
- user = userModels.User.objects.get(pk=uid)
- context.update({"user": user})
- context.update({"company_name": user.name})
- except (TypeError, ValueError, OverflowError, userModels.User.DoesNotExist):
- pass
-
- return context
-
- def get(self, request, *args, **kwargs):
- try:
- uid = force_text(urlsafe_base64_decode(self.kwargs.get("uid", None)))
- user = userModels.User.objects.get(pk=uid)
- real_token = userModels.Token.objects.get(pk=user).uuid_str()
- token = self.kwargs.get("token", None)
- except (
- TypeError,
- ValueError,
- OverflowError,
- userModels.User.DoesNotExist,
- userModels.Token.DoesNotExist,
- ):
- raise Http404("Invalid url")
- if token != real_token:
- raise Http404("Invalid url")
- if not user.has_applications_left()[0]:
- raise Http404("You have no applications left")
- return super(SponsorApplicationView, self).get(request, *args, **kwargs)
-
- def post(self, request, *args, **kwargs):
- form = forms.SponsorForm(request.POST, request.FILES)
- try:
- uid = force_text(urlsafe_base64_decode(self.kwargs.get("uid", None)))
- user = userModels.User.objects.get(pk=uid)
- except (TypeError, ValueError, OverflowError, userModels.User.DoesNotExist):
- return Http404("How did you get here?")
- has_applications_left, applied = user.has_applications_left()
- if not has_applications_left:
- form.add_error(None, "You have no applications left")
- elif form.is_valid():
- name = form.cleaned_data["name"]
- app = models.SponsorApplication.objects.filter(user=user, name=name).first()
- if app:
- form.add_error("name", "This name is already taken. Have you applied?")
- else:
- user.pk = None
- user.max_applications = 0
- application = form.save(commit=False)
- user.password = ""
- error = True
- while error:
- user.email = ("+%s@" % applied).join(user.email.split("@"))
- try:
- user.save()
- error = False
- except IntegrityError:
- applied += 1
- application.user = user
- application.save()
- messages.success(request, "We have now received your application. ")
- return render(request, "sponsor_submitted.html")
- c = self.get_context_data()
- c.update({"form": form})
- return render(request, self.template_name, c)
-
-
-class ConvertHackerToMentor(TemplateView):
- template_name = "convert_mentor.html"
-
- def get(self, request, *args, **kwargs):
- if request.user.application.is_invalid():
- return super(ConvertHackerToMentor, self).get(request, *args, **kwargs)
- return Http404
-
- def post(self, request, *args, **kwargs):
- if request.user.application.is_invalid():
- request.user.set_mentor()
- request.user.save()
- messages.success(request, "Thanks for coming as mentor!")
- else:
- messages.error(request, "You have no permissions to do this")
- return HttpResponseRedirect(reverse("dashboard"))
-
-
-class SponsorDashboard(IsSponsorMixin, _OtherApplicationsListView):
- table_class = SponsorListTableWithNoAction
- filterset_class = SponsorFilter
-
- def get_current_tabs(self):
- return None
-
- def get_queryset(self):
- return models.SponsorApplication.objects.filter(user=self.request.user)
-
- def get_context_data(self, **kwargs):
- context = super(SponsorDashboard, self).get_context_data(**kwargs)
- context["otherApplication"] = True
- context["emailCopy"] = False
- return context
-
-
-@is_hacker
-def save_draft(request):
- Application = VIEW_APPLICATION_TYPE.get(request.user.type, models.HackerApplication)
- ApplicationForm = VIEW_APPLICATION_FORM_TYPE.get(
- request.user.type, forms.HackerApplicationForm
- )
- d = models.DraftApplication()
- d.user = request.user
- form_keys = set(dict(ApplicationForm().fields).keys())
- valid_keys = set([field.name for field in Application()._meta.get_fields()])
- d.save_dict(
- dict(
- (k, v)
- for k, v in request.POST.items()
- if k in valid_keys.intersection(form_keys) and v
- )
- )
- d.save()
- return JsonResponse({"saved": True})
-
-
def user_is_in_blacklist(user):
result = True
blacklist_user = models.BlacklistUser.objects.filter(email=user.email).first()
diff --git a/applications/views/draft.py b/applications/views/draft.py
new file mode 100644
index 000000000..1158115d0
--- /dev/null
+++ b/applications/views/draft.py
@@ -0,0 +1,26 @@
+from django.http import JsonResponse
+
+from applications import models, forms
+from applications.views.application import VIEW_APPLICATION_TYPE, VIEW_APPLICATION_FORM_TYPE
+from user.mixins import is_hacker
+
+
+@is_hacker
+def save_draft(request):
+ Application = VIEW_APPLICATION_TYPE.get(request.user.type, models.HackerApplication)
+ ApplicationForm = VIEW_APPLICATION_FORM_TYPE.get(
+ request.user.type, forms.HackerApplicationForm
+ )
+ d = models.DraftApplication()
+ d.user = request.user
+ form_keys = set(dict(ApplicationForm().fields).keys())
+ valid_keys = set([field.name for field in Application()._meta.get_fields()])
+ d.save_dict(
+ dict(
+ (k, v)
+ for k, v in request.POST.items()
+ if k in valid_keys.intersection(form_keys) and v
+ )
+ )
+ d.save()
+ return JsonResponse({"saved": True})
diff --git a/applications/views/mentor.py b/applications/views/mentor.py
new file mode 100644
index 000000000..7254429b1
--- /dev/null
+++ b/applications/views/mentor.py
@@ -0,0 +1,23 @@
+from django.contrib import messages
+from django.http import Http404, HttpResponseRedirect
+from django.views.generic import TemplateView
+
+from app.utils import reverse
+
+
+class ConvertHackerToMentor(TemplateView):
+ template_name = "convert_mentor.html"
+
+ def get(self, request, *args, **kwargs):
+ if request.user.application.is_invalid():
+ return super(ConvertHackerToMentor, self).get(request, *args, **kwargs)
+ return Http404
+
+ def post(self, request, *args, **kwargs):
+ if request.user.application.is_invalid():
+ request.user.set_mentor()
+ request.user.save()
+ messages.success(request, "Thanks for coming as mentor!")
+ else:
+ messages.error(request, "You have no permissions to do this")
+ return HttpResponseRedirect(reverse("dashboard"))
diff --git a/applications/views/sponsor.py b/applications/views/sponsor.py
new file mode 100644
index 000000000..6cbabc559
--- /dev/null
+++ b/applications/views/sponsor.py
@@ -0,0 +1,104 @@
+from django.contrib import messages
+from django.db import IntegrityError
+from django.http import Http404
+from django.shortcuts import render
+from django.utils.encoding import force_text
+from django.utils.http import urlsafe_base64_decode
+from django.views.generic import TemplateView
+
+from applications import models, forms
+from organizers.tables import SponsorFilter, SponsorListTableWithNoAction
+from organizers.views import _OtherApplicationsListView
+from user.mixins import IsSponsorMixin
+from user import models as userModels
+
+
+class SponsorApplicationView(TemplateView):
+ template_name = "dashboard.html"
+
+ def get_context_data(self, **kwargs):
+ context = super(SponsorApplicationView, self).get_context_data(**kwargs)
+ form = forms.SponsorForm()
+ context.update({"form": form, "is_sponsor": True})
+ try:
+ uid = force_text(urlsafe_base64_decode(self.kwargs.get("uid", None)))
+ user = userModels.User.objects.get(pk=uid)
+ context.update({"user": user})
+ context.update({"company_name": user.name})
+ except (TypeError, ValueError, OverflowError, userModels.User.DoesNotExist):
+ pass
+
+ return context
+
+ def get(self, request, *args, **kwargs):
+ try:
+ uid = force_text(urlsafe_base64_decode(self.kwargs.get("uid", None)))
+ user = userModels.User.objects.get(pk=uid)
+ real_token = userModels.Token.objects.get(pk=user).uuid_str()
+ token = self.kwargs.get("token", None)
+ except (
+ TypeError,
+ ValueError,
+ OverflowError,
+ userModels.User.DoesNotExist,
+ userModels.Token.DoesNotExist,
+ ):
+ raise Http404("Invalid url")
+ if token != real_token:
+ raise Http404("Invalid url")
+ if not user.has_applications_left()[0]:
+ raise Http404("You have no applications left")
+ return super(SponsorApplicationView, self).get(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ form = forms.SponsorForm(request.POST, request.FILES)
+ try:
+ uid = force_text(urlsafe_base64_decode(self.kwargs.get("uid", None)))
+ user = userModels.User.objects.get(pk=uid)
+ except (TypeError, ValueError, OverflowError, userModels.User.DoesNotExist):
+ return Http404("How did you get here?")
+ has_applications_left, applied = user.has_applications_left()
+ if not has_applications_left:
+ form.add_error(None, "You have no applications left")
+ elif form.is_valid():
+ name = form.cleaned_data["name"]
+ app = models.SponsorApplication.objects.filter(user=user, name=name).first()
+ if app:
+ form.add_error("name", "This name is already taken. Have you applied?")
+ else:
+ user.pk = None
+ user.max_applications = 0
+ application = form.save(commit=False)
+ user.password = ""
+ error = True
+ while error:
+ user.email = ("+%s@" % applied).join(user.email.split("@"))
+ try:
+ user.save()
+ error = False
+ except IntegrityError:
+ applied += 1
+ application.user = user
+ application.save()
+ messages.success(request, "We have now received your application. ")
+ return render(request, "sponsor_submitted.html")
+ c = self.get_context_data()
+ c.update({"form": form})
+ return render(request, self.template_name, c)
+
+
+class SponsorDashboard(IsSponsorMixin, _OtherApplicationsListView):
+ table_class = SponsorListTableWithNoAction
+ filterset_class = SponsorFilter
+
+ def get_current_tabs(self):
+ return None
+
+ def get_queryset(self):
+ return models.SponsorApplication.objects.filter(user=self.request.user)
+
+ def get_context_data(self, **kwargs):
+ context = super(SponsorDashboard, self).get_context_data(**kwargs)
+ context["otherApplication"] = True
+ context["emailCopy"] = False
+ return context
diff --git a/baggage/views/__init__.py b/baggage/views/__init__.py
new file mode 100644
index 000000000..7d6e8aea9
--- /dev/null
+++ b/baggage/views/__init__.py
@@ -0,0 +1,22 @@
+from baggage.views.volunteer import (
+ baggage_checkIn,
+ baggage_checkOut,
+ organizer_tabs,
+ BaggageList,
+ BaggageHacker,
+ BaggageUsers,
+ BaggageAdd,
+ BaggageDetail,
+ BaggageMap,
+ BaggageHistory,
+ BaggageAPI,
+)
+from baggage.views.hacker import hacker_tabs, BaggageCurrentHacker
+
+__all__ = [
+ 'baggage_checkIn', 'baggage_checkOut', 'organizer_tabs',
+ 'BaggageList', 'BaggageHacker', 'BaggageUsers',
+ 'BaggageAdd', 'BaggageDetail', 'BaggageMap', 'BaggageHistory',
+ 'BaggageAPI',
+ 'hacker_tabs', 'BaggageCurrentHacker',
+]
diff --git a/baggage/views/hacker.py b/baggage/views/hacker.py
new file mode 100644
index 000000000..d95ab964a
--- /dev/null
+++ b/baggage/views/hacker.py
@@ -0,0 +1,27 @@
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.urls import reverse
+from django_filters.views import FilterView
+from django_tables2 import SingleTableMixin
+
+from app.mixins import TabsViewMixin
+from baggage.models import Bag, BAG_ADDED
+from baggage.tables import BaggageCurrentHackerTable, BaggageListFilter
+
+
+def hacker_tabs(user):
+ t = [('Baggage', reverse('baggage_currenthacker'), False)]
+ return t
+
+
+class BaggageCurrentHacker(LoginRequiredMixin, TabsViewMixin, SingleTableMixin, FilterView):
+ template_name = 'baggage_currenthacker.html'
+ table_class = BaggageCurrentHackerTable
+ filterset_class = BaggageListFilter
+ table_pagination = {'per_page': 100}
+
+ def get_current_tabs(self):
+ return hacker_tabs(self.request.user)
+
+ def get_queryset(self):
+ user = self.request.user
+ return Bag.objects.filter(status=BAG_ADDED, owner=user)
diff --git a/baggage/views.py b/baggage/views/volunteer.py
similarity index 93%
rename from baggage/views.py
rename to baggage/views/volunteer.py
index da0dfd14e..eb07bc9c2 100644
--- a/baggage/views.py
+++ b/baggage/views/volunteer.py
@@ -1,29 +1,26 @@
import json
+import base64
+import time
+from django.conf import settings
+from django.contrib import messages
+from django.core.files.base import ContentFile
+from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse
+from django.shortcuts import redirect, get_object_or_404
from django.urls import reverse
+from django_filters.views import FilterView
+from django_tables2 import SingleTableMixin
+from rest_framework.views import APIView
from app.mixins import TabsViewMixin
from app.services.messages import MessageManager
-from baggage.tables import BaggageListTable, BaggageListFilter, BaggageUsersTable
-from baggage.tables import BaggageUsersFilter, BaggageCurrentHackerTable
-from baggage.models import Bag, BAG_ADDED, BAG_REMOVED, Room
-from user.models import User
-from checkin.models import CheckIn
-from django_tables2 import SingleTableMixin
-from django_filters.views import FilterView
from app.views import TabsView
-from rest_framework.views import APIView
-from django.contrib import messages
-from django.http import HttpResponseRedirect, Http404
-from django.shortcuts import redirect, get_object_or_404
+from baggage.models import Bag, BAG_ADDED, BAG_REMOVED, Room
+from baggage.tables import BaggageListTable, BaggageListFilter, BaggageUsersTable, BaggageUsersFilter
from baggage import utils
-import base64
-from django.core.files.base import ContentFile
-import time
+from checkin.models import CheckIn
from user.mixins import IsVolunteerMixin
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.http import HttpResponse, JsonResponse
-from django.conf import settings
+from user.models import User
def baggage_checkIn(request, bag, bagrow, bagcol, bagroom, posmanual, bagspe):
@@ -74,11 +71,6 @@ def organizer_tabs(user):
return t
-def hacker_tabs(user):
- t = [('Baggage', reverse('baggage_currenthacker'), False)]
- return t
-
-
class BaggageList(IsVolunteerMixin, TabsViewMixin, SingleTableMixin, FilterView):
template_name = 'baggage_list.html'
table_class = BaggageListTable
@@ -257,20 +249,6 @@ def get_context_data(self, **kwargs):
return context
-class BaggageCurrentHacker(LoginRequiredMixin, TabsViewMixin, SingleTableMixin, FilterView):
- template_name = 'baggage_currenthacker.html'
- table_class = BaggageCurrentHackerTable
- filterset_class = BaggageListFilter
- table_pagination = {'per_page': 100}
-
- def get_current_tabs(self):
- return hacker_tabs(self.request.user)
-
- def get_queryset(self):
- user = self.request.user
- return Bag.objects.filter(status=BAG_ADDED, owner=user)
-
-
class BaggageAPI(APIView):
def get(self, request, format=None):
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 000000000..77ed2f54a
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,90 @@
+# Contributing — Common Changes
+
+This document walks through the most common yearly changes with exact file paths.
+
+---
+
+## Example 1: Adding or Removing a Field from the Hacker Application
+
+The hacker application form is split across three layers: model, form, and template.
+
+### 1. Update the model
+
+File: `applications/models/hacker.py`
+
+Add or remove the field from the `HackerApplication` model class. Use standard Django field types. Example:
+
+```python
+# Adding a new field
+dietary_restrictions = models.CharField(
+ max_length=200,
+ blank=True,
+ help_text="Any dietary restrictions we should know about?"
+)
+```
+
+### 2. Create and run a migration
+
+```bash
+python manage.py makemigrations
+python manage.py migrate
+```
+
+### 3. Update the form
+
+File: `applications/forms/hacker.py`
+
+Add the field name to the `fields` list in `HackerApplicationForm.Meta`, and optionally customize its widget or label in the `widgets` or `labels` dicts.
+
+### 4. Update the template
+
+File: `applications/templates/include/application_form.html` (or the relevant template — look for the `{% for field in form %}` loop or the specific field rendering)
+
+If you need custom layout for the new field, add it explicitly. Otherwise it will be rendered automatically by the form loop.
+
+### 5. Verify
+
+Start the dev server, go to the hacker application form, and confirm the new field appears.
+
+---
+
+## Example 2: Adding or Removing a Field from the Volunteer or Mentor Application
+
+Same pattern as the hacker application, different files:
+
+
+| Layer | Volunteer | Mentor |
+| -------- | ------------------------------------------------------------------- | ------------------------------- |
+| Model | `applications/models/volunteer.py` | `applications/models/mentor.py` |
+| Form | `applications/forms/volunteer.py` | `applications/forms/mentor.py` |
+| Template | `applications/templates/` (look for volunteer/mentor form template) | same |
+
+
+Run `python manage.py makemigrations && python manage.py migrate` after changing the model.
+
+---
+
+## Example 3: Changing the Travel Reimbursement Cap
+
+File: `app/hackathon_variables.py`
+
+Find and update the reimbursement limit variables. No migration or code change needed — these are config values read at runtime.
+
+---
+
+## Where to Look for Other Things
+
+
+| What you want to change | Where to look |
+| ------------------------------------------------------- | ------------------------------------- |
+| Application status flow (pending → invited → confirmed) | `applications/models/base.py` |
+| Who can review applications and vote | `organizers/views/review.py` |
+| Batch invite algorithm | `organizers/views/batch_ops.py` |
+| Email content | See `docs/email-templates.md` |
+| Check-in logic | `checkin/views.py` |
+| User roles and permissions | `user/models.py`, `user/mixins.py` |
+| Hackathon name, dates, deadlines | `app/hackathon_variables.py` |
+| Global styles | `app/static/css/main.css` |
+| Bootstrap component styles | `app/static/css/bootstrap-overrides/` |
+
+
diff --git a/docs/email-templates.md b/docs/email-templates.md
new file mode 100644
index 000000000..151c11bdd
--- /dev/null
+++ b/docs/email-templates.md
@@ -0,0 +1,88 @@
+# Email Templates
+
+myhackupc sends emails using Django's template system. Each email consists of two template files:
+- `*_subject.txt` — the subject line (one line of text, no HTML)
+- `*_message.html` — the email body (HTML)
+
+---
+
+## All Emails the System Sends
+
+| Email | Sent when | App | Template path |
+|---|---|---|---|
+| Invitation - Hacker | User is invited as a hacker | applications | `applications/templates/mails/invitation_hacker_*` |
+| Volunteer invitation | (sending code currently disabled) | applications | `applications/templates/mails/invitation_volunteer_*` |
+| Mentor invitation | (sending code currently disabled) | applications | `applications/templates/mails/invitation_mentor_*` |
+| Confirmation | Hacker confirms their attendance | applications | `applications/templates/mails/confirmation_*` |
+| Last Reminder | Reminder before event starts | applications | `applications/templates/mails/last_reminder_*` |
+| Reimbursement | Hacker is eligible for travel reimbursement | reimbursement | `reimbursement/templates/mails/reimbursement_*` |
+| Reject Receipt | Submitted receipt is rejected | reimbursement | `reimbursement/templates/mails/reject_receipt_*` |
+| No Reimbursement | Hacker is not eligible for reimbursement | reimbursement | `reimbursement/templates/mails/no_reimbursement_*` |
+| Travel Tickets Upload | Hacker needs to upload travel tickets | reimbursement | `reimbursement/templates/mails/travel_tickets_upload_*` |
+| Ticket Accepted | Travel ticket is accepted | reimbursement | `reimbursement/templates/mails/ticket_accepted_*` |
+| Devpost Upload | Hacker needs to submit project to Devpost | reimbursement | `reimbursement/templates/mails/devpost_upload_*` |
+| Project Invalidated | Submitted Devpost project is invalid | reimbursement | `reimbursement/templates/mails/project_invalidated_*` |
+| Devpost Approved | Devpost project is approved | reimbursement | `reimbursement/templates/mails/devpost_approved_*` |
+| Verify Email | User needs to verify their email address | user | `user/templates/mails/verify_email_*` |
+| Password Reset | User requests password reset | user | `user/templates/mails/password_reset_*` |
+| Sponsor Link | Admin shares sponsor signup link with user | user | `user/templates/mails/sponsor_link_*` |
+
+Shared email components (footer, buttons, images) are in `app/templates/mails/include/`. App-specific includes also live alongside their templates: `applications/templates/mails/include/` and `reimbursement/templates/mails/include/`.
+
+---
+
+## How to Edit Email Content
+
+1. Find the email in the table above
+2. Open the `*_message.html` file — this is the email body
+3. Edit the HTML content. You can use Django template tags (`{{ variable }}`, `{% if %}`, etc.)
+4. To change the subject line, edit the `*_subject.txt` file
+
+---
+
+## Variables Available in Each Template
+
+Each email has a set of variables passed to it from the Python code. Here is where those variables are defined:
+
+| Email group | Python file | Context variables |
+|---|---|---|
+| Invite (hacker) | `applications/emails.py` → `create_invite_email` | `name`, `reimb`, `confirm_url`, `cancel_url`, `hybrid_option` |
+| Confirmation | `applications/emails.py` → `create_confirmation_email` | `name`, `token`, `qr_url`, `cancel_url`, `is_hacker`, `is_sponsor` |
+| Last reminder | `applications/emails.py` → `create_lastreminder_email` | `name`, `type`, `confirm_url`, `cancel_url`, `is_hacker`, `is_sponsor` |
+| Reimbursement emails | `reimbursement/emails.py` | `app`, `reimb`, `confirm_url`, `form_url`, `cancel_url` |
+| User emails | `user/emails.py` | `user`, `activate_url` (for verify email); `user`, `reset_url` (for password reset); `user`, `user_sponsor_url`, `app_sponsor_url`, `sponsor_name` (for sponsor link) |
+
+---
+
+## How to Add a New Variable to an Email
+
+Example: you want to add `{{ deadline }}` to the hacker invitation email.
+
+### Step 1: Find the Python function that creates the email
+
+Open `applications/emails.py` and find `create_invite_email`. You will see a `c = { ... }` dictionary — this is the context passed to the template.
+
+### Step 2: Add the variable to the context dict
+
+```python
+c = {
+ 'name': application.user.get_full_name,
+ 'reimb': getattr(application.user, 'reimbursement', None),
+ 'confirm_url': str(reverse('dashboard', request=request)),
+ 'cancel_url': str(reverse('cancel_app', request=request, kwargs={'id': application.uuid_str})),
+ 'hybrid_option': 'Online' if getattr(application, 'online', False) else 'Live',
+ 'deadline': settings.HACKATHON_END, # ← add your variable here
+}
+```
+
+### Step 3: Use the variable in the template
+
+Open `applications/templates/mails/invitation_hacker_message.html` and add:
+
+```html
+The deadline to confirm is {{ deadline }}.
+```
+
+### Step 4: Verify
+
+Trigger the email in local dev (or use the Django shell) and confirm the variable renders correctly.
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 000000000..444d69246
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,80 @@
+# Getting Started
+
+myhackupc is the registration and event management tool for HackUPC. It is a Django web application used by three groups:
+
+- **Hackers** — apply, confirm attendance, request travel reimbursement
+- **Organizers** — review applications, vote, send invites, manage check-in, hardware, meals, baggage
+- **Volunteers/Mentors** — access check-in and their own application flow
+
+---
+
+## Codebase Structure
+
+The project is a standard Django multi-app repository. Each feature lives in its own app folder. Here is what each active app does:
+
+| App | Purpose |
+|---|---|
+| `app/` | Core config: settings, base templates, global CSS, shared utilities, hackathon variables |
+| `applications/` | Hacker, volunteer, mentor, and sponsor application forms and status flow |
+| `organizers/` | Application review, voting, batch invites, waitlist/blacklist management |
+| `user/` | Authentication: sign up, login, password reset, email verification, user roles |
+| `reimbursement/` | Travel reimbursement requests, receipt upload, organizer approval flow |
+| `checkin/` | QR-code-based event check-in for hackers, volunteers, and mentors |
+| `baggage/` | Baggage drop-off and pick-up tracking |
+| `hardware/` | Hardware lab lending requests |
+| `meals/` | Meal distribution tracking per hacker |
+| `teams/` | Team formation and membership |
+| `stats/` | Organizer-facing analytics dashboards |
+
+Excluded from active development: `judging`, `offer`, `discord`.
+
+---
+
+## Where Things Live
+
+### Templates
+Each app has its own `templates/` folder. For example:
+- Hacker application form: `applications/templates/`
+- Organizer review interface: `organizers/templates/`
+- Email templates: `/templates/mails/` in the relevant app
+
+### Styles
+- Global layout, typography, theme colors: `app/static/css/main.css`
+- Bootstrap component overrides: `app/static/css/bootstrap-overrides/` (one file per component)
+- App-specific styles: each app has its own `static/css/` folder (e.g. `baggage/static/css/baggage.css`, `hardware/static/css/hw.css`)
+
+### URLs
+| URL prefix | App |
+|---|---|
+| `/` | `applications` (hacker dashboard) |
+| `/applications/` | `organizers` (review interface) |
+| `/user/` | `user` (auth) |
+| `/reimbursement/` | `reimbursement` |
+| `/checkin/` | `checkin` |
+| `/baggage/` | `baggage` |
+| `/hardware/` | `hardware` |
+| `/meals/` | `meals` |
+| `/teams/` | `teams` |
+| `/stats/` | `stats` |
+| `/admin/` | Django admin |
+
+Note: `/reimbursement/`, `/baggage/`, and `/hardware/` are only registered when their respective feature flags are enabled in `app/hackathon_variables.py`.
+
+---
+
+## The Most Important Config File
+
+**`app/hackathon_variables.py`** — this is what you touch every year. It contains:
+- Application open/close deadlines
+- Reimbursement cap amounts
+- Feature flags (baggage, hardware, reimbursement, discord)
+- Hackathon name, domain, email addresses
+- Slack channel IDs
+
+---
+
+## Next Steps
+
+- **Set up your local environment:** see `docs/setting-up.md`
+- **Make a change:** see `docs/contributing.md`
+- **Edit an email template:** see `docs/email-templates.md`
diff --git a/docs/setting-up.md b/docs/setting-up.md
new file mode 100644
index 000000000..d673791e9
--- /dev/null
+++ b/docs/setting-up.md
@@ -0,0 +1,87 @@
+# Setting Up myhackupc Locally
+
+## Prerequisites
+
+- Python 3.10
+- `virtualenv`
+
+---
+
+## Installation
+
+```bash
+git clone https://github.com/hackupc/myhackupc && cd myhackupc
+virtualenv env --python=python3.10
+source ./env/bin/activate
+pip install -r requirements.txt
+```
+
+> **Note on psycopg2-binary:** If you get an error, install openssl@3 and export LDFLAGS, CPPFLAGS, and PKG_CONFIG_PATH before running pip install.
+
+---
+
+## Configuration
+
+Open `app/hackathon_variables.py` and set the required variables. You can reference the variables documented in `README.md` under "Available environment variables".
+
+Open `app/hackathon_variables.py` and set at minimum:
+- `HACKATHON_NAME`
+- `HACKATHON_DOMAIN` (use `localhost:8000` for local dev)
+- Application deadlines
+
+---
+
+## Database
+
+```bash
+python manage.py migrate
+python manage.py createsuperuser
+```
+
+---
+
+## Running the Dev Server
+
+```bash
+source ./env/bin/activate
+python manage.py runserver
+```
+
+Visit http://localhost:8000. Log in with the superuser you created.
+
+---
+
+## Environment Variables (Optional)
+
+| Variable | Purpose |
+|----------|---------|
+| **SG_KEY** | SendGrid API Key. Mandatory if you want to use SendGrid as your email backend. You can manage them [here](https://app.sendgrid.com/settings/api_keys). If not added, the system will write all emails to the filesystem for preview. |
+| **PROD_MODE** | (optional) Disables Django debug mode. |
+| **SECRET** | (optional) Sets web application secret. You can generate a random secret with python running: `os.urandom(24)` |
+| **DATABASE_URL** | (optional) URL to connect to the database. If not set, defaults to django default SQLite database. See schema for different databases [here](https://github.com/kennethreitz/dj-database-url#url-schema). |
+| **DATABASE_SECURE** | (optional) Whether or not to use SSL to connect to the database. Defaults to `true`. |
+| **DOMAIN** | (optional) Domain where app will be running. Default: localhost:8000 |
+| **SL_TOKEN** | (optional) Slack token to invite hackers automatically on confirmation. You can obtain it [here](https://api.slack.com/custom-integrations/legacy-tokens) |
+| **SL_TEAM** | (optional) Slack team name (xxx on xxx.slack.com) |
+| **DROPBOX_OAUTH2_TOKEN** | (optional) Enables DropBox as file upload server instead of local computer. |
+| **SL_BOT_ID** | (optional) Slack bot ID to send messages from. |
+| **SL_BOT_TOKEN** | (optional) Slack bot token to send messages. |
+| **SL_BOT_CHANNEL** | (optional) General channel to refer from the bot messages. |
+| **SL_BOT_DIRECTOR1** | (optional) User ID of one of the directors. |
+| **SL_BOT_DIRECTOR2** | (optional) User ID of the other director. |
+| **MLH_CLIENT_SECRET** | (optional) Enables MyMLH as a sign up option. Format is `client_id@client_secret` |
+| **CAS_SERVER** | (optional) Enables login for other platforms |
+| **GOOGLE_WALLET_APPLICATION_CREDENTIALS** | (optional) The path to the json key file containing all google-related API credentials |
+| **GOOGLE_WALLET_ISSUER_ID** | (optional) The issuer ID of Google Wallet Pass API |
+| **GOOGLE_WALLET_CLASS_SUFFIX** | (optional) The name of the class created at the [Google Wallet Console](https://pay.google.com/business/console/passes/) |
+
+---
+
+## Verifying Your Changes
+
+After making code changes:
+
+1. Restart the dev server (`Ctrl+C`, then `python manage.py runserver`)
+2. Navigate to the affected page in the browser
+3. If you changed a model, run `python manage.py makemigrations && python manage.py migrate`
+4. If you changed CSS, hard-refresh the browser (Ctrl+Shift+R / Cmd+Shift+R)
diff --git a/hardware/views/__init__.py b/hardware/views/__init__.py
new file mode 100644
index 000000000..70677ffb5
--- /dev/null
+++ b/hardware/views/__init__.py
@@ -0,0 +1,7 @@
+from hardware.views.hacker import HardwareBorrowingsView, HardwareListView
+from hardware.views.admin import hardware_tabs, HardwareAdminRequestsView, HardwareAdminView
+
+__all__ = [
+ 'HardwareBorrowingsView', 'HardwareListView',
+ 'hardware_tabs', 'HardwareAdminRequestsView', 'HardwareAdminView',
+]
diff --git a/hardware/views.py b/hardware/views/admin.py
similarity index 74%
rename from hardware/views.py
rename to hardware/views/admin.py
index 1c5b63fea..5cccaa536 100644
--- a/hardware/views.py
+++ b/hardware/views/admin.py
@@ -1,4 +1,3 @@
-from app import hackathon_variables
from app.mixins import TabsViewMixin
from django.core import serializers
from django.http import JsonResponse, HttpResponse
@@ -9,12 +8,12 @@
from django_filters.views import FilterView
from django_tables2 import SingleTableMixin
from django.db.models import Q
-from user.mixins import IsHardwareAdminMixin, IsHackerMixin
+from user.mixins import IsHardwareAdminMixin
from user.models import User
from checkin.models import CheckIn
from hardware.models import Item, ItemType, Borrowing, Request
-from hardware.tables import BorrowingTable, BorrowingFilter, RequestTable, RequestFilter
+from hardware.tables import RequestTable, RequestFilter
def hardware_tabs(user):
@@ -45,79 +44,6 @@ def get_queryset(self):
return Request.objects.all()
-class HardwareBorrowingsView(IsHackerMixin, TabsViewMixin, SingleTableMixin, FilterView):
- template_name = 'hardware_borrowings.html'
- table_class = BorrowingTable
- table_pagination = {'per_page': 50}
- filterset_class = BorrowingFilter
-
- def get_context_data(self, **kwargs):
- context = super(HardwareBorrowingsView, self).get_context_data(**kwargs)
- if not self.request.user.is_hardware_admin:
- context['filter'] = False
- context['table'].exclude = ('id', 'user', 'lending_by', 'return_by')
-
- return context
-
- def get_current_tabs(self):
- return hardware_tabs(self.request.user)
-
- def get_queryset(self):
- if self.request.user.is_hardware_admin:
- return Borrowing.objects.all()
- else:
- return Borrowing.objects.get_queryset().filter(user=self.request.user)
-
-
-class HardwareListView(IsHackerMixin, TabsViewMixin, TemplateView):
- template_name = 'hardware_list.html'
-
- def get_current_tabs(self):
- return hardware_tabs(self.request.user)
-
- def get_context_data(self, **kwargs):
- context = super(HardwareListView, self).get_context_data(**kwargs)
- context['hw_list'] = ItemType.objects.all()
- requests = Request.objects.get_active_by_user(self.request.user)
- context['requests'] = {
- x.item_type.id: x.get_remaining_time() for x in requests
- }
- return context
-
- def req_item(self, request):
- item = ItemType.objects.get(id=request.POST['item_id'])
- if item.get_available_count() > 0:
- item.make_request(request.user)
- return JsonResponse({
- 'ok': True,
- 'minutes': hackathon_variables.HARDWARE_REQUEST_TIME
- })
-
- return JsonResponse({'msg': "ERROR: There are no items available"})
-
- def check_availability(self, request):
- item_ids = request.POST['item_ids[]']
- items = ItemType.objects.filter(id__in=item_ids)
- available_items = []
- for item in items:
- if item.get_available_count() > 0:
- available_items.append({
- "id": item.id,
- "name": item.name
- })
-
- return JsonResponse({
- 'available_items': available_items
- })
-
- def post(self, request):
- if request.is_ajax:
- if 'req_item' in request.POST:
- return self.req_item(request)
- if 'check_availability' in request.POST:
- return self.check_availability(request)
-
-
class HardwareAdminView(IsHardwareAdminMixin, TabsViewMixin, TemplateView):
template_name = 'hardware_admin.html'
diff --git a/hardware/views/hacker.py b/hardware/views/hacker.py
new file mode 100644
index 000000000..78e8cb4c1
--- /dev/null
+++ b/hardware/views/hacker.py
@@ -0,0 +1,84 @@
+from app import hackathon_variables
+from app.mixins import TabsViewMixin
+from django.http import JsonResponse
+from django.views.generic import TemplateView
+from django_filters.views import FilterView
+from django_tables2 import SingleTableMixin
+from user.mixins import IsHackerMixin
+
+from hardware.models import ItemType, Borrowing, Request
+from hardware.tables import BorrowingTable, BorrowingFilter
+from hardware.views.admin import hardware_tabs
+
+
+class HardwareBorrowingsView(IsHackerMixin, TabsViewMixin, SingleTableMixin, FilterView):
+ template_name = 'hardware_borrowings.html'
+ table_class = BorrowingTable
+ table_pagination = {'per_page': 50}
+ filterset_class = BorrowingFilter
+
+ def get_context_data(self, **kwargs):
+ context = super(HardwareBorrowingsView, self).get_context_data(**kwargs)
+ if not self.request.user.is_hardware_admin:
+ context['filter'] = False
+ context['table'].exclude = ('id', 'user', 'lending_by', 'return_by')
+
+ return context
+
+ def get_current_tabs(self):
+ return hardware_tabs(self.request.user)
+
+ def get_queryset(self):
+ if self.request.user.is_hardware_admin:
+ return Borrowing.objects.all()
+ else:
+ return Borrowing.objects.get_queryset().filter(user=self.request.user)
+
+
+class HardwareListView(IsHackerMixin, TabsViewMixin, TemplateView):
+ template_name = 'hardware_list.html'
+
+ def get_current_tabs(self):
+ return hardware_tabs(self.request.user)
+
+ def get_context_data(self, **kwargs):
+ context = super(HardwareListView, self).get_context_data(**kwargs)
+ context['hw_list'] = ItemType.objects.all()
+ requests = Request.objects.get_active_by_user(self.request.user)
+ context['requests'] = {
+ x.item_type.id: x.get_remaining_time() for x in requests
+ }
+ return context
+
+ def req_item(self, request):
+ item = ItemType.objects.get(id=request.POST['item_id'])
+ if item.get_available_count() > 0:
+ item.make_request(request.user)
+ return JsonResponse({
+ 'ok': True,
+ 'minutes': hackathon_variables.HARDWARE_REQUEST_TIME
+ })
+
+ return JsonResponse({'msg': "ERROR: There are no items available"})
+
+ def check_availability(self, request):
+ item_ids = request.POST['item_ids[]']
+ items = ItemType.objects.filter(id__in=item_ids)
+ available_items = []
+ for item in items:
+ if item.get_available_count() > 0:
+ available_items.append({
+ "id": item.id,
+ "name": item.name
+ })
+
+ return JsonResponse({
+ 'available_items': available_items
+ })
+
+ def post(self, request):
+ if request.is_ajax:
+ if 'req_item' in request.POST:
+ return self.req_item(request)
+ if 'check_availability' in request.POST:
+ return self.check_availability(request)
diff --git a/organizers/static/css/organizers.css b/organizers/static/css/organizers.css
new file mode 100644
index 000000000..38615fa52
--- /dev/null
+++ b/organizers/static/css/organizers.css
@@ -0,0 +1,18 @@
+.container-iframe {
+ position: relative;
+ overflow: hidden;
+ width: 100%;
+ padding-top: 56.25%; /* 16:9 Aspect Ratio (divide 9 by 16 = 0.5625) */
+}
+
+.responsive-iframe {
+ display: block;
+ border-style:none;
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+}
diff --git a/organizers/templates/review_resume.html b/organizers/templates/review_resume.html
index aa98e5f29..33164255b 100644
--- a/organizers/templates/review_resume.html
+++ b/organizers/templates/review_resume.html
@@ -1,5 +1,6 @@
{% extends "base_tabs.html" %}
{% load static %}
+{% block extra_head %}{% endblock %}
{% block head_title %}Review resume{% endblock %}
{% block panel %}
{% if app %}
diff --git a/organizers/views/__init__.py b/organizers/views/__init__.py
new file mode 100644
index 000000000..5680d80ff
--- /dev/null
+++ b/organizers/views/__init__.py
@@ -0,0 +1,42 @@
+from organizers.views.application_lists import (
+ hacker_tabs,
+ volunteer_tabs,
+ mentor_tabs,
+ sponsor_tabs,
+ ApplicationsListView,
+ InviteListView,
+ WaitlistedApplicationsListView,
+ DubiousApplicationsListView,
+ BlacklistApplicationsListView,
+ _OtherApplicationsListView,
+ VolunteerApplicationsListView,
+ SponsorApplicationsListView,
+ SponsorUserListView,
+ MentorApplicationsListView,
+)
+from organizers.views.review import (
+ add_vote,
+ add_comment,
+ ApplicationDetailView,
+ ReviewApplicationView,
+ ReviewApplicationDetailView,
+ ReviewVolunteerApplicationView,
+ ReviewSponsorApplicationView,
+ ReviewMentorApplicationView,
+ ReviewResume,
+ VisualizeResume,
+)
+from organizers.views.invite import InviteTeamListView
+
+__all__ = [
+ 'hacker_tabs', 'volunteer_tabs', 'mentor_tabs', 'sponsor_tabs',
+ 'ApplicationsListView', 'InviteListView', 'WaitlistedApplicationsListView',
+ 'DubiousApplicationsListView', 'BlacklistApplicationsListView',
+ '_OtherApplicationsListView', 'VolunteerApplicationsListView',
+ 'SponsorApplicationsListView', 'SponsorUserListView', 'MentorApplicationsListView',
+ 'add_vote', 'add_comment',
+ 'ApplicationDetailView', 'ReviewApplicationView', 'ReviewApplicationDetailView',
+ 'ReviewVolunteerApplicationView', 'ReviewSponsorApplicationView',
+ 'ReviewMentorApplicationView', 'ReviewResume', 'VisualizeResume',
+ 'InviteTeamListView',
+]
diff --git a/organizers/views/application_lists.py b/organizers/views/application_lists.py
new file mode 100644
index 000000000..8073edb3c
--- /dev/null
+++ b/organizers/views/application_lists.py
@@ -0,0 +1,385 @@
+from django.conf import settings
+from django.contrib import messages
+from django.core.exceptions import ValidationError
+from django.http import HttpResponseRedirect, HttpResponse
+from django.urls import reverse
+from django.views import View
+from django_filters.views import FilterView
+from django_tables2 import SingleTableMixin
+from django_tables2.export import ExportMixin
+from django.utils import timezone
+from datetime import timedelta
+
+from app.mixins import TabsViewMixin
+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,
+ APP_REJECTED,
+)
+from organizers import models
+from organizers.tables import (
+ ApplicationsListTable,
+ ApplicationFilter,
+ AdminApplicationsListTable,
+ InviteFilter,
+ DubiousListTable,
+ DubiousApplicationFilter,
+ VolunteerFilter,
+ VolunteerListTable,
+ MentorListTable,
+ MentorFilter,
+ SponsorListTable,
+ SponsorFilter,
+ SponsorUserListTable,
+ SponsorUserFilter,
+ BlacklistListTable,
+ BlacklistApplicationFilter,
+)
+from user.mixins import (
+ IsOrganizerMixin,
+ IsDirectorMixin,
+ HaveDubiousPermissionMixin,
+ HaveSponsorPermissionMixin,
+ HaveMentorPermissionMixin,
+ IsBlacklistAdminMixin,
+)
+from user.models import User, USR_SPONSOR
+
+if getattr(settings, "REIMBURSEMENT_ENABLED", False):
+ from reimbursement.models import Reimbursement, RE_PEND_APPROVAL
+
+
+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))
+ 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
+ ),
+ ),
+ ]
+ )
+ 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 "")
+ )
+ 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),
+ ]
+
+
+def volunteer_tabs(user):
+ 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),
+ ]
+
+
+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"
+
+ def get(self, request, *args, **kwargs):
+ 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()
+ )
+
+ def get_context_data(self, **kwargs):
+ context = super(ApplicationsListView, self).get_context_data(**kwargs)
+ context["otherApplication"] = False
+ list_email = ""
+ for u in context.get("object_list").values_list("user__email", flat=True):
+ list_email += "%s, " % u
+ context["emails"] = list_email
+ return context
+
+
+class InviteListView(TabsViewMixin, IsDirectorMixin, SingleTableMixin, FilterView):
+ template_name = "invite_list.html"
+ table_class = AdminApplicationsListTable
+ filterset_class = InviteFilter
+ table_pagination = {"per_page": 100}
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ 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):
+ return hacker_tabs(self.request.user)
+
+ def get_queryset(self):
+ return models.HackerApplication.annotate_vote(
+ 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")
+ 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",
+ )
+ m = emails.create_invite_email(app, request)
+ if m:
+ mails.append(m)
+ except ValidationError:
+ errors += 1
+ if mails:
+ send_batch_emails(mails)
+ messages.success(request, "%s applications invited" % len(mails))
+ else:
+ errorMsg = "No applications invited"
+ if errors != 0:
+ errorMsg = "%s applications not invited" % errors
+ messages.error(request, errorMsg)
+
+ return HttpResponseRedirect(reverse("invite_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"
+ table_class = DubiousListTable
+ filterset_class = DubiousApplicationFilter
+ 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"
+ 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"
+ )
+
+
+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"
+
+ def get(self, request, *args, **kwargs):
+ request.session["edit_app_back"] = "blacklist"
+ 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_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"
+
+ def get_context_data(self, **kwargs):
+ context = super(_OtherApplicationsListView, self).get_context_data(**kwargs)
+ context["otherApplication"] = True
+ list_email = ""
+ for u in context.get("object_list").values_list(self.email_field, flat=True):
+ list_email += "%s, " % u
+ context["emails"] = list_email
+ return context
+
+
+class VolunteerApplicationsListView(IsOrganizerMixin, _OtherApplicationsListView):
+ table_class = VolunteerListTable
+ filterset_class = VolunteerFilter
+
+ def get_queryset(self):
+ return models.VolunteerApplication.objects.all()
+
+ def get_current_tabs(self):
+ return volunteer_tabs(self.request.user)
+
+
+class SponsorApplicationsListView(
+ HaveSponsorPermissionMixin, _OtherApplicationsListView
+):
+ table_class = SponsorListTable
+ filterset_class = SponsorFilter
+ 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
+ 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"
+ table_class = SponsorUserListTable
+ filterset_class = SponsorUserFilter
+
+ def get_current_tabs(self):
+ return sponsor_tabs(self.request.user)
+
+ def get_context_data(self, **kwargs):
+ context = super(SponsorUserListView, self).get_context_data(**kwargs)
+ context["otherApplication"] = True
+ context["createUser"] = True
+ return context
+
+ def get_queryset(self):
+ return User.objects.filter(type=USR_SPONSOR).exclude(max_applications=0)
+
+
+class MentorApplicationsListView(HaveMentorPermissionMixin, _OtherApplicationsListView):
+ table_class = MentorListTable
+ filterset_class = MentorFilter
+
+ def get_queryset(self):
+ return models.MentorApplication.objects.all()
+
+ def get_current_tabs(self):
+ return mentor_tabs(self.request.user)
diff --git a/organizers/views/invite.py b/organizers/views/invite.py
new file mode 100644
index 000000000..7f1ecfe2e
--- /dev/null
+++ b/organizers/views/invite.py
@@ -0,0 +1,140 @@
+# Create your views here.
+from django.conf import settings
+from django.contrib import messages
+from django.core.exceptions import ValidationError
+from django.db.models import Count, Avg, F, Q, CharField
+from django.db.models.functions import Concat
+from django.http import HttpResponseRedirect
+from django.urls import reverse
+from django.views.generic import TemplateView
+from django_tables2 import SingleTableMixin
+from django.utils import timezone
+
+from app.mixins import TabsViewMixin
+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,
+ APP_REJECTED,
+)
+from organizers import models
+from organizers.tables import AdminTeamListTable
+from user.mixins import IsDirectorMixin
+
+from organizers.views.application_lists import hacker_tabs
+
+
+class InviteTeamListView(
+ TabsViewMixin, IsDirectorMixin, SingleTableMixin, TemplateView
+):
+ template_name = "invite_list.html"
+ table_class = AdminTeamListTable
+ table_pagination = {"per_page": 100}
+
+ def get_current_tabs(self):
+ return hacker_tabs(self.request.user)
+
+ def get_queryset(self):
+ 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()
+
+ 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)
+ )
+ mails = []
+ errors = 0
+ for app in apps:
+ try:
+ app.invite(
+ request.user,
+ online=request.POST.get("force_online", "false") == "true",
+ )
+ m = emails.create_invite_email(app, request)
+ mails.append(m)
+ except ValidationError:
+ errors += 1
+ if mails:
+ send_batch_emails(mails)
+ messages.success(request, "%s applications invited" % len(mails))
+ else:
+ errorMsg = "No applications invited"
+ if errors != 0:
+ errorMsg = "%s applications not invited" % errors
+ messages.error(request, errorMsg)
+
+ return HttpResponseRedirect(reverse("invite_teams_list"))
diff --git a/organizers/views.py b/organizers/views/review.py
similarity index 61%
rename from organizers/views.py
rename to organizers/views/review.py
index d94165861..87858f480 100644
--- a/organizers/views.py
+++ b/organizers/views/review.py
@@ -7,16 +7,11 @@
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.db import IntegrityError
-from django.db.models import Count, Avg, F, Q, CharField
-from django.db.models.functions import Concat
+from django.db.models import Count, Q
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
-from django_tables2.export import ExportMixin
from django.utils import timezone
from datetime import timedelta
@@ -24,51 +19,22 @@
from app.mixins import TabsViewMixin
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 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 teams.models import Team
from user.mixins import (
IsOrganizerMixin,
- IsDirectorMixin,
- HaveDubiousPermissionMixin,
HaveSponsorPermissionMixin,
HaveMentorPermissionMixin,
- IsBlacklistAdminMixin,
)
-from user.models import User, USR_SPONSOR
-if getattr(settings, "REIMBURSEMENT_ENABLED", False):
- from reimbursement.models import Reimbursement, RE_PEND_APPROVAL
+from organizers.views.application_lists import hacker_tabs
def add_vote(application, user, tech_rat, pers_rat):
@@ -99,198 +65,6 @@ def add_comment(application, user, text):
return comment
-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))
- 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
- ),
- ),
- ]
- )
- 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 "")
- )
- 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),
- ]
-
-
-def volunteer_tabs(user):
- 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),
- ]
-
-
-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"
-
- def get(self, request, *args, **kwargs):
- 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()
- )
-
- def get_context_data(self, **kwargs):
- context = super(ApplicationsListView, self).get_context_data(**kwargs)
- context["otherApplication"] = False
- list_email = ""
- for u in context.get("object_list").values_list("user__email", flat=True):
- list_email += "%s, " % u
- context["emails"] = list_email
- return context
-
-
-class InviteListView(TabsViewMixin, IsDirectorMixin, SingleTableMixin, FilterView):
- template_name = "invite_list.html"
- table_class = AdminApplicationsListTable
- filterset_class = InviteFilter
- table_pagination = {"per_page": 100}
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- 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):
- return hacker_tabs(self.request.user)
-
- def get_queryset(self):
- return models.HackerApplication.annotate_vote(
- 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")
- 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",
- )
- m = emails.create_invite_email(app, request)
- if m:
- mails.append(m)
- except ValidationError:
- errors += 1
- if mails:
- send_batch_emails(mails)
- messages.success(request, "%s applications invited" % len(mails))
- else:
- errorMsg = "No applications invited"
- if errors != 0:
- errorMsg = "%s applications not invited" % errors
- messages.error(request, errorMsg)
-
- return HttpResponseRedirect(reverse("invite_list"))
-
-
class ApplicationDetailView(TabsViewMixin, IsOrganizerMixin, TemplateView):
template_name = "application_detail.html"
@@ -666,255 +440,6 @@ def can_vote(self):
return True
-class InviteTeamListView(
- TabsViewMixin, IsDirectorMixin, SingleTableMixin, TemplateView
-):
- template_name = "invite_list.html"
- table_class = AdminTeamListTable
- table_pagination = {"per_page": 100}
-
- def get_current_tabs(self):
- return hacker_tabs(self.request.user)
-
- def get_queryset(self):
- 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()
-
- 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)
- )
- mails = []
- errors = 0
- for app in apps:
- try:
- app.invite(
- request.user,
- online=request.POST.get("force_online", "false") == "true",
- )
- m = emails.create_invite_email(app, request)
- mails.append(m)
- except ValidationError:
- errors += 1
- if mails:
- send_batch_emails(mails)
- messages.success(request, "%s applications invited" % len(mails))
- else:
- errorMsg = "No applications invited"
- if errors != 0:
- errorMsg = "%s applications not invited" % errors
- messages.error(request, errorMsg)
-
- 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"
- table_class = DubiousListTable
- filterset_class = DubiousApplicationFilter
- 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"
- 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"
- )
-
-
-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"
-
- def get(self, request, *args, **kwargs):
- request.session["edit_app_back"] = "blacklist"
- 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_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"
-
- def get_context_data(self, **kwargs):
- context = super(_OtherApplicationsListView, self).get_context_data(**kwargs)
- context["otherApplication"] = True
- list_email = ""
- for u in context.get("object_list").values_list(self.email_field, flat=True):
- list_email += "%s, " % u
- context["emails"] = list_email
- return context
-
-
-class VolunteerApplicationsListView(IsOrganizerMixin, _OtherApplicationsListView):
- table_class = VolunteerListTable
- filterset_class = VolunteerFilter
-
- def get_queryset(self):
- return models.VolunteerApplication.objects.all()
-
- def get_current_tabs(self):
- return volunteer_tabs(self.request.user)
-
-
-class SponsorApplicationsListView(
- HaveSponsorPermissionMixin, _OtherApplicationsListView
-):
- table_class = SponsorListTable
- filterset_class = SponsorFilter
- 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
- 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"
- table_class = SponsorUserListTable
- filterset_class = SponsorUserFilter
-
- def get_current_tabs(self):
- return sponsor_tabs(self.request.user)
-
- def get_context_data(self, **kwargs):
- context = super(SponsorUserListView, self).get_context_data(**kwargs)
- context["otherApplication"] = True
- context["createUser"] = True
- return context
-
- def get_queryset(self):
- return User.objects.filter(type=USR_SPONSOR).exclude(max_applications=0)
-
-
-class MentorApplicationsListView(HaveMentorPermissionMixin, _OtherApplicationsListView):
- table_class = MentorListTable
- filterset_class = MentorFilter
-
- def get_queryset(self):
- return models.MentorApplication.objects.all()
-
- def get_current_tabs(self):
- return mentor_tabs(self.request.user)
-
-
class ReviewVolunteerApplicationView(IsOrganizerMixin, TabsViewMixin, TemplateView):
template_name = "other_application_detail.html"
diff --git a/reimbursement/views/__init__.py b/reimbursement/views/__init__.py
new file mode 100644
index 000000000..8a96b5a2c
--- /dev/null
+++ b/reimbursement/views/__init__.py
@@ -0,0 +1,13 @@
+from reimbursement.views.hacker import ReimbursementHacker
+from reimbursement.views.organizer import (
+ ReimbursementDetail,
+ ReceiptReview,
+ ReimbursementListView,
+ SendReimbursementListView,
+)
+
+__all__ = [
+ 'ReimbursementHacker',
+ 'ReimbursementDetail', 'ReceiptReview',
+ 'ReimbursementListView', 'SendReimbursementListView',
+]
diff --git a/reimbursement/views/hacker.py b/reimbursement/views/hacker.py
new file mode 100644
index 000000000..5effa5e1f
--- /dev/null
+++ b/reimbursement/views/hacker.py
@@ -0,0 +1,83 @@
+from django.contrib import messages
+from django.http import HttpResponseRedirect, Http404
+from django.shortcuts import render
+
+from app.utils import reverse, hacker_tabs
+from app.views import TabsView
+from reimbursement import forms, models
+from user.mixins import IsHackerMixin
+
+
+class ReimbursementHacker(IsHackerMixin, TabsView):
+ template_name = "reimbursement_hacker.html"
+
+ def get_current_tabs(self):
+ return hacker_tabs(self.request.user)
+
+ def get_context_data(self, **kwargs):
+ c = super(ReimbursementHacker, self).get_context_data(**kwargs)
+ reimb = getattr(self.request.user, "reimbursement", None)
+ if not reimb:
+ raise Http404
+ c.update(
+ {
+ "form": forms.ReceiptSubmissionReceipt(
+ instance=self.request.user.reimbursement
+ )
+ }
+ )
+ return c
+
+ def post(self, request, *args, **kwargs):
+ # check reimbursment status and act accordingly
+ # if status is pending demo link, then validate the devpost link
+ # if status is pending receipt, then validate the receipt
+ if request.user.reimbursement.status == models.RE_PEND_TICKET:
+ try:
+ form = forms.ReceiptSubmissionReceipt(
+ request.POST, request.FILES, instance=request.user.reimbursement
+ )
+ except Exception:
+ form = forms.ReceiptSubmissionReceipt(request.POST, request.FILES)
+ if form.is_valid():
+ reimb = form.save(commit=False)
+ reimb.hacker = request.user
+ # set status to pending demo link
+ reimb.status = models.RE_PEND_APPROVAL
+ reimb.save()
+ messages.success(
+ request,
+ "We have now received your reimbursement. "
+ "Processing will take some time, so please be patient.",
+ )
+ return HttpResponseRedirect(reverse("reimbursement_dashboard"))
+ else:
+ c = self.get_context_data()
+ c.update({"form": form})
+ return render(request, self.template_name, c)
+ else:
+ try:
+ form = forms.DevpostValidationForm(
+ request.POST, instance=request.user.reimbursement
+ )
+ except Exception:
+ form = forms.DevpostValidationForm(request.POST)
+ if form.is_valid():
+ print("valid")
+ reimb = form.save(commit=False)
+ reimb.devpost = form.cleaned_data.get("devpost")
+ reimb.status = models.RE_PEND_DEMO_VAL
+ reimb.save()
+ messages.success(
+ request,
+ "We have now received your demo link. "
+ "Processing will take some time, so please be patient.",
+ )
+
+ return HttpResponseRedirect(reverse("reimbursement_dashboard"))
+ else:
+ print(form.errors)
+ print("invalid")
+ c = self.get_context_data()
+ c.update({"form": form})
+ return render(request, self.template_name, c)
diff --git a/reimbursement/views.py b/reimbursement/views/organizer.py
similarity index 77%
rename from reimbursement/views.py
rename to reimbursement/views/organizer.py
index 99a63af11..788feb73a 100644
--- a/reimbursement/views.py
+++ b/reimbursement/views/organizer.py
@@ -1,12 +1,12 @@
from django.contrib import messages
from django.core.exceptions import ValidationError
-from django.http import HttpResponseRedirect, Http404
+from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404
from django_filters.views import FilterView
from django_tables2 import SingleTableMixin
from app.mixins import TabsViewMixin
-from app.utils import reverse, hacker_tabs
+from app.utils import reverse
from app.views import TabsView
from applications import models as app_mod
from applications.emails import send_batch_emails
@@ -17,85 +17,10 @@
SendReimbursementTable,
SendReimbursementFilter,
)
-from user.mixins import IsOrganizerMixin, IsDirectorMixin, IsHackerMixin
+from user.mixins import IsOrganizerMixin, IsDirectorMixin
from organizers.views import hacker_tabs as organizer_tabs
-class ReimbursementHacker(IsHackerMixin, TabsView):
- template_name = "reimbursement_hacker.html"
-
- def get_current_tabs(self):
- return hacker_tabs(self.request.user)
-
- def get_context_data(self, **kwargs):
- c = super(ReimbursementHacker, self).get_context_data(**kwargs)
- reimb = getattr(self.request.user, "reimbursement", None)
- if not reimb:
- raise Http404
- c.update(
- {
- "form": forms.ReceiptSubmissionReceipt(
- instance=self.request.user.reimbursement
- )
- }
- )
- return c
-
- def post(self, request, *args, **kwargs):
- # check reimbursment status and act accordingly
- # if status is pending demo link, then validate the devpost link
- # if status is pending receipt, then validate the receipt
- if request.user.reimbursement.status == models.RE_PEND_TICKET:
- try:
- form = forms.ReceiptSubmissionReceipt(
- request.POST, request.FILES, instance=request.user.reimbursement
- )
- except Exception:
- form = forms.ReceiptSubmissionReceipt(request.POST, request.FILES)
- if form.is_valid():
- reimb = form.save(commit=False)
- reimb.hacker = request.user
- # set status to pending demo link
- reimb.status = models.RE_PEND_APPROVAL
- reimb.save()
- messages.success(
- request,
- "We have now received your reimbursement. "
- "Processing will take some time, so please be patient.",
- )
- return HttpResponseRedirect(reverse("reimbursement_dashboard"))
- else:
- c = self.get_context_data()
- c.update({"form": form})
- return render(request, self.template_name, c)
- else:
- try:
- form = forms.DevpostValidationForm(
- request.POST, instance=request.user.reimbursement
- )
- except Exception:
- form = forms.DevpostValidationForm(request.POST)
- if form.is_valid():
- print("valid")
- reimb = form.save(commit=False)
- reimb.devpost = form.cleaned_data.get("devpost")
- reimb.status = models.RE_PEND_DEMO_VAL
- reimb.save()
- messages.success(
- request,
- "We have now received your demo link. "
- "Processing will take some time, so please be patient.",
- )
-
- return HttpResponseRedirect(reverse("reimbursement_dashboard"))
- else:
- print(form.errors)
- print("invalid")
- c = self.get_context_data()
- c.update({"form": form})
- return render(request, self.template_name, c)
-
-
class ReimbursementDetail(IsOrganizerMixin, TabsView):
template_name = "reimbursement_detail.html"
diff --git a/stats/static/css/stats.css b/stats/static/css/stats.css
new file mode 100644
index 000000000..6d9b9cc65
--- /dev/null
+++ b/stats/static/css/stats.css
@@ -0,0 +1,15 @@
+.c3-axis-y text {
+ fill: #000000;
+}
+.c3-axis-x text {
+ fill: #000000;
+}
+.c3-legend-item {
+ fill: #000000;
+}
+.c3 .c3-axis line, .c3 .c3-axis path {
+ stroke: #000000;
+}
+.c3-tooltip tr {
+ color: #222222;
+}
diff --git a/stats/templates/c3_base.html b/stats/templates/c3_base.html
index 682d3803f..e6a2915f0 100644
--- a/stats/templates/c3_base.html
+++ b/stats/templates/c3_base.html
@@ -4,6 +4,7 @@
{% block extra_head %}
+
{% endblock %}
{% block extra_scripts %}
diff --git a/stats/templates/c3_table_base.html b/stats/templates/c3_table_base.html
index dd5ca3542..608de24e3 100644
--- a/stats/templates/c3_table_base.html
+++ b/stats/templates/c3_table_base.html
@@ -4,6 +4,7 @@
{% block extra_head %}
+
{% endblock %}
{% block extra_scripts %}
diff --git a/user/views/__init__.py b/user/views/__init__.py
new file mode 100644
index 000000000..f0afb9aaf
--- /dev/null
+++ b/user/views/__init__.py
@@ -0,0 +1,15 @@
+from user.views.authentication import (
+ login, signup, Logout, activate,
+ password_reset, password_reset_confirm, password_reset_complete, password_reset_done,
+ verify_email_required, set_password, send_email_verification,
+ callback, SponsorRegister,
+)
+from user.views.profile import UserProfile, DeleteAccount
+
+__all__ = [
+ 'login', 'signup', 'Logout', 'activate',
+ 'password_reset', 'password_reset_confirm', 'password_reset_complete', 'password_reset_done',
+ 'verify_email_required', 'set_password', 'send_email_verification',
+ 'callback', 'SponsorRegister',
+ 'UserProfile', 'DeleteAccount',
+]
diff --git a/user/views.py b/user/views/authentication.py
similarity index 87%
rename from user/views.py
rename to user/views/authentication.py
index a7978aa49..d486bd69d 100644
--- a/user/views.py
+++ b/user/views/authentication.py
@@ -16,7 +16,7 @@
from applications import models as a_models
from user import forms, models, tokens, providers
from user.forms import SetPasswordForm, PasswordResetForm
-from user.mixins import HaveSponsorPermissionMixin, IsHackerMixin
+from user.mixins import HaveSponsorPermissionMixin
from user.models import User
from user.verification import check_recaptcha, check_client_ip, reset_tries
@@ -293,43 +293,3 @@ def post(self, request, *args, **kwargs):
context = self.get_context_data()
context.update({'form': form})
return TemplateResponse(request, self.template_name, context)
-
-
-class UserProfile(IsHackerMixin, TemplateView):
- template_name = 'profile.html'
-
- def get_context_data(self, *args, **kwargs):
- context = super(UserProfile, self).get_context_data(**kwargs)
- form = forms.ProfileForm(initial={
- 'name': self.request.user.name,
- 'email': self.request.user.email,
- 'type': self.request.user.type if self.request.user.can_change_type() else 'H',
- 'non_change_type': self.request.user.get_type_display(),
- }, type_active=self.request.user.can_change_type())
- context.update({'form': form})
- return context
-
- def post(self, request, *args, **kwargs):
- form = forms.ProfileForm(request.POST, type_active=request.user.can_change_type())
- if form.is_valid():
- name = form.cleaned_data['name']
- request.user.name = name
- if request.user.can_change_type():
- type = form.cleaned_data['type']
- request.user.type = type
- request.user.save()
- messages.success(request, "Profile saved successfully")
- c = self.get_context_data()
- else:
- c = self.get_context_data()
- c.update({'form': form})
- return render(request, self.template_name, c)
-
-
-class DeleteAccount(IsHackerMixin, TemplateView):
- template_name = 'confirm_delete.html'
-
- def post(self, request, *args, **kwargs):
- request.user.delete()
- messages.success(request, "User deleted successfully")
- return HttpResponseRedirect(reverse('root'))
diff --git a/user/views/profile.py b/user/views/profile.py
new file mode 100644
index 000000000..aac9f3287
--- /dev/null
+++ b/user/views/profile.py
@@ -0,0 +1,48 @@
+from django.contrib import messages
+from django.http import HttpResponseRedirect
+from django.shortcuts import render
+from django.views.generic import TemplateView
+
+from app.utils import reverse
+from user import forms
+from user.mixins import IsHackerMixin
+
+
+class UserProfile(IsHackerMixin, TemplateView):
+ template_name = 'profile.html'
+
+ def get_context_data(self, *args, **kwargs):
+ context = super(UserProfile, self).get_context_data(**kwargs)
+ form = forms.ProfileForm(initial={
+ 'name': self.request.user.name,
+ 'email': self.request.user.email,
+ 'type': self.request.user.type if self.request.user.can_change_type() else 'H',
+ 'non_change_type': self.request.user.get_type_display(),
+ }, type_active=self.request.user.can_change_type())
+ context.update({'form': form})
+ return context
+
+ def post(self, request, *args, **kwargs):
+ form = forms.ProfileForm(request.POST, type_active=request.user.can_change_type())
+ if form.is_valid():
+ name = form.cleaned_data['name']
+ request.user.name = name
+ if request.user.can_change_type():
+ type = form.cleaned_data['type']
+ request.user.type = type
+ request.user.save()
+ messages.success(request, "Profile saved successfully")
+ c = self.get_context_data()
+ else:
+ c = self.get_context_data()
+ c.update({'form': form})
+ return render(request, self.template_name, c)
+
+
+class DeleteAccount(IsHackerMixin, TemplateView):
+ template_name = 'confirm_delete.html'
+
+ def post(self, request, *args, **kwargs):
+ request.user.delete()
+ messages.success(request, "User deleted successfully")
+ return HttpResponseRedirect(reverse('root'))