diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de99b6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,157 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +.idea/ diff --git a/backend/api/__init__.py b/backend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/apps.py b/backend/api/apps.py new file mode 100644 index 0000000..d87006d --- /dev/null +++ b/backend/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = 'api' diff --git a/backend/api/pagination.py b/backend/api/pagination.py new file mode 100644 index 0000000..76b0f5d --- /dev/null +++ b/backend/api/pagination.py @@ -0,0 +1,5 @@ +from rest_framework.pagination import PageNumberPagination + + +class GamesAndFriendsPagination(PageNumberPagination): + page_size = 10 diff --git a/backend/api/permissions.py b/backend/api/permissions.py new file mode 100644 index 0000000..334ca0b --- /dev/null +++ b/backend/api/permissions.py @@ -0,0 +1,48 @@ +from rest_framework import permissions + + +class PlayerPermission(permissions.BasePermission): + + def has_permission(self, request, view): + return (request.user.is_authenticated + and not request.user.is_company) + + def has_object_permission(self, request, view, obj): + return (request.user.is_authenticated + and not request.user.is_company) + + +class CompanyPermission(permissions.BasePermission): + + def has_permission(self, request, view): + return (request.user.is_authenticated + and request.user.is_company) + + def has_object_permission(self, request, view, obj): + return (request.user.is_authenticated + and request.user.is_company) + + +class ReadOnlyPermission(permissions.BasePermission): + + def has_permission(self, request, view): + return request.method in permissions.SAFE_METHODS + + def has_object_permission(self, request, view, obj): + return request.method in permissions.SAFE_METHODS + + +class IsOwnerOrReadOnly(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + return True + + return obj.company == request.user + + +class IsPubOwnerOrReadOnly(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + return True + + return obj.pub.company == request.user diff --git a/backend/api/serializers.py b/backend/api/serializers.py new file mode 100644 index 0000000..90dda9b --- /dev/null +++ b/backend/api/serializers.py @@ -0,0 +1,319 @@ +from djoser.serializers import ( + UserSerializer, + UserCreatePasswordRetypeSerializer, + PasswordRetypeSerializer, + CurrentPasswordSerializer +) +from rest_framework import serializers +from rest_framework.generics import get_object_or_404 + +from users.models import CustomUser, FriendshipRequest +from pubs.models import Pub, Menu +from games.models import Game, GameUser, Stage, StageMenu + + +class CustomUserCreateSerializer(UserCreatePasswordRetypeSerializer): + + class Meta: + model = CustomUser + fields = ( + 'id', + 'username', + 'email', + 'password', + 'role', + 'bio', + 'photo', + 'registered_office', + 'phone_number' + ) + required_fields = ( + 'username', + 'email', + 'password' + ) + + +class CustomUserSerializer(UserSerializer): + is_friend = serializers.SerializerMethodField() + + class Meta: + model = CustomUser + fields = ( + 'id', + 'username', + 'role', + 'bio', + 'photo', + 'registered_office', + 'phone_number', + 'is_friend' + ) + + def get_is_friend(self, obj): + request_user = self.context['request'].user + + return (request_user.is_authenticated + and obj in request_user.friends.all()) + + +class CustomUserMeSerializer(CustomUserSerializer): + + class Meta: + model = CustomUser + fields = ( + 'id', + 'username', + 'email', + 'role', + 'bio', + 'photo', + 'registered_office', + 'phone_number', + 'is_friend' + ) + + +class CustomPasswordSerializer(PasswordRetypeSerializer): + current_password = serializers.CharField(required=True) + + +class SetEmailSerializer( + serializers.ModelSerializer, + CurrentPasswordSerializer +): + new_email = serializers.EmailField(required=True) + + class Meta: + model = CustomUser + fields = ('new_email', 'current_password') + + def validate(self, attrs): + user = self.context['request'].user or self.user + assert user is not None + + if attrs['new_email'] == user.email: + raise serializers.ValidationError({ + 'new_email': 'Введена та же почта, что и в аккаунте' + }) + return super().validate(attrs) + + +class FriendshipRequestSerializer(serializers.ModelSerializer): + from_user = serializers.CharField( + source='from_user.username', + read_only=True + ) + to_user = serializers.CharField( + source='to_user.username', + read_only=True + ) + + class Meta: + model = FriendshipRequest + fields = '__all__' + + def validate(self, data): + user = CustomUser.objects.get(id=self.context['request'].user.id) + friend = CustomUser.objects.get(id=self.context['friend_id']) + + if user == friend: + raise serializers.ValidationError( + 'Нельзя добавить в друзья самого себя!' + ) + + if friend.is_company: + raise serializers.ValidationError( + 'Нельзя добавить в друзья аккаунт комапнии!' + ) + + if FriendshipRequest.objects.filter( + from_user=user, + to_user=friend + ).exists(): + raise serializers.ValidationError('Заявка уже отправлена!') + + if friend in user.friends.all(): + raise serializers.ValidationError( + 'Пользователь уже есть у Вас в друзьях!' + ) + + return data + + +class PubSerializer(serializers.ModelSerializer): + company = serializers.SlugRelatedField( + slug_field='username', + read_only=True + ) + + class Meta: + model = Pub + fields = ( + 'id', + 'name', + 'pub_address', + 'company' + ) + + +class PubInStageSerializer(serializers.ModelSerializer): + + class Meta: + model = Pub + fields = ( + 'name', + 'pub_address' + ) + + +class MenuSerializer(serializers.ModelSerializer): + pub = serializers.IntegerField( + source='pub.id', + read_only=True + ) + + class Meta: + model = Menu + fields = ( + 'id', + 'alcohol_name', + 'alcohol_percent', + 'cost', + 'pub' + ) + + def validate_alcohol_name(self, alcohol_name): + user = CustomUser.objects.get(id=self.context['request'].user.id) + pub = Pub.objects.get(company=user) + if Menu.objects.filter(pub=pub, alcohol_name=alcohol_name).exists(): + raise serializers.ValidationError( + 'Такой алкоголь уже есть в меню.' + ) + return alcohol_name + + def validate_cost(self, cost): + if cost < 0: + raise serializers.ValidationError( + 'Цена не может быть меньше 0.' + ) + return cost + + def validate_alcohol_percent(self, alcohol_percent): + if alcohol_percent > 100: + raise serializers.ValidationError( + 'Процент содержания спирта не может быть больше 100%.' + ) + elif alcohol_percent < 0: + raise serializers.ValidationError( + 'Процент содержания спирта не может быть меньше 0%.' + ) + return alcohol_percent + + +class GameCreateSerializer(serializers.ModelSerializer): + players = serializers.ListField( + child=serializers.PrimaryKeyRelatedField( + queryset=CustomUser.objects.all() + ), + write_only=True + ) + + class Meta: + model = Game + fields = '__all__' + required_fields = ( + 'name', + 'difficulty_level', + 'budget_level', + 'players' + ) + + def create(self, validated_data): + players = validated_data.pop('players') + game = Game.objects.create(**validated_data) + + for player in players: + GameUser.objects.create( + game=game, + user=player + ) + + return game + + def update(self, instance, validated_data): + players = validated_data.pop('players') + instance.name = validated_data.get('name', instance.name) + instance.difficulty_level = validated_data.get( + 'difficulty_level', + instance.difficulty_level + ) + instance.budget_level = validated_data.get( + 'budget_level', + instance.budget_level + ) + instance.save() + + GameUser.objects.filter(game=instance).delete() + for player in players: + GameUser.objects.create( + user=get_object_or_404( + CustomUser, + id=player.id), + game=instance + ) + + return instance + + +class MenuInStageSerializer(serializers.ModelSerializer): + + class Meta: + model = Menu + fields = ( + 'id', + 'alcohol_name', + 'alcohol_percent', + 'cost' + ) + + +class StageSerializer(serializers.ModelSerializer): + pub = PubInStageSerializer(read_only=True) + drinks = serializers.SerializerMethodField() + + class Meta: + model = Stage + fields = ( + 'id', + 'pub', + 'drinks' + ) + + def get_drinks(self, obj): + drinks_ids = StageMenu.objects.filter( + stage=obj + ).values_list('drink_id') + drinks = Menu.objects.filter(id__in=drinks_ids) + + return MenuInStageSerializer(drinks, many=True).data + + +class FinishGameSerializer(serializers.ModelSerializer): + class Meta: + model = GameUser + fields = ('user', 'player_status') + + +class GameSerializer(serializers.ModelSerializer): + stats = serializers.SerializerMethodField() + + class Meta: + model = Game + exclude = ('players',) + + def get_stats(self, obj): + return FinishGameSerializer( + GameUser.objects.filter(game=obj), + many=True + ).data diff --git a/backend/api/urls.py b/backend/api/urls.py new file mode 100644 index 0000000..4344b3d --- /dev/null +++ b/backend/api/urls.py @@ -0,0 +1,69 @@ +from django.urls import path, include, re_path +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework import permissions +from rest_framework.routers import DefaultRouter + +from api.views import ( + CustomUserViewSet, + FriendViewSet, + FriendshipRequestCreateDestroyViewSet, + FriendshipRequestViewSet, + PubViewSet, + MenuViewSet, + GameViewSet, + StartGameAPIView, + FinishGameAPIView +) + +router_v1 = DefaultRouter() + +router_v1.register('users/friends', FriendViewSet, basename='friends') +router_v1.register( + r'users/(?P\d+)/friend', + FriendshipRequestCreateDestroyViewSet, + basename='send_delete_friendship-request' +) +router_v1.register( + 'users/friendship-requests', + FriendshipRequestViewSet, + basename='friendship_requests' +) +router_v1.register('users', CustomUserViewSet, basename='users') +router_v1.register('pubs', PubViewSet, basename='pubs') +router_v1.register( + r'pubs/(?P\d+)/menu', MenuViewSet, basename='menu' +) +router_v1.register('games', GameViewSet, basename='games') + +schema_view = get_schema_view( + openapi.Info( + title="PubGolf API", + default_version='v1.0', + description="Документация для PubGolf API", + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + +urlpatterns = [ + path('v1/auth/', include('djoser.urls.authtoken')), + re_path( + r'v1/games/(?P\d+)/start', + StartGameAPIView.as_view(), + name='start_game' + ), + re_path( + r'v1/games/(?P\d+)/finish', + FinishGameAPIView.as_view(), + name='finish_game' + ), + path('v1/', include(router_v1.urls)), + path('v1/', include('djoser.urls.base')), + path( + 'v1/redoc/', + schema_view.with_ui('redoc', cache_timeout=0), + name='schema-redoc' + ), +] diff --git a/backend/api/views.py b/backend/api/views.py new file mode 100644 index 0000000..35273bf --- /dev/null +++ b/backend/api/views.py @@ -0,0 +1,355 @@ +import datetime as dt +from collections import defaultdict + +from django.shortcuts import get_object_or_404 +from djoser.views import UserViewSet +from rest_framework import ( + viewsets, + mixins, + status, + permissions, + serializers, + generics +) +from rest_framework.decorators import action +from rest_framework.permissions import SAFE_METHODS +from rest_framework.response import Response + +from api.pagination import GamesAndFriendsPagination +from api.permissions import ( + PlayerPermission, + IsOwnerOrReadOnly, + IsPubOwnerOrReadOnly +) +from api.serializers import ( + FriendshipRequestSerializer, + CustomUserCreateSerializer, + CustomPasswordSerializer, + CustomUserSerializer, + CustomUserMeSerializer, + SetEmailSerializer, + PubSerializer, + MenuSerializer, + GameSerializer, + GameCreateSerializer, + StageSerializer, + FinishGameSerializer, +) +from users.models import CustomUser, FriendshipRequest +from pubs.models import Pub, Menu +from games.models import Game, Stage, StageMenu, GameUser + + +class CustomUserViewSet(UserViewSet): + + def get_serializer_class(self): + if self.action == 'create': + return CustomUserCreateSerializer + elif self.action == 'set_password': + return CustomPasswordSerializer + elif self.action == 'set_username': + return serializers.SetUsernameSerializer + elif self.action == 'set_email': + return SetEmailSerializer + elif self.action == 'me': + return CustomUserMeSerializer + return CustomUserSerializer + + def get_permissions(self): + if self.action == 'retrieve': + self.permission_classes = [permissions.IsAuthenticated] + elif self.action == 'set_email': + self.permission_classes = [permissions.CurrentUserOrAdmin] + return super().get_permissions() + + def get_queryset(self): + return CustomUser.objects.all() + + @action(['post'], detail=False, url_path='set_email') + def set_email(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = self.request.user + new_email = serializer.data['new_email'] + + setattr(user, 'email', new_email) + user.save() + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class FriendshipRequestBaseViewSet(viewsets.GenericViewSet): + serializer_class = FriendshipRequestSerializer + permission_classes = (PlayerPermission,) + + +class FriendshipRequestViewSet( + mixins.ListModelMixin, + FriendshipRequestBaseViewSet +): + def get_queryset(self): + request = self.request + + if ( + 'from-me' in request.query_params + and request.query_params['from-me'] + ): + return request.user.from_me_requests.all() + + return request.user.to_me_requests + + @action(methods=['post'], detail=True, url_path='accept') + def accept_request(self, request, pk): + friend_request = FriendshipRequest.objects.get(id=pk) + + if friend_request.to_user == request.user: + friend_request.to_user.friends.add(friend_request.from_user) + friend_request.from_user.friends.add(friend_request.to_user) + friend_request.delete() + return Response( + {'status': 'Пользователь добавлен в друзья'}, + status=status.HTTP_201_CREATED + ) + + return Response(status=status.HTTP_404_NOT_FOUND) + + +class FriendshipRequestCreateDestroyViewSet( + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + FriendshipRequestBaseViewSet +): + queryset = FriendshipRequest.objects.all() + + def get_serializer_context(self): + context = super().get_serializer_context() + context['friend_id'] = self.kwargs.get('user_id') + + return context + + def create(self, request, *args, **kwargs): + user_id = self.kwargs.get('user_id') + + to_friend_request = FriendshipRequest.objects.filter( + from_user_id=user_id, + to_user=request.user + ) + + if to_friend_request.exists(): + to_user = get_object_or_404(CustomUser, id=user_id) + request.user.friends.add(to_user) + to_user.friends.add(request.user) + to_friend_request.delete() + return Response( + {'status': 'Пользователь добавлен в друзья'}, + status=status.HTTP_201_CREATED + ) + + return super().create(request) + + def perform_create(self, serializer): + request_user = self.request.user + + serializer.save( + from_user=request_user, + to_user=get_object_or_404( + CustomUser, + id=self.kwargs.get('user_id') + ) + ) + + @action(methods=['delete'], detail=True) + def delete(self, request, user_id): + get_object_or_404( + FriendshipRequest, + from_user=request.user, + to_user=user_id + ).delete() + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class FriendViewSet( + mixins.ListModelMixin, + viewsets.GenericViewSet +): + serializer_class = CustomUserSerializer + + def get_queryset(self): + return self.request.user.friends.all() + + @action(methods=['delete'], detail=True) + def delete(self, request, pk): + friend_to_delete = get_object_or_404(CustomUser, id=pk) + request.user.friends.remove(friend_to_delete) + friend_to_delete.friends.remove(request.user) + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class PubViewSet(viewsets.ModelViewSet): + + queryset = Pub.objects.all() + serializer_class = PubSerializer + permission_classes = (IsOwnerOrReadOnly,) + + def perform_create(self, serializer): + serializer.save(company=self.request.user) + + +class MenuViewSet( + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet +): + + serializer_class = MenuSerializer + permission_classes = (IsPubOwnerOrReadOnly, ) + + def get_queryset(self): + return Menu.objects.filter(pub=self.kwargs.get('pub_id')) + + def retrieve(self, request, pk=None, pub_id=None): + serializer = self.get_serializer(self.get_object()) + return Response(serializer.data) + + def perform_create(self, serializer): + serializer.save(pub=Pub.objects.get(company=self.request.user)) + + +class GameViewSet(viewsets.ModelViewSet): + permission_classes = (PlayerPermission,) + + def get_serializer_class(self): + if self.request.method in SAFE_METHODS: + return GameSerializer + return GameCreateSerializer + + def get_queryset(self): + return Game.objects.filter(players=self.request.user) + + @action(methods=['delete'], detail=True) + def delete(self, request, pk): + get_object_or_404( + Game, + id=pk + ).delete() + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class StartGameAPIView(generics.RetrieveAPIView): + permission_classes = (PlayerPermission,) + + def get(self, request, game_id): + game = get_object_or_404( + Game, + id=int(game_id) + ) + + if game.status != 'created': + return Response( + data={'detail': 'Игра уже начата или завершена.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + difficulty_level = game.difficulty_level + budget_level = game.budget_level + + all_drinks = Menu.objects.all() + all_drinks_count = all_drinks.count() + interval = all_drinks_count / 3 + + ordered_by_alcohol = all_drinks.order_by('alcohol_percent') + if difficulty_level == 'underbeerman': + drinks_ids = ordered_by_alcohol[ + :interval + ].values_list('id', flat=True) + elif difficulty_level == 'fan': + drinks_ids = ordered_by_alcohol[ + interval:2*interval + ].values_list('id', flat=True) + elif difficulty_level == 'freelanholic': + drinks_ids = ordered_by_alcohol[ + 2*interval: + ].values_list('id', flat=True) + + ordered_by_cost = Menu.objects.filter( + id__in=drinks_ids + ).order_by('cost') + + interval = interval / 3 + if budget_level == 'homeless': + drinks = ordered_by_cost[:interval] + elif budget_level == 'fan': + drinks = ordered_by_cost[interval:2 * interval] + elif budget_level == 'major': + drinks = ordered_by_cost[2 * interval:] + + pubs_drinks = defaultdict(list) + for drink in drinks: + pubs_drinks[drink.pub_id].append(drink.id) + + stages = [] + for pub, pub_drinks in pubs_drinks.items(): + stages.append( + Stage.objects.create( + game=game, + pub=Pub.objects.get(id=int(pub)) + ) + ) + + for stage in stages: + for drink in drinks: + if stage.pub_id == drink.pub_id: + StageMenu.objects.create( + stage=stage, + drink=drink + ) + + serialized_stages = StageSerializer(stages, many=True).data + + game.start_time = dt.datetime.now() + game.status = 'started' + game.save() + + return Response( + data={'id': game.id, 'stages': serialized_stages}, + status=status.HTTP_200_OK + ) + + +class FinishGameAPIView(generics.CreateAPIView): + permission_classes = (PlayerPermission,) + serializer_class = FinishGameSerializer + + def post(self, request, game_id): + games_users = GameUser.objects.filter(game_id=game_id) + + serializer = self.get_serializer(data=request.data, many=True) + serializer.is_valid(raise_exception=True) + + serialized_stats = serializer.data + + for stat in serialized_stats: + game_user = games_users.get( + user_id=stat.get('user') + ) + game_user.player_status = stat.get('player_status') + game_user.save() + + game = get_object_or_404( + Game, + id=int(game_id) + ) + game.finish_time = dt.datetime.now() + game.status = 'finished' + game.save() + + return Response( + data=serialized_stats, + status=status.HTTP_200_OK + ) diff --git a/backend/games/__init__.py b/backend/games/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/games/admin.py b/backend/games/admin.py new file mode 100644 index 0000000..3197a32 --- /dev/null +++ b/backend/games/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin + +from .models import Game, GameUser + + +@admin.register(Game) +class GameAdmin(admin.ModelAdmin): + list_display = ('name', 'status') + search_fields = ('name',) + + +@admin.register(GameUser) +class GameUserAdmin(admin.ModelAdmin): + list_display = ('user', 'game') + search_fields = ('game',) diff --git a/backend/games/apps.py b/backend/games/apps.py new file mode 100644 index 0000000..1a3efec --- /dev/null +++ b/backend/games/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GamesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'games' diff --git a/backend/games/migrations/0001_initial.py b/backend/games/migrations/0001_initial.py new file mode 100644 index 0000000..25429cb --- /dev/null +++ b/backend/games/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 4.1.7 on 2023-05-09 17:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Game', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=150, unique=True, verbose_name='Название комнаты')), + ('difficulty_level', models.CharField(choices=[('underbeerman', 'Подпивасник'), ('', ''), ('', '')], max_length=50, verbose_name='Уровень сложности')), + ('budget_level', models.CharField(choices=[('', ''), ('', ''), ('', '')], max_length=50, verbose_name='Уровень бюджета')), + ('status', models.CharField(choices=[('created', 'Создана комната'), ('started', 'Игра стартовала'), ('finished', 'Игра завершилась')], max_length=50, verbose_name='Статус игры')), + ('start_time', models.DateTimeField(blank=True, null=True, verbose_name='Время старта игры')), + ('finish_time', models.DateTimeField(blank=True, null=True, verbose_name='Время завершения игры')), + ], + options={ + 'verbose_name': 'Игра', + 'verbose_name_plural': 'Игры', + }, + ), + migrations.CreateModel( + name='Stage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.game')), + ], + ), + migrations.CreateModel( + name='GameUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.game')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/games/migrations/0002_invitation.py b/backend/games/migrations/0002_invitation.py new file mode 100644 index 0000000..79acaa0 --- /dev/null +++ b/backend/games/migrations/0002_invitation.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.7 on 2023-05-15 15:26 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('games', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Invitation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations_received', to=settings.AUTH_USER_MODEL, verbose_name='Получатель')), + ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations_sent', to=settings.AUTH_USER_MODEL, verbose_name='Отправитель')), + ], + options={ + 'verbose_name': 'Приглашение в комнату', + 'verbose_name_plural': 'Приглашения в комнату', + }, + ), + ] diff --git a/backend/games/migrations/0003_alter_game_budget_level_alter_gameuser_game_and_more.py b/backend/games/migrations/0003_alter_game_budget_level_alter_gameuser_game_and_more.py new file mode 100644 index 0000000..b2915a1 --- /dev/null +++ b/backend/games/migrations/0003_alter_game_budget_level_alter_gameuser_game_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.7 on 2023-05-16 09:05 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('games', '0002_invitation'), + ] + + operations = [ + migrations.AlterField( + model_name='game', + name='budget_level', + field=models.CharField(choices=[('homeless', 'Бомж'), ('fan', 'Любитель'), ('major', 'Мажор')], max_length=50, verbose_name='Уровень бюджета'), + ), + migrations.AlterField( + model_name='gameuser', + name='game', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.game', verbose_name='Комната'), + ), + migrations.AlterField( + model_name='gameuser', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Игрок'), + ), + ] diff --git a/backend/games/migrations/0004_alter_game_difficulty_level.py b/backend/games/migrations/0004_alter_game_difficulty_level.py new file mode 100644 index 0000000..df996cf --- /dev/null +++ b/backend/games/migrations/0004_alter_game_difficulty_level.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-05-16 09:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0003_alter_game_budget_level_alter_gameuser_game_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='game', + name='difficulty_level', + field=models.CharField(choices=[('underbeerman', 'Подпивасник'), ('fan', 'Любитель'), ('freelanholic', 'Фриланголик')], max_length=50, verbose_name='Уровень сложности'), + ), + ] diff --git a/backend/games/migrations/0005_invitation_game.py b/backend/games/migrations/0005_invitation_game.py new file mode 100644 index 0000000..9674eb5 --- /dev/null +++ b/backend/games/migrations/0005_invitation_game.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.7 on 2023-05-17 16:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0004_alter_game_difficulty_level'), + ] + + operations = [ + migrations.AddField( + model_name='invitation', + name='game', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='games.game', verbose_name='Комната'), + preserve_default=False, + ), + ] diff --git a/backend/games/migrations/0006_gameuser_player_status_delete_invitation.py b/backend/games/migrations/0006_gameuser_player_status_delete_invitation.py new file mode 100644 index 0000000..6fb25ac --- /dev/null +++ b/backend/games/migrations/0006_gameuser_player_status_delete_invitation.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.7 on 2023-05-22 12:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0005_invitation_game'), + ] + + operations = [ + migrations.AddField( + model_name='gameuser', + name='player_status', + field=models.CharField(choices=[('playing', 'Играет'), ('won', 'Выиграл'), ('lost', 'Проиграл')], default='playing', max_length=50, verbose_name='Статус игрока'), + ), + migrations.DeleteModel( + name='Invitation', + ), + ] diff --git a/backend/games/migrations/0007_alter_game_status.py b/backend/games/migrations/0007_alter_game_status.py new file mode 100644 index 0000000..c0e5291 --- /dev/null +++ b/backend/games/migrations/0007_alter_game_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-05-22 13:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0006_gameuser_player_status_delete_invitation'), + ] + + operations = [ + migrations.AlterField( + model_name='game', + name='status', + field=models.CharField(choices=[('created', 'Создана комната'), ('started', 'Игра стартовала'), ('finished', 'Игра завершилась')], default='created', max_length=50, verbose_name='Статус игры'), + ), + ] diff --git a/backend/games/migrations/0008_game_players.py b/backend/games/migrations/0008_game_players.py new file mode 100644 index 0000000..61314a2 --- /dev/null +++ b/backend/games/migrations/0008_game_players.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.7 on 2023-05-22 13:41 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('games', '0007_alter_game_status'), + ] + + operations = [ + migrations.AddField( + model_name='game', + name='players', + field=models.ManyToManyField(through='games.GameUser', to=settings.AUTH_USER_MODEL, verbose_name='Игроки'), + ), + ] diff --git a/backend/games/migrations/0009_alter_gameuser_unique_together.py b/backend/games/migrations/0009_alter_gameuser_unique_together.py new file mode 100644 index 0000000..53fe306 --- /dev/null +++ b/backend/games/migrations/0009_alter_gameuser_unique_together.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.7 on 2023-05-22 14:13 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('games', '0008_game_players'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='gameuser', + unique_together={('user', 'game')}, + ), + ] diff --git a/backend/games/migrations/0010_stage_pub_alter_stage_unique_together.py b/backend/games/migrations/0010_stage_pub_alter_stage_unique_together.py new file mode 100644 index 0000000..0154962 --- /dev/null +++ b/backend/games/migrations/0010_stage_pub_alter_stage_unique_together.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.7 on 2023-05-25 10:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pubs', '0001_initial'), + ('games', '0009_alter_gameuser_unique_together'), + ] + + operations = [ + migrations.AddField( + model_name='stage', + name='pub', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='pubs.pub'), + preserve_default=False, + ), + migrations.AlterUniqueTogether( + name='stage', + unique_together={('game', 'pub')}, + ), + ] diff --git a/backend/games/migrations/0011_stagemenu.py b/backend/games/migrations/0011_stagemenu.py new file mode 100644 index 0000000..220e4e3 --- /dev/null +++ b/backend/games/migrations/0011_stagemenu.py @@ -0,0 +1,26 @@ +# Generated by Django 4.1.7 on 2023-06-01 14:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pubs', '0001_initial'), + ('games', '0010_stage_pub_alter_stage_unique_together'), + ] + + operations = [ + migrations.CreateModel( + name='StageMenu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('drink', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pubs.menu')), + ('stage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.stage')), + ], + options={ + 'unique_together': {('stage', 'drink')}, + }, + ), + ] diff --git a/backend/games/migrations/__init__.py b/backend/games/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/games/models.py b/backend/games/models.py new file mode 100644 index 0000000..170cd79 --- /dev/null +++ b/backend/games/models.py @@ -0,0 +1,132 @@ +from django.db import models + +from pubs.models import Pub, Menu +from users.models import CustomUser + +DIFFICULTY_LEVELS = ( + ('underbeerman', 'Подпивасник'), + ('fan', 'Любитель'), + ('freelanholic', 'Фриланголик') +) +BUDGET_LEVELS = ( + ('homeless', 'Бомж'), + ('fan', 'Любитель'), + ('major', 'Мажор') +) +GAME_STATUSES = ( + ('created', 'Создана комната'), + ('started', 'Игра стартовала'), + ('finished', 'Игра завершилась') +) +PLAYER_STATUSES = ( + ('playing', 'Играет'), + ('won', 'Выиграл'), + ('lost', 'Проиграл') +) + + +class Game(models.Model): + """Модель комнаты.""" + + name = models.CharField( + unique=True, + max_length=150, + verbose_name='Название комнаты' + ) + difficulty_level = models.CharField( + max_length=50, + choices=DIFFICULTY_LEVELS, + verbose_name='Уровень сложности' + ) + budget_level = models.CharField( + max_length=50, + choices=BUDGET_LEVELS, + verbose_name='Уровень бюджета' + ) + status = models.CharField( + max_length=50, + choices=GAME_STATUSES, + default='created', + verbose_name='Статус игры' + ) + start_time = models.DateTimeField( + blank=True, + null=True, + verbose_name='Время старта игры' + ) + finish_time = models.DateTimeField( + blank=True, + null=True, + verbose_name='Время завершения игры' + ) + players = models.ManyToManyField( + CustomUser, + through='GameUser', + verbose_name='Игроки', + ) + + class Meta: + verbose_name = 'Игра' + verbose_name_plural = 'Игры' + + def __str__(self): + return self.name + + +class GameUser(models.Model): + user = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + verbose_name='Игрок' + ) + game = models.ForeignKey( + Game, + on_delete=models.CASCADE, + verbose_name='Комната' + ) + player_status = models.CharField( + max_length=50, + choices=PLAYER_STATUSES, + default='playing', + verbose_name='Статус игрока' + ) + + class Meta: + unique_together = ('user', 'game') + + def __str__(self): + return f'{self.user} --- {self.game}: {self.player_status}' + + +class Stage(models.Model): + game = models.ForeignKey( + Game, + on_delete=models.CASCADE + ) + pub = models.ForeignKey( + Pub, + on_delete=models.CASCADE + ) + + class Meta: + unique_together = ('game', 'pub') + + def __str__(self): + return f'{self.game} --- {self.pub}' + + +class StageMenu(models.Model): + stage = models.ForeignKey( + Stage, + on_delete=models.CASCADE + ) + drink = models.ForeignKey( + Menu, + on_delete=models.CASCADE + ) + + class Meta: + unique_together = ('stage', 'drink') + + def __str__(self): + return f'{self.stage} --- {self.drink}' diff --git a/backend/manage.py b/backend/manage.py new file mode 100644 index 0000000..83157b3 --- /dev/null +++ b/backend/manage.py @@ -0,0 +1,20 @@ +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pub_golf.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/backend/pub_golf/__init__.py b/backend/pub_golf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/pub_golf/asgi.py b/backend/pub_golf/asgi.py new file mode 100644 index 0000000..8667204 --- /dev/null +++ b/backend/pub_golf/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pub_golf.settings') + +application = get_asgi_application() diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py new file mode 100644 index 0000000..798c089 --- /dev/null +++ b/backend/pub_golf/settings.py @@ -0,0 +1,141 @@ +import os +from pathlib import Path + +from django.core.management.utils import get_random_secret_key +from dotenv import load_dotenv + +load_dotenv() + +BASE_DIR = Path(__file__).resolve().parent.parent + + +SECRET_KEY = os.getenv('SECRET_KEY', default=get_random_secret_key()) + +DEBUG = True + +ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', default='*').split() + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'drf_yasg', + 'corsheaders', + 'rest_framework', + 'rest_framework.authtoken', + 'djoser', + 'users.apps.UsersConfig', + 'api.apps.ApiConfig', + 'games.apps.GamesConfig', + 'pubs.apps.PubsConfig', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'corsheaders.middleware.CorsMiddleware', +] + +ROOT_URLCONF = 'pub_golf.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'pub_golf.wsgi.application' + + +# Database + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +AUTH_USER_MODEL = "users.CustomUser" + +# Password validation + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization + +LANGUAGE_CODE = 'ru-ru' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) + +STATIC_URL = 'static/' + +# Default primary key field type + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# DRF + +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.TokenAuthentication', + ], +} + +# Djoser + +DJOSER = { + 'HIDE_USERS': False, + 'SET_USERNAME_RETYPE': True, + 'PERMISSIONS': { + 'user_list': ['api.permissions.ReadOnlyPermission'], + }, +} + +CORS_ALLOW_ALL_ORIGINS = True diff --git a/backend/pub_golf/urls.py b/backend/pub_golf/urls.py new file mode 100644 index 0000000..3c9938d --- /dev/null +++ b/backend/pub_golf/urls.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/', include('api.urls')) +] diff --git a/backend/pub_golf/wsgi.py b/backend/pub_golf/wsgi.py new file mode 100644 index 0000000..d38e94a --- /dev/null +++ b/backend/pub_golf/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pub_golf.settings') + +application = get_wsgi_application() diff --git a/backend/pubs/__init__.py b/backend/pubs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/pubs/admin.py b/backend/pubs/admin.py new file mode 100644 index 0000000..5a5177a --- /dev/null +++ b/backend/pubs/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin + +from .models import Pub, Menu + + +@admin.register(Pub) +class PubAdmin(admin.ModelAdmin): + list_display = ('name', 'pub_address', 'company') + search_fields = ('pub_address',) + list_filter = ('company__username',) + + +@admin.register(Menu) +class MenuAdmin(admin.ModelAdmin): + list_display = ('pk', 'alcohol_name', 'alcohol_percent', 'cost', 'pub') + search_fields = ('alcohol_name',) + list_filter = ('pub__name', 'alcohol_percent') diff --git a/backend/pubs/apps.py b/backend/pubs/apps.py new file mode 100644 index 0000000..fbd0a56 --- /dev/null +++ b/backend/pubs/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PubsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'pubs' diff --git a/backend/pubs/migrations/0001_initial.py b/backend/pubs/migrations/0001_initial.py new file mode 100644 index 0000000..af9e2cc --- /dev/null +++ b/backend/pubs/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 4.1.7 on 2023-05-09 17:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Pub', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Название')), + ('pub_address', models.CharField(max_length=255, verbose_name='Адрес')), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pubs', to=settings.AUTH_USER_MODEL, verbose_name='Компания')), + ], + options={ + 'verbose_name': 'Паб', + 'verbose_name_plural': 'Пабы', + }, + ), + migrations.CreateModel( + name='Menu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('alcohol_name', models.CharField(max_length=50, verbose_name='Название')), + ('alcohol_percent', models.PositiveIntegerField(verbose_name='Процент спирта')), + ('cost', models.PositiveIntegerField(verbose_name='Цена')), + ('pub', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='menu', to='pubs.pub', verbose_name='Меню')), + ], + options={ + 'verbose_name': 'Меню', + 'verbose_name_plural': 'Меню', + }, + ), + ] diff --git a/backend/pubs/migrations/__init__.py b/backend/pubs/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/pubs/models.py b/backend/pubs/models.py new file mode 100644 index 0000000..8102c58 --- /dev/null +++ b/backend/pubs/models.py @@ -0,0 +1,57 @@ +from django.db import models + +from users.models import CustomUser + + +class Pub(models.Model): + """Модель паба.""" + + name = models.CharField( + max_length=255, + verbose_name='Название' + ) + pub_address = models.CharField( + max_length=255, + verbose_name='Адрес' + ) + company = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + verbose_name='Компания', + related_name='pubs' + ) + + class Meta: + verbose_name = 'Паб' + verbose_name_plural = 'Пабы' + + def __str__(self): + return f'{self.company.username} --- {self.name}' + + +class Menu(models.Model): + """Модель меню.""" + + alcohol_name = models.CharField( + max_length=50, + verbose_name='Название', + ) + alcohol_percent = models.PositiveIntegerField( + verbose_name='Процент спирта', + ) + cost = models.PositiveIntegerField( + verbose_name='Цена', + ) + pub = models.ForeignKey( + Pub, + on_delete=models.CASCADE, + related_name='menu', + verbose_name='Меню' + ) + + class Meta: + verbose_name = 'Меню' + verbose_name_plural = 'Меню' + + def __str__(self): + return self.alcohol_name diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..aed25d2 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,7 @@ +Django==4.1.7 +djangorestframework==3.14.0 +Pillow==9.4.0 +djoser==2.1.0 +python-dotenv==1.0.0 +drf-yasg==1.21.5 +django-cors-headers==4.0.0 diff --git a/backend/users/__init__.py b/backend/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/users/admin.py b/backend/users/admin.py new file mode 100644 index 0000000..8dafeb2 --- /dev/null +++ b/backend/users/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from .models import CustomUser + + +@admin.register(CustomUser) +class UserAdmin(admin.ModelAdmin): + list_display = ('username', 'email', 'role') + search_fields = ('username', 'email', 'role', 'phone_number') + list_filter = ('role',) diff --git a/backend/users/apps.py b/backend/users/apps.py new file mode 100644 index 0000000..72b1401 --- /dev/null +++ b/backend/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'users' diff --git a/backend/users/migrations/0001_initial.py b/backend/users/migrations/0001_initial.py new file mode 100644 index 0000000..3da2756 --- /dev/null +++ b/backend/users/migrations/0001_initial.py @@ -0,0 +1,64 @@ +# Generated by Django 4.1.7 on 2023-05-15 15:30 + +from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.validators +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='CustomUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='Почта')), + ('role', models.CharField(blank=True, choices=[('player', 'Аутентифицированный пользователь'), ('company', 'Компания')], default='player', max_length=30, verbose_name='Роль')), + ('bio', models.TextField(blank=True, null=True, verbose_name='Доп. информация')), + ('registered_office', models.CharField(blank=True, max_length=256, null=True, verbose_name='Юр. адрес')), + ('phone_number', models.CharField(blank=True, max_length=12, null=True, unique=True, validators=[django.core.validators.RegexValidator(regex='^\\+7[0-9]{10}$')], verbose_name='Телефонный номер')), + ('photo', models.ImageField(blank=True, null=True, upload_to='', verbose_name='Фото')), + ('friends', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'Пользователь', + 'verbose_name_plural': 'Пользователи', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='FriendshipRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='from_me_requests', to=settings.AUTH_USER_MODEL, verbose_name='Отправитель')), + ('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='to_me_requests', to=settings.AUTH_USER_MODEL, verbose_name='Получатель')), + ], + options={ + 'verbose_name': 'Заявка в друзья', + 'verbose_name_plural': 'Заявки в друзья', + }, + ), + ] diff --git a/backend/users/migrations/0002_alter_friendshiprequest_unique_together.py b/backend/users/migrations/0002_alter_friendshiprequest_unique_together.py new file mode 100644 index 0000000..76a8437 --- /dev/null +++ b/backend/users/migrations/0002_alter_friendshiprequest_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.7 on 2023-05-22 14:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='friendshiprequest', + unique_together={('from_user', 'to_user')}, + ), + ] diff --git a/backend/users/migrations/__init__.py b/backend/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/users/models.py b/backend/users/models.py new file mode 100644 index 0000000..1e31b0c --- /dev/null +++ b/backend/users/models.py @@ -0,0 +1,82 @@ +from django.contrib.auth.models import AbstractUser +from django.core.validators import RegexValidator +from django.db import models + +ROLE_CHOICES = ( + ('player', 'Аутентифицированный пользователь'), + ('company', 'Компания') +) + + +class CustomUser(AbstractUser): + email = models.EmailField( + unique=True, + max_length=254, + verbose_name='Почта', + ) + role = models.CharField( + max_length=30, + choices=ROLE_CHOICES, + blank=True, + default='player', + verbose_name='Роль', + ) + bio = models.TextField( + blank=True, + null=True, + verbose_name='Доп. информация', + ) + registered_office = models.CharField( + max_length=256, + blank=True, + null=True, + verbose_name='Юр. адрес' + ) + phone_number = models.CharField( + max_length=12, + validators=[RegexValidator(regex=r'^\+7[0-9]{10}$')], + unique=True, + blank=True, + null=True, + verbose_name='Телефонный номер' + ) + photo = models.ImageField( + blank=True, + null=True, + verbose_name='Фото' + ) + friends = models.ManyToManyField( + 'CustomUser', + blank=True + ) + + class Meta: + verbose_name = 'Пользователь' + verbose_name_plural = 'Пользователи' + + @property + def is_company(self): + return self.role == 'company' + + +class FriendshipRequest(models.Model): + from_user = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + related_name='from_me_requests', + verbose_name='Отправитель' + ) + to_user = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + related_name='to_me_requests', + verbose_name='Получатель' + ) + + class Meta: + unique_together = ('from_user', 'to_user') + verbose_name = 'Заявка в друзья' + verbose_name_plural = 'Заявки в друзья' + + def __str__(self): + return f'{self.from_user} --- {self.to_user}'