diff --git a/Dockerfile b/Dockerfile index b82c4fcd..8dbfdc43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/accounts/admin.py b/accounts/admin.py index 574d169f..59bd3dde 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -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 @@ -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) diff --git a/accounts/migrations/0019_emailtemplate.py b/accounts/migrations/0019_emailtemplate.py new file mode 100644 index 00000000..7620335c --- /dev/null +++ b/accounts/migrations/0019_emailtemplate.py @@ -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)), + ], + ), + ] diff --git a/accounts/migrations/0020_auto_20230104_1655.py b/accounts/migrations/0020_auto_20230104_1655.py new file mode 100644 index 00000000..cd5e70c7 --- /dev/null +++ b/accounts/migrations/0020_auto_20230104_1655.py @@ -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), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 4735770b..970e3f4f 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 24f7aea0..e8b5d687 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 @@ -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 diff --git a/hackathon/tasks.py b/hackathon/tasks.py new file mode 100644 index 00000000..600d1ea2 --- /dev/null +++ b/hackathon/tasks.py @@ -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.") diff --git a/hackathon/templates/hackathon/enrolment_email.txt b/hackathon/templates/hackathon/enrolment_email.txt new file mode 100644 index 00000000..6ec62ba7 --- /dev/null +++ b/hackathon/templates/hackathon/enrolment_email.txt @@ -0,0 +1,36 @@ +

Hi {student},

+ +

Thank you so much for registering for the {hackathon}!

+ +

+ What does participation involve?
+ 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! +

+ +

+ What am I committing to?
+ 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. +

+ +

+ IMPORTANT!
+ 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. +

+ +

+ Register for the Intro Webinar!
+ Please check the #hackathon channel for details on how to register for the intro and project presentations webinar. +

+ +

+ Need Help?
+ Please ask any questions in the #hackathon channel, the HackTeam are ready and happy to help out. You can ask them a question by using the @hackteam tag on slack. +

+ +

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. + +

+ Happy Hacking!
+ The Code Institute Community Team +

diff --git a/hackathon/views.py b/hackathon/views.py index ca7712cf..b82f6284 100644 --- a/hackathon/views.py +++ b/hackathon/views.py @@ -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 @@ -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(): @@ -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( diff --git a/main/__init__.py b/main/__init__.py index e69de29b..cd042640 100644 --- a/main/__init__.py +++ b/main/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ['celery_app'] diff --git a/main/celery.py b/main/celery.py new file mode 100644 index 00000000..2898ee47 --- /dev/null +++ b/main/celery.py @@ -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() diff --git a/main/settings.py b/main/settings.py index 25a36ee5..4ff6399f 100644 --- a/main/settings.py +++ b/main/settings.py @@ -36,6 +36,7 @@ "allauth.account", "allauth.socialaccount", "crispy_forms", + "django_celery_results", # custom apps "accounts", @@ -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" @@ -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" diff --git a/requirements.txt b/requirements.txt index 9753b318..241ec1a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 @@ -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 @@ -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 diff --git a/static/img/hackathon_header.png b/static/img/hackathon_header.png new file mode 100644 index 00000000..01c8497a Binary files /dev/null and b/static/img/hackathon_header.png differ