Skip to content

Commit

Permalink
Merge branch 'develop' into amb
Browse files Browse the repository at this point in the history
  • Loading branch information
ragimov700 authored Mar 2, 2024
2 parents 555d462 + 9ee3f56 commit 2cc559e
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 11 deletions.
8 changes: 7 additions & 1 deletion backend/ambassadors/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin

from .models import Ambassador, AmbassadorGoal, TrainingProgram
from .models import Ambassador, AmbassadorGoal, Content, TrainingProgram


@admin.register(TrainingProgram)
Expand All @@ -19,4 +19,10 @@ class AmbassadorAdmin(admin.ModelAdmin):
'city', 'status', 'reg_date')
list_filter = ('gender', 'country', 'city', 'status', 'reg_date')
search_fields = ('full_name', 'country', 'city', 'email', 'phone_number')
filter_horizontal = ('amb_goal',)


@admin.register(Content)
class ContentAdmin(admin.ModelAdmin):
list_display = ('id', 'full_name', 'telegram', 'link', 'guide', 'status',)
filter_horizontal = ('amb_goals',)
22 changes: 22 additions & 0 deletions backend/ambassadors/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,25 @@
('approved', 'Одобрена'),
('rejected', 'Не одобрена'),
]

MERCH_CHOICES = [
('hoody', 'Толстовка'),
('coffee', 'Кофе'),
('stickers', 'Стикеры'),
('plus', 'Плюс'),
('arzamas', 'Арзамас'),
('shopper', 'Шоппер'),
('backpack', 'Рюкзак'),
('cross_bag', 'Сумка кросс'),
('socks', 'Носки'),
('50%_discount', 'Скидка 50%'),
('alice_mini', 'Алиса мини'),
('alice_big', 'Алиса биг'),
('student_club_at_night', 'Клуб учащихся ночью'),
]

STATUS_SEND_CHOICES = [
('new', 'Новая заявка'),
('address_verified', 'Адрес проверен'),
('sent_to_logisticians', 'Отправлена логистам'),
]
60 changes: 60 additions & 0 deletions backend/ambassadors/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
CLOTHING_SIZES_CHOICES,
CONTENT_STATUS_CHOICES,
GENDER_CHOICES,
MERCH_CHOICES,
STATUS_CHOICES,
STATUS_SEND_CHOICES,
)


Expand Down Expand Up @@ -134,3 +136,61 @@ def __str__(self):

class Meta:
verbose_name = 'Контент'
verbose_name_plural = 'Контент'


class Merchandise(models.Model):
'''
Модель мерча
'''

name = models.CharField(
max_length=20,
choices=MERCH_CHOICES,
verbose_name='Название мерча'
)
price = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name='Стоимость мерча',
)

def __str__(self):
return self.name

class Meta:
verbose_name = 'Мерч'


class Sending_a_merch(models.Model):
'''
Заявка на отправку мерча
'''

name_merch = models.ForeignKey(
Merchandise,
on_delete=models.CASCADE,
verbose_name='Название мерча',
)
ambassador = models.ForeignKey(
Ambassador,
on_delete=models.CASCADE,
verbose_name='Амбассадор'
)
status_send = models.CharField(
max_length=20,
default='new',
choices=STATUS_SEND_CHOICES,
verbose_name='Статус отправки',
)
created_date = models.DateField(
auto_now_add=True,
verbose_name='Дата создания',
)
comment = models.TextField(
max_length=200,
verbose_name='Комментарий менеджера'
)

class Meta:
verbose_name = 'Заявка на отправку мерча'
66 changes: 66 additions & 0 deletions backend/api/v1/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from django_filters import FilterSet
from django_filters.filters import CharFilter
from rest_framework.exceptions import ValidationError

from ambassadors.choices import CONTENT_STATUS_CHOICES
from ambassadors.models import Content


class UniversalChoiceFilter(CharFilter):
"""
Универсальный класс для фильтрации по Choice полю.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def filter(self, qs, value):
"""
Метод фильтрации.
"""
model = self.parent.Meta.model
field = model._meta.get_field(self.field_name)
choices = dict(field.choices)

if value and value in choices.values():
reverse_choices = {v: k for k, v in choices.items()}
value = reverse_choices.get(value)
else:
raise ValidationError(
f"Недопустимое значение для поля: {value}")

return super().filter(qs, value)


class BaseChoiceFilter(FilterSet):
"""Базовый класс для фильтрации по полю status."""
status = UniversalChoiceFilter()

class Meta:
abstract = True


class ContentStatusFilter(BaseChoiceFilter):
"""Класс для фильтрации Контента по полю status."""

class Meta(BaseChoiceFilter.Meta):
model = Content
fields = ['status']
status_choices = CONTENT_STATUS_CHOICES

# class ContentFilter(filters.FilterSet):
# """Запасной вариант, если не использовать логику с базовым классом """
# status = CharFilter(method='filter_status')
#
# def filter_status(self, queryset, name, value):
# # Получение технического значения из человекочитаемого
# reverse_choices = {v: k for k, v in Content.CONTENT_STATUS_CHOICES}
# technical_value = reverse_choices.get(value)
# if technical_value is not None:
# return queryset.filter(**{name: technical_value})
# else:
# raise ValueError(f"Недопустимое значение для статуса: {value}")
#
# class Meta:
# model = Content
# fields = []
73 changes: 73 additions & 0 deletions backend/api/v1/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from rest_framework import serializers

from ambassadors.choices import CONTENT_STATUS_CHOICES
from ambassadors.models import (Ambassador, AmbassadorGoal, Content,
TrainingProgram)

from .utils import format_telegram_username

from ambassadors.models import (
Ambassador,
AmbassadorGoal,
Expand Down Expand Up @@ -30,11 +36,78 @@ class Meta:
fields = ('id', 'name')


class ChoiceField(serializers.ChoiceField):
"""
Поле для обработки выборочных данных,
позволяющее представление и ввод в человекочитаемом формате.
"""

def to_representation(self, obj):
"""
Преобразует значение поля в его человекочитаемый формат для вывода.
"""
if obj == '' and self.allow_blank:
return obj
return self._choices[obj]

def to_internal_value(self, data):
"""
Преобразует человекочитаемое значение
обратно во внутреннее представление.
"""
if data == '' and self.allow_blank:
return ''

for key, val in self._choices.items():
if val == data:
return key
self.fail('invalid_choice', input=data)


class ContentSerializer(serializers.ModelSerializer):
"""
Сериализатор для модели Контента.
"""

status = ChoiceField(choices=CONTENT_STATUS_CHOICES, required=False)

class Meta:
"""
Класс Meta указывает на модель и поля,
которые будут использоваться сериализатором.
"""
model = Content
fields = '__all__'

def create(self, validated_data):
"""
Создаёт новый экземпляр контента на основе проверенных данных.
"""
ambassador = Ambassador.objects.filter(
telegram=validated_data['telegram']
).first()
if ambassador:
validated_data['ambassador'] = ambassador
validated_data['full_name'] = ambassador.full_name
content = Content.objects.create(**validated_data)
return content

def to_internal_value(self, instance):
"""
Преобразует данные перед сохранением,
обрабатывает логическое поле 'guide'.
"""
guide = instance.get('guide')
if guide is not None:
instance['guide'] = bool(guide)
return super().to_internal_value(instance)

def validate_telegram(self, value):
"""
Проверяет и форматирует имя пользователя в Telegram перед сохранением.
"""
return format_telegram_username(value)


class YandexFormAmbassadorCreateSerializer(serializers.ModelSerializer):
"""
Expand Down
2 changes: 1 addition & 1 deletion backend/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@

urlpatterns = [
path('', include(router.urls)),
path('', include('djoser.urls')),
# path('', include('djoser.urls')),
path('auth/', include('djoser.urls.authtoken')),
]
9 changes: 9 additions & 0 deletions backend/api/v1/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
def format_telegram_username(input_str: str) -> str:
"""
Функция принимает строку и преобразует ее в формат '@username'.
"""
if input_str.startswith('https://t.me/'):
username = '@' + input_str.split('/')[-1]
else:
username = input_str if input_str.startswith('@') else f'@{input_str}'
return username
12 changes: 11 additions & 1 deletion backend/api/v1/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, viewsets
from rest_framework.pagination import LimitOffsetPagination

from ambassadors.models import Ambassador, Content

Expand All @@ -8,6 +10,7 @@
ContentSerializer,
YandexFormAmbassadorCreateSerializer,
)
from .filters import ContentStatusFilter


class AmbassadorViewSet(viewsets.ModelViewSet):
Expand All @@ -23,5 +26,12 @@ def get_serializer_class(self):


class ContentViewSet(viewsets.ModelViewSet):
"""Viewset для модели Контента
Позволяет фильтровать выборку по полям status и full_name.
"""
queryset = Content.objects.all()
serializer_class = ContentSerializer
pagination_class = LimitOffsetPagination
filterset_class = ContentStatusFilter
filter_backends = (DjangoFilterBackend, filters.SearchFilter)
search_fields = ('full_name',)
1 change: 1 addition & 0 deletions backend/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'django_filters',
'rest_framework.authtoken',
'djoser',
'ambassadors.apps.AmbassadorsConfig',
Expand Down
3 changes: 2 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ social-auth-app-django==5.4.0
social-auth-core==4.5.3
sqlparse==0.4.4
urllib3==2.2.1
daphne==4.1.0
daphne==4.1.0
django-filter-23.5
7 changes: 0 additions & 7 deletions backend/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,3 @@ class User(AbstractUser):
phone_number = models.CharField(
validators=[phone_regex], max_length=16, unique=True
)

AbstractUser.REQUIRED_FIELDS += (
'patronymic',
'phone_number',
'first_name',
'last_name',
)

0 comments on commit 2cc559e

Please sign in to comment.