Skip to content

Commit

Permalink
Merge pull request #495 from tamuhack-org/sandeep-dev
Browse files Browse the repository at this point in the history
merge for testing emails
  • Loading branch information
skandrigi authored Nov 29, 2024
2 parents 68742b7 + fc6265c commit 92c75e6
Show file tree
Hide file tree
Showing 28 changed files with 299 additions and 155 deletions.
Binary file added dump.rdb
Binary file not shown.
2 changes: 2 additions & 0 deletions hiss/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: gunicorn hiss.wsgi --log-file -
worker: celery -A hiss worker --loglevel=info
6 changes: 4 additions & 2 deletions hiss/application/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def build_approval_email(
"event_year": settings.EVENT_YEAR,
"confirmation_deadline": confirmation_deadline,
"organizer_email": settings.ORGANIZER_EMAIL,
"event_date_text": settings.EVENT_DATE_TEXT,
}
html_message = render_to_string("application/emails/approved.html", context)
message = strip_tags(html_message)
Expand All @@ -78,6 +79,7 @@ def build_rejection_email(application: Application) -> Tuple[str, str, None, Lis
"organizer_name": settings.ORGANIZER_NAME,
"event_year": settings.EVENT_YEAR,
"organizer_email": settings.ORGANIZER_EMAIL,
"event_date_text": settings.EVENT_DATE_TEXT,
}
html_message = render_to_string("application/emails/rejected.html", context)
message = strip_tags(html_message)
Expand All @@ -102,7 +104,7 @@ def approve(_modeladmin, _request: HttpRequest, queryset: QuerySet) -> None:
print(f"approval email built for {approval_email[-1:]}")
email_tuples.append(approval_email)
application.save()
send_mass_html_mail(email_tuples)
send_mass_html_mail.delay(email_tuples)


def reject(_modeladmin, _request: HttpRequest, queryset: QuerySet) -> None:
Expand All @@ -115,7 +117,7 @@ def reject(_modeladmin, _request: HttpRequest, queryset: QuerySet) -> None:
application.status = STATUS_REJECTED
email_tuples.append(build_rejection_email(application))
application.save()
send_mass_html_mail(email_tuples)
send_mass_html_mail.delay(email_tuples)


def resend_confirmation(_modeladmin, _request: HttpRequest, queryset: QuerySet) -> None:
Expand Down
109 changes: 49 additions & 60 deletions hiss/application/emails.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,14 @@
from celery import shared_task
import json
from io import BytesIO

import pyqrcode
from django.conf import settings
from django.core import mail
from django.template.loader import render_to_string
from django.utils import html

from application.models import Application
from application.apple_wallet import get_apple_wallet_pass_url

import threading
from django.core.mail import EmailMessage

#create separate threading class for confirmation email since it has a QR code

class EmailQRThread(threading.Thread):
def __init__(self, subject, msg, html_msg, recipient_email, qr_stream):
self.subject = subject
self.msg = msg
self.html_msg = html_msg
self.recipient_email = recipient_email
self.qr_stream = qr_stream
threading.Thread.__init__(self)

def run(self):
qr_code = pyqrcode.create(self.qr_content)
qr_stream = BytesIO()
qr_code.png(qr_stream, scale=5)

email = mail.EmailMultiAlternatives(
self.subject, self.msg, from_email=None, to=[self.recipient_email]
)
email.attach_alternative(self.html_msg, "text/html")
email.attach("code.png", self.qr_stream.getvalue(), "text/png")

# if above code is defined directly in function, it will run synchronously
# therefore need to directly define in threading class to run asynchronously

email.send()
from django.conf import settings
from django.core.mail import EmailMultiAlternatives

def send_creation_email(app: Application) -> None:
"""
Expand All @@ -59,37 +29,56 @@ def send_creation_email(app: Application) -> None:
# send_html_email is threaded from the User class
# see user/models.py

app.user.send_html_email(template_name, context, subject)
app.user.send_html_email.delay(template_name, context, subject)


def send_confirmation_email(app: Application) -> None:
@shared_task
def send_confirmation_email(app_id: int) -> None:
"""
Sends a confirmation email to a user, which contains their QR code as well as additional event information.
:param app: The user's application
:type app: Application
:param app_id: The ID of the user's application
:type app_id: int
:return: None
"""
subject = f"HowdyHack Waitlist: Important Day-Of Information"
email_template = "application/emails/confirmed.html"
context = {
"first_name": app.first_name,
"event_name": settings.EVENT_NAME,
"organizer_name": settings.ORGANIZER_NAME,
"event_year": settings.EVENT_YEAR,
"organizer_email": settings.ORGANIZER_EMAIL,
"apple_wallet_url": get_apple_wallet_pass_url(app.user.email),
}
html_msg = render_to_string(email_template, context)
plain_msg = html.strip_tags(html_msg)

qr_content = json.dumps(
{
try:
app = Application.objects.get(id=app_id)
subject = f"TAMUhack: Important Day-Of Information"
email_template = "application/emails/confirmed.html"

if app.status == "E":
subject = f"TAMUhack Waitlist: Important Day-of Information!"
email_template = "application/emails/confirmed-waitlist.html"

context = {
"first_name": app.first_name,
"last_name": app.last_name,
"email": app.user.email,
"university": app.school.name,
"event_name": settings.EVENT_NAME,
"organizer_name": settings.ORGANIZER_NAME,
"event_year": settings.EVENT_YEAR,
"organizer_email": settings.ORGANIZER_EMAIL,
"apple_wallet_url": get_apple_wallet_pass_url(app.user.email),
"event_date_text": settings.EVENT_DATE_TEXT,
}
)

email_thread = EmailQRThread(subject, plain_msg, html_msg, app.user.email, qr_content)
email_thread.start()

html_msg = render_to_string(email_template, context)
plain_msg = html.strip_tags(html_msg)
email = mail.EmailMultiAlternatives(
subject, plain_msg, from_email=None, to=[app.user.email]
)

email.attach_alternative(html_msg, "text/html")
qr_content = json.dumps(
{
"first_name": app.first_name,
"last_name": app.last_name,
"email": app.user.email,
"university": app.school.name,
}
)

qr_code = pyqrcode.create(qr_content)
qr_stream = BytesIO()
qr_code.png(qr_stream, scale=5)
email.attach("code.png", qr_stream.getvalue(), "image/png")
print(f"sending confirmation email to {app.user.email}")
email.send()
except Exception as e:
print(f"Error sending confirmation email: {e}")
4 changes: 2 additions & 2 deletions hiss/application/fixtures/schools.json
Original file line number Diff line number Diff line change
Expand Up @@ -10984,7 +10984,7 @@
},
{
"model": "application.school",
"pk": 2075,
"pk": 2074,
"fields": {
"name": "Texas A&M University - San Antonio"
}
Expand Down Expand Up @@ -14519,7 +14519,7 @@
},
{
"model": "application.school",
"pk": 2074,
"pk": 2075,
"fields": {
"name": "Other"
}
Expand Down
59 changes: 59 additions & 0 deletions hiss/application/meal_groups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from django.db import transaction
from models import Application

NUM_GROUPS = 4
RESTRICTED_FRONTLOAD_FACTOR = 1.3

def assign_food_groups():
veg_apps = []
nobeef_apps = []
nopork_apps = []
allergy_apps = []
othernonveg_apps = []

applicants = Application.objects.filter(status__in=['A', 'E', 'C'])

for app in applicants:
if "Vegetarian" in app.dietary_restrictions or "Vegan" in app.dietary_restrictions:
veg_apps.append(app)
elif "No-Beef" in app.dietary_restrictions:
nobeef_apps.append(app)
elif "No-Pork" in app.dietary_restrictions:
nopork_apps.append(app)
elif "Food-Allergy" in app.dietary_restrictions:
allergy_apps.append(app)
else:
othernonveg_apps.append(app)

restricted_apps = veg_apps + nobeef_apps + nopork_apps + allergy_apps
num_apps = len(restricted_apps) + len(othernonveg_apps)

group_size = num_apps // NUM_GROUPS
restricted_percent = len(restricted_apps) / num_apps
restricted_target = restricted_percent * RESTRICTED_FRONTLOAD_FACTOR
restricted_per_group = restricted_target * group_size

groups = [[] for _ in range(NUM_GROUPS)]
group_restricted_count = [0] * NUM_GROUPS

# Assign restricted applicants
for i in range(NUM_GROUPS):
groups[i] = restricted_apps[:int(restricted_per_group)]
restricted_apps = restricted_apps[int(restricted_per_group):]
group_restricted_count[i] = len(groups[i])

# Assign unrestricted applicants
for i in range(NUM_GROUPS):
groups[i] += othernonveg_apps[:group_size - group_restricted_count[i]]
othernonveg_apps = othernonveg_apps[group_size - group_restricted_count[i]:]
groups[-1] += othernonveg_apps

# Update database with meal groups
with transaction.atomic():
for i, group in enumerate(groups):
group_letter = chr(65 + i)
for app in group:
app.meal_group = group_letter
app.save()

return {f"Group {chr(65 + i)}": len(group) for i, group in enumerate(groups)}
87 changes: 86 additions & 1 deletion hiss/application/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,92 @@ class Application(models.Model):
user = models.ForeignKey("user.User", on_delete=models.CASCADE, null=False)
status = models.CharField(
choices=STATUS_OPTIONS, max_length=1, default=STATUS_PENDING
)
)

def get_next_meal_group(self):
"""
Determines the next meal group considering frontloading for restricted groups
using the RESTRICTED_FRONTLOAD_FACTOR.
"""
RESTRICTED_FRONTLOAD_FACTOR = 1.3

meal_groups = ['A', 'B', 'C', 'D']
group_distribution = {group: 0 for group in meal_groups}
restricted_distribution = {group: 0 for group in meal_groups}
total_confirmed = Application.objects.filter(status='C').exclude(meal_group__isnull=True)

for group in meal_groups:
group_distribution[group] = total_confirmed.filter(meal_group=group).count()
restricted_distribution[group] = total_confirmed.filter(
meal_group=group,
dietary_restrictions__icontains="Vegetarian"
).count() + total_confirmed.filter(
meal_group=group,
dietary_restrictions__icontains="No-Beef"
).count() + total_confirmed.filter(
meal_group=group,
dietary_restrictions__icontains="No-Pork"
).count() + total_confirmed.filter(
meal_group=group,
dietary_restrictions__icontains="Food-Allergy"
).count()

total_apps = sum(group_distribution.values())
total_restricted = sum(restricted_distribution.values())
if total_apps == 0:
return meal_groups[0]

base_restricted_percent = total_restricted / total_apps if total_apps else 0
target_restricted_percent = {
group: base_restricted_percent * (RESTRICTED_FRONTLOAD_FACTOR if i < len(meal_groups) // 2 else 1.0)
for i, group in enumerate(meal_groups)
}
target_restricted_count = {
group: target_restricted_percent[group] * group_distribution[group] if group_distribution[group] > 0 else 0
for group in meal_groups
}

restricted_gap = {
group: target_restricted_count[group] - restricted_distribution[group]
for group in meal_groups
}
prioritized_group = max(restricted_gap, key=restricted_gap.get)

last_assigned_group = (
total_confirmed.order_by('-datetime_submitted')
.values_list('meal_group', flat=True)
.first()
)
if last_assigned_group:
next_index = (meal_groups.index(last_assigned_group) + 1) % len(meal_groups)
else:
next_index = 0

# use frontloaded group if it aligns with round-robin or is significantly better
if prioritized_group == meal_groups[next_index]:
return meal_groups[next_index]
elif restricted_gap[prioritized_group] > restricted_gap[meal_groups[next_index]]:
return prioritized_group
else:
return meal_groups[next_index]

def assign_meal_group(self):
"""
Assigns a meal group based on the current status and dietary restrictions.
"""
if self.status == 'C': # Confirmed
self.meal_group = self.get_next_meal_group()
elif self.status == 'E': # Waitlisted
self.meal_group = 'E'
else:
self.meal_group = None

def save(self, *args, **kwargs):
"""
Overrides save to ensure meal group assignment logic is applied.
"""
self.assign_meal_group()
super().save(*args, **kwargs)

# ABOUT YOU
first_name = models.CharField(
Expand Down
9 changes: 5 additions & 4 deletions hiss/application/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views import generic

from django.db import transaction
from application.emails import send_confirmation_email, send_creation_email
from application.forms import ApplicationModelForm
from application.models import (
Expand Down Expand Up @@ -103,9 +103,10 @@ def post(self, request: HttpRequest, *args, **kwargs):
raise PermissionDenied(
"You can't confirm your application if it hasn't been approved."
)
app.status = STATUS_CONFIRMED
app.save()
send_confirmation_email(app)
with transaction.atomic():
app.status = STATUS_CONFIRMED
app.save()
send_confirmation_email(app)
return redirect(reverse_lazy("status"))


Expand Down
6 changes: 3 additions & 3 deletions hiss/customauth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.http import HttpResponse
from django.shortcuts import redirect, render, get_object_or_404
from django.urls import reverse_lazy
from django.utils.encoding import force_bytes, force_text
from django.utils.encoding import force_bytes, force_str
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.views import generic

Expand All @@ -26,7 +26,7 @@ def send_confirmation_email(curr_domain: RequestSite, user: User) -> None:
"event_name": settings.EVENT_NAME,
"organizer_name": settings.ORGANIZER_NAME,
}
user.send_html_email(template_name, context, subject)
user.send_html_email.delay(template_name, context, subject)


# Create your views here.
Expand Down Expand Up @@ -58,7 +58,7 @@ class ActivateView(views.View):
def get(self, request, *_args, **kwargs):
user = None
try:
uid = force_text(urlsafe_base64_decode(kwargs["uidb64"]))
uid = force_str(urlsafe_base64_decode(kwargs["uidb64"]))
user = get_user_model().objects.get(id=int(uid))
except (
TypeError,
Expand Down
4 changes: 4 additions & 0 deletions hiss/hiss/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from __future__ import absolute_import, unicode_literals
from .celery import app as celery_app

__all__ = ('celery_app',)
Loading

0 comments on commit 92c75e6

Please sign in to comment.