diff --git a/apps/collection/views/back.py b/apps/collection/views/back.py index 5f5088ab..e5ddab58 100644 --- a/apps/collection/views/back.py +++ b/apps/collection/views/back.py @@ -118,7 +118,7 @@ class GuideListCreateView(GuideBaseView, generics.ListCreateAPIView): ### Response Return paginated list of guides. - I.e.: + E.g.: ``` { "count": 58, diff --git a/apps/establishment/models.py b/apps/establishment/models.py index c813ad74..f2ad7c2e 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -17,7 +17,6 @@ from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import Case, ExpressionWrapper, F, Prefetch, Q, Subquery, When -from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField @@ -679,7 +678,7 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, @property def visible_tags_detail(self): """Removes some tags from detail Establishment representation""" - return self.visible_tags.exclude(category__index_name__in=['tag']) + return self.visible_tags.exclude(category__index_name__in=['tag', 'shop_category']) def recalculate_public_mark(self): fresh_review = self.reviews.published().order_by('-modified').first() @@ -1145,7 +1144,7 @@ class EmployeeQuerySet(models.QuerySet): queryset=EstablishmentEmployee.objects.actual() .prefetch_related('establishment', 'position').order_by('-from_date'), to_attr='prefetched_establishment_employee'), - 'awards' + Prefetch('awards', queryset=Award.objects.select_related('award_type')) ) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 45c79433..e6d4e819 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -1,5 +1,6 @@ from functools import lru_cache +from django.contrib.contenttypes.models import ContentType from django.db.models import F from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ @@ -14,7 +15,8 @@ from establishment import models, serializers as model_serializers from establishment.models import ContactEmail, ContactPhone, EstablishmentEmployee from establishment.serializers.common import ContactPhonesSerializer from gallery.models import Image -from location.serializers import AddressDetailSerializer, TranslatedField, AddressBaseSerializer +from location.serializers import AddressDetailSerializer, TranslatedField +from main import models as main_models from main.models import Currency from main.serializers import AwardSerializer from tag.serializers import TagBaseSerializer @@ -22,8 +24,6 @@ from utils.decorators import with_base_attributes from utils.methods import string_random from utils.serializers import ImageBaseSerializer, ProjectModelSerializer, TimeZoneChoiceField, \ PhoneMixinSerializer -from main import models as main_models -from django.contrib.contenttypes.models import ContentType def phones_handler(phones_list, establishment): @@ -59,7 +59,7 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria queryset=models.Address.objects.all(), write_only=True ) - address = AddressBaseSerializer(read_only=True, allow_null=True) + address = AddressDetailSerializer(read_only=True, allow_null=True) transliterated_name = serializers.CharField( required=False, allow_null=True, allow_blank=True ) @@ -363,7 +363,10 @@ class EmployeeBackSerializers(PhoneMixinSerializer, serializers.ModelSerializer) @staticmethod @lru_cache(maxsize=32) def get_qs(obj): - return obj.establishmentemployee_set.actual().annotate( + return obj.establishmentemployee_set.actual().only( + 'establishment', + 'from_date', + ).annotate( public_mark=F('establishment__public_mark'), est_id=F('establishment__id'), est_slug=F('establishment__slug'), @@ -526,10 +529,8 @@ class EstEmployeeBackSerializer(EmployeeBackSerializers): 'toque_number', 'available_for_events', 'photo', + 'photo_id', ] - extra_kwargs = { - 'phone': {'write_only': True} - } class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer): diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index ce1fea10..64bf736d 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -63,7 +63,7 @@ urlpatterns = [ path('employees/search/', views.EmployeesListSearchViews.as_view(), name='employees-search'), path('employees//', views.EmployeeRUDView.as_view(), name='employees-rud'), path('employees//', views.RemoveAwardView.as_view(), name='employees-award-delete'), - path('/employee//position/', + path('/employee//position//', views.EstablishmentEmployeeCreateView.as_view(), name='employees-establishment-create'), path('employee/position//delete/', views.EstablishmentEmployeeDeleteView.as_view(), diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index d089922b..15f208de 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -60,6 +60,7 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP def get_queryset(self): return super().get_queryset() \ + .with_extended_address_related() \ .with_certain_tag_category_related('category', 'restaurant_category') \ .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \ .with_certain_tag_category_related('shop_category', 'artisan_category') \ @@ -68,8 +69,28 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP class EmployeeEstablishmentPositionsView(generics.ListAPIView): - """Establishment by employee view.""" - + """ + ## Establishment employee positions filtered by employee identifier. + ### *GET* + #### Description + Return paginated list of results from an intermediate table filtered by employee + identifier, that contains connection between employee establishment, + employee hiring dates, position, status `'I' (Idle)`, `'A' (Accepted)`, `'D' (Declined)`. + ##### Response + ``` + { + "count": 58, + "next": 2, + "previous": null, + "results": [ + { + "id": 1, + ... + } + ] + } + ``` + """ queryset = models.EstablishmentEmployee.objects.all() serializer_class = serializers.EstablishmentEmployeePositionsSerializer permission_classes = get_permission_classes( @@ -84,7 +105,26 @@ class EmployeeEstablishmentPositionsView(generics.ListAPIView): class EmployeeEstablishmentsListView(generics.ListAPIView): - """Establishment by employee list view.""" + """ + ## Employee establishments filtered by employee identifier. + ### *GET* + #### Description + Return paginated list of establishments filtered by employee identifier. + ##### Response + ``` + { + "count": 58, + "next": 2, + "previous": null, + "results": [ + { + "id": 1, + ... + } + ] + } + ``` + """ serializer_class = serializers.EstablishmentListCreateSerializer permission_classes = get_permission_classes( IsEstablishmentManager, @@ -98,8 +138,25 @@ class EmployeeEstablishmentsListView(generics.ListAPIView): class EmployeePositionsListView(generics.ListAPIView): - """Establishment position by employee list view.""" - + """ + ## Paginated list of establishments filtered by employee identifier + ### *GET* + #### Description + Return a paginated list of establishments of an employee by employee identifier. + ##### Response + ``` + { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + ... + } + } + ``` + """ queryset = models.Establishment.objects.all() serializer_class = serializers.EstablishmentPositionListSerializer permission_classes = get_permission_classes( @@ -500,7 +557,7 @@ class EmployeeListCreateView(generics.ListCreateAPIView): * position_id (`int`) - filter by employees position identifier * public_mark (`str`) - filter by establishment public mark * toque_number (`str`) - filter by establishment toque number - * username (`str`) - filter by username or name + * username (`str`) - filter by a username or name #### Response ``` @@ -530,17 +587,18 @@ class EmployeeListCreateView(generics.ListCreateAPIView): #### Request Required fields: - * available_for_events (bool) - flag that responds for availability for events + * name (`str`) - employee name Non-required fields: - * name (`str`) - name - * last_name (`str`) - last name + * name (`str`) - employee name + * last_name (`str`) - employee last name * user (`int`) - user identifier * sex (`int`) - enum: `0 (Male), 1 (Female)` * birth_date (`str`) - birth datetime (datetime in a format `ISO-8601`) * email (`str`) - email address - * phone (`str`) - phone number in format `E164` + * phone (`str`) - phone number in a format `E164` * photo_id (`int`) - photo identifier + * available_for_events (bool) - flag that responds for availability for events """ filter_class = filters.EmployeeBackFilter serializer_class = serializers.EmployeeBackSerializers @@ -550,13 +608,44 @@ class EmployeeListCreateView(generics.ListCreateAPIView): IsEstablishmentAdministrator, ) + def get_queryset(self): + qs = super().get_queryset() + if self.request.country_code: + qs = qs.filter(establishments__address__city__country__code=self.request.country_code) + return qs + class EmployeesListSearchViews(generics.ListAPIView): - """Employee search view""" + """ + ## Employee search view. + ### *GET* + ##### Description + Return a non-paginated list of employees. + Available filters: + * search (`str`) - filter by name or last name with mistakes + * position_id (`int`) - filter by employees position identifier + * public_mark (`str`) - filter by establishment public mark + * toque_number (`str`) - filter by establishment toque number + * username (`str`) - filter by a username or name + (with limitations by the minimum number of characters) + + ###### Response + ``` + [ + { + "id": 1, + ... + } + ] + ``` + """ pagination_class = None - queryset = models.Employee.objects.all().with_back_office_related().select_related('photo') filter_class = filters.EmployeeBackSearchFilter serializer_class = serializers.EmployeeBackSerializers + queryset = ( + models.Employee.objects.with_back_office_related() + .select_related('photo') + ) permission_classes = get_permission_classes( IsEstablishmentManager, IsEstablishmentAdministrator, @@ -564,7 +653,47 @@ class EmployeesListSearchViews(generics.ListAPIView): class EstablishmentEmployeeListView(generics.ListCreateAPIView): - """Establishment emplyoees list view.""" + """ + ## Establishment employees List/Create view. + ### *GET* + #### Description + Returning non-paginated list of employees by establishment identifier. + ##### Response + E.g.: + ``` + [ + { + "id": 1, + ... + } + ] + ``` + + ### *POST* + #### Description + Create a new instance of employee for establishment by establishment identifier. + #### Request + Required: + * name (`str`) - employee name + + Additional: + * last_name (`str`) - employee last name + * user (`int`) - user identifier + * sex (`int`) - enum: `0 (Male), 1 (Female)` + * birth_date (`str`) - birth datetime (datetime in a format `ISO-8601`) + * email (`str`) - email address + * phone (`str`) - phone number in a format `E164` + * available_for_events (bool) - flag that responds for availability for events + * photo_id (`int`) - photo identifier + + #### Response + ``` + { + "id": 1, + ... + } + ``` + """ serializer_class = serializers.EstEmployeeBackSerializer pagination_class = None permission_classes = get_permission_classes( @@ -584,7 +713,51 @@ class EstablishmentEmployeeListView(generics.ListCreateAPIView): class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): - """Employee RUD view.""" + """ + ## Employee Retrieve/Update/Destroy view + ### *GET* + #### Description + Retrieve a serialized object of employee. + ##### Response + ``` + { + "id": 1, + ... + } + ``` + + ### *PUT*/*PATCH* + #### Description + Completely/Partially update an employee object. + ##### Request + Available fields: + * name (`str`) - employee name + * last_name (`str`) - employee last name + * sex (`enum`) - 0 (Male), 1 (Female) + * birth_date (`str`) - datetime in a format `ISO-8601` + * email (`str`) - employee email address + * phone (`str`) - phone number in E164 format + * toque_number (`int`) - employee toque number + * available_for_events (`bool`) - flag that responds for availability for events + * photo_id (`int`) - image identifier + ##### Response + Return an employee serialized object + E.g.: + ``` + { + "id": 1, + ... + } + ``` + + ### *DELETE* + #### Description + Delete an instance of employee + ##### Response + ``` + No content + ``` + """ serializer_class = serializers.EmployeeBackSerializers queryset = models.Employee.objects.with_back_office_related() permission_classes = get_permission_classes( @@ -594,6 +767,16 @@ class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): class RemoveAwardView(generics.DestroyAPIView): + """ + ## Remove award view. + ### *DELETE* + #### Description + Remove an award from an employee by an employee identifier and an award identifier. + ##### Response + ``` + No content + ``` + """ lookup_field = 'pk' serializer_class = serializers.EmployeeBackSerializers queryset = models.Employee.objects.with_back_office_related() @@ -856,6 +1039,29 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews, class EstablishmentEmployeeCreateView(generics.CreateAPIView): + """ + ## Create employee position for establishment + ### *POST* + #### Description + Creating position for an employee for establishment, + by `establishment identifier`, `employee identifier` and + `position identifier`. + + ##### Request data + Available fields: + * from_date - datetime (datetime in a format `ISO-8601`), by default `timezone.now()` + * to_date - datetime (datetime in a format `ISO-8601`), by default `null` + + ##### Response data + E.g.: + ``` + { + "id": 47405, + "from_date": "2020-02-06T11:01:04.961000Z", + "to_date": "2020-02-06T11:01:04.961000Z" + } + ``` + """ serializer_class = serializers.EstablishmentEmployeeCreateSerializer queryset = models.EstablishmentEmployee.objects.all() permission_classes = get_permission_classes( @@ -865,6 +1071,18 @@ class EstablishmentEmployeeCreateView(generics.CreateAPIView): class EstablishmentEmployeeDeleteView(generics.DestroyAPIView): + """ + ## Delete employee position for establishment + ### *DELETE* + #### Description + Deleting position for an employee from establishment, by `position identifier`. + + + ##### Response data + ``` + No content + ``` + """ queryset = EstablishmentEmployee.objects.all() permission_classes = get_permission_classes( IsEstablishmentManager, @@ -889,7 +1107,25 @@ class EstablishmentPositionListView(generics.ListAPIView): class EstablishmentAdminView(generics.ListAPIView): - """Establishment admin list view.""" + """ + ## List establishment admins + ### *GET* + #### Description + Returning paginated list of establishment administrators by establishment slug. + ##### Response + ``` + { + "count": 58, + "next": 2, + "previous": null, + "results": [ + { + "id": 1, + ... + } + ] + } +``` """ serializer_class = serializers.EstablishmentAdminListSerializer permission_classes = get_permission_classes( IsEstablishmentManager, diff --git a/apps/report/migrations/0002_report_locale.py b/apps/report/migrations/0002_report_locale.py new file mode 100644 index 00000000..d49c2ded --- /dev/null +++ b/apps/report/migrations/0002_report_locale.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2020-02-06 13:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='report', + name='locale', + field=models.CharField(max_length=10, null=True, verbose_name='locale'), + ), + ] diff --git a/apps/report/models.py b/apps/report/models.py index b89e992d..856d1f04 100644 --- a/apps/report/models.py +++ b/apps/report/models.py @@ -1,22 +1,26 @@ from django.conf import settings from django.core.mail import send_mail from django.db import models +from django.template.loader import render_to_string +from django.utils.functional import cached_property from django.utils.text import gettext_lazy as _ from report.tasks import send_report_task +from translation.models import SiteInterfaceDictionary from utils.models import ProjectBaseMixin class ReportManager(models.Manager): """Manager for model Report.""" - def make(self, source: int, category, url: str, description: str): + def make(self, source: int, category, url: str, description: str, locale: str): """Make object.""" obj = self.create( source=source, category=category, url=url, - description=description + description=description, + locale=locale, ) if settings.USE_CELERY: send_report_task.delay(obj.id) @@ -60,6 +64,8 @@ class Report(ProjectBaseMixin): verbose_name=_('category')) url = models.URLField(verbose_name=_('URL')) description = models.TextField(verbose_name=_('description')) + locale = models.CharField(max_length=10, null=True, + verbose_name=_('locale')) objects = ReportManager.from_queryset(ReportQuerySet)() @@ -70,19 +76,46 @@ class Report(ProjectBaseMixin): def __str__(self): """Implement `str` dunder method.""" - return f'{self.id}: {self.get_category_display()} ({self.url})' + return f'{self.id}: {self.get_category_display()} ({self.url}, {self.locale})' - def get_body_email_message(self): + @cached_property + def support_email_note(self): + keyword = 'support.email.note' + default_note = ( + 'You received this message because you are an ' + 'administrator with privileges to manage this request.' + ) + + note_qs = SiteInterfaceDictionary.objects.filter(keywords=keyword) + if note_qs.exists(): + return note_qs.first().text.get(self.locale, default_note) + return default_note + + @cached_property + def report_message(self): + return render_to_string( + template_name=settings.REPORT_TEMPLATE, + context={ + 'source_value': self.get_source_display(), + 'source_page_url': self.url, + 'source_screen_language': self.locale, + 'request_category': self.get_category_display(), + 'request_description': self.description, + 'support_email_note': self.support_email_note, + } + ) + + @cached_property + def base_template(self): """Prepare the body of the email message""" return { - 'subject': self.get_category_display(), - 'message': str(self.description), - 'html_message': self.description, + 'subject': _('[TECH_REQUEST] A new technical request has been created.'), + 'message': self.report_message, + 'html_message': self.report_message, '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()) - + send_mail(**self.base_template) diff --git a/apps/report/serializers.py b/apps/report/serializers.py index ef94836d..7eab76d4 100644 --- a/apps/report/serializers.py +++ b/apps/report/serializers.py @@ -1,4 +1,5 @@ """DRF-serializers for application report.""" +from django.utils.functional import cached_property from rest_framework import serializers from report.models import Report @@ -18,15 +19,25 @@ class ReportBaseSerializer(serializers.ModelSerializer): 'category_display', 'url', 'description', + 'locale', ] extra_kwargs = { 'source': {'required': False}, - 'category': {'write_only': True} + 'category': {'write_only': True}, + 'locale': {'write_only': True} } + @cached_property + def locale(self) -> str: + """Return locale from request.""" + request = self.context.get('request') + if hasattr(request, 'locale'): + return request.locale + def validate(self, attrs): """An overridden validate method.""" attrs['source'] = self.context.get('view').get_source() + attrs['locale'] = self.locale return attrs def create(self, validated_data): diff --git a/apps/report/views/back.py b/apps/report/views/back.py index 6735a3c2..993507dc 100644 --- a/apps/report/views/back.py +++ b/apps/report/views/back.py @@ -13,7 +13,7 @@ class ReportListCreateView(ReportBaseView, ListCreateAPIView): * category: integer (0 - Bug, 1 - Suggestion improvement) * url: char (URL) * description: text (problem description) - I.e.: + E.g.: ``` { "category": 1, diff --git a/apps/report/views/common.py b/apps/report/views/common.py index 40b155bf..5fff213b 100644 --- a/apps/report/views/common.py +++ b/apps/report/views/common.py @@ -38,7 +38,7 @@ class ReportRetrieveView(ReportBaseView, generics.RetrieveAPIView): ## View for retrieving serialized instance. ### Response Return serialized object. - I.e.: + E.g.: ``` { "count": 7, diff --git a/apps/report/views/web.py b/apps/report/views/web.py index 95477a21..6f1dfbfc 100644 --- a/apps/report/views/web.py +++ b/apps/report/views/web.py @@ -13,7 +13,7 @@ class ReportListCreateView(ReportBaseView, ListCreateAPIView): * category: integer (0 - Bug, 1 - Suggestion improvement) * url: char (URL) * description: text (problem description) - I.e.: + E.g.: ``` { "category": 1, diff --git a/apps/utils/models.py b/apps/utils/models.py index eeb52c31..2f2a39d1 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -11,6 +11,7 @@ from django.contrib.postgres.fields.jsonb import KeyTextTransform from django.core.validators import FileExtensionValidator from django.shortcuts import get_object_or_404 from django.utils import timezone +from django.utils.functional import cached_property from django.utils.html import mark_safe from django.utils.translation import ugettext_lazy as _, get_language from easy_thumbnails.fields import ThumbnailerImageField @@ -516,13 +517,13 @@ def default_menu_bool_array(): class PhoneModelMixin: """Mixin for PhoneNumberField.""" - @property + @cached_property def country_calling_code(self): """Return phone code from PhonеNumberField.""" if hasattr(self, 'phone') and self.phone: return f'+{self.phone.country_code}' - @property + @cached_property def national_calling_number(self): """Return phone national number from from PhonеNumberField.""" if hasattr(self, 'phone') and (self.phone and hasattr(self.phone, 'national_number')): diff --git a/project/settings/base.py b/project/settings/base.py index 70eb553e..237d634c 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -462,6 +462,7 @@ CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html' NEWS_EMAIL_TEMPLATE = 'news/news_email.html' NOTIFICATION_PASSWORD_TEMPLATE = 'account/password_change_email.html' NOTIFICATION_SUBSCRIBE_TEMPLATE = 'notification/update_email.html' +REPORT_TEMPLATE = 'report/tech_support_template.html' # COOKIES @@ -568,4 +569,4 @@ 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' +EMAIL_TECHNICAL_SUPPORT = 'tech_requests@gaultmillau.com' diff --git a/project/templates/report/tech_support_template.html b/project/templates/report/tech_support_template.html new file mode 100644 index 00000000..acb085ff --- /dev/null +++ b/project/templates/report/tech_support_template.html @@ -0,0 +1,49 @@ + + + + + + + + + Tech Support + + + +
+ +
+ + New technical + request has been created +
+
+

+ Source: {{ source_value }} +

+

Source page: {{ source_page_url }}

+

User language: {{ source_screen_language }}

+

Request category: {{ request_category }} Request {{ request_description }}

+
+ +
+ + + \ No newline at end of file