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 all 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
1 change: 1 addition & 0 deletions .github/workflows/django_cd_dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tests_be.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ env:
EMAIL_HOST_PASSWORD: Test1234
CORS_ORIGIN_WHITELIST: ''
ALLOWED_ENV_HOST: ''
REDIS_URL: ${{ vars.REDIS_URL }}


jobs:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ env
/.DS_Store
__pycache__/
/FrontEnd/node_modules
django.log

BackEnd/public/*
!BackEnd/public/media/.gitkeep
Expand Down
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()
34 changes: 34 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 = config("REDIS_URL")
CELERY_RESULT_BACKEND = config("REDIS_URL")

DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
Expand Down Expand Up @@ -234,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,
},
},
}
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
25 changes: 25 additions & 0 deletions BackEnd/profiles/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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)
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()
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
40 changes: 21 additions & 19 deletions BackEnd/profiles/tests/test_approve_moderation_request.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import patch

from rest_framework import status
from rest_framework.test import APITestCase, APIClient

Expand All @@ -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")
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -364,3 +365,4 @@ def test_approve_banner_and_logo_empty_image_fields(self):
},
response.json(),
)
mock_manager.assert_called_once()
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
Loading
Loading