Skip to content

Commit

Permalink
#M20. Add automated emails to signup and hackathon registration (#277)
Browse files Browse the repository at this point in the history
* adding basic celery setting

* Adding celery and model to store Email Templates

* Fixing grammer in template
  • Loading branch information
stefdworschak authored Jan 6, 2023
1 parent 310ac81 commit ab4b7c0
Show file tree
Hide file tree
Showing 14 changed files with 190 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM ubuntu:20.04

RUN apt-get update -y
RUN apt-get install python3 python3-pip libmysqlclient-dev mysql-client vim -y
RUN apt-get install python3 python3-pip libmysqlclient-dev mysql-client vim sqlite3 -y

WORKDIR /hackathon-app
COPY ./requirements.txt /hackathon-app/requirements.txt
Expand Down
7 changes: 6 additions & 1 deletion accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.contrib.auth.decorators import login_required

from .models import CustomUser, Organisation
from .models import CustomUser, Organisation, EmailTemplate
from accounts.models import SlackSiteSettings


Expand Down Expand Up @@ -49,8 +49,13 @@ class CustomUserAdmin(BaseUserAdmin):
readonly_fields = ('last_login', 'date_joined', 'user_type')


class EmailTemplateAdmin(admin.ModelAdmin):
list_display = ('display_name', 'subject', 'template_name', 'is_active', )


# sign-in via allauth required before accessing the admin panel
admin.site.login = login_required(admin.site.login)
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Organisation)
admin.site.register(SlackSiteSettings)
admin.site.register(EmailTemplate, EmailTemplateAdmin)
26 changes: 26 additions & 0 deletions accounts/migrations/0019_emailtemplate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.1.13 on 2023-01-04 15:38

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('accounts', '0018_slacksitesettings'),
]

operations = [
migrations.CreateModel(
name='EmailTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('display_name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, null=True)),
('template_name', models.CharField(max_length=255)),
('subject', models.CharField(max_length=1048)),
('plain_text_message', models.TextField()),
('html_message', models.TextField(blank=True, null=True)),
('is_active', models.BooleanField(default=True)),
],
),
]
22 changes: 22 additions & 0 deletions accounts/migrations/0020_auto_20230104_1655.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 3.1.13 on 2023-01-04 16:55

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('accounts', '0019_emailtemplate'),
]

operations = [
migrations.AlterModelOptions(
name='emailtemplate',
options={'verbose_name': 'Email Template', 'verbose_name_plural': 'Email Templates'},
),
migrations.AlterField(
model_name='emailtemplate',
name='template_name',
field=models.CharField(max_length=255, unique=True),
),
]
17 changes: 17 additions & 0 deletions accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,20 @@ def __str__(self):
class Meta:
verbose_name = 'Slack Site Settings'
verbose_name_plural = 'Slack Site Settings'


class EmailTemplate(models.Model):
display_name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
template_name = models.CharField(max_length=255, unique=True)
subject = models.CharField(max_length=1048)
plain_text_message = models.TextField()
html_message = models.TextField(null=True, blank=True)
is_active = models.BooleanField(default=True)

class Meta:
verbose_name = 'Email Template'
verbose_name_plural = 'Email Templates'

def __str__(self):
return self.display_name
14 changes: 14 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ services:
- "8000:8000"
tty: true
stdin_open: true

hackathon-worker:
image: hackathon-app
environment:
- ENV_FILE=/hackathon-app/.env
- DEVELOPMENT=1
entrypoint: ["celery", "-A", "main", "worker", "-l", "info"]
volumes:
- ./data/:/hackathon-app/data/
- ./.env:/hackathon-app/.env

mysql:
image: docker.io/mysql:5.6.36
Expand All @@ -45,8 +55,12 @@ services:
MYSQL_PASSWORD: gummyball
volumes:
- ./data/mysql:/var/lib/mysql
- ./hackathon/:/hackathon-app/hackathon/

smtp:
image: mailhog/mailhog:v1.0.1
ports:
- "8026:8025"

redis:
image: redis
36 changes: 36 additions & 0 deletions hackathon/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import logging
import os

from celery import shared_task
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import send_mail
from smtplib import SMTPException

from accounts.models import EmailTemplate, SlackSiteSettings

logger = logging.getLogger(__name__)


@shared_task
def send_email_from_template(user_email, user_name, hackathon_display_name, template_name):
try:
template = EmailTemplate.objects.get(template_name=template_name, is_active=True)
user_name = user_name or user_email
slack_settings = SlackSiteSettings.objects.first()
if slack_settings and slack_settings.enable_welcome_emails:
send_mail(
subject=template.subject.format(hackathon=hackathon_display_name),
message=template.plain_text_message.format(student=user_name, hackathon=hackathon_display_name),
html_message=template.html_message.format(student=user_name, hackathon=hackathon_display_name),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user_email],
fail_silently=False,
)
logger.info("Email {template_name} sucessfully sent to user {user.id}.")
except ObjectDoesNotExist:
logger.exception(
(f"There is no template with the name {template_name}."
"Please create it on the Django Admin Panel"))
except SMTPException:
logger.exception("There was an issue sending the email.")
36 changes: 36 additions & 0 deletions hackathon/templates/hackathon/enrolment_email.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<p>Hi {student},</p>

<p>Thank you so much for registering for the {hackathon}!</p>

<p>
<strong>What does participation involve?</strong><br>
You'll be assigned to a team, and work together building a project based on the assigned theme in a limited number of days. Don't worry if you have limited coding experience, all levels are welcome, and we encourage alumni to participate!
</p>

<p>
<strong>What am I committing to?</strong><br>
We recommend at bare minimum you dedicate a minimum of 8-10 hours over the duration of the Hackathon. You will be expected to actively contribute to your team, not just observe.
Please check your calendar and confirm that you are available before registering, as dropping out really lets your team down, and the team will be one person fewer.
</p>

<p>
<strong>IMPORTANT!</strong><br>
Please ensure your Profile is up to date, especially the 'Latest Module' entry. This is vital for the team selection process. We try our best to balance the teams fairly, so it really helps you and your team to be accurate with your profile.
</p>

<p>
<strong>Register for the Intro Webinar!</strong><br>
Please check the <a href="https://code-institute-room.slack.com/archives/CDAFARB71" target="_blank">#hackathon</a> channel for details on how to register for the intro and project presentations webinar.
</p>

<p>
<strong>Need Help?</strong><br>
Please ask any questions in the <a href="https://code-institute-room.slack.com/archives/CDAFARB71" target="_blank">#hackathon</a> channel, the HackTeam are ready and happy to help out. You can ask them a question by using the @hackteam tag on slack.
</p>

<p>Thanks again for signing up, we are excited to see what you and your team will create! Remember, hackathons are about team-building, learning and most importantly having fun.

<p>
Happy Hacking!<br>
The Code Institute Community Team
</p>
5 changes: 5 additions & 0 deletions hackathon/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
HackAwardForm, HackTeamForm
from .lists import AWARD_CATEGORIES
from .helpers import format_date, query_scores, create_judges_scores_table
from .tasks import send_email_from_template

from accounts.models import UserType
from accounts.decorators import can_access, has_access_to_hackathon
Expand Down Expand Up @@ -416,14 +417,17 @@ def enroll_toggle(request):
id=request.POST.get("hackathon-id"))
if request.user in hackathon.judges.all():
hackathon.judges.remove(request.user)
send_email_from_template.apply_async(args=[request.user.email, request.user.first_name, hackathon.display_name, 'withdraw_judge'])
messages.success(request, "You have withdrawn from judging.")
elif request.user in hackathon.participants.all():
hackathon.participants.remove(request.user)
send_email_from_template.apply_async(args=[request.user.email, request.user.first_name, hackathon.display_name, 'withdraw_participant'])
messages.success(request,
"You have withdrawn from this Hackaton.")
elif (request.POST.get('enrollment-type') == 'judge'
and request.user.user_type in judge_user_types):
hackathon.judges.add(request.user)
send_email_from_template.apply_async(args=[request.user.email, request.user.first_name, hackathon.display_name, 'enroll_judge'])
messages.success(request, "You have enrolled as a facilitator/judge.") # noqa: E501
else:
if hackathon.max_participants_reached():
Expand All @@ -432,6 +436,7 @@ def enroll_toggle(request):
return redirect(reverse('hackathon:view_hackathon', kwargs={
'hackathon_id': request.POST.get("hackathon-id")}))
hackathon.participants.add(request.user)
send_email_from_template.apply_async(args=[request.user.email, request.user.first_name, hackathon.display_name, 'enroll_participant'])
messages.success(request, "You have enrolled successfully.")

return redirect(reverse(
Expand Down
3 changes: 3 additions & 0 deletions main/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .celery import app as celery_app

__all__ = ['celery_app']
11 changes: 11 additions & 0 deletions main/celery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import os

from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')

app = Celery('main')

app.config_from_object('django.conf:settings', namespace='CELERY')

app.autodiscover_tasks()
11 changes: 9 additions & 2 deletions main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"allauth.account",
"allauth.socialaccount",
"crispy_forms",
"django_celery_results",

# custom apps
"accounts",
Expand Down Expand Up @@ -100,14 +101,14 @@

EMAIL_BACKEND = os.environ.get(
'EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend')
DEFAULT_FROM_EMAIL = (os.environ.get('DEFAULT_FROM_EMAIL')
or os.environ.get("SUPPORT_EMAIL"))
if EMAIL_BACKEND == 'django.core.mail.backends.smtp.EmailBackend':
EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'False') == 'True'
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 25))
EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = (os.environ.get('DEFAULT_FROM_EMAIL')
or os.environ.get("SUPPORT_EMAIL"))

AUTH_USER_MODEL = "accounts.CustomUser"
ACCOUNT_SIGNUP_FORM_CLASS = "accounts.forms.SignupForm"
Expand Down Expand Up @@ -163,6 +164,12 @@
},
]

# Celery
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER', 'redis://redis:6379')
CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://redis:6379') # noqa: E501
CELERY_ACCEPT_CONTENT = os.environ.get('CELERY_ACCEPT_CONTENT', 'application/json').split(',') # noqa: E501
CELERY_TASK_SERIALIZER = os.environ.get('CELERY_TASK_SERIALIZER', 'json')
CELERY_RESULT_SERIALIZER = os.environ.get('CELERY_RESULT_SERIALIZER', 'json')

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
Expand Down
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ appdirs==1.4.3
asgiref==3.2.10
backcall==0.2.0
CacheControl==0.12.6
celery==5.2.3
certifi==2020.6.20
chardet==3.0.4
colorama==0.4.3
Expand All @@ -13,6 +14,8 @@ distro==1.4.0
dj-database-url==0.5.0
Django==3.1.13
django-allauth==0.42.0
django-celery-beat==2.2.1
django-celery-results==2.2.0
django-crispy-forms==1.9.2
django-extensions==3.1.0
graphviz==0.16
Expand Down Expand Up @@ -45,6 +48,7 @@ python-dotenv==0.14.0
python3-openid==3.2.0
pytoml==0.1.21
pytz==2020.1
redis==4.1.1
requests==2.24.0
requests-oauthlib==1.3.0
retrying==1.3.3
Expand Down
Binary file added static/img/hackathon_header.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit ab4b7c0

Please sign in to comment.