diff --git a/Dockerfile b/Dockerfile index 030717cdd793a0a7367cade822888bf852029aff..a16be4644e4aa73edef391ca20b95acf9cf654bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.1 +FROM python:3.11 ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 @@ -13,7 +13,6 @@ WORKDIR $APP_HOME COPY ./requirements/production.txt requirements.txt -RUN apt-get -y update && apt-get install -y python python-pip python-dev python-django-extensions postgresql-client netcat RUN pip install -r requirements.txt COPY ./src $APP_HOME diff --git a/k8s/deployment.yml b/k8s/deployment.yml index b979cc6878ed625e3d8ed86a31c45ace5a7070a5..513978576f27f859b3f19b0ed21c936ee168b094 100644 --- a/k8s/deployment.yml +++ b/k8s/deployment.yml @@ -107,6 +107,22 @@ spec: limits: memory: 200Mi cpu: "2" + - name: worker + image: harbor.sch.bme.hu/kszk/kszkepzes-backend:##IMAGETAG## + imagePullPolicy: "IfNotPresent" + envFrom: + - configMapRef: + name: kszkepzes-config + - secretRef: + name: kszkepzes-secret-config + command: ["python3"] + args: ["-m", "celery", "-A", "kszkepzes", "worker", "-l", "info"] + resources: + requests: + cpu: "100m" + limits: + memory: 600Mi + cpu: "2" volumes: - name: kszkepzes-media-volume persistentVolumeClaim: diff --git a/k8s/redis.values.yaml b/k8s/redis.values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8791d6943ec3e220dee398fb881f4571a5ff9824 --- /dev/null +++ b/k8s/redis.values.yaml @@ -0,0 +1,11 @@ +# https://artifacthub.io/packages/helm/bitnami/redis +master: + resources: + limits: + memory: 2Gi + cpu: 2 +replica: + resources: + limits: + memory: 2Gi + cpu: 2 diff --git a/requirements/base.in b/requirements/base.in index 962a298f6a18f7690943f1df7e8af0c61a963e5a..b9837710c0a62685aba81dca1dc0c7ba90a60f7a 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -1,4 +1,5 @@ -Django==2.2.4 -djangorestframework==3.10.2 -django-solo==1.1.3 -django-import-export==1.2.0 +Django~=4.1 +djangorestframework~=3.14 +django-solo~=2.0 +django-import-export~=3.1 +celery~=5.2 diff --git a/requirements/production.in b/requirements/production.in index 2ef446c78ccb7cf01c48ff5cee36a04d5f6f6abc..5fa215f62e6cfdc67d77ce8aae0f3f086e83c8c3 100644 --- a/requirements/production.in +++ b/requirements/production.in @@ -1,11 +1,13 @@ -r base.in psycopg2-binary -gunicorn==19.7.1 -flake8==3.7.8 -pip-tools==4.1.0 -django-extensions==2.2.1 -python-language-server==0.28.2 -drf-yasg==1.16.1 -packaging==19.1 -Pillow==7.0.0 -djangorestframework-api-key==1.4.1 \ No newline at end of file +redis +gunicorn~=20.1 +flake8~=6.0 +pip-tools~=6.12 +django-extensions~=3.2 +python-language-server~=0.36 +drf-yasg~=1.21 +packaging~=23.0 +Pillow~=9.4 +djangorestframework-api-key~=2.3 + diff --git a/requirements/production.txt b/requirements/production.txt index 7cdc80bd945fd1c0c126cfee9d379ae6c4c884f9..75af8c7b7b9dd2cbaf14c87f61a0ca1862d1c6e8 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -1,70 +1,170 @@ # -# This file is autogenerated by pip-compile -# To update, run: +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: # # pip-compile --output-file=requirements/production.txt requirements/production.in # -attrs==19.3.0 # via packaging -certifi==2018.1.18 # via requests -chardet==3.0.4 # via requests -click==7.0 # via pip-tools -coreapi==2.3.3 # via drf-yasg -coreschema==0.0.4 # via coreapi, drf-yasg -defusedxml==0.5.0 # via python3-openid, social-auth-core -diff-match-patch==20121119 # via django-import-export -django-extensions==2.2.1 -django-import-export==1.2.0 -django-social-authsch==0.1 -django-solo==1.1.3 -django==2.2.4 -djangorestframework-api-key==1.4.1 -djangorestframework==3.10.2 -drf-yasg==1.16.1 -entrypoints==0.3 # via flake8 -et-xmlfile==1.0.1 # via openpyxl -flake8==3.7.8 -future==0.18.2 # via python-language-server -gunicorn==19.7.1 -idna==2.6 # via requests -importlib-metadata==1.5.0 # via pluggy -inflection==0.3.1 # via drf-yasg -itypes==1.1.0 # via coreapi -jdcal==1.3 # via openpyxl -jedi==0.14.1 # via python-language-server -jinja2==2.11.1 # via coreschema -markupsafe==1.1.1 # via jinja2 -mccabe==0.6.1 # via flake8 -oauthlib==2.0.6 # via requests-oauthlib, social-auth-core -odfpy==1.3.6 # via tablib -openpyxl==2.5.0 # via tablib -packaging==19.1 -parso==0.6.1 # via jedi -pillow==7.0.0 -pip-tools==4.1.0 -pluggy==0.13.1 # via python-language-server -psycopg2-binary==2.8.4 -pycodestyle==2.5.0 # via flake8 -pyflakes==2.1.1 # via flake8 -pyjwt==1.5.3 # via social-auth-core -pyparsing==2.4.6 # via packaging -python-jsonrpc-server==0.3.4 # via python-language-server -python-language-server==0.28.2 -python3-openid==3.1.0 # via social-auth-core -pytz==2017.3 # via django -pyyaml==5.3 # via tablib -requests-oauthlib==0.8.0 # via social-auth-core -requests==2.22.0 # via coreapi, requests-oauthlib, social-auth-core -ruamel.yaml.clib==0.2.0 # via ruamel.yaml -ruamel.yaml==0.16.7 # via drf-yasg -six==1.11.0 # via django-extensions, drf-yasg, packaging, pip-tools, social-auth-app-django, social-auth-core -social-auth-app-django==2.1.0 # via django-social-authsch -social-auth-core==1.6.0 # via django-social-authsch, social-auth-app-django -sqlparse==0.3.0 # via django -tablib==0.12.1 # via django-import-export -ujson==1.35 # via python-jsonrpc-server -unicodecsv==0.14.1 # via tablib -uritemplate==3.0.1 # via coreapi, drf-yasg -urllib3==1.25.8 # via requests -xlrd==1.1.0 # via tablib -xlwt==1.3.0 # via tablib -zipp==2.1.0 # via importlib-metadata +amqp==5.1.1 + # via kombu +asgiref==3.6.0 + # via django +billiard==3.6.4.0 + # via celery +build==0.10.0 + # via pip-tools +celery==5.2.7 + # via -r requirements/production.in +certifi==2018.1.18 + # via requests +chardet==3.0.4 + # via requests +click==8.1.3 + # via + # celery + # click-didyoumean + # click-plugins + # click-repl + # pip-tools +click-didyoumean==0.3.0 + # via celery +click-plugins==1.1.1 + # via celery +click-repl==0.2.0 + # via celery +coreapi==2.3.3 + # via drf-yasg +coreschema==0.0.4 + # via + # coreapi + # drf-yasg +diff-match-patch==20121119 + # via django-import-export +django==4.1.7 + # via + # -r requirements/base.in + # django-extensions + # django-import-export + # django-solo + # djangorestframework + # drf-yasg +django-extensions==3.2.1 + # via -r requirements/production.in +django-import-export==3.1.0 + # via -r requirements/base.in +django-solo==2.0.0 + # via -r requirements/base.in +djangorestframework==3.14.0 + # via + # -r requirements/base.in + # drf-yasg +djangorestframework-api-key==2.3.0 + # via -r requirements/production.in +drf-yasg==1.21.5 + # via -r requirements/production.in +et-xmlfile==1.0.1 + # via openpyxl +flake8==6.0.0 + # via -r requirements/production.in +gunicorn==20.1.0 + # via -r requirements/production.in +idna==2.6 + # via requests +inflection==0.3.1 + # via drf-yasg +itypes==1.1.0 + # via coreapi +jedi==0.17.2 + # via python-language-server +jinja2==2.11.1 + # via coreschema +kombu==5.2.4 + # via celery +markuppy==1.14 + # via tablib +markupsafe==1.1.1 + # via jinja2 +mccabe==0.7.0 + # via flake8 +odfpy==1.3.6 + # via tablib +openpyxl==3.1.2 + # via tablib +packaging==23.0 + # via + # -r requirements/production.in + # build + # drf-yasg +parso==0.7.1 + # via jedi +pillow==9.4.0 + # via -r requirements/production.in +pip-tools==6.12.3 + # via -r requirements/production.in +pluggy==0.13.1 + # via python-language-server +prompt-toolkit==3.0.38 + # via click-repl +pycodestyle==2.10.0 + # via flake8 +pyflakes==3.0.1 + # via flake8 +pyproject-hooks==1.0.0 + # via build +python-jsonrpc-server==0.4.0 + # via python-language-server +python-language-server==0.36.2 + # via -r requirements/production.in +pytz==2022.7.1 + # via + # celery + # djangorestframework + # drf-yasg +pyyaml==5.3 + # via tablib +requests==2.22.0 + # via coreapi +ruamel-yaml==0.17.21 + # via drf-yasg +ruamel-yaml-clib==0.2.7 + # via ruamel-yaml +six==1.11.0 + # via click-repl +sqlparse==0.3.0 + # via django +tablib[html,ods,xls,xlsx,yaml]==3.3.0 + # via django-import-export +tomli==2.0.1 + # via + # build + # pyproject-hooks +ujson==5.7.0 + # via + # python-jsonrpc-server + # python-language-server +uritemplate==3.0.1 + # via + # coreapi + # drf-yasg +urllib3==1.25.8 + # via requests +vine==5.0.0 + # via + # amqp + # celery + # kombu +wcwidth==0.2.6 + # via prompt-toolkit +wheel==0.38.4 + # via pip-tools +xlrd==1.1.0 + # via tablib +xlwt==1.3.0 + # via tablib +psycopg2-binary==2.9.5 + # via -r requirements/production.in +redis==4.5.1 + # via -r requirements/production.in + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/src/account/auth_pipeline.py b/src/account/auth_pipeline.py index 34ca0180920054311bbad8835511cac3ae367065..75169911a72eea2d59c01ef0e668739966ac7ead 100644 --- a/src/account/auth_pipeline.py +++ b/src/account/auth_pipeline.py @@ -1,5 +1,5 @@ from django.core import exceptions -from common import email +from common.email import registration from . import models @@ -11,4 +11,4 @@ def create_profile(backend, user, response, *args, **kwargs): except exceptions.ObjectDoesNotExist: models.Profile.objects.create(user=user) if user.email is not None: - email.registration(user) + registration.delay(user) diff --git a/src/account/serializers.py b/src/account/serializers.py index 728074ec705b8670c0eb54f36de968e82bce8230..b0b04f62383c14e1017d20def2f8a1e838c3ae69 100644 --- a/src/account/serializers.py +++ b/src/account/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers from account import models -from common import email +from common.email import admitted, denied class ChoiceSerializer(serializers.ModelSerializer): @@ -59,9 +59,9 @@ class ProfileSerializer_User(serializers.ModelSerializer): new_role = validated_data.get('role', instance.role) if instance.role != new_role: if new_role == 'Student': - email.admitted(instance.user) + admitted.delay(instance.user) if new_role == 'Denied': - email.denied(instance.user) + denied.delay(instance.user) return super().update(instance, validated_data) def get_full_name(self, obj): @@ -118,9 +118,9 @@ class ProfileSerializer_Staff(serializers.ModelSerializer): new_role = validated_data.get('role', instance.role) if instance.role != new_role: if new_role == 'Student': - email.admitted(instance.user) + admitted.delay(instance.user) if new_role == 'Denied': - email.denied(instance.user) + denied.delay(instance.user) return super().update(instance, validated_data) def get_full_name(self, obj): diff --git a/src/common/email.py b/src/common/email.py index 53b3ba10b7b6336b21974afa3d3c27c9d0f8160b..4c89adb81196177eed5a89aa60989c4343e25976 100644 --- a/src/common/email.py +++ b/src/common/email.py @@ -1,7 +1,9 @@ import os -from django.core.mail import EmailMessage -import codecs import sys +import codecs +from django.core.mail import EmailMessage +from celery import shared_task + SENDER_EMAIL = os.getenv('SENDER_MAIL', 'kepzes@kszk.bme.hu') HOMEWORK_LINK = os.getenv( @@ -13,7 +15,7 @@ def get_full_name(user): def read_email(name): - with codecs.open('common/emails/' + name, 'r', 'utf-8') as myfile: + with codecs.open('worker/emails/' + name, 'r', 'utf-8') as myfile: data = myfile.read() return data @@ -34,30 +36,31 @@ def send_out_mail(subject, message, SENDER_EMAIL, receiver_email): email.send() +@shared_task() def registration(user): subject = "KSZKépzés regisztráció" message = read_email('registration.txt') message = str.format(message % {'name': get_full_name(user)}) send_out_mail(subject, message, SENDER_EMAIL, [user.email, ]) - pass +@shared_task() def admitted(user): subject = "Jelentkezés eredménye" message = read_email('admitted.txt') message = str.format(message % {'name': get_full_name(user)}) send_out_mail(subject, message, SENDER_EMAIL, [user.email, ]) - pass +@shared_task() def denied(user): subject = "Jelentkezés eredménye" message = read_email('denied.txt') message = str.format(message % {'name': get_full_name(user)}) send_out_mail(subject, message, SENDER_EMAIL, [user.email, ]) - pass +@shared_task() def new_homework(user, deadline): deadline = deadline.strftime('%Y-%m-%d %H:%M') subject = "Új házifeladat" @@ -65,9 +68,9 @@ def new_homework(user, deadline): message = str.format( message % {'name': get_full_name(user), 'link': HOMEWORK_LINK, 'deadline': deadline}) send_out_mail(subject, message, SENDER_EMAIL, [user.email, ]) - pass +@shared_task() def homework_corrected(user, title, accepted): subject = "Házifeladat eredménye" if accepted: @@ -80,4 +83,3 @@ def homework_corrected(user, title, accepted): 'status': status, 'title': title}) send_out_mail(subject, message, SENDER_EMAIL, [user.email, ]) - pass diff --git a/src/homework/serializers.py b/src/homework/serializers.py index d2ccfb16c195fadf9eacd35507eb78c1e78b13c7..498e4e01ea0eb0ad0d1aed1ba737a7dcbd40daac 100755 --- a/src/homework/serializers.py +++ b/src/homework/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from django.utils import timezone from . import models -from common import email +from common.email import homework_corrected, new_homework class TaskSerializer(serializers.ModelSerializer): @@ -17,12 +17,11 @@ class TaskSerializer(serializers.ModelSerializer): return data def create(self, validated_data): - # TODO: Worker timeout. Do something with outlook request limitations - # profiles = Profile.objects.filter( - # role="Student").exclude( - # user__email='') - # for profile in profiles: - # email.new_homework(profile.user, validated_data.get('deadline')) + profiles = Profile.objects.filter( + role="Student").exclude( + user__email='') + for profile in profiles: + new_homework.delay(profile.user, validated_data.get('deadline')) return self.Meta.model.objects.create(**validated_data) @@ -61,7 +60,7 @@ class SolutionSerializer_Student(serializers.ModelSerializer): def update(self, instance, validated_data): if instance.corrected is not True and validated_data.get( 'corrected', instance.corrected) is True: - email.homework_corrected( + homework_corrected.delay( instance.created_by.user, instance.task.title, validated_data.get('accepted', instance.accepted) @@ -105,7 +104,7 @@ class SolutionSerializer_Staff(serializers.ModelSerializer): def update(self, instance, validated_data): if instance.corrected is not True and validated_data.get( 'corrected', instance.corrected) is True: - email.homework_corrected( + homework_corrected.delay( instance.created_by.user, instance.task.title, validated_data.get('accepted', instance.accepted) diff --git a/src/kszkepzes/__init__.py b/src/kszkepzes/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..53f4ccb1d8eb50e131da7b0e3abdd22cf9e0151c 100644 --- a/src/kszkepzes/__init__.py +++ b/src/kszkepzes/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ("celery_app",) diff --git a/src/kszkepzes/celery.py b/src/kszkepzes/celery.py new file mode 100644 index 0000000000000000000000000000000000000000..99d24f1e18df42f038b65343f997cf61acc91d46 --- /dev/null +++ b/src/kszkepzes/celery.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +import os +from celery import Celery + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kszkepzes.settings.local") +app = Celery("kszkepzes_worker") +app.config_from_object("django.conf:settings", namespace="CELERY") +app.autodiscover_tasks() diff --git a/src/kszkepzes/settings/local.py b/src/kszkepzes/settings/local.py index 1c4e0b5cb5ea38e79c195fc5e345f921fb5a8849..b33d4eee82f8ec168872988188abef05a897adcf 100644 --- a/src/kszkepzes/settings/local.py +++ b/src/kszkepzes/settings/local.py @@ -9,3 +9,5 @@ EMAIL_USE_TLS = True EMAIL_PORT = 587 EMAIL_HOST_USER = os.getenv('EMAIL') EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD') +CELERY_BROKER_URL = "redis://localhost:6379" +CELERY_RESULT_BACKEND = "redis://localhost:6379" diff --git a/src/kszkepzes/settings/production.py b/src/kszkepzes/settings/production.py index 708db649d24e59cffc227e63f009d092e1ed7706..8ece79a47c289298c9f6abf4caa2e6520eb339de 100644 --- a/src/kszkepzes/settings/production.py +++ b/src/kszkepzes/settings/production.py @@ -30,3 +30,7 @@ EMAIL_PORT = 587 EMAIL_USE_TLS = True EMAIL_HOST_USER = os.getenv('SMTP_USER') EMAIL_HOST_PASSWORD = os.getenv('SMTP_PASSWORD') + +CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL') +CELERY_RESULT_BACKEND = os.getenv('CELERY_BROKER_URL') +