Merge branch 'develop' of ssh://gl.id-east.ru:222/gm/gm-backend into develop

This commit is contained in:
Александр Пархомин 2020-02-06 17:45:25 +03:00
commit bb040fffdf
14 changed files with 393 additions and 44 deletions

View File

@ -118,7 +118,7 @@ class GuideListCreateView(GuideBaseView, generics.ListCreateAPIView):
### Response
Return paginated list of guides.
I.e.:
E.g.:
```
{
"count": 58,

View File

@ -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'))
)

View File

@ -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):

View File

@ -63,7 +63,7 @@ urlpatterns = [
path('employees/search/', views.EmployeesListSearchViews.as_view(), name='employees-search'),
path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
path('employees/<int:pk>/<int:award_id>', views.RemoveAwardView.as_view(), name='employees-award-delete'),
path('<int:establishment_id>/employee/<int:employee_id>/position/<int:position_id>',
path('<int:establishment_id>/employee/<int:employee_id>/position/<int:position_id>/',
views.EstablishmentEmployeeCreateView.as_view(),
name='employees-establishment-create'),
path('employee/position/<int:pk>/delete/', views.EstablishmentEmployeeDeleteView.as_view(),

View File

@ -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,

View File

@ -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'),
),
]

View File

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

View File

@ -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):

View File

@ -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,

View File

@ -38,7 +38,7 @@ class ReportRetrieveView(ReportBaseView, generics.RetrieveAPIView):
## View for retrieving serialized instance.
### Response
Return serialized object.
I.e.:
E.g.:
```
{
"count": 7,

View File

@ -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,

View File

@ -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')):

View File

@ -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'

File diff suppressed because one or more lines are too long