Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

618 profile moderation make profile moderation request autoapproved #739

Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8d76d33
Added celery and redis configuration
AlexanderSychev2005 Jul 29, 2024
4ce9c78
Updated compose and added celery and redis configuration
AlexanderSychev2005 Jul 31, 2024
99e16c9
Added test Celery task
YanZhylavy Aug 9, 2024
699a4f3
Fixed Redis working
AlexanderSychev2005 Aug 9, 2024
428a9ac
Merge branch 'develop' into 618-profile-moderation-make-profile-moder…
YanZhylavy Aug 14, 2024
f278579
removed duplicate env var CORS_ORIGIN_WHITELIST from docker-compose.d…
YanZhylavy Aug 14, 2024
0233749
Merge branch 'develop' into 618-profile-moderation-make-profile-moder…
YanZhylavy Aug 14, 2024
f619ae3
First version of autoapprove, untested, database migrations is not ge…
YanZhylavy Aug 14, 2024
8e75621
formatted with black
YanZhylavy Aug 16, 2024
da5ace6
Merge branch 'develop' into 618-profile-moderation-make-profile-moder…
YanZhylavy Aug 19, 2024
1ce6bed
Tuned database table for storing celery task info, generated migratio…
YanZhylavy Aug 21, 2024
fcfb24b
Merged develop branch with 'reject' logic, resolved conflicts
YanZhylavy Aug 23, 2024
063a9e2
Refactored moderation logic, extended ModerationManger with autoappro…
YanZhylavy Aug 23, 2024
de203ba
Formatted with Black
YanZhylavy Aug 23, 2024
a082852
Deleted one more unused import
YanZhylavy Aug 23, 2024
5a2d565
Merge branch 'develop' into 618-profile-moderation-make-profile-moder…
YanZhylavy Aug 23, 2024
b66682f
Merge branch 'develop' into 618-profile-moderation-make-profile-moder…
YanZhylavy Aug 24, 2024
764e7d8
Refactored using of ModerationManager and ApprovedImageDeleter, added…
YanZhylavy Aug 24, 2024
c436b02
unit tests for moderation email sending fixed
YanZhylavy Aug 24, 2024
4e3e24e
Black formatting
YanZhylavy Aug 24, 2024
2c9c5ae
Black formatting again
YanZhylavy Aug 24, 2024
23dbe89
Mocked autoapprove in tests
YanZhylavy Aug 24, 2024
b33063c
Updated completness_count, added it into autoapprove task, corrected …
YanZhylavy Aug 24, 2024
16f0911
Merge branch 'develop' into 618-profile-moderation-make-profile-moder…
YanZhylavy Aug 27, 2024
4a0513e
Patch autoapprove scheduling in tests for moderator's approve
YanZhylavy Aug 28, 2024
bdbff4b
Add exception handling and logging for celery autoapprove, enhance au…
YanZhylavy Aug 28, 2024
6ababd2
Add network for celery service into docker-compose.dev.yml
YanZhylavy Aug 28, 2024
af5bfe3
Add REDIS_URL env var for CI/CD django_cd_dev.yml
YanZhylavy Aug 28, 2024
d1b51c3
Add REDIS_URL env var for docker-compose
YanZhylavy Aug 28, 2024
6c37887
Add REDIS_URL env var for docker-compose - api-dev service
YanZhylavy Aug 28, 2024
32565db
Change os.environ.get() to config() for REDIS_URL in settings
YanZhylavy Aug 28, 2024
120c688
Change env var REDIS_URL for backend test workflow
YanZhylavy Aug 28, 2024
66cc757
Merge branch 'develop' into 618-profile-moderation-make-profile-moder…
YanZhylavy Aug 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions BackEnd/administration/migrations/0003_autoapprovetask.py
Original file line number Diff line number Diff line change
@@ -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",
),
),
],
),
]
6 changes: 6 additions & 0 deletions BackEnd/administration/models.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -26,3 +27,8 @@ 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)
3 changes: 3 additions & 0 deletions BackEnd/forum/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .celery import app as celery_app

__all__ = ("celery_app",)
9 changes: 9 additions & 0 deletions BackEnd/forum/celery.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 3 additions & 0 deletions BackEnd/forum/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@

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")

DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
Expand Down
4 changes: 4 additions & 0 deletions BackEnd/profiles/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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 utils.moderation.image_moderation import ModerationManager


class ActivitySerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -478,6 +479,9 @@ def update(self, instance, validated_data):
else:
raise serializers.ValidationError("Invalid action provided.")

moderation_manager = ModerationManager(profile=instance)
moderation_manager.revoke_deprecated_autoapprove()

instance.status_updated_at = now()
instance.save()
return instance
21 changes: 21 additions & 0 deletions BackEnd/profiles/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from celery import shared_task

from .models import Profile
from images.models import ProfileImage
from utils.completeness_counter import completeness_count


@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
profile.save()
banner.save()
logo.save()
completeness_count(profile)
2 changes: 1 addition & 1 deletion BackEnd/profiles/templates/profiles/email_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<p>Вам надійшов запит на затвердження змін в обліковому записі компанії {{ profile_name }} на сайті CraftMerge.</p>
<p>Перегляньте зміни та затвердіть або скасуйте їх.</p>
{% else %}
<p>Інформуємо про те що попередньо доданий контент було видалено</p>
<p>Інформуємо про те що попередньо доданий контент було видалено користувачем.</p>
{% endif %}
<p><b>Дата змін: </b>{{ updated_at }} UTC</p>
{% if banner %}
Expand Down
8 changes: 7 additions & 1 deletion BackEnd/profiles/tests/test_crud_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
36 changes: 22 additions & 14 deletions BackEnd/profiles/tests/test_email_sending.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand Down Expand Up @@ -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},
)

Expand All @@ -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())
Expand All @@ -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
Expand All @@ -184,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.banner_logo, {"banner": None, "logo": None}
)
self.assertEqual(self.manager.images, {"banner": None, "logo": None})
17 changes: 15 additions & 2 deletions BackEnd/profiles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from utils.completeness_counter import completeness_count
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 utils.moderation.handle_approved_images import ApprovedImagesDeleter

from forum.pagination import ForumPagination
from .models import SavedCompany, Profile, Category, Activity, Region
Expand Down Expand Up @@ -215,9 +217,20 @@ def perform_destroy(self, instance):

def perform_update(self, serializer):
profile = serializer.save()
completeness_count(profile)
send_moderation_email(profile)
SavedCompany.objects.filter(company=profile).update(is_updated=True)
completeness_count(profile)
deletion_checker = ApprovedImagesDeleter(profile)
deletion_checker.handle_potential_deletion()
moderation_manager = ModerationManager(profile)
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)
moderation_manager.schedule_autoapprove()


class ProfileViewCreate(CreateAPIView):
Expand Down
3 changes: 3 additions & 0 deletions BackEnd/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
redis==5.0.7
asgiref==3.7.2
Django==4.2.3
djangorestframework==3.14.0
Expand All @@ -18,4 +19,6 @@ black==23.9.1
drf-spectacular==0.26.5
ratelimit==2.2.1
django-debug-toolbar==4.3.0
celery==5.4.0


11 changes: 8 additions & 3 deletions BackEnd/utils/completeness_counter.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 6 additions & 2 deletions BackEnd/utils/moderation/handle_approved_images.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from ..completeness_counter import completeness_count


class ApprovedImages:
class ApprovedImagesDeleter:
"""
Entity that handles the deletion of approved images if a user deletes an image under moderation.
"""

def __init__(self, profile):
self.profile = profile

Expand All @@ -11,7 +15,7 @@ def delete_approved_image(self, approved_image):
approved_image.save()
completeness_count(self.profile)

def check_approved_images(self):
def handle_potential_deletion(self):
if not self.profile.banner:
self.delete_approved_image(self.profile.banner_approved)

Expand Down
43 changes: 40 additions & 3 deletions BackEnd/utils/moderation/image_moderation.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
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}
self.images = {"banner": None, "logo": None}
self.content_deleted = False

def handle_approved_status(self, secondary_image):
Expand Down Expand Up @@ -41,15 +45,48 @@ def needs_moderation(self, image):
def check_for_moderation(self):
if self.needs_moderation(self.profile.banner):
self.update_pending_status()
self.banner_logo["banner"] = self.profile.banner
self.images["banner"] = self.profile.banner
elif not self.profile.banner and self.profile.logo:
self.handle_approved_status(self.profile.logo)

if self.needs_moderation(self.profile.logo):
self.update_pending_status()
self.banner_logo["logo"] = self.profile.logo
self.images["logo"] = self.profile.logo
elif not self.profile.logo and self.profile.banner:
self.handle_approved_status(self.profile.banner)

self.handle_undefined_status()
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
)
* 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()
Loading
Loading