From 8d76d33670d9608802ceac26275204ffed0e6f21 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 29 Jul 2024 17:57:35 +0300 Subject: [PATCH 01/25] Added celery and redis configuration --- BackEnd/forum/__init__.py | 3 +++ BackEnd/forum/celery.py | 9 +++++++++ BackEnd/forum/settings.py | 3 +++ BackEnd/requirements.txt | 2 ++ 4 files changed, 17 insertions(+) create mode 100644 BackEnd/forum/celery.py diff --git a/BackEnd/forum/__init__.py b/BackEnd/forum/__init__.py index e69de29bb..e31568a6b 100644 --- a/BackEnd/forum/__init__.py +++ b/BackEnd/forum/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ("celery_app",) \ No newline at end of file diff --git a/BackEnd/forum/celery.py b/BackEnd/forum/celery.py new file mode 100644 index 000000000..b1ab9190f --- /dev/null +++ b/BackEnd/forum/celery.py @@ -0,0 +1,9 @@ +import os +from celery import Celery + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'forum.settings') +app = Celery('forum') + +app.config_from_object('django.conf:settings', namespace='CELERY') + +app.autodiscover_tasks() \ No newline at end of file diff --git a/BackEnd/forum/settings.py b/BackEnd/forum/settings.py index 0a594cb77..6dce63760 100644 --- a/BackEnd/forum/settings.py +++ b/BackEnd/forum/settings.py @@ -116,6 +116,9 @@ WSGI_APPLICATION = "forum.wsgi.application" +CELERY_BROKER_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0") +CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL", "redis://localhost:6379/0") + DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", diff --git a/BackEnd/requirements.txt b/BackEnd/requirements.txt index 1515cce7c..bb2973b9c 100644 --- a/BackEnd/requirements.txt +++ b/BackEnd/requirements.txt @@ -18,4 +18,6 @@ black==23.9.1 drf-spectacular==0.26.5 ratelimit==2.2.1 django-debug-toolbar==4.3.0 +celery==5.4.0 +redis==5.0.7 From 4ce9c78352bf97ad8da992ea67b1942d6b940a35 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 31 Jul 2024 15:48:31 +0300 Subject: [PATCH 02/25] Updated compose and added celery and redis configuration --- docker-compose.dev.yml | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3e9b43360..b7a3f334e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -30,6 +30,8 @@ services: - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD} - CORS_ORIGIN_WHITELIST=${CORS_ORIGIN_WHITELIST} - ALLOWED_ENV_HOST=${ALLOWED_ENV_HOST} + - CELERY_BROKER_URL=${REDIS_URL} + - CELERY_RESULT_BACKEND=${REDIS_URL} networks: - forum_network frontend: @@ -46,7 +48,41 @@ services: target: production networks: - forum_network - + celery: + build: ./BackEnd + container_name: celery + command: celery -A forum worker --loglevel=info + volumes: + - static:/home/forum/app/public/static + - media:/home/forum/app/public/media + environment: + - CORS_ORIGIN_WHITELIST=${CORS_ORIGIN_WHITELIST} + - DEBUG=${DEBUG} + - SECRET_KEY=${SECRET_KEY} + - ENGINE=${ENGINE} + - DB_HOST=${DB_HOST} + - DB_PORT=${DB_PORT} + - PG_DB=${PG_DB} + - PG_USER=${PG_USER} + - PG_PASSWORD=${PG_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + - EMAIL_BACKEND=${EMAIL_BACKEND} + - EMAIL_HOST=${EMAIL_HOST} + - EMAIL_PORT=${EMAIL_PORT} + - EMAIL_USE_TLS=${EMAIL_USE_TLS} + - EMAIL_HOST_USER=${EMAIL_HOST_USER} + - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD} + - CORS_ORIGIN_WHITELIST=${CORS_ORIGIN_WHITELIST} + - ALLOWED_ENV_HOST=${ALLOWED_ENV_HOST} + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/0 + redis: + image: redis:latest + container_name: redis + ports: + - 6379:6379 + networks: + - forum_network networks: forum_network: From 99e16c9a26fa03189cfc1ad1966ab90d2204f602 Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Fri, 9 Aug 2024 15:49:52 +0300 Subject: [PATCH 03/25] Added test Celery task --- BackEnd/profiles/tasks.py | 13 +++++++++++++ BackEnd/profiles/views.py | 2 ++ 2 files changed, 15 insertions(+) create mode 100644 BackEnd/profiles/tasks.py diff --git a/BackEnd/profiles/tasks.py b/BackEnd/profiles/tasks.py new file mode 100644 index 000000000..795a079b6 --- /dev/null +++ b/BackEnd/profiles/tasks.py @@ -0,0 +1,13 @@ +from celery import shared_task + +from .models import Profile + +@shared_task +def t_cel(profile_id, text): + p = Profile.objects.get(pk=profile_id) + p.address = text + p.save() + + + + diff --git a/BackEnd/profiles/views.py b/BackEnd/profiles/views.py index b51cf90ed..30842d767 100644 --- a/BackEnd/profiles/views.py +++ b/BackEnd/profiles/views.py @@ -44,6 +44,7 @@ ) from .filters import ProfileFilter +from .tasks import t_cel class SavedCompaniesCreate(CreateAPIView): """ @@ -204,6 +205,7 @@ def perform_destroy(self, instance): def perform_update(self, serializer): profile = serializer.save() + task = t_cel.apply_async((self.kwargs.get("pk"), self.request.data.get("official_name","None")), countdown=10) completeness_count(profile) From 699a4f3bfd270d50130c91ad7ba9988494d06868 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 9 Aug 2024 17:35:21 +0300 Subject: [PATCH 04/25] Fixed Redis working --- BackEnd/forum/settings.py | 4 ++-- BackEnd/requirements.txt | 3 ++- docker-compose.dev.yml | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/BackEnd/forum/settings.py b/BackEnd/forum/settings.py index 6dce63760..3e21371ca 100644 --- a/BackEnd/forum/settings.py +++ b/BackEnd/forum/settings.py @@ -116,8 +116,8 @@ WSGI_APPLICATION = "forum.wsgi.application" -CELERY_BROKER_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0") -CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL", "redis://localhost:6379/0") +CELERY_BROKER_URL = os.environ.get('REDIS_URL') +CELERY_RESULT_BACKEND = os.environ.get('REDIS_URL') DATABASES = { "default": { diff --git a/BackEnd/requirements.txt b/BackEnd/requirements.txt index bb2973b9c..c59c62de0 100644 --- a/BackEnd/requirements.txt +++ b/BackEnd/requirements.txt @@ -1,3 +1,4 @@ +redis==5.0.7 asgiref==3.7.2 Django==4.2.3 djangorestframework==3.14.0 @@ -19,5 +20,5 @@ drf-spectacular==0.26.5 ratelimit==2.2.1 django-debug-toolbar==4.3.0 celery==5.4.0 -redis==5.0.7 + diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index b7a3f334e..3bbe9f247 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -74,8 +74,8 @@ services: - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD} - CORS_ORIGIN_WHITELIST=${CORS_ORIGIN_WHITELIST} - ALLOWED_ENV_HOST=${ALLOWED_ENV_HOST} - - CELERY_BROKER_URL=redis://redis:6379/0 - - CELERY_RESULT_BACKEND=redis://redis:6379/0 + - CELERY_BROKER_URL=${REDIS_URL} + - CELERY_RESULT_BACKEND=${REDIS_URL} redis: image: redis:latest container_name: redis From f27857999375e9493941c98bac716b659fb3d5d2 Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Wed, 14 Aug 2024 11:57:41 +0300 Subject: [PATCH 05/25] removed duplicate env var CORS_ORIGIN_WHITELIST from docker-compose.dev.yml --- docker-compose.dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3bbe9f247..7c1fda8b1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -72,7 +72,6 @@ services: - EMAIL_USE_TLS=${EMAIL_USE_TLS} - EMAIL_HOST_USER=${EMAIL_HOST_USER} - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD} - - CORS_ORIGIN_WHITELIST=${CORS_ORIGIN_WHITELIST} - ALLOWED_ENV_HOST=${ALLOWED_ENV_HOST} - CELERY_BROKER_URL=${REDIS_URL} - CELERY_RESULT_BACKEND=${REDIS_URL} From f619ae3201aa263b83d41174095537f4624d9c9f Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Wed, 14 Aug 2024 20:20:25 +0300 Subject: [PATCH 06/25] First version of autoapprove, untested, database migrations is not generated and not applied --- BackEnd/administration/models.py | 8 ++++++++ BackEnd/profiles/serializers.py | 7 +++++++ BackEnd/profiles/tasks.py | 17 +++++++++++++---- BackEnd/profiles/views.py | 18 ++++++++++++++---- BackEnd/utils/moderation/send_email.py | 5 ++++- 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/BackEnd/administration/models.py b/BackEnd/administration/models.py index 7f1d0041a..0289558c7 100644 --- a/BackEnd/administration/models.py +++ b/BackEnd/administration/models.py @@ -1,5 +1,6 @@ from django.db import models from django.core.exceptions import ValidationError +from profiles.models import Profile def validate_auto_moderation_hours(value: int): @@ -26,3 +27,10 @@ def get_auto_moderation_hours(cls): pk=1, defaults={"auto_moderation_hours": 12} ) return obj + + +class AutoapproveTask(models.Model): + celery_task_id = models.CharField() + profile = models.ForeignKey(Profile,on_delete=models.CASCADE) + banner = models.CharField() + logo = models.CharField() \ No newline at end of file diff --git a/BackEnd/profiles/serializers.py b/BackEnd/profiles/serializers.py index dc04586c1..0f30fe04e 100644 --- a/BackEnd/profiles/serializers.py +++ b/BackEnd/profiles/serializers.py @@ -1,3 +1,4 @@ +from celery.result import AsyncResult from rest_framework import serializers from django.utils.timezone import now from .models import ( @@ -11,6 +12,7 @@ from images.models import ProfileImage from utils.regions_ukr_names import get_regions_ukr_names_as_string from utils.moderation.moderation_action import ModerationAction +from administration.models import AutoapproveTask class ActivitySerializer(serializers.ModelSerializer): @@ -426,6 +428,11 @@ def update(self, instance, validated_data): instance.status = instance.APPROVED instance.status_updated_at = now() instance.save() + autoappove_instance = AutoapproveTask.objects.filter( + profile=instance, banner=str(banner_approved.uuid), logo=str(logo_approved.uuid)).first() + if autoappove_instance: + celery_task = AsyncResult(id=autoappove_instance.celery_task_id) + celery_task.revoke() return instance else: raise serializers.ValidationError("Invalid action provided.") diff --git a/BackEnd/profiles/tasks.py b/BackEnd/profiles/tasks.py index 795a079b6..3406bac4a 100644 --- a/BackEnd/profiles/tasks.py +++ b/BackEnd/profiles/tasks.py @@ -1,12 +1,21 @@ from celery import shared_task from .models import Profile +from images.models import ProfileImage @shared_task -def t_cel(profile_id, text): - p = Profile.objects.get(pk=profile_id) - p.address = text - p.save() +def celery_autoapprove(profile_id, banner_uuid, logo_uuid): + profile = Profile.objects.get(pk=profile_id) + profile.status = "approved" + banner = ProfileImage.objects.get(pk=banner_uuid) + logo = ProfileImage.objects.get(pk=logo_uuid) + banner.is_approved = True + logo.is_approved = True + profile.banner_approved = banner + profile.logo_approved = logo + profile.save() + banner.save() + logo.save() diff --git a/BackEnd/profiles/views.py b/BackEnd/profiles/views.py index 92a8f6d0a..71839742c 100644 --- a/BackEnd/profiles/views.py +++ b/BackEnd/profiles/views.py @@ -20,7 +20,7 @@ from rest_framework.response import Response from drf_spectacular.utils import extend_schema, PolymorphicProxySerializer from utils.completeness_counter import completeness_count -from utils.moderation.send_email import send_moderation_email +from utils.moderation.send_email import check_for_moderation_and_send_email from utils.moderation.encode_decode_id import decode_id from forum.pagination import ForumPagination @@ -49,7 +49,9 @@ ) from .filters import ProfileFilter -from .tasks import t_cel +from .tasks import celery_autoapprove +from administration.models import AutoapproveTask, AutoModeration + class SavedCompaniesCreate(CreateAPIView): """ @@ -210,9 +212,17 @@ def perform_destroy(self, instance): def perform_update(self, serializer): profile = serializer.save() - task = t_cel.apply_async((self.kwargs.get("pk"), self.request.data.get("official_name","None")), countdown=10) completeness_count(profile) - send_moderation_email(profile) + moderation_needed = check_for_moderation_and_send_email(profile) + if moderation_needed: + banner_uuid = str(profile.banner.uuid) + logo_uuid = str(profile.logo.uuid) + delay = AutoModeration.get_auto_moderation_hours().auto_moderation_hours + result = celery_autoapprove.apply_async( + (profile.id, banner_uuid, logo_uuid), countdown=delay) + task = AutoapproveTask( + celery_task_id=result.id, profile=profile, logo=logo_uuid, banner=banner_uuid) + task.save() class ProfileViewCreate(CreateAPIView): diff --git a/BackEnd/utils/moderation/send_email.py b/BackEnd/utils/moderation/send_email.py index 330ae8fd2..a15d1008b 100644 --- a/BackEnd/utils/moderation/send_email.py +++ b/BackEnd/utils/moderation/send_email.py @@ -49,7 +49,7 @@ def attach_image(email, image, content_id): email.attach(img) -def send_moderation_email(profile): +def check_for_moderation_and_send_email(profile): manager = ModerationManager(profile) if manager.check_for_moderation(): update_time = profile.status_updated_at.strftime("%d.%m.%Y %H:%M") @@ -92,3 +92,6 @@ def send_moderation_email(profile): attach_image(email, logo, logo.uuid) email.send(fail_silently=False) + return True + else: + return False \ No newline at end of file From 8e756211f5b4c763093851db10d4a1a1bd16336a Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Fri, 16 Aug 2024 17:04:04 +0300 Subject: [PATCH 07/25] formatted with black --- BackEnd/administration/models.py | 4 ++-- BackEnd/forum/__init__.py | 2 +- BackEnd/forum/celery.py | 8 ++++---- BackEnd/forum/settings.py | 4 ++-- BackEnd/profiles/models.py | 1 - BackEnd/profiles/serializers.py | 9 +++++++-- BackEnd/profiles/tasks.py | 5 +---- BackEnd/profiles/views.py | 13 ++++++++++--- BackEnd/utils/moderation/send_email.py | 2 +- 9 files changed, 28 insertions(+), 20 deletions(-) diff --git a/BackEnd/administration/models.py b/BackEnd/administration/models.py index 0289558c7..276d33b97 100644 --- a/BackEnd/administration/models.py +++ b/BackEnd/administration/models.py @@ -31,6 +31,6 @@ def get_auto_moderation_hours(cls): class AutoapproveTask(models.Model): celery_task_id = models.CharField() - profile = models.ForeignKey(Profile,on_delete=models.CASCADE) + profile = models.ForeignKey(Profile, on_delete=models.CASCADE) banner = models.CharField() - logo = models.CharField() \ No newline at end of file + logo = models.CharField() diff --git a/BackEnd/forum/__init__.py b/BackEnd/forum/__init__.py index e31568a6b..53f4ccb1d 100644 --- a/BackEnd/forum/__init__.py +++ b/BackEnd/forum/__init__.py @@ -1,3 +1,3 @@ from .celery import app as celery_app -__all__ = ("celery_app",) \ No newline at end of file +__all__ = ("celery_app",) diff --git a/BackEnd/forum/celery.py b/BackEnd/forum/celery.py index b1ab9190f..e2c191ef7 100644 --- a/BackEnd/forum/celery.py +++ b/BackEnd/forum/celery.py @@ -1,9 +1,9 @@ import os from celery import Celery -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'forum.settings') -app = Celery('forum') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "forum.settings") +app = Celery("forum") -app.config_from_object('django.conf:settings', namespace='CELERY') +app.config_from_object("django.conf:settings", namespace="CELERY") -app.autodiscover_tasks() \ No newline at end of file +app.autodiscover_tasks() diff --git a/BackEnd/forum/settings.py b/BackEnd/forum/settings.py index 3e21371ca..2e5656850 100644 --- a/BackEnd/forum/settings.py +++ b/BackEnd/forum/settings.py @@ -116,8 +116,8 @@ WSGI_APPLICATION = "forum.wsgi.application" -CELERY_BROKER_URL = os.environ.get('REDIS_URL') -CELERY_RESULT_BACKEND = os.environ.get('REDIS_URL') +CELERY_BROKER_URL = os.environ.get("REDIS_URL") +CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL") DATABASES = { "default": { diff --git a/BackEnd/profiles/models.py b/BackEnd/profiles/models.py index 5e92391f0..696a75a20 100644 --- a/BackEnd/profiles/models.py +++ b/BackEnd/profiles/models.py @@ -24,7 +24,6 @@ def __str__(self): class Profile(models.Model): - UNDEFINED = "undefined" PENDING = "pending" BLOCKED = "blocked" diff --git a/BackEnd/profiles/serializers.py b/BackEnd/profiles/serializers.py index 0f30fe04e..a92b39a10 100644 --- a/BackEnd/profiles/serializers.py +++ b/BackEnd/profiles/serializers.py @@ -429,9 +429,14 @@ def update(self, instance, validated_data): instance.status_updated_at = now() instance.save() autoappove_instance = AutoapproveTask.objects.filter( - profile=instance, banner=str(banner_approved.uuid), logo=str(logo_approved.uuid)).first() + profile=instance, + banner=str(banner_approved.uuid), + logo=str(logo_approved.uuid), + ).first() if autoappove_instance: - celery_task = AsyncResult(id=autoappove_instance.celery_task_id) + celery_task = AsyncResult( + id=autoappove_instance.celery_task_id + ) celery_task.revoke() return instance else: diff --git a/BackEnd/profiles/tasks.py b/BackEnd/profiles/tasks.py index 3406bac4a..416e3a03c 100644 --- a/BackEnd/profiles/tasks.py +++ b/BackEnd/profiles/tasks.py @@ -3,6 +3,7 @@ from .models import Profile from images.models import ProfileImage + @shared_task def celery_autoapprove(profile_id, banner_uuid, logo_uuid): profile = Profile.objects.get(pk=profile_id) @@ -16,7 +17,3 @@ def celery_autoapprove(profile_id, banner_uuid, logo_uuid): profile.save() banner.save() logo.save() - - - - diff --git a/BackEnd/profiles/views.py b/BackEnd/profiles/views.py index 71839742c..4968e8def 100644 --- a/BackEnd/profiles/views.py +++ b/BackEnd/profiles/views.py @@ -217,11 +217,18 @@ def perform_update(self, serializer): if moderation_needed: banner_uuid = str(profile.banner.uuid) logo_uuid = str(profile.logo.uuid) - delay = AutoModeration.get_auto_moderation_hours().auto_moderation_hours + delay = ( + AutoModeration.get_auto_moderation_hours().auto_moderation_hours + ) result = celery_autoapprove.apply_async( - (profile.id, banner_uuid, logo_uuid), countdown=delay) + (profile.id, banner_uuid, logo_uuid), countdown=delay + ) task = AutoapproveTask( - celery_task_id=result.id, profile=profile, logo=logo_uuid, banner=banner_uuid) + celery_task_id=result.id, + profile=profile, + logo=logo_uuid, + banner=banner_uuid, + ) task.save() diff --git a/BackEnd/utils/moderation/send_email.py b/BackEnd/utils/moderation/send_email.py index a15d1008b..e77f631b8 100644 --- a/BackEnd/utils/moderation/send_email.py +++ b/BackEnd/utils/moderation/send_email.py @@ -94,4 +94,4 @@ def check_for_moderation_and_send_email(profile): email.send(fail_silently=False) return True else: - return False \ No newline at end of file + return False From 1ce6bede342a554eea8be8a1f2936c0fb59e45f9 Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Wed, 21 Aug 2024 14:43:36 +0300 Subject: [PATCH 08/25] Tuned database table for storing celery task info, generated migrations, autoapprove functionality works but code is ugly. Manually tasted with modarators approve and autoapprove --- .../migrations/0003_autoapprovetask.py | 36 +++++++++++++++++++ BackEnd/administration/models.py | 3 +- BackEnd/forum/settings.py | 4 +-- BackEnd/profiles/serializers.py | 7 ++-- .../templates/profiles/email_template.html | 2 +- BackEnd/profiles/views.py | 13 ++++--- 6 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 BackEnd/administration/migrations/0003_autoapprovetask.py diff --git a/BackEnd/administration/migrations/0003_autoapprovetask.py b/BackEnd/administration/migrations/0003_autoapprovetask.py new file mode 100644 index 000000000..fefb782d5 --- /dev/null +++ b/BackEnd/administration/migrations/0003_autoapprovetask.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.3 on 2024-08-19 15:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("profiles", "0021_savedcompany_is_updated"), + ("administration", "0002_create_initial_auto_moderation_hours"), + ] + + operations = [ + migrations.CreateModel( + name="AutoapproveTask", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("celery_task_id", models.CharField()), + ( + "profile", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="profiles.profile", + ), + ), + ], + ), + ] diff --git a/BackEnd/administration/models.py b/BackEnd/administration/models.py index 276d33b97..5a792ec7e 100644 --- a/BackEnd/administration/models.py +++ b/BackEnd/administration/models.py @@ -32,5 +32,4 @@ def get_auto_moderation_hours(cls): class AutoapproveTask(models.Model): celery_task_id = models.CharField() profile = models.ForeignKey(Profile, on_delete=models.CASCADE) - banner = models.CharField() - logo = models.CharField() + diff --git a/BackEnd/forum/settings.py b/BackEnd/forum/settings.py index 2e5656850..be2d4e42d 100644 --- a/BackEnd/forum/settings.py +++ b/BackEnd/forum/settings.py @@ -116,8 +116,8 @@ WSGI_APPLICATION = "forum.wsgi.application" -CELERY_BROKER_URL = os.environ.get("REDIS_URL") -CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL") +CELERY_BROKER_URL = os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/0") +CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/0") DATABASES = { "default": { diff --git a/BackEnd/profiles/serializers.py b/BackEnd/profiles/serializers.py index cd6b1abfb..9a9000646 100644 --- a/BackEnd/profiles/serializers.py +++ b/BackEnd/profiles/serializers.py @@ -465,16 +465,13 @@ def update(self, instance, validated_data): instance.status = instance.APPROVED instance.status_updated_at = now() instance.save() - autoappove_instance = AutoapproveTask.objects.filter( - profile=instance, - banner=str(banner_approved.uuid), - logo=str(logo_approved.uuid), - ).first() + autoappove_instance = AutoapproveTask.objects.filter(profile=instance).first() if autoappove_instance: celery_task = AsyncResult( id=autoappove_instance.celery_task_id ) celery_task.revoke() + autoappove_instance.delete() return instance else: raise serializers.ValidationError("Invalid action provided.") diff --git a/BackEnd/profiles/templates/profiles/email_template.html b/BackEnd/profiles/templates/profiles/email_template.html index 4d3635b3f..ec64189a8 100644 --- a/BackEnd/profiles/templates/profiles/email_template.html +++ b/BackEnd/profiles/templates/profiles/email_template.html @@ -60,7 +60,7 @@ {% endif %}

Примітка: запит буде автоматично затверджений через {{ moderation_time }}

diff --git a/BackEnd/profiles/views.py b/BackEnd/profiles/views.py index 27629973c..f5e2c5222 100644 --- a/BackEnd/profiles/views.py +++ b/BackEnd/profiles/views.py @@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404 import django_filters from djoser import utils as djoser_utils +from celery.result import AsyncResult from rest_framework.generics import ( CreateAPIView, ListCreateAPIView, @@ -225,15 +226,19 @@ def perform_update(self, serializer): logo_uuid = str(profile.logo.uuid) delay = ( AutoModeration.get_auto_moderation_hours().auto_moderation_hours - ) + ) * 60 * 60 result = celery_autoapprove.apply_async( (profile.id, banner_uuid, logo_uuid), countdown=delay ) + old_task = AutoapproveTask.objects.filter(profile=profile).first() + if old_task: + celery_old_task = AsyncResult(id=old_task.celery_task_id) + celery_old_task.revoke() + old_task.delete() + task = AutoapproveTask( celery_task_id=result.id, - profile=profile, - logo=logo_uuid, - banner=banner_uuid, + profile=profile ) task.save() SavedCompany.objects.filter(company=profile).update(is_updated=True) From 063a9e22c412739a4e2a2db08a5a435639fb5a99 Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Fri, 23 Aug 2024 11:29:36 +0300 Subject: [PATCH 09/25] Refactored moderation logic, extended ModerationManger with autoapprove functionality, removed redundant code from view and serializer, refactored email sending utility function --- BackEnd/profiles/serializers.py | 13 +-- BackEnd/profiles/views.py | 34 +++----- BackEnd/utils/moderation/image_moderation.py | 33 +++++++- BackEnd/utils/moderation/send_email.py | 84 ++++++++++---------- 4 files changed, 84 insertions(+), 80 deletions(-) diff --git a/BackEnd/profiles/serializers.py b/BackEnd/profiles/serializers.py index 31528ad8d..1128ad081 100644 --- a/BackEnd/profiles/serializers.py +++ b/BackEnd/profiles/serializers.py @@ -12,7 +12,7 @@ from images.models import ProfileImage from utils.regions_ukr_names import get_regions_ukr_names_as_string from utils.moderation.moderation_action import ModerationAction -from administration.models import AutoapproveTask +from utils.moderation.image_moderation import ModerationManager class ActivitySerializer(serializers.ModelSerializer): @@ -474,14 +474,9 @@ def update(self, instance, validated_data): else: raise serializers.ValidationError("Invalid action provided.") - autoappove_instance = AutoapproveTask.objects.filter(profile=instance).first() - if autoappove_instance: - celery_task = AsyncResult( - id=autoappove_instance.celery_task_id - ) - celery_task.revoke() - autoappove_instance.delete() - + moderation_manager = ModerationManager(profile=instance) + moderation_manager.revoke_deprecated_autoapprove() + instance.status_updated_at = now() instance.save() return instance diff --git a/BackEnd/profiles/views.py b/BackEnd/profiles/views.py index d3c0dc749..bd95e32e4 100644 --- a/BackEnd/profiles/views.py +++ b/BackEnd/profiles/views.py @@ -20,8 +20,9 @@ from rest_framework.response import Response from drf_spectacular.utils import extend_schema, PolymorphicProxySerializer from utils.completeness_counter import completeness_count -from utils.moderation.send_email import check_for_moderation_and_send_email +from utils.moderation.send_email import send_moderation_email from utils.moderation.encode_decode_id import decode_id +from utils.moderation.image_moderation import ModerationManager from forum.pagination import ForumPagination from .models import SavedCompany, Profile, Category, Activity, Region @@ -51,7 +52,7 @@ from .filters import ProfileFilter from .tasks import celery_autoapprove -from administration.models import AutoapproveTask, AutoModeration + class SavedCompaniesCreate(CreateAPIView): @@ -219,29 +220,14 @@ def perform_destroy(self, instance): def perform_update(self, serializer): profile = serializer.save() - completeness_count(profile) - moderation_needed = check_for_moderation_and_send_email(profile) - if moderation_needed: - banner_uuid = str(profile.banner.uuid) - logo_uuid = str(profile.logo.uuid) - delay = ( - AutoModeration.get_auto_moderation_hours().auto_moderation_hours - ) * 60 * 60 - result = celery_autoapprove.apply_async( - (profile.id, banner_uuid, logo_uuid), countdown=delay - ) - old_task = AutoapproveTask.objects.filter(profile=profile).first() - if old_task: - celery_old_task = AsyncResult(id=old_task.celery_task_id) - celery_old_task.revoke() - old_task.delete() - - task = AutoapproveTask( - celery_task_id=result.id, - profile=profile - ) - task.save() SavedCompany.objects.filter(company=profile).update(is_updated=True) + completeness_count(profile) + moderation_manager = ModerationManager(profile) + if moderation_manager.check_for_moderation(): + send_moderation_email(profile) + moderation_manager.schedule_autoapprove() + + class ProfileViewCreate(CreateAPIView): diff --git a/BackEnd/utils/moderation/image_moderation.py b/BackEnd/utils/moderation/image_moderation.py index 188c8cbc5..77b6b7180 100644 --- a/BackEnd/utils/moderation/image_moderation.py +++ b/BackEnd/utils/moderation/image_moderation.py @@ -1,11 +1,14 @@ from django.utils.timezone import now +from celery.result import AsyncResult + +from administration.models import AutoapproveTask, AutoModeration +from profiles.tasks import celery_autoapprove class ModerationManager: def __init__(self, profile): self.profile = profile self.moderation_is_needed = False - self.banner_logo = {"banner": None, "logo": None} def update_status(self): self.profile.status = self.profile.PENDING @@ -19,8 +22,32 @@ def needs_moderation(self, image): def check_for_moderation(self): if self.needs_moderation(self.profile.banner): self.update_status() - self.banner_logo["banner"] = self.profile.banner if self.needs_moderation(self.profile.logo): self.update_status() - self.banner_logo["logo"] = self.profile.logo return self.moderation_is_needed + + def schedule_autoapprove(self): + self.revoke_deprecated_autoapprove() + banner_uuid = str(self.profile.banner.uuid) + logo_uuid = str(self.profile.logo.uuid) + delay = ( + AutoModeration.get_auto_moderation_hours().auto_moderation_hours + ) * 60 * 60 + result = celery_autoapprove.apply_async( + (self.profile.id, banner_uuid, logo_uuid), countdown=delay + ) + + task = AutoapproveTask( + celery_task_id=result.id, + profile=self.profile + ) + task.save() + + + def revoke_deprecated_autoapprove(self): + deprecated_task = AutoapproveTask.objects.filter(profile=self.profile).first() + + if deprecated_task: + celery_deprecated_task = AsyncResult(id=deprecated_task.celery_task_id) + celery_deprecated_task.revoke() + deprecated_task.delete() diff --git a/BackEnd/utils/moderation/send_email.py b/BackEnd/utils/moderation/send_email.py index 7c68560a0..b0b1fdf10 100644 --- a/BackEnd/utils/moderation/send_email.py +++ b/BackEnd/utils/moderation/send_email.py @@ -49,51 +49,47 @@ def attach_image(email, image, content_id): email.attach(img) -def check_for_moderation_and_send_email(profile): - manager = ModerationManager(profile) - if manager.check_for_moderation(): - update_time = profile.status_updated_at.strftime("%d.%m.%Y %H:%M") - update_date = profile.status_updated_at.strftime("%d.%m.%Y") - banner = manager.banner_logo["banner"] - logo = manager.banner_logo["logo"] - context = { - "profile_name": profile.name, - "protocol": PROTOCOL, - "domain": DOMAIN, - "banner": banner, - "logo": logo, - "updated_at": update_time, - "moderation_time": define_ending( - AutoModeration.get_auto_moderation_hours().auto_moderation_hours - ), - "approve_url": generate_profile_moderation_url( - profile.id, banner, logo, "approve" - ), - "reject_url": generate_profile_moderation_url( - profile.id, banner, logo, "reject" - ), - } - - email_body = render_to_string("profiles/email_template.html", context) - email = EmailMultiAlternatives( - subject=f"{profile.name} - {update_date}: Запит " - "на затвердження змін в обліковому записі компанії", - body=email_body, - from_email=settings.EMAIL_HOST_USER, - to=[ - settings.EMAIL_HOST_USER, - ], - ) +def send_moderation_email(profile): + update_time = profile.status_updated_at.strftime("%d.%m.%Y %H:%M") + update_date = profile.status_updated_at.strftime("%d.%m.%Y") + banner = profile.banner + logo = profile.logo + context = { + "profile_name": profile.name, + "protocol": PROTOCOL, + "domain": DOMAIN, + "banner": banner, + "logo": logo, + "updated_at": update_time, + "moderation_time": define_ending( + AutoModeration.get_auto_moderation_hours().auto_moderation_hours + ), + "approve_url": generate_profile_moderation_url( + profile.id, banner, logo, "approve" + ), + "reject_url": generate_profile_moderation_url( + profile.id, banner, logo, "reject" + ), + } + + email_body = render_to_string("profiles/email_template.html", context) + email = EmailMultiAlternatives( + subject=f"{profile.name} - {update_date}: Запит " + "на затвердження змін в обліковому записі компанії", + body=email_body, + from_email=settings.EMAIL_HOST_USER, + to=[ + settings.EMAIL_HOST_USER, + ], + ) + + email.content_subtype = EMAIL_CONTENT_SUBTYPE - email.content_subtype = EMAIL_CONTENT_SUBTYPE + if banner: + attach_image(email, banner, banner.uuid) - if banner: - attach_image(email, banner, banner.uuid) + if logo: + attach_image(email, logo, logo.uuid) - if logo: - attach_image(email, logo, logo.uuid) + email.send(fail_silently=False) - email.send(fail_silently=False) - return True - else: - return False From de203ba74a3a28438376ef6745ebe8910ec8c1dd Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Fri, 23 Aug 2024 11:37:27 +0300 Subject: [PATCH 10/25] Formatted with Black --- BackEnd/administration/models.py | 1 - BackEnd/profiles/serializers.py | 3 +- BackEnd/profiles/views.py | 12 ++--- BackEnd/utils/moderation/image_moderation.py | 48 ++++++++++---------- BackEnd/utils/moderation/send_email.py | 1 - 5 files changed, 29 insertions(+), 36 deletions(-) diff --git a/BackEnd/administration/models.py b/BackEnd/administration/models.py index 5a792ec7e..3c33043c0 100644 --- a/BackEnd/administration/models.py +++ b/BackEnd/administration/models.py @@ -32,4 +32,3 @@ def get_auto_moderation_hours(cls): class AutoapproveTask(models.Model): celery_task_id = models.CharField() profile = models.ForeignKey(Profile, on_delete=models.CASCADE) - diff --git a/BackEnd/profiles/serializers.py b/BackEnd/profiles/serializers.py index 1128ad081..a13aacbcc 100644 --- a/BackEnd/profiles/serializers.py +++ b/BackEnd/profiles/serializers.py @@ -1,4 +1,3 @@ -from celery.result import AsyncResult from rest_framework import serializers from django.utils.timezone import now from .models import ( @@ -473,7 +472,7 @@ def update(self, instance, validated_data): else: raise serializers.ValidationError("Invalid action provided.") - + moderation_manager = ModerationManager(profile=instance) moderation_manager.revoke_deprecated_autoapprove() diff --git a/BackEnd/profiles/views.py b/BackEnd/profiles/views.py index bd95e32e4..b8af1fd1d 100644 --- a/BackEnd/profiles/views.py +++ b/BackEnd/profiles/views.py @@ -5,7 +5,6 @@ from django.shortcuts import get_object_or_404 import django_filters from djoser import utils as djoser_utils -from celery.result import AsyncResult from rest_framework.generics import ( CreateAPIView, ListCreateAPIView, @@ -51,9 +50,6 @@ ) from .filters import ProfileFilter -from .tasks import celery_autoapprove - - class SavedCompaniesCreate(CreateAPIView): """ @@ -223,11 +219,9 @@ def perform_update(self, serializer): SavedCompany.objects.filter(company=profile).update(is_updated=True) completeness_count(profile) moderation_manager = ModerationManager(profile) - if moderation_manager.check_for_moderation(): - send_moderation_email(profile) - moderation_manager.schedule_autoapprove() - - + if moderation_manager.check_for_moderation(): + send_moderation_email(profile) + moderation_manager.schedule_autoapprove() class ProfileViewCreate(CreateAPIView): diff --git a/BackEnd/utils/moderation/image_moderation.py b/BackEnd/utils/moderation/image_moderation.py index 77b6b7180..747aa15bf 100644 --- a/BackEnd/utils/moderation/image_moderation.py +++ b/BackEnd/utils/moderation/image_moderation.py @@ -25,29 +25,31 @@ def check_for_moderation(self): if self.needs_moderation(self.profile.logo): self.update_status() return self.moderation_is_needed - - def schedule_autoapprove(self): - self.revoke_deprecated_autoapprove() - banner_uuid = str(self.profile.banner.uuid) - logo_uuid = str(self.profile.logo.uuid) - delay = ( - AutoModeration.get_auto_moderation_hours().auto_moderation_hours - ) * 60 * 60 - result = celery_autoapprove.apply_async( - (self.profile.id, banner_uuid, logo_uuid), countdown=delay - ) - - task = AutoapproveTask( - celery_task_id=result.id, - profile=self.profile - ) - task.save() + def schedule_autoapprove(self): + self.revoke_deprecated_autoapprove() + banner_uuid = str(self.profile.banner.uuid) + logo_uuid = str(self.profile.logo.uuid) + delay = ( + (AutoModeration.get_auto_moderation_hours().auto_moderation_hours) + * 60 + * 60 + ) + result = celery_autoapprove.apply_async( + (self.profile.id, banner_uuid, logo_uuid), countdown=delay + ) + + task = AutoapproveTask(celery_task_id=result.id, profile=self.profile) + task.save() def revoke_deprecated_autoapprove(self): - deprecated_task = AutoapproveTask.objects.filter(profile=self.profile).first() - - if deprecated_task: - celery_deprecated_task = AsyncResult(id=deprecated_task.celery_task_id) - celery_deprecated_task.revoke() - deprecated_task.delete() + deprecated_task = AutoapproveTask.objects.filter( + profile=self.profile + ).first() + + if deprecated_task: + celery_deprecated_task = AsyncResult( + id=deprecated_task.celery_task_id + ) + celery_deprecated_task.revoke() + deprecated_task.delete() diff --git a/BackEnd/utils/moderation/send_email.py b/BackEnd/utils/moderation/send_email.py index b0b1fdf10..895696901 100644 --- a/BackEnd/utils/moderation/send_email.py +++ b/BackEnd/utils/moderation/send_email.py @@ -92,4 +92,3 @@ def send_moderation_email(profile): attach_image(email, logo, logo.uuid) email.send(fail_silently=False) - From a0828522b16f8a1f58faa43149e9957dbfc645ca Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Fri, 23 Aug 2024 11:54:06 +0300 Subject: [PATCH 11/25] Deleted one more unused import --- BackEnd/utils/moderation/send_email.py | 1 - 1 file changed, 1 deletion(-) diff --git a/BackEnd/utils/moderation/send_email.py b/BackEnd/utils/moderation/send_email.py index 895696901..4b7e6bd99 100644 --- a/BackEnd/utils/moderation/send_email.py +++ b/BackEnd/utils/moderation/send_email.py @@ -7,7 +7,6 @@ from django.conf import settings from django.template.loader import render_to_string from administration.models import AutoModeration -from .image_moderation import ModerationManager from .encode_decode_id import encode_id From 764e7d8505aadee285fce36d4d979bb61c2c94e2 Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Sat, 24 Aug 2024 12:09:45 +0300 Subject: [PATCH 12/25] Refactored using of ModerationManager and ApprovedImageDeleter, added company name into service email about content deletion. Manually tested, unittests to be fixed --- .../templates/profiles/email_template.html | 2 +- BackEnd/profiles/views.py | 7 +++-- BackEnd/utils/moderation/image_moderation.py | 29 +++++++++++-------- BackEnd/utils/moderation/send_email.py | 3 +- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/BackEnd/profiles/templates/profiles/email_template.html b/BackEnd/profiles/templates/profiles/email_template.html index dd0f389bf..8ca19e2e2 100644 --- a/BackEnd/profiles/templates/profiles/email_template.html +++ b/BackEnd/profiles/templates/profiles/email_template.html @@ -48,7 +48,7 @@

Вам надійшов запит на затвердження змін в обліковому записі компанії {{ profile_name }} на сайті CraftMerge.

Перегляньте зміни та затвердіть або скасуйте їх.

{% else %} -

Інформуємо про те що попередньо доданий контент було видалено

+

Інформуємо про те що попередньо доданий контент від {{profile_name}} було видалено

{% endif %}

Дата змін: {{ updated_at }} UTC

{% if banner %} diff --git a/BackEnd/profiles/views.py b/BackEnd/profiles/views.py index 879ed4047..4236a5f77 100644 --- a/BackEnd/profiles/views.py +++ b/BackEnd/profiles/views.py @@ -222,11 +222,14 @@ def perform_update(self, serializer): deletion_checker = ApprovedImagesDeleter(profile) deletion_checker.handle_potential_deletion() moderation_manager = ModerationManager(profile) - if moderation_manager.check_for_moderation(): + if ( + moderation_manager.check_for_moderation() + or moderation_manager.content_deleted + ): banner = moderation_manager.images["banner"] logo = moderation_manager.images["logo"] is_deleted = moderation_manager.content_deleted - send_moderation_email(profile,banner, logo, is_deleted) + send_moderation_email(profile, banner, logo, is_deleted) moderation_manager.schedule_autoapprove() diff --git a/BackEnd/utils/moderation/image_moderation.py b/BackEnd/utils/moderation/image_moderation.py index 7e17ccc51..d9f6cff3b 100644 --- a/BackEnd/utils/moderation/image_moderation.py +++ b/BackEnd/utils/moderation/image_moderation.py @@ -60,19 +60,24 @@ def check_for_moderation(self): def schedule_autoapprove(self): self.revoke_deprecated_autoapprove() - banner_uuid = str(self.profile.banner.uuid) - logo_uuid = str(self.profile.logo.uuid) - delay = ( - (AutoModeration.get_auto_moderation_hours().auto_moderation_hours) - * 60 - * 60 - ) - result = celery_autoapprove.apply_async( - (self.profile.id, banner_uuid, logo_uuid), countdown=delay - ) + if self.needs_moderation and not self.content_deleted: + banner_uuid = str(self.profile.banner.uuid) + logo_uuid = str(self.profile.logo.uuid) + delay = ( + ( + AutoModeration.get_auto_moderation_hours().auto_moderation_hours + ) + * 60 + * 60 + ) + result = celery_autoapprove.apply_async( + (self.profile.id, banner_uuid, logo_uuid), countdown=delay + ) - task = AutoapproveTask(celery_task_id=result.id, profile=self.profile) - task.save() + task = AutoapproveTask( + celery_task_id=result.id, profile=self.profile + ) + task.save() def revoke_deprecated_autoapprove(self): deprecated_task = AutoapproveTask.objects.filter( diff --git a/BackEnd/utils/moderation/send_email.py b/BackEnd/utils/moderation/send_email.py index 2737ba211..7c13cbf28 100644 --- a/BackEnd/utils/moderation/send_email.py +++ b/BackEnd/utils/moderation/send_email.py @@ -6,7 +6,6 @@ from django.core.mail import EmailMultiAlternatives from django.conf import settings from django.template.loader import render_to_string -from .handle_approved_images import ApprovedImages from administration.models import AutoModeration from .encode_decode_id import encode_id @@ -58,7 +57,7 @@ def send_moderation_email(profile, banner, logo, content_is_deleted): "domain": DOMAIN, "banner": banner, "logo": logo, - "banner_logo_deleted": content_is_deleted, + "banner_logo_deleted": content_is_deleted, "updated_at": update_time, "moderation_time": define_ending( AutoModeration.get_auto_moderation_hours().auto_moderation_hours From c436b0219d9a02ac5637861be204ea810d04366d Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Sat, 24 Aug 2024 12:30:26 +0300 Subject: [PATCH 13/25] unit tests for moderation email sending fixed --- BackEnd/profiles/tests/test_email_sending.py | 34 +++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/BackEnd/profiles/tests/test_email_sending.py b/BackEnd/profiles/tests/test_email_sending.py index 15dbbb0dd..e7a5d35f4 100644 --- a/BackEnd/profiles/tests/test_email_sending.py +++ b/BackEnd/profiles/tests/test_email_sending.py @@ -23,15 +23,15 @@ def setUp(self): edrpou="99999999", ) - def test_send_moderation_email_no_banner_no_logo(self): - send_moderation_email(self.profile) - self.assertEqual(len(mail.outbox), 0) - def test_send_moderation_email(self): self.profile.banner = self.banner self.profile.logo = self.logo - - send_moderation_email(self.profile) + manager = ModerationManager(self.profile) + manager.check_for_moderation() + banner = manager.images["banner"] + logo = manager.images["logo"] + content_is_deleted = manager.content_deleted + send_moderation_email(self.profile, banner, logo, content_is_deleted) self.assertEqual(len(mail.outbox), 1) email_data = mail.outbox[0] @@ -59,7 +59,12 @@ def test_send_moderation_email(self): def test_send_moderation_email_only_banner(self): self.profile.banner = self.banner - send_moderation_email(self.profile) + manager = ModerationManager(self.profile) + manager.check_for_moderation() + banner = manager.images["banner"] + logo = manager.images["logo"] + content_is_deleted = manager.content_deleted + send_moderation_email(self.profile, banner, logo, content_is_deleted) self.assertEqual(len(mail.outbox), 1) email_data = mail.outbox[0] @@ -83,7 +88,12 @@ def test_send_moderation_email_only_banner(self): def test_send_moderation_email_only_logo(self): self.profile.logo = self.logo - send_moderation_email(self.profile) + manager = ModerationManager(self.profile) + manager.check_for_moderation() + banner = manager.images["banner"] + logo = manager.images["logo"] + content_is_deleted = manager.content_deleted + send_moderation_email(self.profile, banner, logo, content_is_deleted) self.assertEqual(len(mail.outbox), 1) email_data = mail.outbox[0] @@ -150,7 +160,7 @@ def test_check_for_moderation(self, mock_now): self.assertEqual(self.profile.status_updated_at, mock_now.return_value) self.assertTrue(self.manager.moderation_is_needed) self.assertEqual( - self.manager.banner_logo, + self.manager.images, {"banner": self.banner, "logo": self.logo}, ) @@ -163,7 +173,7 @@ def test_check_for_moderation_deleted_banner(self, mock_now): self.assertEqual(self.profile.status_updated_at, mock_now.return_value) self.assertTrue(self.manager.moderation_is_needed) self.assertEqual( - self.manager.banner_logo, {"banner": None, "logo": self.logo} + self.manager.images, {"banner": None, "logo": self.logo} ) @mock.patch("utils.moderation.image_moderation.now", return_value=now()) @@ -175,7 +185,7 @@ def test_check_for_moderation_deleted_logo(self, mock_now): self.assertEqual(self.profile.status_updated_at, mock_now.return_value) self.assertTrue(self.manager.moderation_is_needed) self.assertEqual( - self.manager.banner_logo, {"banner": self.banner, "logo": None} + self.manager.images, {"banner": self.banner, "logo": None} ) # needs improvement for undefined status @@ -185,5 +195,5 @@ def test_check_for_moderation_deleted_both(self): self.manager.check_for_moderation() self.assertFalse(self.manager.moderation_is_needed) self.assertEqual( - self.manager.banner_logo, {"banner": None, "logo": None} + self.manager.images, {"banner": None, "logo": None} ) From 4e3e24ed23374cc201d2b6fc82a7749682fd2c0c Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Sat, 24 Aug 2024 12:45:33 +0300 Subject: [PATCH 14/25] Black formatting --- BackEnd/profiles/tests/test_email_sending.py | 10 ++++------ BackEnd/utils/moderation/handle_approved_images.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/BackEnd/profiles/tests/test_email_sending.py b/BackEnd/profiles/tests/test_email_sending.py index e7a5d35f4..f27d27663 100644 --- a/BackEnd/profiles/tests/test_email_sending.py +++ b/BackEnd/profiles/tests/test_email_sending.py @@ -29,7 +29,7 @@ def test_send_moderation_email(self): manager = ModerationManager(self.profile) manager.check_for_moderation() banner = manager.images["banner"] - logo = manager.images["logo"] + logo = manager.images["logo"] content_is_deleted = manager.content_deleted send_moderation_email(self.profile, banner, logo, content_is_deleted) @@ -62,7 +62,7 @@ def test_send_moderation_email_only_banner(self): manager = ModerationManager(self.profile) manager.check_for_moderation() banner = manager.images["banner"] - logo = manager.images["logo"] + logo = manager.images["logo"] content_is_deleted = manager.content_deleted send_moderation_email(self.profile, banner, logo, content_is_deleted) @@ -91,7 +91,7 @@ def test_send_moderation_email_only_logo(self): manager = ModerationManager(self.profile) manager.check_for_moderation() banner = manager.images["banner"] - logo = manager.images["logo"] + logo = manager.images["logo"] content_is_deleted = manager.content_deleted send_moderation_email(self.profile, banner, logo, content_is_deleted) @@ -194,6 +194,4 @@ def test_check_for_moderation_deleted_both(self): self.profile.logo = None self.manager.check_for_moderation() self.assertFalse(self.manager.moderation_is_needed) - self.assertEqual( - self.manager.images, {"banner": None, "logo": None} - ) + self.assertEqual(self.manager.images, {"banner": None, "logo": None}) diff --git a/BackEnd/utils/moderation/handle_approved_images.py b/BackEnd/utils/moderation/handle_approved_images.py index 0dcb79c0f..c48d0c80b 100644 --- a/BackEnd/utils/moderation/handle_approved_images.py +++ b/BackEnd/utils/moderation/handle_approved_images.py @@ -4,7 +4,7 @@ class ApprovedImagesDeleter: """ - Entity that handles deletion of approved images in case if user deletes image under moderation. + Entity that handles the deletion of approved images if a user deletes an image under moderation. """ def __init__(self, profile): From 2c9c5aeb61ff373eba11c8a4004e5caa325de0aa Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Sat, 24 Aug 2024 12:46:43 +0300 Subject: [PATCH 15/25] Black formatting again --- BackEnd/utils/moderation/handle_approved_images.py | 1 - 1 file changed, 1 deletion(-) diff --git a/BackEnd/utils/moderation/handle_approved_images.py b/BackEnd/utils/moderation/handle_approved_images.py index c48d0c80b..7ba5aae4b 100644 --- a/BackEnd/utils/moderation/handle_approved_images.py +++ b/BackEnd/utils/moderation/handle_approved_images.py @@ -2,7 +2,6 @@ class ApprovedImagesDeleter: - """ Entity that handles the deletion of approved images if a user deletes an image under moderation. """ From 23dbe8915aaf92416afa3875c6857d1e3f9989e9 Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Sat, 24 Aug 2024 13:01:48 +0300 Subject: [PATCH 16/25] Mocked autoapprove in tests --- BackEnd/profiles/tests/test_crud_profile.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/BackEnd/profiles/tests/test_crud_profile.py b/BackEnd/profiles/tests/test_crud_profile.py index 762308ae0..55564b66c 100644 --- a/BackEnd/profiles/tests/test_crud_profile.py +++ b/BackEnd/profiles/tests/test_crud_profile.py @@ -786,12 +786,17 @@ def test_full_update_profile_authorized_with_partial_data(self): ) self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) + @mock.patch( + "utils.moderation.image_moderation.ModerationManager.schedule_autoapprove" + ) @mock.patch( "utils.moderation.send_email.attach_image", new_callable=mock.mock_open, read_data=b"image", ) - def test_full_update_profile_authorized_with_full_data(self, mock_file): + def test_full_update_profile_authorized_with_full_data( + self, mock_file, mock_autoapprove + ): category = CategoryFactory() activity = ActivityFactory() region = RegionFactory() @@ -826,6 +831,7 @@ def test_full_update_profile_authorized_with_full_data(self, mock_file): status.HTTP_200_OK, response.status_code, response.content ) mock_file.assert_called() + mock_autoapprove.assert_called_once() def test_full_update_profile_unauthorized(self): category = CategoryFactory() From b33063c5a315a02bae2a1dfff3426422911e8d9a Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Sat, 24 Aug 2024 14:05:14 +0300 Subject: [PATCH 17/25] Updated completness_count, added it into autoapprove task, corrected email template for deleted contetnt --- BackEnd/profiles/tasks.py | 2 ++ .../profiles/templates/profiles/email_template.html | 2 +- BackEnd/utils/completeness_counter.py | 11 ++++++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/BackEnd/profiles/tasks.py b/BackEnd/profiles/tasks.py index 416e3a03c..090fd22af 100644 --- a/BackEnd/profiles/tasks.py +++ b/BackEnd/profiles/tasks.py @@ -2,6 +2,7 @@ from .models import Profile from images.models import ProfileImage +from utils.completeness_counter import completeness_count @shared_task @@ -17,3 +18,4 @@ def celery_autoapprove(profile_id, banner_uuid, logo_uuid): profile.save() banner.save() logo.save() + completeness_count(profile) diff --git a/BackEnd/profiles/templates/profiles/email_template.html b/BackEnd/profiles/templates/profiles/email_template.html index 8ca19e2e2..386d47de6 100644 --- a/BackEnd/profiles/templates/profiles/email_template.html +++ b/BackEnd/profiles/templates/profiles/email_template.html @@ -48,7 +48,7 @@

Вам надійшов запит на затвердження змін в обліковому записі компанії {{ profile_name }} на сайті CraftMerge.

Перегляньте зміни та затвердіть або скасуйте їх.

{% else %} -

Інформуємо про те що попередньо доданий контент від {{profile_name}} було видалено

+

Інформуємо про те що попередньо доданий контент було видалено користувачем.

{% endif %}

Дата змін: {{ updated_at }} UTC

{% if banner %} diff --git a/BackEnd/utils/completeness_counter.py b/BackEnd/utils/completeness_counter.py index b2e34035a..1d38403ed 100644 --- a/BackEnd/utils/completeness_counter.py +++ b/BackEnd/utils/completeness_counter.py @@ -1,11 +1,16 @@ -from profiles.models import Profile, Activity, Category, Region +from profiles.models import Activity, Category, Region +from images.models import ProfileImage def completeness_count(instance): instance.completeness = 0 - if instance.banner_approved: + if instance.banner_approved and ProfileImage.objects.filter( + is_deleted=False, uuid=instance.banner_approved.uuid + ): instance.completeness += 100 - if instance.logo_approved: + if instance.logo_approved and ProfileImage.objects.filter( + is_deleted=False, uuid=instance.logo_approved.uuid + ): instance.completeness += 1 if Region.objects.all().filter(profile=instance.id): instance.completeness += 1 From 4a0513e82b46959d2b0a6c20488a9b9318caa6bd Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Wed, 28 Aug 2024 10:16:51 +0300 Subject: [PATCH 18/25] Patch autoapprove scheduling in tests for moderator's approve --- .../tests/test_approve_moderation_request.py | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/BackEnd/profiles/tests/test_approve_moderation_request.py b/BackEnd/profiles/tests/test_approve_moderation_request.py index 446155c31..e4feda0b7 100644 --- a/BackEnd/profiles/tests/test_approve_moderation_request.py +++ b/BackEnd/profiles/tests/test_approve_moderation_request.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + from rest_framework import status from rest_framework.test import APITestCase, APIClient @@ -11,9 +13,9 @@ from utils.dump_response import dump # noqa +@patch("profiles.views.ModerationManager.schedule_autoapprove") class TestProfileModeration(APITestCase): def setUp(self) -> None: - self.banner = ProfileimageFactory(image_type="banner") self.logo = ProfileimageFactory(image_type="logo") self.second_banner = ProfileimageFactory(image_type="banner") @@ -26,8 +28,7 @@ def setUp(self) -> None: self.moderator_client = APIClient() - def test_approve_banner_and_logo(self): - + def test_approve_banner_and_logo(self, mock_manager): # user updates both banner and logo self.user_client.patch( path="/api/profiles/{profile_id}".format( @@ -66,9 +67,9 @@ def test_approve_banner_and_logo(self): self.assertEqual(self.profile.banner_approved, self.profile.banner) self.assertEqual(self.profile.logo_approved, self.profile.logo) self.assertEqual(self.profile.APPROVED, self.profile.status) + mock_manager.assert_called_once() - def test_approve_banner(self): - + def test_approve_banner(self, mock_manager): # user updates only banner self.user_client.patch( path="/api/profiles/{profile_id}".format( @@ -102,9 +103,9 @@ def test_approve_banner(self): self.assertTrue(self.banner.is_approved) self.assertEqual(self.profile.banner_approved, self.profile.banner) self.assertEqual(self.profile.APPROVED, self.profile.status) + mock_manager.assert_called_once() - def test_approve_logo(self): - + def test_approve_logo(self, mock_manager): # user updates logo self.user_client.patch( path="/api/profiles/{profile_id}".format( @@ -138,9 +139,9 @@ def test_approve_logo(self): self.assertTrue(self.logo.is_approved) self.assertEqual(self.profile.logo_approved, self.profile.logo) self.assertEqual(self.profile.APPROVED, self.profile.status) + mock_manager.assert_called_once() - def test_approve_banner_and_logo_processed_request(self): - + def test_approve_banner_and_logo_processed_request(self, mock_manager): # user updates both banner and logo self.user_client.patch( path="/api/profiles/{profile_id}".format( @@ -186,9 +187,9 @@ def test_approve_banner_and_logo_processed_request(self): }, response.json(), ) + mock_manager.assert_called_once() - def test_approve_banner_and_logo_outdated_request(self): - + def test_approve_banner_and_logo_outdated_request(self, mock_manager): # user updates both banner and logo self.user_client.patch( path="/api/profiles/{profile_id}".format( @@ -241,9 +242,9 @@ def test_approve_banner_and_logo_outdated_request(self): self.assertNotEqual(self.profile.banner, first_banner) self.assertNotEqual(self.profile.logo, first_logo) self.assertEqual(self.profile.PENDING, self.profile.status) + mock_manager.assert_called() - def test_approve_banner_and_logo_wrong_action(self): - + def test_approve_banner_and_logo_wrong_action(self, mock_manager): # user updates both banner and logo self.user_client.patch( path="/api/profiles/{profile_id}".format( @@ -272,9 +273,9 @@ def test_approve_banner_and_logo_wrong_action(self): self.assertEqual( {"action": ["Action is not allowed"]}, response.json() ) + mock_manager.assert_called_once() - def test_approve_banner_and_logo_error_in_signed_id(self): - + def test_approve_banner_and_logo_error_in_signed_id(self, mock_manager): # user updates both banner and logo self.user_client.patch( path="/api/profiles/{profile_id}".format( @@ -301,9 +302,9 @@ def test_approve_banner_and_logo_error_in_signed_id(self): self.assertEqual(status.HTTP_404_NOT_FOUND, response.status_code) self.assertEqual({"detail": "Not found."}, response.json()) + mock_manager.assert_called_once() - def test_approve_banner_and_logo_non_existing_profile(self): - + def test_approve_banner_and_logo_non_existing_profile(self, mock_manager): # user updates both banner and logo self.user_client.patch( path="/api/profiles/{profile_id}".format( @@ -330,9 +331,9 @@ def test_approve_banner_and_logo_non_existing_profile(self): self.assertEqual(status.HTTP_404_NOT_FOUND, response.status_code) self.assertEqual({"detail": "Not found."}, response.json()) + mock_manager.assert_called_once() - def test_approve_banner_and_logo_empty_image_fields(self): - + def test_approve_banner_and_logo_empty_image_fields(self, mock_manager): # user updates both banner and logo self.user_client.patch( path="/api/profiles/{profile_id}".format( @@ -364,3 +365,4 @@ def test_approve_banner_and_logo_empty_image_fields(self): }, response.json(), ) + mock_manager.assert_called_once() From bdbff4b485863b094a5ddd83fe75918ef689ed0a Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Wed, 28 Aug 2024 12:31:16 +0300 Subject: [PATCH 19/25] Add exception handling and logging for celery autoapprove, enhance autoapprove by reducing db updates of unchanged images, correct profile status setting for autoapprove --- BackEnd/forum/settings.py | 31 +++++++++ BackEnd/profiles/tasks.py | 22 +++--- BackEnd/utils/moderation/image_moderation.py | 71 ++++++++++++-------- 3 files changed, 87 insertions(+), 37 deletions(-) diff --git a/BackEnd/forum/settings.py b/BackEnd/forum/settings.py index be2d4e42d..8bb6bd4df 100644 --- a/BackEnd/forum/settings.py +++ b/BackEnd/forum/settings.py @@ -237,3 +237,34 @@ def show_toolbar(request): DEBUG_TOOLBAR_CONFIG = { "SHOW_TOOLBAR_CALLBACK": show_toolbar, } + + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {message}", + "style": "{", + }, + "simple": { + "format": "{levelname} {message}", + "style": "{", + }, + }, + "handlers": { + "file": { + "level": "ERROR", + "class": "logging.FileHandler", + "filename": os.path.join(BASE_DIR, "django.log"), + "formatter": "verbose", + }, + }, + "loggers": { + "utils": { + "handlers": ["file"], + "level": "ERROR", + "propagate": True, + }, + }, +} diff --git a/BackEnd/profiles/tasks.py b/BackEnd/profiles/tasks.py index 090fd22af..26ae6f508 100644 --- a/BackEnd/profiles/tasks.py +++ b/BackEnd/profiles/tasks.py @@ -8,14 +8,18 @@ @shared_task def celery_autoapprove(profile_id, banner_uuid, logo_uuid): profile = Profile.objects.get(pk=profile_id) - profile.status = "approved" - banner = ProfileImage.objects.get(pk=banner_uuid) - logo = ProfileImage.objects.get(pk=logo_uuid) - banner.is_approved = True - logo.is_approved = True - profile.banner_approved = banner - profile.logo_approved = logo + if banner_uuid: + banner = ProfileImage.objects.get(pk=banner_uuid) + banner.is_approved = True + profile.banner_approved = banner + banner.save() + + if logo_uuid: + logo = ProfileImage.objects.get(pk=logo_uuid) + logo.is_approved = True + profile.logo_approved = logo + logo.save() + + profile.status = profile.AUTOAPPROVED profile.save() - banner.save() - logo.save() completeness_count(profile) diff --git a/BackEnd/utils/moderation/image_moderation.py b/BackEnd/utils/moderation/image_moderation.py index d9f6cff3b..ad7abf531 100644 --- a/BackEnd/utils/moderation/image_moderation.py +++ b/BackEnd/utils/moderation/image_moderation.py @@ -1,10 +1,17 @@ +import logging + from django.utils.timezone import now from celery.result import AsyncResult +from kombu.exceptions import OperationalError +from redis.exceptions import ConnectionError from administration.models import AutoapproveTask, AutoModeration from profiles.tasks import celery_autoapprove +logger = logging.getLogger(__name__) + + class ModerationManager: def __init__(self, profile): self.profile = profile @@ -59,34 +66,42 @@ def check_for_moderation(self): return self.moderation_is_needed def schedule_autoapprove(self): - self.revoke_deprecated_autoapprove() - if self.needs_moderation and not self.content_deleted: - banner_uuid = str(self.profile.banner.uuid) - logo_uuid = str(self.profile.logo.uuid) - delay = ( - ( - AutoModeration.get_auto_moderation_hours().auto_moderation_hours + try: + self.revoke_deprecated_autoapprove() + if self.needs_moderation and not self.content_deleted: + banner = self.images.get("banner") + logo = self.images.get("logo") + banner_uuid = str(banner.uuid) if banner else None + logo_uuid = str(logo.uuid) if logo else None + delay = ( + ( + AutoModeration.get_auto_moderation_hours().auto_moderation_hours + ) + * 60 + * 60 + ) + result = celery_autoapprove.apply_async( + (self.profile.id, banner_uuid, logo_uuid), countdown=delay ) - * 60 - * 60 - ) - result = celery_autoapprove.apply_async( - (self.profile.id, banner_uuid, logo_uuid), countdown=delay - ) - - task = AutoapproveTask( - celery_task_id=result.id, profile=self.profile - ) - task.save() + + task = AutoapproveTask( + celery_task_id=result.id, profile=self.profile + ) + task.save() + except (OperationalError, ConnectionError) as e: + logger.error(e) def revoke_deprecated_autoapprove(self): - deprecated_task = AutoapproveTask.objects.filter( - profile=self.profile - ).first() - - if deprecated_task: - celery_deprecated_task = AsyncResult( - id=deprecated_task.celery_task_id - ) - celery_deprecated_task.revoke() - deprecated_task.delete() + try: + deprecated_task = AutoapproveTask.objects.filter( + profile=self.profile + ).first() + + if deprecated_task: + celery_deprecated_task = AsyncResult( + id=deprecated_task.celery_task_id + ) + celery_deprecated_task.revoke() + deprecated_task.delete() + except (OperationalError, ConnectionError) as e: + logger.error(e) From 6ababd2f5fac88fe1a50b08a38c17541b30e5c68 Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Wed, 28 Aug 2024 12:43:17 +0300 Subject: [PATCH 20/25] Add network for celery service into docker-compose.dev.yml --- .gitignore | 1 + docker-compose.dev.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 5e3f7aeab..3a93d2988 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ env /.DS_Store __pycache__/ /FrontEnd/node_modules +django.log BackEnd/public/* !BackEnd/public/media/.gitkeep diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7c1fda8b1..a5a19123e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -75,6 +75,8 @@ services: - ALLOWED_ENV_HOST=${ALLOWED_ENV_HOST} - CELERY_BROKER_URL=${REDIS_URL} - CELERY_RESULT_BACKEND=${REDIS_URL} + networks: + - forum_network redis: image: redis:latest container_name: redis From af5bfe3a26f1223c24155da339e100a51557c3cd Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Wed, 28 Aug 2024 13:27:07 +0300 Subject: [PATCH 21/25] Add REDIS_URL env var for CI/CD django_cd_dev.yml --- .github/workflows/django_cd_dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/django_cd_dev.yml b/.github/workflows/django_cd_dev.yml index 8591fc107..cde116e20 100644 --- a/.github/workflows/django_cd_dev.yml +++ b/.github/workflows/django_cd_dev.yml @@ -21,6 +21,7 @@ env: REACT_APP_BASE_API_URL: ${{ vars.REACT_APP_BASE_API_URL }} REACT_APP_PUBLIC_URL: ${{ vars.REACT_APP_PUBLIC_URL }} ALLOWED_ENV_HOST: ${{ vars.ALLOWED_ENV_HOST }} + REDIS_URL: ${{ vars.REDIS_URL }} jobs: deploy: From d1b51c3ab3b5c0834f1457f5193e3ee559e7d1af Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Wed, 28 Aug 2024 13:53:21 +0300 Subject: [PATCH 22/25] Add REDIS_URL env var for docker-compose --- docker-compose.dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index a5a19123e..d5f0d5478 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -75,6 +75,7 @@ services: - ALLOWED_ENV_HOST=${ALLOWED_ENV_HOST} - CELERY_BROKER_URL=${REDIS_URL} - CELERY_RESULT_BACKEND=${REDIS_URL} + - REDIS_URL=${REDIS_URL} networks: - forum_network redis: From 6c37887f1101f5863f25f94c60fddede19bdc4f7 Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Wed, 28 Aug 2024 13:54:54 +0300 Subject: [PATCH 23/25] Add REDIS_URL env var for docker-compose - api-dev service --- docker-compose.dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index d5f0d5478..61ce28e05 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -32,6 +32,7 @@ services: - ALLOWED_ENV_HOST=${ALLOWED_ENV_HOST} - CELERY_BROKER_URL=${REDIS_URL} - CELERY_RESULT_BACKEND=${REDIS_URL} + - REDIS_URL=${REDIS_URL} networks: - forum_network frontend: From 32565dbb73e10417d6f28502dd010cd6102feace Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Wed, 28 Aug 2024 15:16:07 +0300 Subject: [PATCH 24/25] Change os.environ.get() to config() for REDIS_URL in settings --- BackEnd/forum/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BackEnd/forum/settings.py b/BackEnd/forum/settings.py index 8bb6bd4df..2755a889b 100644 --- a/BackEnd/forum/settings.py +++ b/BackEnd/forum/settings.py @@ -116,8 +116,8 @@ WSGI_APPLICATION = "forum.wsgi.application" -CELERY_BROKER_URL = os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/0") -CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/0") +CELERY_BROKER_URL = config("REDIS_URL") +CELERY_RESULT_BACKEND = config("REDIS_URL") DATABASES = { "default": { From 120c688819c1113fc0932d1071d0358708279b36 Mon Sep 17 00:00:00 2001 From: Yan Zhylavy Date: Wed, 28 Aug 2024 15:22:08 +0300 Subject: [PATCH 25/25] Change env var REDIS_URL for backend test workflow --- .github/workflows/tests_be.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests_be.yml b/.github/workflows/tests_be.yml index 68352ecd2..4e3a662b9 100644 --- a/.github/workflows/tests_be.yml +++ b/.github/workflows/tests_be.yml @@ -17,6 +17,7 @@ env: EMAIL_HOST_PASSWORD: Test1234 CORS_ORIGIN_WHITELIST: '' ALLOWED_ENV_HOST: '' + REDIS_URL: ${{ vars.REDIS_URL }} jobs: