diff --git a/apps/report/__init__.py b/apps/report/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/report/admin.py b/apps/report/admin.py new file mode 100644 index 00000000..7acadf69 --- /dev/null +++ b/apps/report/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from report.models import Report + + +@admin.register(Report) +class ReportAdmin(admin.ModelAdmin): + """Report admin model.""" diff --git a/apps/report/apps.py b/apps/report/apps.py new file mode 100644 index 00000000..107087c3 --- /dev/null +++ b/apps/report/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig +from django.utils.text import gettext_lazy as _ + + +class ReportConfig(AppConfig): + name = 'report' + verbose_name = _('Report') + diff --git a/apps/report/filters.py b/apps/report/filters.py new file mode 100644 index 00000000..541b2607 --- /dev/null +++ b/apps/report/filters.py @@ -0,0 +1,26 @@ +"""Filters for application report.""" +from django_filters import rest_framework as filters + +from report.models import Report + + +class ReportFilterSet(filters.FilterSet): + """Report filter set.""" + source = filters.ChoiceFilter( + choices=Report.SOURCE_CHOICES, + help_text='Filter allow filtering QuerySet by a field - source.' + 'Choices - 0 (Back office), 1 (Web), 2 (Mobile)' + ) + category = filters.ChoiceFilter( + choices=Report.CATEGORY_CHOICES, + help_text='Filter allow filtering QuerySet by a field - category.' + 'Choices - 0 (Bug), 1 (Suggestion/improvement)' + ) + + class Meta: + """Meta class.""" + model = Report + fields = ( + 'source', + 'category', + ) diff --git a/apps/report/migrations/0001_initial.py b/apps/report/migrations/0001_initial.py new file mode 100644 index 00000000..b051d636 --- /dev/null +++ b/apps/report/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2.7 on 2020-02-05 12:16 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Report', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('source', models.PositiveSmallIntegerField(choices=[(0, 'Back office'), (1, 'Web'), (2, 'Mobile')], verbose_name='source')), + ('category', models.PositiveSmallIntegerField(choices=[(0, 'Bug'), (1, 'Suggestion/improvement')], verbose_name='category')), + ('url', models.URLField(verbose_name='URL')), + ('description', models.TextField(verbose_name='description')), + ], + options={ + 'verbose_name': 'Report', + 'verbose_name_plural': 'Reports', + }, + ), + ] diff --git a/apps/report/migrations/__init__.py b/apps/report/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/report/models.py b/apps/report/models.py new file mode 100644 index 00000000..ef3d197e --- /dev/null +++ b/apps/report/models.py @@ -0,0 +1,84 @@ +from django.conf import settings +from django.core.mail import send_mail +from django.db import models +from django.utils.text import gettext_lazy as _ + +from report.tasks import send_report_task +from utils.models import ProjectBaseMixin + + +class ReportManager(models.Manager): + """Manager for model Report.""" + + def make(self, source: int, category, url: str, description: str): + """Make object.""" + obj = self.create( + source=source, + category=category, + url=url, + description=description + ) + if settings.USE_CELERY: + send_report_task.delay(obj.id) + else: + send_report_task(obj.id) + return obj + + +class ReportQuerySet(models.QuerySet): + """QuerySet for model Report.""" + + def by_source(self, source: int): + """Return QuerySet filtered by a source.""" + return self.filter(source=source) + + +class Report(ProjectBaseMixin): + """Report model.""" + + BACK_OFFICE = 0 + WEB = 1 + MOBILE = 2 + + SOURCE_CHOICES = ( + (BACK_OFFICE, _('Back office')), + (WEB, _('Web')), + (MOBILE, _('Mobile')), + ) + + BUG = 0 + SUGGESTION_IMPROVEMENT = 1 + + CATEGORY_CHOICES = ( + (BUG, _('Bug')), + (SUGGESTION_IMPROVEMENT, _('Suggestion/improvement')), + ) + + source = models.PositiveSmallIntegerField(choices=SOURCE_CHOICES, + verbose_name=_('source')) + category = models.PositiveSmallIntegerField(choices=CATEGORY_CHOICES, + verbose_name=_('category')) + url = models.URLField(verbose_name=_('URL')) + description = models.TextField(verbose_name=_('description')) + + objects = ReportManager.from_queryset(ReportQuerySet)() + + class Meta: + """Meta class.""" + verbose_name = _('Report') + verbose_name_plural = _('Reports') + + def get_body_email_message(self): + """Prepare the body of the email message""" + return { + 'subject': self.get_category_display(), + 'message': str(self.description), + 'html_message': self.description, + 'from_email': settings.EMAIL_HOST_USER, + 'recipient_list': [settings.EMAIL_TECHNICAL_SUPPORT, ], + } + + def send_email(self): + """Send an email reset user password""" + send_mail(**self.get_body_email_message()) + diff --git a/apps/report/serializers.py b/apps/report/serializers.py new file mode 100644 index 00000000..ef94836d --- /dev/null +++ b/apps/report/serializers.py @@ -0,0 +1,34 @@ +"""DRF-serializers for application report.""" +from rest_framework import serializers + +from report.models import Report + + +class ReportBaseSerializer(serializers.ModelSerializer): + """Serializer for model Report.""" + + category_display = serializers.CharField(source='get_category_display', read_only=True) + + class Meta: + """Meta class.""" + model = Report + fields = [ + 'id', + 'category', + 'category_display', + 'url', + 'description', + ] + extra_kwargs = { + 'source': {'required': False}, + 'category': {'write_only': True} + } + + def validate(self, attrs): + """An overridden validate method.""" + attrs['source'] = self.context.get('view').get_source() + return attrs + + def create(self, validated_data): + """An overridden create method.""" + return self.Meta.model.objects.make(**validated_data) diff --git a/apps/report/tasks.py b/apps/report/tasks.py new file mode 100644 index 00000000..e7392e5e --- /dev/null +++ b/apps/report/tasks.py @@ -0,0 +1,18 @@ +"""Report app tasks.""" +import logging + +from celery import shared_task + +logger = logging.getLogger(__name__) + + +@shared_task +def send_report_task(report_id: int): + from report.models import Report + + report_qs = Report.objects.filter(id=report_id) + if report_qs.exists(): + report = report_qs.first() + report.send_email() + else: + logger.error(f'Error sending report {report_id}') diff --git a/apps/report/tests.py b/apps/report/tests.py new file mode 100644 index 00000000..a39b155a --- /dev/null +++ b/apps/report/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/apps/report/urls/__init__.py b/apps/report/urls/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/report/urls/back.py b/apps/report/urls/back.py new file mode 100644 index 00000000..2748f056 --- /dev/null +++ b/apps/report/urls/back.py @@ -0,0 +1,10 @@ +"""Back office URL patterns for application report.""" +from django.urls import path + +from report.views import back as views + +app_name = 'report' + +urlpatterns = [ + path('', views.ReportListCreateView.as_view(), name='report-list-create'), +] diff --git a/apps/report/urls/common.py b/apps/report/urls/common.py new file mode 100644 index 00000000..4f48f826 --- /dev/null +++ b/apps/report/urls/common.py @@ -0,0 +1,9 @@ +"""Common URL patterns for application report.""" +from django.urls import path + +from report.views import common as views + +app_name = 'report' +urlpatterns = [ + path('/', views.ReportRetrieveView.as_view(), name='report-retrieve') +] diff --git a/apps/report/urls/mobile.py b/apps/report/urls/mobile.py new file mode 100644 index 00000000..f72deddc --- /dev/null +++ b/apps/report/urls/mobile.py @@ -0,0 +1,5 @@ +"""Mobile URL patterns for application report.""" + +app_name = 'report' + +urlpatterns = [] diff --git a/apps/report/urls/web.py b/apps/report/urls/web.py new file mode 100644 index 00000000..52370dc3 --- /dev/null +++ b/apps/report/urls/web.py @@ -0,0 +1,11 @@ +"""Web URL patterns for application report.""" +from django.urls import path + +from report.urls.common import urlpatterns as common_urlpatterns +from report.views import web as views + +app_name = 'report' + +urlpatterns = common_urlpatterns + [ + path('', views.ReportListCreateView.as_view(), name='report-list-create'), +] diff --git a/apps/report/views/__init__.py b/apps/report/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/report/views/back.py b/apps/report/views/back.py new file mode 100644 index 00000000..1f6e64aa --- /dev/null +++ b/apps/report/views/back.py @@ -0,0 +1,39 @@ +"""Views for application report.""" +from rest_framework.generics import ListCreateAPIView + +from report.models import Report +from report.views.common import ReportBaseView + + +class ReportListCreateView(ReportBaseView, ListCreateAPIView): + """ + ## View for getting list of reports or create a new one. + ### POST-request data + Request data attributes: + * category: integer (0 - Bug, 1 - Suggestion improvement) + * url: char (URL) + * description: text (problem description) + I.e.: + ``` + { + "category": 1, + "url": "http://google.com", + "description": "Description" + } + ``` + + ### Response + *GET* + Return paginated list of reports. + + *POST* + Creates a new report, and returns a serialized object. + + ### Description + Method that allows getting list of reports or create a new one and return serialized object. + """ + + @staticmethod + def get_source(): + """Return a source for view.""" + return Report.BACK_OFFICE diff --git a/apps/report/views/common.py b/apps/report/views/common.py new file mode 100644 index 00000000..40b155bf --- /dev/null +++ b/apps/report/views/common.py @@ -0,0 +1,58 @@ +"""Common views for application report.""" +from rest_framework import generics + +from report.filters import ReportFilterSet +from report.models import Report +from report.serializers import ReportBaseSerializer +from utils.methods import get_permission_classes +from utils.permissions import ( + IsEstablishmentManager, IsContentPageManager, IsReviewManager, + IsModerator, IsRestaurantInspector, IsArtisanInspector, + IsWineryWineInspector, IsDistilleryLiquorInspector, IsProducerFoodInspector, + IsEstablishmentAdministrator +) + + +class ReportBaseView(generics.GenericAPIView): + """ + ## Report base view. + """ + queryset = Report.objects.all() + serializer_class = ReportBaseSerializer + filter_class = ReportFilterSet + permission_classes = get_permission_classes( + IsEstablishmentManager, IsContentPageManager, IsReviewManager, + IsModerator, IsRestaurantInspector, IsArtisanInspector, + IsWineryWineInspector, IsDistilleryLiquorInspector, IsProducerFoodInspector, + IsEstablishmentAdministrator + ) + + @staticmethod + def get_source(): + """Return a source for view.""" + return NotImplementedError('You must implement "get_source" method') + + +class ReportRetrieveView(ReportBaseView, generics.RetrieveAPIView): + """ + ## View for retrieving serialized instance. + ### Response + Return serialized object. + I.e.: + ``` + { + "count": 7, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + ... + } + ] + } + ``` + + ### Description + Method that allows retrieving serialized report object. + """ diff --git a/apps/report/views/web.py b/apps/report/views/web.py new file mode 100644 index 00000000..36a64008 --- /dev/null +++ b/apps/report/views/web.py @@ -0,0 +1,39 @@ +"""Views for application report.""" +from rest_framework.generics import ListCreateAPIView + +from report.models import Report +from report.views.common import ReportBaseView + + +class ReportListCreateView(ReportBaseView, ListCreateAPIView): + """ + ## View for getting list of reports or create a new one. + ### POST-request data + Request data attributes: + * category: integer (0 - Bug, 1 - Suggestion improvement) + * url: char (URL) + * description: text (problem description) + I.e.: + ``` + { + "category": 1, + "url": "http://google.com", + "description": "Description" + } + ``` + + ### Response + *GET* + Return paginated list of reports. + + *POST* + Creates a new report, and returns a serialized object. + + ### Description + Method that allows getting list of reports or create a new one and return serialized object. + """ + + @staticmethod + def get_source(): + """Return a source for view.""" + return Report.WEB diff --git a/apps/translation/models.py b/apps/translation/models.py index 86e5e52a..8af17be7 100644 --- a/apps/translation/models.py +++ b/apps/translation/models.py @@ -1,11 +1,13 @@ """Translation app models.""" +from django.apps import apps from django.contrib.postgres.fields import JSONField from django.contrib.postgres.indexes import GinIndex from django.db import models from django.utils.translation import gettext_lazy as _ -from django.apps import apps + from utils.models import ProjectBaseMixin, LocaleManagerMixin + class LanguageQuerySet(models.QuerySet): """QuerySet for model Language""" @@ -55,7 +57,7 @@ class SiteInterfaceDictionaryManager(LocaleManagerMixin): Tag = apps.get_model('tag', 'Tag') """Creates or updates translation for EXISTING in DB Tag""" if not tag.pk or not isinstance(tag, Tag): - raise NotImplementedError + raise NotImplementedError() if tag.translation: tag.translation.text = translations tag.translation.page = 'tag' @@ -74,7 +76,7 @@ class SiteInterfaceDictionaryManager(LocaleManagerMixin): """Creates or updates translation for EXISTING in DB TagCategory""" TagCategory = apps.get_model('tag', 'TagCategory') if not tag_category.pk or not isinstance(tag_category, TagCategory): - raise NotImplementedError + raise NotImplementedError() if tag_category.translation: tag_category.translation.text = translations tag_category.translation.page = 'tag' diff --git a/apps/utils/models.py b/apps/utils/models.py index b21b64de..eeb52c31 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -143,7 +143,7 @@ class OAuthProjectMixin: def get_source(self): """Method to get of platform""" - return NotImplementedError + return NotImplementedError() class BaseAttributes(ProjectBaseMixin): diff --git a/project/settings/base.py b/project/settings/base.py index 91e5ab20..70eb553e 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -76,6 +76,7 @@ PROJECT_APPS = [ 'favorites.apps.FavoritesConfig', 'rating.apps.RatingConfig', 'tag.apps.TagConfig', + 'report.apps.ReportConfig', ] EXTERNAL_APPS = [ @@ -566,3 +567,5 @@ COUNTRY_CALLING_CODES = { CALLING_CODES_ANTILLES_GUYANE_WEST_INDIES = [590, 594, 1758, 596] DEFAULT_CALLING_CODE_ANTILLES_GUYANE_WEST_INDIES = 590 + +EMAIL_TECHNICAL_SUPPORT = 'it-report@gaultmillau.com' diff --git a/project/settings/local.py b/project/settings/local.py index 2c822c53..1d5d7de5 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -123,3 +123,5 @@ EMAIL_PORT = 587 # ADD TRANSFER TO INSTALLED APPS INSTALLED_APPS.append('transfer.apps.TransferConfig') + +EMAIL_TECHNICAL_SUPPORT = 'a.feteleu@spider.ru' diff --git a/project/urls/back.py b/project/urls/back.py index 8245e492..c3d6b7ea 100644 --- a/project/urls/back.py +++ b/project/urls/back.py @@ -18,4 +18,5 @@ urlpatterns = [ namespace='advertisement')), path('main/', include('main.urls.back')), path('partner/', include('partner.urls.back')), + path('report/', include('report.urls.back')), ] diff --git a/project/urls/web.py b/project/urls/web.py index 99c12937..838a6f94 100644 --- a/project/urls/web.py +++ b/project/urls/web.py @@ -36,5 +36,5 @@ urlpatterns = [ path('favorites/', include('favorites.urls')), path('timetables/', include('timetable.urls.web')), path('products/', include('product.urls.web')), - + path('report/', include('report.urls.web')), ]