From e6025d5a0958bf2c9f24ee41054eaa509f08d49d Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Wed, 22 Feb 2023 19:45:15 +0300 Subject: [PATCH 01/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 backend/requirements.txt diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..a82e1bc --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,6 @@ +alembic==1.9.4 +fastapi==0.92.0 +pydantic==1.10.5 +python-dotenv==0.21.1 +SQLAlchemy==2.0.4 +uvicorn==0.20.0 From ca1673e4494b1d5ebe4d0900085bc47c8ba84854 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Wed, 22 Feb 2023 19:46:56 +0300 Subject: [PATCH 02/81] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2=D0=B0=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/__init__.py | 0 backend/app/database.py | 13 +++++++++++++ backend/app/main.py | 5 +++++ 3 files changed, 18 insertions(+) create mode 100644 backend/app/__init__.py create mode 100644 backend/app/database.py create mode 100644 backend/app/main.py diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..4e00674 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,13 @@ +from sqlalchemy import create_engine, MetaData +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///../pub_golf.db" +# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..296b314 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,5 @@ +from fastapi import FastAPI + +from .database import Base + +app = FastAPI() From a4120bdde5de808db220adafafb54f023ac5b5ff Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Wed, 22 Feb 2023 19:47:26 +0300 Subject: [PATCH 03/81] =?UTF-8?q?=D0=9D=D0=B0=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BD=20alembic=20=D0=B4=D0=BB=D1=8F=20=D0=BC=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/alembic.ini | 105 +++++++++++++++++++++++++++++++++ backend/alembic/env.py | 85 ++++++++++++++++++++++++++ backend/alembic/script.py.mako | 24 ++++++++ 3 files changed, 214 insertions(+) create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..081e43d --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,105 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = sqlite:///./pub_golf.db + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..f21fa8d --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,85 @@ +import os +import sys + +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(BASE_DIR) + +from app.users import models +from app.main import Base + +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..55df286 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} From 9969675849b0dfd94ba64e75036f9be58a43b00b Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Wed, 22 Feb 2023 19:54:17 +0300 Subject: [PATCH 04/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..341257c --- /dev/null +++ b/.gitignore @@ -0,0 +1,163 @@ +# 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/ + +# Alembic +versions/ + +# db +pub_golf.db \ No newline at end of file From ecf719b98c120f6863e7f63c10dd23740bd8ea65 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Wed, 22 Feb 2023 20:13:37 +0300 Subject: [PATCH 05/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/users/__init__.py | 0 backend/app/users/models.py | 59 +++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 backend/app/users/__init__.py create mode 100644 backend/app/users/models.py diff --git a/backend/app/users/__init__.py b/backend/app/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/users/models.py b/backend/app/users/models.py new file mode 100644 index 0000000..8f33ecc --- /dev/null +++ b/backend/app/users/models.py @@ -0,0 +1,59 @@ +from sqlalchemy import ( + Column, + Integer, + String, + Boolean, + Text, + ForeignKey, + Table +) +from sqlalchemy.orm import relationship + +from ..database import Base + + +class Company(Base): + __tablename__ = 'companies' + + id = Column(Integer, primary_key=True, index=True) + login = Column(String, unique=True, index=True, nullable=False) + hashed_password = Column(String, nullable=False) + address = Column(Text, nullable=False) + photo = Column(String) + about = Column(Text) + email = Column(String, unique=True, nullable=False) + phone_number = Column(String, unique=True, nullable=False) + is_active = Column(Boolean, default=True) + + +class Staff(Base): + __tablename__ = 'staff' + + id = Column(Integer, primary_key=True, index=True) + login = Column(String, unique=True, index=True, nullable=False) + hashed_password = Column(String, nullable=False) + is_active = Column(Boolean, default=True) + + +friends = Table( + 'friends', + Base.metadata, + Column('player_id', Integer, ForeignKey('players.id')), + Column('friend_id', Integer, ForeignKey('players.id')) +) + + +class Player(Base): + __tablename__ = 'players' + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + hashed_password = Column(String) + is_active = Column(Boolean, default=True) + + friends = relationship( + 'Player', secondary=friends, + primaryjoin=id == friends.c.player_id, + secondaryjoin=id == friends.c.friend_id, + backref='friends' + ) From 097cb9e81346181437ec3c3111b286d1e3263571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Mon, 27 Feb 2023 16:12:49 +0300 Subject: [PATCH 06/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BF=D0=B8=D0=B4=D0=B0=D0=BD=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=20=D1=81=D1=85=D0=B5=D0=BC=D1=8B=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=8E=D0=B7=D0=B5=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/users/schemas.py | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 backend/app/users/schemas.py diff --git a/backend/app/users/schemas.py b/backend/app/users/schemas.py new file mode 100644 index 0000000..95f2c59 --- /dev/null +++ b/backend/app/users/schemas.py @@ -0,0 +1,83 @@ +from pydantic import BaseModel, validator, constr +from pydantic.networks import EmailStr + +login_regex = r'[A-Za-z0-9@#$%^&+=]' +password_regex = r'[A-Za-z0-9@#$%^&+=]{8,}' + + +def validate_re_password(value: str, values: dict) -> str: + if 'password' in values and value != values['password']: + raise ValueError('Пароли не совпадают') + return value + + +class CompanyBase(BaseModel): + login: constr(strip_whitespace=True, max_length=120, regex=login_regex) + password: constr(regex=password_regex) + address: str + photo: str | None = None + about: str | None = None + email: EmailStr + phone_number: constr(strip_whitespace=True) + + @validator('phone_number') + def validate_phone_number(cls, value): + if len(value) != 12 or value[0] != '+': + raise ValueError('Неверный формат номера телефона.') + return value + + +class CompanyCreate(CompanyBase): + re_password: str + + _validate_re_password = validator( + 're_password', allow_reuse=True)(validate_re_password) + + +class Company(CompanyBase): + id: int + is_active: bool + # pubs: list[Pub] = [] + + class Config: + orm_mode = True + + +class StaffBase(BaseModel): + login: constr(strip_whitespace=True, max_length=120, regex=login_regex) + password: constr(regex=password_regex) + + +class StaffCreate(StaffBase): + re_password: str + + _validate_re_password = validator( + 're_password', allow_reuse=True)(validate_re_password) + + +class Staff(StaffBase): + id: int + is_active: bool + + class Config: + orm_mode = True + + +class PlayerBase(BaseModel): + email: EmailStr + password: constr(regex=password_regex) + + +class PlayerCreate(PlayerBase): + re_password: str + + _validate_re_password = validator( + 're_password', allow_reuse=True)(validate_re_password) + + +class Player(PlayerBase): + id: int + is_active: bool + + class Config: + orm_mode = True From d5b0683a0ddc8dee9810efa008e8d60c9bb0183c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Fri, 3 Mar 2023 12:50:01 +0300 Subject: [PATCH 07/81] created pubs model --- .vscode/settings.json | 4 ++++ backend/alembic/env.py | 2 +- backend/app/pubs/__init__.py | 0 backend/app/pubs/models.py | 24 ++++++++++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json create mode 100644 backend/app/pubs/__init__.py create mode 100644 backend/app/pubs/models.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..33fe63f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.linting.flake8Enabled": true, + "python.linting.enabled": true +} \ No newline at end of file diff --git a/backend/alembic/env.py b/backend/alembic/env.py index f21fa8d..fc0e8cb 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -22,7 +22,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) -from app.users import models +from app.pubs import models from app.main import Base target_metadata = Base.metadata diff --git a/backend/app/pubs/__init__.py b/backend/app/pubs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/pubs/models.py b/backend/app/pubs/models.py new file mode 100644 index 0000000..aaef30d --- /dev/null +++ b/backend/app/pubs/models.py @@ -0,0 +1,24 @@ +from sqlalchemy import ( + Column, + Integer, + String, + Boolean, + Text, + ForeignKey, + Table +) +from sqlalchemy.orm import relationship + +from ..database import Base + + +class Pub(Base): + """Модель пабов.""" + + __tablename__ = 'pubs' + + id = Column(Integer, primary_key=True, index=True) + address = Column(Text, nullable=False) + email = Column(String, nullable=False) + phone_number = Column(String, unique=True, nullable=False) + # company = ForeignKey From c7c14c3bac17c87e1658083b4ea7ae428eef1e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Fri, 3 Mar 2023 13:24:12 +0300 Subject: [PATCH 08/81] created alcohol model --- backend/app/pubs/models.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/app/pubs/models.py b/backend/app/pubs/models.py index aaef30d..14b09f1 100644 --- a/backend/app/pubs/models.py +++ b/backend/app/pubs/models.py @@ -22,3 +22,13 @@ class Pub(Base): email = Column(String, nullable=False) phone_number = Column(String, unique=True, nullable=False) # company = ForeignKey + + +class Alcohol(Base): + """Модель алкоголя.""" + + __tablename__ = 'alcohol' + + id = Column(Integer, primary_key=True, index=True) + name = Column(Text, nullable=False) + cost = Column(Integer, nullable=False) From 182da5c011853775a95a5e684cf61ab1662b8f24 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Mon, 6 Mar 2023 15:24:38 +0300 Subject: [PATCH 09/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=84=D0=B0=D0=B9=D0=BB=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=83=D1=81=D0=BA=D0=B0=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/run_server.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 backend/run_server.py diff --git a/backend/run_server.py b/backend/run_server.py new file mode 100644 index 0000000..630eee1 --- /dev/null +++ b/backend/run_server.py @@ -0,0 +1,10 @@ +import uvicorn + +if __name__ == '__main__': + uvicorn.run( + 'app.main:app', host='127.0.0.1', + port=8000, + reload=True, + log_level='debug', + reload_dirs=['.'] + ) From 5adb957dd8d38946c31798bc3d03a05c1b716fcc Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Mon, 6 Mar 2023 15:24:55 +0300 Subject: [PATCH 10/81] =?UTF-8?q?=D0=9D=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/requirements.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/requirements.txt b/backend/requirements.txt index a82e1bc..5b5dfc4 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,18 @@ alembic==1.9.4 +bcrypt==4.0.1 +certifi==2022.12.7 +cffi==1.15.1 +cryptography==39.0.1 +ecdsa==0.18.0 +email-validator==1.3.1 fastapi==0.92.0 +passlib==1.7.4 +pyasn1==0.4.8 +pycparser==2.21 pydantic==1.10.5 python-dotenv==0.21.1 +python-jose==3.3.0 +python-multipart==0.0.5 +rsa==4.9 SQLAlchemy==2.0.4 uvicorn==0.20.0 From ef7a9b341ccbbf3b832e64141067a333a8dcaa1a Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Mon, 6 Mar 2023 15:27:18 +0300 Subject: [PATCH 11/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8=20=D1=81=D0=B5=D1=81=D1=81=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=B1=D0=B0=D0=B7=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/database.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/app/database.py b/backend/app/database.py index 4e00674..ca2d4d0 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -11,3 +11,11 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() From d32ec44f3bced1beae5648efa97a3f6285132d31 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Mon, 6 Mar 2023 15:28:25 +0300 Subject: [PATCH 12/81] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/users/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/users/models.py b/backend/app/users/models.py index 8f33ecc..4f78b28 100644 --- a/backend/app/users/models.py +++ b/backend/app/users/models.py @@ -9,7 +9,7 @@ ) from sqlalchemy.orm import relationship -from ..database import Base +from app.database import Base class Company(Base): From 6b71fe758eac47bf84623f937e5b087aaabc7f51 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Mon, 6 Mar 2023 15:28:57 +0300 Subject: [PATCH 13/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=81=D1=85=D0=B5=D0=BC=D1=8B=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/users/schemas.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/app/users/schemas.py b/backend/app/users/schemas.py index 95f2c59..81ec990 100644 --- a/backend/app/users/schemas.py +++ b/backend/app/users/schemas.py @@ -81,3 +81,12 @@ class Player(PlayerBase): class Config: orm_mode = True + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + email: EmailStr | None = None From 5d538c0ed169e4832891280d75dd3a9ef10480f8 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Mon, 6 Mar 2023 15:29:58 +0300 Subject: [PATCH 14/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20(=D0=B2=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81?= =?UTF-8?q?=D0=B5...)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/users/router.py | 100 ++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 backend/app/users/router.py diff --git a/backend/app/users/router.py b/backend/app/users/router.py new file mode 100644 index 0000000..b9c959a --- /dev/null +++ b/backend/app/users/router.py @@ -0,0 +1,100 @@ +from datetime import timedelta, datetime + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import jwt, JWTError +from passlib.context import CryptContext +from pydantic import EmailStr +from sqlalchemy.orm import Session +from starlette import status + +from app.users import models +from app.users import schemas +from app.database import get_db + +SECRET_KEY = "85d2725c3b40ceb35c5571e6ca947b900b0588032a9061fba4014c6f23619686" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +router = APIRouter() + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_player(db: Session, email: EmailStr): + return db.get(models.Player, email) + + +def authenticate_user(db: Session, email: EmailStr, password: str): + user = get_player(db, email) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def get_current_player( + db: Session = Depends(get_db), + token: str = Depends(oauth2_scheme) +): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Токен недействителен.", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + email: EmailStr = payload.get("sub") + if email is None: + raise credentials_exception + token_data = schemas.TokenData(email=email) + except JWTError: + raise credentials_exception + player = get_player(db, email=token_data.email) + if player is None: + raise credentials_exception + return player + + +@router.post("/token", response_model=schemas.Token) +async def login_for_access_token( + db: Session = Depends(get_db), + form_data: OAuth2PasswordRequestForm = Depends() +): + user = authenticate_user(db, form_data.email, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Неверная почта или пароль.", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + + From 78262a1db024725972874ca93b1496127c456526 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Mon, 6 Mar 2023 15:30:22 +0300 Subject: [PATCH 15/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=80=D0=BE=D1=83=D1=82=D0=B5=D1=80=20=D1=8E?= =?UTF-8?q?=D0=B7=D0=B5=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/app/main.py b/backend/app/main.py index 296b314..f9d9a8b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,5 +1,7 @@ from fastapi import FastAPI -from .database import Base +from app.users.router import router as users_router app = FastAPI() + +app.include_router(users_router) From 7a4b24a3a31c3099312cd8a578514301ad907115 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Tue, 7 Mar 2023 14:04:33 +0300 Subject: [PATCH 16/81] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B9=D0=BA=D0=B8=20=D0=B1=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/alembic/env.py | 2 +- backend/app/database.py | 6 +++--- backend/app/main.py | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/alembic/env.py b/backend/alembic/env.py index f21fa8d..5a955f7 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -22,8 +22,8 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) +from app.database import Base from app.users import models -from app.main import Base target_metadata = Base.metadata diff --git a/backend/app/database.py b/backend/app/database.py index ca2d4d0..8b7d8ac 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -2,11 +2,11 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -SQLALCHEMY_DATABASE_URL = "sqlite:///../pub_golf.db" -# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" +SQLALCHEMY_DATABASE_URL = 'sqlite:///./pub_golf.db' +# SQLALCHEMY_DATABASE_URL = 'postgresql://user:password@postgresserver/db' engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} + SQLALCHEMY_DATABASE_URL, connect_args={'check_same_thread': False} ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/backend/app/main.py b/backend/app/main.py index f9d9a8b..f1b95ee 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,7 +1,11 @@ from fastapi import FastAPI +from app import users +from app.database import engine from app.users.router import router as users_router +users.models.Base.metadata.create_all(bind=engine) + app = FastAPI() app.include_router(users_router) From 10e98f34bb2fe0d59c52814f5c90ea0c54ca2f7a Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Tue, 7 Mar 2023 14:05:18 +0300 Subject: [PATCH 17/81] =?UTF-8?q?=D0=97=D0=B0=D0=B2=D0=B5=D1=80=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F/=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D0=BE=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/users/models.py | 7 ++--- backend/app/users/router.py | 57 ++++++++++++++++++++++++------------ backend/app/users/schemas.py | 20 ++++++++++--- 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/backend/app/users/models.py b/backend/app/users/models.py index 4f78b28..b394529 100644 --- a/backend/app/users/models.py +++ b/backend/app/users/models.py @@ -16,7 +16,7 @@ class Company(Base): __tablename__ = 'companies' id = Column(Integer, primary_key=True, index=True) - login = Column(String, unique=True, index=True, nullable=False) + username = Column(String, unique=True, index=True, nullable=False) hashed_password = Column(String, nullable=False) address = Column(Text, nullable=False) photo = Column(String) @@ -30,7 +30,7 @@ class Staff(Base): __tablename__ = 'staff' id = Column(Integer, primary_key=True, index=True) - login = Column(String, unique=True, index=True, nullable=False) + username = Column(String, unique=True, index=True, nullable=False) hashed_password = Column(String, nullable=False) is_active = Column(Boolean, default=True) @@ -54,6 +54,5 @@ class Player(Base): friends = relationship( 'Player', secondary=friends, primaryjoin=id == friends.c.player_id, - secondaryjoin=id == friends.c.friend_id, - backref='friends' + secondaryjoin=id == friends.c.friend_id ) diff --git a/backend/app/users/router.py b/backend/app/users/router.py index b9c959a..b7e466c 100644 --- a/backend/app/users/router.py +++ b/backend/app/users/router.py @@ -1,7 +1,7 @@ from datetime import timedelta, datetime from fastapi import APIRouter, Depends, HTTPException -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi.security import OAuth2PasswordBearer from jose import jwt, JWTError from passlib.context import CryptContext from pydantic import EmailStr @@ -12,15 +12,15 @@ from app.users import schemas from app.database import get_db -SECRET_KEY = "85d2725c3b40ceb35c5571e6ca947b900b0588032a9061fba4014c6f23619686" -ALGORITHM = "HS256" +SECRET_KEY = '85d2725c3b40ceb35c5571e6ca947b900b0588032a9061fba4014c6f23619686' +ALGORITHM = 'HS256' ACCESS_TOKEN_EXPIRE_MINUTES = 30 router = APIRouter() -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token') def verify_password(plain_password, hashed_password): @@ -32,7 +32,7 @@ def get_password_hash(password): def get_player(db: Session, email: EmailStr): - return db.get(models.Player, email) + return db.query(models.Player).filter(models.Player.email == email).first() def authenticate_user(db: Session, email: EmailStr, password: str): @@ -50,7 +50,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) - to_encode.update({"exp": expire}) + to_encode.update({'exp': expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @@ -61,12 +61,12 @@ def get_current_player( ): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Токен недействителен.", - headers={"WWW-Authenticate": "Bearer"}, + detail='Токен недействителен.', + headers={'WWW-Authenticate': 'Bearer'}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - email: EmailStr = payload.get("sub") + email: EmailStr = payload.get('sub') if email is None: raise credentials_exception token_data = schemas.TokenData(email=email) @@ -78,23 +78,44 @@ def get_current_player( return player -@router.post("/token", response_model=schemas.Token) +@router.post('/auth/signup', response_model=schemas.Player) +async def sign_up( + player: schemas.PlayerCreate, + db: Session = Depends(get_db), +): + if get_player(db, player.email): + raise HTTPException( + status_code=400, + detail='Пользователь с такой почтой уже существует.' + ) + hashed_password = pwd_context.hash(player.password) + db_player = models.Player( + email=player.email, + hashed_password=hashed_password + ) + db.add(db_player) + db.commit() + db.refresh(db_player) + return db_player + + +@router.post('/auth/signin', response_model=schemas.Token) async def login_for_access_token( + player: schemas.PlayerLogin, db: Session = Depends(get_db), - form_data: OAuth2PasswordRequestForm = Depends() ): - user = authenticate_user(db, form_data.email, form_data.password) - if not user: + player = authenticate_user(db, player.email, player.password) + if not player: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Неверная почта или пароль.", - headers={"WWW-Authenticate": "Bearer"}, + detail='Неверная почта или пароль.', + headers={'WWW-Authenticate': 'Bearer'}, ) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( - data={"sub": user.username}, expires_delta=access_token_expires + data={'sub': player.email}, expires_delta=access_token_expires ) - return {"access_token": access_token, "token_type": "bearer"} + return {'access_token': access_token, 'token_type': 'bearer'} diff --git a/backend/app/users/schemas.py b/backend/app/users/schemas.py index 81ec990..1cdacff 100644 --- a/backend/app/users/schemas.py +++ b/backend/app/users/schemas.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, validator, constr from pydantic.networks import EmailStr -login_regex = r'[A-Za-z0-9@#$%^&+=]' +username_regex = r'[A-Za-z0-9@#$%^&+=]' password_regex = r'[A-Za-z0-9@#$%^&+=]{8,}' @@ -12,7 +12,11 @@ def validate_re_password(value: str, values: dict) -> str: class CompanyBase(BaseModel): - login: constr(strip_whitespace=True, max_length=120, regex=login_regex) + username: constr( + strip_whitespace=True, + max_length=120, + regex=username_regex + ) password: constr(regex=password_regex) address: str photo: str | None = None @@ -44,7 +48,11 @@ class Config: class StaffBase(BaseModel): - login: constr(strip_whitespace=True, max_length=120, regex=login_regex) + username: constr( + strip_whitespace=True, + max_length=120, + regex=username_regex + ) password: constr(regex=password_regex) @@ -65,16 +73,20 @@ class Config: class PlayerBase(BaseModel): email: EmailStr - password: constr(regex=password_regex) class PlayerCreate(PlayerBase): + password: constr(regex=password_regex) re_password: str _validate_re_password = validator( 're_password', allow_reuse=True)(validate_re_password) +class PlayerLogin(PlayerBase): + password: str + + class Player(PlayerBase): id: int is_active: bool From 7d03c5e781e0bdda79f60223fb30ca2466cb9599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 9 Mar 2023 18:22:55 +0300 Subject: [PATCH 18/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=8D=D0=BD=D0=B4=D0=BF=D0=BE=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80=D0=BE=D1=81=D0=BC=D0=BE?= =?UTF-8?q?=D1=82=D1=80=D0=B0=20=D1=81=D0=B2=D0=BE=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=B0=D0=BA=D0=BA=D0=B0=D1=83=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/auth/router.py | 100 +++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 backend/app/auth/router.py diff --git a/backend/app/auth/router.py b/backend/app/auth/router.py new file mode 100644 index 0000000..6b07b8a --- /dev/null +++ b/backend/app/auth/router.py @@ -0,0 +1,100 @@ +from datetime import timedelta +from enum import Enum + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from jose import jwt, JWTError +from pydantic import EmailStr +from sqlalchemy.orm import Session +from starlette import status + +from app import players +from app.auth import schemas, utils +from app.auth.utils import SECRET_KEY, ALGORITHM +from app.database import get_db + +router = APIRouter() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token') +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +class UserTypes(Enum): + player: str = 'player' + company: str = 'company' + + +@router.post('/auth/players/signup', response_model=players.schemas.Player) +def sign_up_players( + player: players.schemas.PlayerCreate, + db: Session = Depends(get_db), +): + if players.crud.get_player(db, player.email): + raise HTTPException( + status_code=400, + detail='Пользователь с такой почтой уже существует.' + ) + + return players.crud.create_player( + db, + player, + utils.get_password_hash(player.password) + ) + + +@router.post('/auth/players/signin', response_model=schemas.Token) +def sign_in_players( + player: players.schemas.PlayerLogin, + db: Session = Depends(get_db), +): + player = utils.authenticate_user(db, player.email, player.password) + if not player: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Неверная почта или пароль.', + headers={'WWW-Authenticate': 'Bearer'}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = utils.create_access_token( + data={'sub': player.email}, expires_delta=access_token_expires + ) + return {'access_token': access_token, 'token_type': 'bearer'} + + +def get_current_user(db: Session, token: str, user_type: UserTypes): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Токен недействителен.', + headers={'WWW-Authenticate': 'Bearer'}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + email: EmailStr = payload.get('sub') + if email is None: + raise credentials_exception + token_data = schemas.TokenData(email=email) + except JWTError: + raise credentials_exception + if user_type == UserTypes.player: + user = players.crud.get_player(db, email=token_data.email) + # elif user_type == UserTypes.company: + # user = companies.crud.get_company(db, email=token_data.email) + if user is None: + raise credentials_exception + return user + + +@router.get('/players/me', response_model=players.schemas.Player) +def get_current_player( + db: Session = Depends(get_db), + token: str = Depends(oauth2_scheme) +): + return get_current_user(db, token, UserTypes.player) + + +# @router.get('/companies/me', response_model=companies.schemas.Company) +# def get_current_player( +# db: Session = Depends(get_db), +# token: str = Depends(oauth2_scheme) +# ): +# return get_current_user(db, token, UserTypes.company) From 23305a2c5ccd05dd26a19919cb158f10048706a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 9 Mar 2023 18:23:40 +0300 Subject: [PATCH 19/81] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80?= =?UTF-8?q?=D0=B0=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/alembic/env.py | 3 +- backend/app/{users => auth}/__init__.py | 0 backend/app/auth/schemas.py | 10 ++ backend/app/auth/utils.py | 41 ++++++++ backend/app/database.py | 2 +- backend/app/main.py | 9 +- backend/app/players/__init__.py | 0 backend/app/players/crud.py | 24 +++++ backend/app/{users => players}/models.py | 16 +-- backend/app/players/router.py | 18 ++++ backend/app/{users => players}/schemas.py | 45 -------- backend/app/users/router.py | 121 ---------------------- 12 files changed, 103 insertions(+), 186 deletions(-) rename backend/app/{users => auth}/__init__.py (100%) create mode 100644 backend/app/auth/schemas.py create mode 100644 backend/app/auth/utils.py create mode 100644 backend/app/players/__init__.py create mode 100644 backend/app/players/crud.py rename backend/app/{users => players}/models.py (68%) create mode 100644 backend/app/players/router.py rename backend/app/{users => players}/schemas.py (55%) delete mode 100644 backend/app/users/router.py diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 5a955f7..a126a2a 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -23,7 +23,8 @@ sys.path.append(BASE_DIR) from app.database import Base -from app.users import models +# from app.companies import models +from app.players import models target_metadata = Base.metadata diff --git a/backend/app/users/__init__.py b/backend/app/auth/__init__.py similarity index 100% rename from backend/app/users/__init__.py rename to backend/app/auth/__init__.py diff --git a/backend/app/auth/schemas.py b/backend/app/auth/schemas.py new file mode 100644 index 0000000..a57b4ee --- /dev/null +++ b/backend/app/auth/schemas.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, EmailStr + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + email: EmailStr | None = None diff --git a/backend/app/auth/utils.py b/backend/app/auth/utils.py new file mode 100644 index 0000000..b33bd72 --- /dev/null +++ b/backend/app/auth/utils.py @@ -0,0 +1,41 @@ +from datetime import timedelta, datetime + +from jose import jwt +from passlib.context import CryptContext +from pydantic import EmailStr +from sqlalchemy.orm import Session + +from app.players import crud + +SECRET_KEY = '85d2725c3b40ceb35c5571e6ca947b900b0588032a9061fba4014c6f23619686' +ALGORITHM = 'HS256' + +pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def authenticate_user(db: Session, email: EmailStr, password: str): + user = crud.get_player(db, email) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({'exp': expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt diff --git a/backend/app/database.py b/backend/app/database.py index 8b7d8ac..100e50c 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -1,4 +1,4 @@ -from sqlalchemy import create_engine, MetaData +from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker diff --git a/backend/app/main.py b/backend/app/main.py index f1b95ee..9f041d2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,11 +1,14 @@ from fastapi import FastAPI -from app import users +from app import players from app.database import engine -from app.users.router import router as users_router +from app.auth.router import router as users_router +from app.players.router import router as players_router -users.models.Base.metadata.create_all(bind=engine) +players.models.Base.metadata.create_all(bind=engine) +# companies.models.Base.metadata.create_all(bind=engine) app = FastAPI() app.include_router(users_router) +app.include_router(players_router) diff --git a/backend/app/players/__init__.py b/backend/app/players/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/players/crud.py b/backend/app/players/crud.py new file mode 100644 index 0000000..1e9ca0c --- /dev/null +++ b/backend/app/players/crud.py @@ -0,0 +1,24 @@ +from pydantic import EmailStr +from sqlalchemy.orm import Session + +from app.players import schemas +from app.players import models + + +def get_player(db: Session, email: EmailStr): + return db.query(models.Player).filter(models.Player.email == email).first() + + +def create_player( + db: Session, + player: schemas.PlayerCreate, + hashed_password: str +): + db_player = models.Player( + email=player.email, + hashed_password=hashed_password + ) + db.add(db_player) + db.commit() + db.refresh(db_player) + return db_player diff --git a/backend/app/users/models.py b/backend/app/players/models.py similarity index 68% rename from backend/app/users/models.py rename to backend/app/players/models.py index b394529..30a1d6a 100644 --- a/backend/app/users/models.py +++ b/backend/app/players/models.py @@ -3,7 +3,6 @@ Integer, String, Boolean, - Text, ForeignKey, Table ) @@ -12,20 +11,6 @@ from app.database import Base -class Company(Base): - __tablename__ = 'companies' - - id = Column(Integer, primary_key=True, index=True) - username = Column(String, unique=True, index=True, nullable=False) - hashed_password = Column(String, nullable=False) - address = Column(Text, nullable=False) - photo = Column(String) - about = Column(Text) - email = Column(String, unique=True, nullable=False) - phone_number = Column(String, unique=True, nullable=False) - is_active = Column(Boolean, default=True) - - class Staff(Base): __tablename__ = 'staff' @@ -49,6 +34,7 @@ class Player(Base): id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, index=True) hashed_password = Column(String) + photo = Column(String) is_active = Column(Boolean, default=True) friends = relationship( diff --git a/backend/app/players/router.py b/backend/app/players/router.py new file mode 100644 index 0000000..e7948cc --- /dev/null +++ b/backend/app/players/router.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from jose import jwt, JWTError +from pydantic import EmailStr +from sqlalchemy.orm import Session +from starlette import status + +from app import players, auth +from app.database import get_db +from app.auth.utils import SECRET_KEY, ALGORITHM +from app.players import crud + +router = APIRouter() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token') + + + diff --git a/backend/app/users/schemas.py b/backend/app/players/schemas.py similarity index 55% rename from backend/app/users/schemas.py rename to backend/app/players/schemas.py index 1cdacff..45aa6c4 100644 --- a/backend/app/users/schemas.py +++ b/backend/app/players/schemas.py @@ -11,42 +11,6 @@ def validate_re_password(value: str, values: dict) -> str: return value -class CompanyBase(BaseModel): - username: constr( - strip_whitespace=True, - max_length=120, - regex=username_regex - ) - password: constr(regex=password_regex) - address: str - photo: str | None = None - about: str | None = None - email: EmailStr - phone_number: constr(strip_whitespace=True) - - @validator('phone_number') - def validate_phone_number(cls, value): - if len(value) != 12 or value[0] != '+': - raise ValueError('Неверный формат номера телефона.') - return value - - -class CompanyCreate(CompanyBase): - re_password: str - - _validate_re_password = validator( - 're_password', allow_reuse=True)(validate_re_password) - - -class Company(CompanyBase): - id: int - is_active: bool - # pubs: list[Pub] = [] - - class Config: - orm_mode = True - - class StaffBase(BaseModel): username: constr( strip_whitespace=True, @@ -93,12 +57,3 @@ class Player(PlayerBase): class Config: orm_mode = True - - -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - email: EmailStr | None = None diff --git a/backend/app/users/router.py b/backend/app/users/router.py deleted file mode 100644 index b7e466c..0000000 --- a/backend/app/users/router.py +++ /dev/null @@ -1,121 +0,0 @@ -from datetime import timedelta, datetime - -from fastapi import APIRouter, Depends, HTTPException -from fastapi.security import OAuth2PasswordBearer -from jose import jwt, JWTError -from passlib.context import CryptContext -from pydantic import EmailStr -from sqlalchemy.orm import Session -from starlette import status - -from app.users import models -from app.users import schemas -from app.database import get_db - -SECRET_KEY = '85d2725c3b40ceb35c5571e6ca947b900b0588032a9061fba4014c6f23619686' -ALGORITHM = 'HS256' -ACCESS_TOKEN_EXPIRE_MINUTES = 30 - -router = APIRouter() - -pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token') - - -def verify_password(plain_password, hashed_password): - return pwd_context.verify(plain_password, hashed_password) - - -def get_password_hash(password): - return pwd_context.hash(password) - - -def get_player(db: Session, email: EmailStr): - return db.query(models.Player).filter(models.Player.email == email).first() - - -def authenticate_user(db: Session, email: EmailStr, password: str): - user = get_player(db, email) - if not user: - return False - if not verify_password(password, user.hashed_password): - return False - return user - - -def create_access_token(data: dict, expires_delta: timedelta | None = None): - to_encode = data.copy() - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(minutes=15) - to_encode.update({'exp': expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt - - -def get_current_player( - db: Session = Depends(get_db), - token: str = Depends(oauth2_scheme) -): - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail='Токен недействителен.', - headers={'WWW-Authenticate': 'Bearer'}, - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - email: EmailStr = payload.get('sub') - if email is None: - raise credentials_exception - token_data = schemas.TokenData(email=email) - except JWTError: - raise credentials_exception - player = get_player(db, email=token_data.email) - if player is None: - raise credentials_exception - return player - - -@router.post('/auth/signup', response_model=schemas.Player) -async def sign_up( - player: schemas.PlayerCreate, - db: Session = Depends(get_db), -): - if get_player(db, player.email): - raise HTTPException( - status_code=400, - detail='Пользователь с такой почтой уже существует.' - ) - hashed_password = pwd_context.hash(player.password) - db_player = models.Player( - email=player.email, - hashed_password=hashed_password - ) - db.add(db_player) - db.commit() - db.refresh(db_player) - return db_player - - -@router.post('/auth/signin', response_model=schemas.Token) -async def login_for_access_token( - player: schemas.PlayerLogin, - db: Session = Depends(get_db), -): - player = authenticate_user(db, player.email, player.password) - if not player: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail='Неверная почта или пароль.', - headers={'WWW-Authenticate': 'Bearer'}, - ) - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={'sub': player.email}, expires_delta=access_token_expires - ) - return {'access_token': access_token, 'token_type': 'bearer'} - - - From 2da836e92affcfeb16575a08808763ba4f833d97 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Fri, 10 Mar 2023 17:16:29 +0300 Subject: [PATCH 20/81] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80?= =?UTF-8?q?=D0=B0=20=D0=BF=D1=80=D0=B8=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/alembic/env.py | 3 +- backend/app/{auth => crud}/__init__.py | 0 backend/app/crud/crud_companies.py | 10 ++++ .../{players/crud.py => crud/crud_players.py} | 10 ++-- backend/app/database.py | 2 +- backend/app/main.py | 12 ++--- backend/app/{players => models}/__init__.py | 0 backend/app/models/models_companies.py | 22 +++++++++ .../models.py => models/models_players.py} | 0 backend/app/players/router.py | 18 ------- backend/app/routers/__init__.py | 0 .../router.py => routers/router_auth.py} | 43 ++++++++--------- backend/app/{auth => routers}/utils.py | 4 +- backend/app/schemas/__init__.py | 0 .../schemas.py => schemas/schemas_auth.py} | 0 backend/app/schemas/schemas_companies.py | 47 +++++++++++++++++++ .../schemas.py => schemas/schemas_players.py} | 0 17 files changed, 115 insertions(+), 56 deletions(-) rename backend/app/{auth => crud}/__init__.py (100%) create mode 100644 backend/app/crud/crud_companies.py rename backend/app/{players/crud.py => crud/crud_players.py} (57%) rename backend/app/{players => models}/__init__.py (100%) create mode 100644 backend/app/models/models_companies.py rename backend/app/{players/models.py => models/models_players.py} (100%) delete mode 100644 backend/app/players/router.py create mode 100644 backend/app/routers/__init__.py rename backend/app/{auth/router.py => routers/router_auth.py} (66%) rename backend/app/{auth => routers}/utils.py (93%) create mode 100644 backend/app/schemas/__init__.py rename backend/app/{auth/schemas.py => schemas/schemas_auth.py} (100%) create mode 100644 backend/app/schemas/schemas_companies.py rename backend/app/{players/schemas.py => schemas/schemas_players.py} (100%) diff --git a/backend/alembic/env.py b/backend/alembic/env.py index a126a2a..1d83ebc 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -23,8 +23,7 @@ sys.path.append(BASE_DIR) from app.database import Base -# from app.companies import models -from app.players import models +from app.models import models_players, models_companies target_metadata = Base.metadata diff --git a/backend/app/auth/__init__.py b/backend/app/crud/__init__.py similarity index 100% rename from backend/app/auth/__init__.py rename to backend/app/crud/__init__.py diff --git a/backend/app/crud/crud_companies.py b/backend/app/crud/crud_companies.py new file mode 100644 index 0000000..283586e --- /dev/null +++ b/backend/app/crud/crud_companies.py @@ -0,0 +1,10 @@ +from pydantic import EmailStr +from sqlalchemy.orm import Session + +from app.companies import models + + +def get_company(db: Session, email: EmailStr): + return db.query(models.Company).filter( + models.Company.email == email + ).first() diff --git a/backend/app/players/crud.py b/backend/app/crud/crud_players.py similarity index 57% rename from backend/app/players/crud.py rename to backend/app/crud/crud_players.py index 1e9ca0c..bf000d0 100644 --- a/backend/app/players/crud.py +++ b/backend/app/crud/crud_players.py @@ -1,20 +1,20 @@ from pydantic import EmailStr from sqlalchemy.orm import Session -from app.players import schemas -from app.players import models +from app.schemas import schemas_players +from app.models import models_players def get_player(db: Session, email: EmailStr): - return db.query(models.Player).filter(models.Player.email == email).first() + return db.query(models_players.Player).filter(models_players.Player.email == email).first() def create_player( db: Session, - player: schemas.PlayerCreate, + player: schemas_players.PlayerCreate, hashed_password: str ): - db_player = models.Player( + db_player = models_players.Player( email=player.email, hashed_password=hashed_password ) diff --git a/backend/app/database.py b/backend/app/database.py index 100e50c..b84aa97 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -2,7 +2,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -SQLALCHEMY_DATABASE_URL = 'sqlite:///./pub_golf.db' +SQLALCHEMY_DATABASE_URL = 'sqlite:///../pub_golf.db' # SQLALCHEMY_DATABASE_URL = 'postgresql://user:password@postgresserver/db' engine = create_engine( diff --git a/backend/app/main.py b/backend/app/main.py index 9f041d2..cf38154 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,14 +1,12 @@ from fastapi import FastAPI -from app import players +from app.models import models_players, models_companies +from app.routers import router_auth from app.database import engine -from app.auth.router import router as users_router -from app.players.router import router as players_router -players.models.Base.metadata.create_all(bind=engine) -# companies.models.Base.metadata.create_all(bind=engine) +models_players.Base.metadata.create_all(bind=engine) +models_companies.Base.metadata.create_all(bind=engine) app = FastAPI() -app.include_router(users_router) -app.include_router(players_router) +app.include_router(router_auth.router) diff --git a/backend/app/players/__init__.py b/backend/app/models/__init__.py similarity index 100% rename from backend/app/players/__init__.py rename to backend/app/models/__init__.py diff --git a/backend/app/models/models_companies.py b/backend/app/models/models_companies.py new file mode 100644 index 0000000..719be8d --- /dev/null +++ b/backend/app/models/models_companies.py @@ -0,0 +1,22 @@ +from sqlalchemy import ( + Column, + Integer, + String, + Boolean, + Text, +) + +from app.database import Base + + +class Company(Base): + __tablename__ = 'companies' + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + hashed_password = Column(String, nullable=False) + address = Column(Text, nullable=False) + photo = Column(String) + about = Column(Text) + phone_number = Column(String, unique=True, nullable=False) + is_active = Column(Boolean, default=True) diff --git a/backend/app/players/models.py b/backend/app/models/models_players.py similarity index 100% rename from backend/app/players/models.py rename to backend/app/models/models_players.py diff --git a/backend/app/players/router.py b/backend/app/players/router.py deleted file mode 100644 index e7948cc..0000000 --- a/backend/app/players/router.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from fastapi.security import OAuth2PasswordBearer -from jose import jwt, JWTError -from pydantic import EmailStr -from sqlalchemy.orm import Session -from starlette import status - -from app import players, auth -from app.database import get_db -from app.auth.utils import SECRET_KEY, ALGORITHM -from app.players import crud - -router = APIRouter() - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token') - - - diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/auth/router.py b/backend/app/routers/router_auth.py similarity index 66% rename from backend/app/auth/router.py rename to backend/app/routers/router_auth.py index 6b07b8a..50cd881 100644 --- a/backend/app/auth/router.py +++ b/backend/app/routers/router_auth.py @@ -8,9 +8,10 @@ from sqlalchemy.orm import Session from starlette import status -from app import players -from app.auth import schemas, utils -from app.auth.utils import SECRET_KEY, ALGORITHM +from app.crud import crud_players +from app.schemas import schemas_auth, schemas_players, schemas_companies +from app.routers.utils import SECRET_KEY, ALGORITHM, get_password_hash, \ + authenticate_user, create_access_token from app.database import get_db router = APIRouter() @@ -24,30 +25,30 @@ class UserTypes(Enum): company: str = 'company' -@router.post('/auth/players/signup', response_model=players.schemas.Player) +@router.post('/auth/players/signup', response_model=schemas_players.Player) def sign_up_players( - player: players.schemas.PlayerCreate, + player: schemas_players.PlayerCreate, db: Session = Depends(get_db), ): - if players.crud.get_player(db, player.email): + if crud_players.get_player(db, player.email): raise HTTPException( status_code=400, detail='Пользователь с такой почтой уже существует.' ) - return players.crud.create_player( + return crud_players.create_player( db, player, - utils.get_password_hash(player.password) + get_password_hash(player.password) ) -@router.post('/auth/players/signin', response_model=schemas.Token) +@router.post('/auth/players/signin', response_model=schemas_auth.Token) def sign_in_players( - player: players.schemas.PlayerLogin, + player: schemas_players.PlayerLogin, db: Session = Depends(get_db), ): - player = utils.authenticate_user(db, player.email, player.password) + player = authenticate_user(db, player.email, player.password) if not player: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -55,7 +56,7 @@ def sign_in_players( headers={'WWW-Authenticate': 'Bearer'}, ) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = utils.create_access_token( + access_token = create_access_token( data={'sub': player.email}, expires_delta=access_token_expires ) return {'access_token': access_token, 'token_type': 'bearer'} @@ -72,11 +73,11 @@ def get_current_user(db: Session, token: str, user_type: UserTypes): email: EmailStr = payload.get('sub') if email is None: raise credentials_exception - token_data = schemas.TokenData(email=email) + token_data = schemas_auth.TokenData(email=email) except JWTError: raise credentials_exception if user_type == UserTypes.player: - user = players.crud.get_player(db, email=token_data.email) + user = crud_players.get_player(db, email=token_data.email) # elif user_type == UserTypes.company: # user = companies.crud.get_company(db, email=token_data.email) if user is None: @@ -84,7 +85,7 @@ def get_current_user(db: Session, token: str, user_type: UserTypes): return user -@router.get('/players/me', response_model=players.schemas.Player) +@router.get('/players/me', response_model=schemas_players.Player) def get_current_player( db: Session = Depends(get_db), token: str = Depends(oauth2_scheme) @@ -92,9 +93,9 @@ def get_current_player( return get_current_user(db, token, UserTypes.player) -# @router.get('/companies/me', response_model=companies.schemas.Company) -# def get_current_player( -# db: Session = Depends(get_db), -# token: str = Depends(oauth2_scheme) -# ): -# return get_current_user(db, token, UserTypes.company) +@router.get('/companies/me', response_model=schemas_companies.Company) +def get_current_player( + db: Session = Depends(get_db), + token: str = Depends(oauth2_scheme) +): + return get_current_user(db, token, UserTypes.company) diff --git a/backend/app/auth/utils.py b/backend/app/routers/utils.py similarity index 93% rename from backend/app/auth/utils.py rename to backend/app/routers/utils.py index b33bd72..83bde59 100644 --- a/backend/app/auth/utils.py +++ b/backend/app/routers/utils.py @@ -5,7 +5,7 @@ from pydantic import EmailStr from sqlalchemy.orm import Session -from app.players import crud +from app.crud import crud_players SECRET_KEY = '85d2725c3b40ceb35c5571e6ca947b900b0588032a9061fba4014c6f23619686' ALGORITHM = 'HS256' @@ -22,7 +22,7 @@ def get_password_hash(password): def authenticate_user(db: Session, email: EmailStr, password: str): - user = crud.get_player(db, email) + user = crud_players.get_player(db, email) if not user: return False if not verify_password(password, user.hashed_password): diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/auth/schemas.py b/backend/app/schemas/schemas_auth.py similarity index 100% rename from backend/app/auth/schemas.py rename to backend/app/schemas/schemas_auth.py diff --git a/backend/app/schemas/schemas_companies.py b/backend/app/schemas/schemas_companies.py new file mode 100644 index 0000000..73d19da --- /dev/null +++ b/backend/app/schemas/schemas_companies.py @@ -0,0 +1,47 @@ +from pydantic import BaseModel, validator, constr +from pydantic.networks import EmailStr + +username_regex = r'[A-Za-z0-9@#$%^&+=]' +password_regex = r'[A-Za-z0-9@#$%^&+=]{8,}' + + +def validate_re_password(value: str, values: dict) -> str: + if 'password' in values and value != values['password']: + raise ValueError('Пароли не совпадают') + return value + + +class CompanyBase(BaseModel): + username: constr( + strip_whitespace=True, + max_length=120, + regex=username_regex + ) + password: constr(regex=password_regex) + address: str + photo: str | None = None + about: str | None = None + email: EmailStr + phone_number: constr(strip_whitespace=True) + + @validator('phone_number') + def validate_phone_number(cls, value): + if len(value) != 12 or value[0] != '+': + raise ValueError('Неверный формат номера телефона.') + return value + + +class CompanyCreate(CompanyBase): + re_password: str + + _validate_re_password = validator( + 're_password', allow_reuse=True)(validate_re_password) + + +class Company(CompanyBase): + id: int + is_active: bool + # pubs: list[Pub] = [] + + class Config: + orm_mode = True diff --git a/backend/app/players/schemas.py b/backend/app/schemas/schemas_players.py similarity index 100% rename from backend/app/players/schemas.py rename to backend/app/schemas/schemas_players.py From 4679063ed592ff172a584927369b81e33e0d6efe Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Mon, 13 Mar 2023 16:53:15 +0300 Subject: [PATCH 21/81] =?UTF-8?q?=D0=94=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=B8=20=D0=B2=D1=85=D0=BE=D0=B4=20=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B8=D0=B3=D1=80=D0=BE=D0=BA=D0=BE=D0=B2=20=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=B0=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/crud/crud_companies.py | 26 +++++-- backend/app/crud/crud_players.py | 13 ++-- backend/app/crud/utils.py | 7 ++ backend/app/database.py | 2 +- backend/app/models/models_companies.py | 3 +- backend/app/models/models_players.py | 1 - backend/app/routers/router_auth.py | 90 +++++++++--------------- backend/app/routers/utils.py | 84 ++++++++++++++++++---- backend/app/schemas/schemas_auth.py | 8 +++ backend/app/schemas/schemas_companies.py | 21 ++++-- 10 files changed, 166 insertions(+), 89 deletions(-) create mode 100644 backend/app/crud/utils.py diff --git a/backend/app/crud/crud_companies.py b/backend/app/crud/crud_companies.py index 283586e..5e30322 100644 --- a/backend/app/crud/crud_companies.py +++ b/backend/app/crud/crud_companies.py @@ -1,10 +1,28 @@ from pydantic import EmailStr from sqlalchemy.orm import Session -from app.companies import models +from app.models import models_companies +from app.crud.utils import get_password_hash +from app.schemas.schemas_companies import CompanyCreate -def get_company(db: Session, email: EmailStr): - return db.query(models.Company).filter( - models.Company.email == email +def get_company(db: Session, username: str): + return db.query(models_companies.Company).filter( + models_companies.Company.username == username ).first() + + +def create_company(db: Session, company: CompanyCreate): + db_company = models_companies.Company( + username=company.username, + address=company.address, + photo=company.photo, + about=company.about, + email=company.email, + phone_number=company.phone_number, + hashed_password=get_password_hash(company.password) + ) + db.add(db_company) + db.commit() + db.refresh(db_company) + return db_company diff --git a/backend/app/crud/crud_players.py b/backend/app/crud/crud_players.py index bf000d0..b1bd269 100644 --- a/backend/app/crud/crud_players.py +++ b/backend/app/crud/crud_players.py @@ -1,22 +1,21 @@ from pydantic import EmailStr from sqlalchemy.orm import Session +from app.crud.utils import get_password_hash from app.schemas import schemas_players from app.models import models_players def get_player(db: Session, email: EmailStr): - return db.query(models_players.Player).filter(models_players.Player.email == email).first() + return db.query(models_players.Player).filter( + models_players.Player.email == email + ).first() -def create_player( - db: Session, - player: schemas_players.PlayerCreate, - hashed_password: str -): +def create_player(db: Session, player: schemas_players.PlayerCreate): db_player = models_players.Player( email=player.email, - hashed_password=hashed_password + hashed_password=get_password_hash(player.password) ) db.add(db_player) db.commit() diff --git a/backend/app/crud/utils.py b/backend/app/crud/utils.py new file mode 100644 index 0000000..04287bf --- /dev/null +++ b/backend/app/crud/utils.py @@ -0,0 +1,7 @@ +from passlib.context import CryptContext + +pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') + + +def get_password_hash(password): + return pwd_context.hash(password) diff --git a/backend/app/database.py b/backend/app/database.py index b84aa97..100e50c 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -2,7 +2,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -SQLALCHEMY_DATABASE_URL = 'sqlite:///../pub_golf.db' +SQLALCHEMY_DATABASE_URL = 'sqlite:///./pub_golf.db' # SQLALCHEMY_DATABASE_URL = 'postgresql://user:password@postgresserver/db' engine = create_engine( diff --git a/backend/app/models/models_companies.py b/backend/app/models/models_companies.py index 719be8d..c919578 100644 --- a/backend/app/models/models_companies.py +++ b/backend/app/models/models_companies.py @@ -13,7 +13,8 @@ class Company(Base): __tablename__ = 'companies' id = Column(Integer, primary_key=True, index=True) - email = Column(String, unique=True, index=True) + username = Column(String, unique=True, index=True, nullable=False) + email = Column(String, unique=True, index=True, nullable=False) hashed_password = Column(String, nullable=False) address = Column(Text, nullable=False) photo = Column(String) diff --git a/backend/app/models/models_players.py b/backend/app/models/models_players.py index 30a1d6a..07fed3b 100644 --- a/backend/app/models/models_players.py +++ b/backend/app/models/models_players.py @@ -34,7 +34,6 @@ class Player(Base): id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, index=True) hashed_password = Column(String) - photo = Column(String) is_active = Column(Boolean, default=True) friends = relationship( diff --git a/backend/app/routers/router_auth.py b/backend/app/routers/router_auth.py index 50cd881..e47901c 100644 --- a/backend/app/routers/router_auth.py +++ b/backend/app/routers/router_auth.py @@ -1,32 +1,23 @@ -from datetime import timedelta -from enum import Enum - from fastapi import APIRouter, Depends, HTTPException from fastapi.security import OAuth2PasswordBearer -from jose import jwt, JWTError -from pydantic import EmailStr from sqlalchemy.orm import Session -from starlette import status -from app.crud import crud_players +from app.crud import crud_players, crud_companies from app.schemas import schemas_auth, schemas_players, schemas_companies -from app.routers.utils import SECRET_KEY, ALGORITHM, get_password_hash, \ - authenticate_user, create_access_token +from app.routers.utils import ( + get_current_user, + sign_in_user +) from app.database import get_db +from app.schemas.schemas_auth import UserTypes router = APIRouter() oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token') -ACCESS_TOKEN_EXPIRE_MINUTES = 30 - - -class UserTypes(Enum): - player: str = 'player' - company: str = 'company' @router.post('/auth/players/signup', response_model=schemas_players.Player) -def sign_up_players( +def create_player( player: schemas_players.PlayerCreate, db: Session = Depends(get_db), ): @@ -35,54 +26,39 @@ def sign_up_players( status_code=400, detail='Пользователь с такой почтой уже существует.' ) - - return crud_players.create_player( - db, - player, - get_password_hash(player.password) - ) + return crud_players.create_player(db, player) -@router.post('/auth/players/signin', response_model=schemas_auth.Token) -def sign_in_players( - player: schemas_players.PlayerLogin, +@router.post( + '/auth/companies/signup', + response_model=schemas_companies.Company +) +def create_company( + company: schemas_companies.CompanyCreate, db: Session = Depends(get_db), ): - player = authenticate_user(db, player.email, player.password) - if not player: + if crud_companies.get_company(db, company.username): raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail='Неверная почта или пароль.', - headers={'WWW-Authenticate': 'Bearer'}, + status_code=400, + detail='Пользователь с таким логином уже существует.' ) - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={'sub': player.email}, expires_delta=access_token_expires - ) - return {'access_token': access_token, 'token_type': 'bearer'} + return crud_companies.create_company(db, company) + +@router.post('/auth/players/signin', response_model=schemas_auth.Token) +def sign_in_player( + player_data: schemas_players.PlayerLogin, + db: Session = Depends(get_db), +): + return sign_in_user(db, player_data) -def get_current_user(db: Session, token: str, user_type: UserTypes): - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail='Токен недействителен.', - headers={'WWW-Authenticate': 'Bearer'}, - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - email: EmailStr = payload.get('sub') - if email is None: - raise credentials_exception - token_data = schemas_auth.TokenData(email=email) - except JWTError: - raise credentials_exception - if user_type == UserTypes.player: - user = crud_players.get_player(db, email=token_data.email) - # elif user_type == UserTypes.company: - # user = companies.crud.get_company(db, email=token_data.email) - if user is None: - raise credentials_exception - return user + +@router.post('/auth/companies/signin', response_model=schemas_auth.Token) +def sign_in_company( + company_data: schemas_companies.CompanyLogin, + db: Session = Depends(get_db), +): + return sign_in_user(db, company_data) @router.get('/players/me', response_model=schemas_players.Player) @@ -94,7 +70,7 @@ def get_current_player( @router.get('/companies/me', response_model=schemas_companies.Company) -def get_current_player( +def get_current_company( db: Session = Depends(get_db), token: str = Depends(oauth2_scheme) ): diff --git a/backend/app/routers/utils.py b/backend/app/routers/utils.py index 83bde59..789b5eb 100644 --- a/backend/app/routers/utils.py +++ b/backend/app/routers/utils.py @@ -1,31 +1,37 @@ from datetime import timedelta, datetime -from jose import jwt -from passlib.context import CryptContext +from fastapi import HTTPException +from jose import jwt, JWTError from pydantic import EmailStr from sqlalchemy.orm import Session +from starlette import status -from app.crud import crud_players +from app.crud import crud_players, crud_companies +from app.crud.utils import pwd_context +from app.schemas.schemas_auth import UserTypes +from app.schemas import schemas_auth, schemas_players, schemas_companies SECRET_KEY = '85d2725c3b40ceb35c5571e6ca947b900b0588032a9061fba4014c6f23619686' ALGORITHM = 'HS256' -pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') +ACCESS_TOKEN_EXPIRE_MINUTES = 30 def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) -def get_password_hash(password): - return pwd_context.hash(password) - - -def authenticate_user(db: Session, email: EmailStr, password: str): - user = crud_players.get_player(db, email) - if not user: +def authenticate_user( + db: Session, + user: schemas_players.PlayerLogin | schemas_companies.CompanyLogin +): + if isinstance(user, schemas_players.PlayerLogin): + db_user = crud_players.get_player(db, user.email) + elif isinstance(user, schemas_companies.CompanyLogin): + db_user = crud_companies.get_company(db, user.username) + if not db_user: return False - if not verify_password(password, user.hashed_password): + if not verify_password(user.password, db_user.hashed_password): return False return user @@ -39,3 +45,57 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode.update({'exp': expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt + + +def get_current_user(db: Session, token: str, user_type: UserTypes): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Токен недействителен.', + headers={'WWW-Authenticate': 'Bearer'}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + if user_type == UserTypes.player: + email: EmailStr = payload.get('sub') + if email is None: + raise credentials_exception + token_data = schemas_auth.TokenData(email=email) + elif user_type == UserTypes.company: + username: str = payload.get('sub') + if username is None: + raise credentials_exception + token_data = schemas_auth.TokenData(username=username) + except JWTError: + raise credentials_exception + user = None + if user_type == UserTypes.player: + user = crud_players.get_player(db, email=token_data.email) + elif user_type == UserTypes.company: + user = crud_companies.get_company(db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +def sign_in_user( + db: Session, + user_data: schemas_players.PlayerLogin | schemas_companies.CompanyLogin +): + user = authenticate_user(db, user_data) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Неверная почта или пароль.', + headers={'WWW-Authenticate': 'Bearer'}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + sub = '' + if type(user_data) == schemas_players.PlayerLogin: + sub = user.email + elif type(user_data) == schemas_companies.CompanyLogin: + sub = user.username + access_token = create_access_token( + data={'sub': sub}, + expires_delta=access_token_expires + ) + return {'access_token': access_token, 'token_type': 'bearer'} diff --git a/backend/app/schemas/schemas_auth.py b/backend/app/schemas/schemas_auth.py index a57b4ee..6faafd4 100644 --- a/backend/app/schemas/schemas_auth.py +++ b/backend/app/schemas/schemas_auth.py @@ -1,6 +1,13 @@ +from enum import Enum + from pydantic import BaseModel, EmailStr +class UserTypes(Enum): + player: str = 'player' + company: str = 'company' + + class Token(BaseModel): access_token: str token_type: str @@ -8,3 +15,4 @@ class Token(BaseModel): class TokenData(BaseModel): email: EmailStr | None = None + username: str | None = None diff --git a/backend/app/schemas/schemas_companies.py b/backend/app/schemas/schemas_companies.py index 73d19da..7e68d66 100644 --- a/backend/app/schemas/schemas_companies.py +++ b/backend/app/schemas/schemas_companies.py @@ -17,29 +17,38 @@ class CompanyBase(BaseModel): max_length=120, regex=username_regex ) + + +class CompanyCreate(CompanyBase): password: constr(regex=password_regex) + re_password: str address: str photo: str | None = None about: str | None = None email: EmailStr phone_number: constr(strip_whitespace=True) + _validate_re_password = validator( + 're_password', allow_reuse=True)(validate_re_password) + @validator('phone_number') def validate_phone_number(cls, value): - if len(value) != 12 or value[0] != '+': + if len(value) != 12 or value[0] != '+' or not value[1:].isdigit(): raise ValueError('Неверный формат номера телефона.') return value -class CompanyCreate(CompanyBase): - re_password: str - - _validate_re_password = validator( - 're_password', allow_reuse=True)(validate_re_password) +class CompanyLogin(CompanyBase): + password: str class Company(CompanyBase): id: int + address: str + photo: str | None = None + about: str | None = None + email: EmailStr + phone_number: constr(strip_whitespace=True) is_active: bool # pubs: list[Pub] = [] From 9460ed1b54928a45ddece51417e2837c69204c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Mon, 13 Mar 2023 18:20:14 +0300 Subject: [PATCH 22/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=B2=D1=81=D0=B5=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D1=81=D0=BE=D0=BD=D0=B0=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/alembic/env.py | 2 +- backend/app/crud/crud_staff.py | 22 +++++++++++++ backend/app/main.py | 13 ++++++-- backend/app/models/models_players.py | 9 ----- backend/app/models/models_staff.py | 12 +++++++ backend/app/routers/router_auth.py | 42 ++++++++++++++---------- backend/app/routers/router_companies.py | 17 ++++++++++ backend/app/routers/router_players.py | 17 ++++++++++ backend/app/routers/router_staff.py | 17 ++++++++++ backend/app/routers/utils.py | 31 +++++++++++++---- backend/app/schemas/schemas_auth.py | 1 + backend/app/schemas/schemas_companies.py | 13 +++----- backend/app/schemas/schemas_players.py | 33 +------------------ backend/app/schemas/schemas_staff.py | 35 ++++++++++++++++++++ backend/app/schemas/utils.py | 8 +++++ 15 files changed, 196 insertions(+), 76 deletions(-) create mode 100644 backend/app/crud/crud_staff.py create mode 100644 backend/app/models/models_staff.py create mode 100644 backend/app/routers/router_companies.py create mode 100644 backend/app/routers/router_players.py create mode 100644 backend/app/routers/router_staff.py create mode 100644 backend/app/schemas/schemas_staff.py create mode 100644 backend/app/schemas/utils.py diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 1d83ebc..42b1f8b 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -23,7 +23,7 @@ sys.path.append(BASE_DIR) from app.database import Base -from app.models import models_players, models_companies +from app.models import models_players, models_companies, models_staff target_metadata = Base.metadata diff --git a/backend/app/crud/crud_staff.py b/backend/app/crud/crud_staff.py new file mode 100644 index 0000000..7759c25 --- /dev/null +++ b/backend/app/crud/crud_staff.py @@ -0,0 +1,22 @@ +from sqlalchemy.orm import Session + +from app.crud.utils import get_password_hash +from app.schemas import schemas_staff +from app.models import models_staff + + +def get_staff(db: Session, username: str): + return db.query(models_staff.Staff).filter( + models_staff.Staff.username == username + ).first() + + +def create_staff(db: Session, staff: schemas_staff.StaffCreate): + db_staff = models_staff.Staff( + username=staff.username, + hashed_password=get_password_hash(staff.password) + ) + db.add(db_staff) + db.commit() + db.refresh(db_staff) + return db_staff diff --git a/backend/app/main.py b/backend/app/main.py index cf38154..31fc11b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,12 +1,21 @@ from fastapi import FastAPI -from app.models import models_players, models_companies -from app.routers import router_auth +from app.models import models_players, models_companies, models_staff +from app.routers import ( + router_auth, + router_players, + router_companies, + router_staff +) from app.database import engine models_players.Base.metadata.create_all(bind=engine) models_companies.Base.metadata.create_all(bind=engine) +models_staff.Base.metadata.create_all(bind=engine) app = FastAPI() app.include_router(router_auth.router) +app.include_router(router_players.router) +app.include_router(router_companies.router) +app.include_router(router_staff.router) diff --git a/backend/app/models/models_players.py b/backend/app/models/models_players.py index 07fed3b..5684976 100644 --- a/backend/app/models/models_players.py +++ b/backend/app/models/models_players.py @@ -11,15 +11,6 @@ from app.database import Base -class Staff(Base): - __tablename__ = 'staff' - - id = Column(Integer, primary_key=True, index=True) - username = Column(String, unique=True, index=True, nullable=False) - hashed_password = Column(String, nullable=False) - is_active = Column(Boolean, default=True) - - friends = Table( 'friends', Base.metadata, diff --git a/backend/app/models/models_staff.py b/backend/app/models/models_staff.py new file mode 100644 index 0000000..b056b3c --- /dev/null +++ b/backend/app/models/models_staff.py @@ -0,0 +1,12 @@ +from sqlalchemy import Column, Integer, String, Boolean + +from app.database import Base + + +class Staff(Base): + __tablename__ = 'staff' + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True, index=True, nullable=False) + hashed_password = Column(String, nullable=False) + is_active = Column(Boolean, default=True) diff --git a/backend/app/routers/router_auth.py b/backend/app/routers/router_auth.py index e47901c..27c3bf6 100644 --- a/backend/app/routers/router_auth.py +++ b/backend/app/routers/router_auth.py @@ -1,20 +1,18 @@ from fastapi import APIRouter, Depends, HTTPException -from fastapi.security import OAuth2PasswordBearer from sqlalchemy.orm import Session -from app.crud import crud_players, crud_companies -from app.schemas import schemas_auth, schemas_players, schemas_companies +from app.crud import crud_players, crud_companies, crud_staff +from app.schemas import schemas_auth, schemas_players, schemas_companies, \ + schemas_staff from app.routers.utils import ( get_current_user, - sign_in_user + sign_in_user, oauth2_scheme ) from app.database import get_db from app.schemas.schemas_auth import UserTypes router = APIRouter() -oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token') - @router.post('/auth/players/signup', response_model=schemas_players.Player) def create_player( @@ -45,6 +43,22 @@ def create_company( return crud_companies.create_company(db, company) +@router.post( + '/auth/staff/signup', + response_model=schemas_staff.Staff +) +def create_staff( + staff: schemas_staff.StaffCreate, + db: Session = Depends(get_db), +): + if crud_staff.get_staff(db, staff.username): + raise HTTPException( + status_code=400, + detail='Пользователь с таким логином уже существует.' + ) + return crud_staff.create_staff(db, staff) + + @router.post('/auth/players/signin', response_model=schemas_auth.Token) def sign_in_player( player_data: schemas_players.PlayerLogin, @@ -61,17 +75,9 @@ def sign_in_company( return sign_in_user(db, company_data) -@router.get('/players/me', response_model=schemas_players.Player) -def get_current_player( - db: Session = Depends(get_db), - token: str = Depends(oauth2_scheme) -): - return get_current_user(db, token, UserTypes.player) - - -@router.get('/companies/me', response_model=schemas_companies.Company) -def get_current_company( +@router.post('/auth/staff/signin', response_model=schemas_auth.Token) +def sign_in_staff( + staff_data: schemas_staff.StaffLogin, db: Session = Depends(get_db), - token: str = Depends(oauth2_scheme) ): - return get_current_user(db, token, UserTypes.company) + return sign_in_user(db, staff_data) diff --git a/backend/app/routers/router_companies.py b/backend/app/routers/router_companies.py new file mode 100644 index 0000000..11f81d1 --- /dev/null +++ b/backend/app/routers/router_companies.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.database import get_db +from app.routers.utils import oauth2_scheme, get_current_user +from app.schemas import schemas_companies +from app.schemas.schemas_auth import UserTypes + +router = APIRouter() + + +@router.get('/companies/me', response_model=schemas_companies.Company) +def get_current_company( + db: Session = Depends(get_db), + token: str = Depends(oauth2_scheme) +): + return get_current_user(db, token, UserTypes.company) diff --git a/backend/app/routers/router_players.py b/backend/app/routers/router_players.py new file mode 100644 index 0000000..774da9b --- /dev/null +++ b/backend/app/routers/router_players.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.database import get_db +from app.routers.utils import get_current_user, oauth2_scheme +from app.schemas import schemas_players +from app.schemas.schemas_auth import UserTypes + +router = APIRouter() + + +@router.get('/players/me', response_model=schemas_players.Player) +def get_current_player( + db: Session = Depends(get_db), + token: str = Depends(oauth2_scheme) +): + return get_current_user(db, token, UserTypes.player) diff --git a/backend/app/routers/router_staff.py b/backend/app/routers/router_staff.py new file mode 100644 index 0000000..6803b22 --- /dev/null +++ b/backend/app/routers/router_staff.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.database import get_db +from app.routers.utils import oauth2_scheme, get_current_user +from app.schemas import schemas_staff +from app.schemas.schemas_auth import UserTypes + +router = APIRouter() + + +@router.get('/staff/me', response_model=schemas_staff.Staff) +def get_current_staff( + db: Session = Depends(get_db), + token: str = Depends(oauth2_scheme) +): + return get_current_user(db, token, UserTypes.staff) diff --git a/backend/app/routers/utils.py b/backend/app/routers/utils.py index 789b5eb..0a55f1a 100644 --- a/backend/app/routers/utils.py +++ b/backend/app/routers/utils.py @@ -1,21 +1,29 @@ from datetime import timedelta, datetime from fastapi import HTTPException +from fastapi.security import OAuth2PasswordBearer from jose import jwt, JWTError from pydantic import EmailStr from sqlalchemy.orm import Session from starlette import status -from app.crud import crud_players, crud_companies +from app.crud import crud_players, crud_companies, crud_staff from app.crud.utils import pwd_context from app.schemas.schemas_auth import UserTypes -from app.schemas import schemas_auth, schemas_players, schemas_companies +from app.schemas import ( + schemas_auth, + schemas_players, + schemas_companies, + schemas_staff +) SECRET_KEY = '85d2725c3b40ceb35c5571e6ca947b900b0588032a9061fba4014c6f23619686' ALGORITHM = 'HS256' ACCESS_TOKEN_EXPIRE_MINUTES = 30 +oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token') + def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) @@ -23,12 +31,16 @@ def verify_password(plain_password, hashed_password): def authenticate_user( db: Session, - user: schemas_players.PlayerLogin | schemas_companies.CompanyLogin + user: schemas_players.PlayerLogin | + schemas_companies.CompanyLogin | + schemas_staff.StaffLogin ): if isinstance(user, schemas_players.PlayerLogin): db_user = crud_players.get_player(db, user.email) elif isinstance(user, schemas_companies.CompanyLogin): db_user = crud_companies.get_company(db, user.username) + elif isinstance(user, schemas_staff.StaffLogin): + db_user = crud_staff.get_staff(db, user.username) if not db_user: return False if not verify_password(user.password, db_user.hashed_password): @@ -60,7 +72,7 @@ def get_current_user(db: Session, token: str, user_type: UserTypes): if email is None: raise credentials_exception token_data = schemas_auth.TokenData(email=email) - elif user_type == UserTypes.company: + elif user_type == UserTypes.company or user_type == UserTypes.staff: username: str = payload.get('sub') if username is None: raise credentials_exception @@ -72,6 +84,8 @@ def get_current_user(db: Session, token: str, user_type: UserTypes): user = crud_players.get_player(db, email=token_data.email) elif user_type == UserTypes.company: user = crud_companies.get_company(db, username=token_data.username) + elif user_type == UserTypes.staff: + user = crud_staff.get_staff(db, username=token_data.username) if user is None: raise credentials_exception return user @@ -79,7 +93,9 @@ def get_current_user(db: Session, token: str, user_type: UserTypes): def sign_in_user( db: Session, - user_data: schemas_players.PlayerLogin | schemas_companies.CompanyLogin + user_data: schemas_players.PlayerLogin | + schemas_companies.CompanyLogin | + schemas_staff.StaffLogin ): user = authenticate_user(db, user_data) if not user: @@ -92,7 +108,10 @@ def sign_in_user( sub = '' if type(user_data) == schemas_players.PlayerLogin: sub = user.email - elif type(user_data) == schemas_companies.CompanyLogin: + elif ( + type(user_data) == schemas_companies.CompanyLogin + or type(user_data) == schemas_staff.StaffLogin + ): sub = user.username access_token = create_access_token( data={'sub': sub}, diff --git a/backend/app/schemas/schemas_auth.py b/backend/app/schemas/schemas_auth.py index 6faafd4..07beae5 100644 --- a/backend/app/schemas/schemas_auth.py +++ b/backend/app/schemas/schemas_auth.py @@ -6,6 +6,7 @@ class UserTypes(Enum): player: str = 'player' company: str = 'company' + staff: str = 'staff' class Token(BaseModel): diff --git a/backend/app/schemas/schemas_companies.py b/backend/app/schemas/schemas_companies.py index 7e68d66..050c32e 100644 --- a/backend/app/schemas/schemas_companies.py +++ b/backend/app/schemas/schemas_companies.py @@ -1,14 +1,11 @@ from pydantic import BaseModel, validator, constr from pydantic.networks import EmailStr -username_regex = r'[A-Za-z0-9@#$%^&+=]' -password_regex = r'[A-Za-z0-9@#$%^&+=]{8,}' - - -def validate_re_password(value: str, values: dict) -> str: - if 'password' in values and value != values['password']: - raise ValueError('Пароли не совпадают') - return value +from app.schemas.utils import ( + username_regex, + password_regex, + validate_re_password +) class CompanyBase(BaseModel): diff --git a/backend/app/schemas/schemas_players.py b/backend/app/schemas/schemas_players.py index 45aa6c4..ff6a759 100644 --- a/backend/app/schemas/schemas_players.py +++ b/backend/app/schemas/schemas_players.py @@ -1,38 +1,7 @@ from pydantic import BaseModel, validator, constr from pydantic.networks import EmailStr -username_regex = r'[A-Za-z0-9@#$%^&+=]' -password_regex = r'[A-Za-z0-9@#$%^&+=]{8,}' - - -def validate_re_password(value: str, values: dict) -> str: - if 'password' in values and value != values['password']: - raise ValueError('Пароли не совпадают') - return value - - -class StaffBase(BaseModel): - username: constr( - strip_whitespace=True, - max_length=120, - regex=username_regex - ) - password: constr(regex=password_regex) - - -class StaffCreate(StaffBase): - re_password: str - - _validate_re_password = validator( - 're_password', allow_reuse=True)(validate_re_password) - - -class Staff(StaffBase): - id: int - is_active: bool - - class Config: - orm_mode = True +from app.schemas.utils import password_regex, validate_re_password class PlayerBase(BaseModel): diff --git a/backend/app/schemas/schemas_staff.py b/backend/app/schemas/schemas_staff.py new file mode 100644 index 0000000..45eaa61 --- /dev/null +++ b/backend/app/schemas/schemas_staff.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel, constr, validator + +from app.schemas.utils import ( + username_regex, + password_regex, + validate_re_password +) + + +class StaffBase(BaseModel): + username: constr( + strip_whitespace=True, + max_length=120, + regex=username_regex + ) + + +class StaffCreate(StaffBase): + password: constr(regex=password_regex) + re_password: str + + _validate_re_password = validator( + 're_password', allow_reuse=True)(validate_re_password) + + +class StaffLogin(StaffBase): + password: str + + +class Staff(StaffBase): + id: int + is_active: bool + + class Config: + orm_mode = True diff --git a/backend/app/schemas/utils.py b/backend/app/schemas/utils.py new file mode 100644 index 0000000..104d27f --- /dev/null +++ b/backend/app/schemas/utils.py @@ -0,0 +1,8 @@ +username_regex = r'[A-Za-z0-9@#$%^&+=]' +password_regex = r'[A-Za-z0-9@#$%^&+=]{8,}' + + +def validate_re_password(value: str, values: dict) -> str: + if 'password' in values and value != values['password']: + raise ValueError('Пароли не совпадают') + return value From e34138ce2d3ed691c32a9eab7ae7fd438ef822c1 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Tue, 14 Mar 2023 13:47:25 +0300 Subject: [PATCH 23/81] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=B4=D1=8B=20=D0=BE=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/router_auth.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/app/routers/router_auth.py b/backend/app/routers/router_auth.py index 27c3bf6..f7c2150 100644 --- a/backend/app/routers/router_auth.py +++ b/backend/app/routers/router_auth.py @@ -1,15 +1,16 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session +from starlette import status from app.crud import crud_players, crud_companies, crud_staff -from app.schemas import schemas_auth, schemas_players, schemas_companies, \ +from app.schemas import ( + schemas_auth, + schemas_players, + schemas_companies, schemas_staff -from app.routers.utils import ( - get_current_user, - sign_in_user, oauth2_scheme ) +from app.routers.utils import sign_in_user from app.database import get_db -from app.schemas.schemas_auth import UserTypes router = APIRouter() @@ -21,7 +22,7 @@ def create_player( ): if crud_players.get_player(db, player.email): raise HTTPException( - status_code=400, + status_code=status.HTTP_400_BAD_REQUEST, detail='Пользователь с такой почтой уже существует.' ) return crud_players.create_player(db, player) @@ -37,7 +38,7 @@ def create_company( ): if crud_companies.get_company(db, company.username): raise HTTPException( - status_code=400, + status_code=status.HTTP_400_BAD_REQUEST, detail='Пользователь с таким логином уже существует.' ) return crud_companies.create_company(db, company) @@ -53,7 +54,7 @@ def create_staff( ): if crud_staff.get_staff(db, staff.username): raise HTTPException( - status_code=400, + status_code=status.HTTP_400_BAD_REQUEST, detail='Пользователь с таким логином уже существует.' ) return crud_staff.create_staff(db, staff) From 09d0fc056eb9eadbd3e0518f0f364eea4d30e825 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Tue, 14 Mar 2023 13:48:31 +0300 Subject: [PATCH 24/81] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0=20=D0=B2=D1=8B=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D0=B5=D0=BD=D0=BE=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D1=83=D1=8E=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/utils.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/app/routers/utils.py b/backend/app/routers/utils.py index 0a55f1a..4be1554 100644 --- a/backend/app/routers/utils.py +++ b/backend/app/routers/utils.py @@ -59,7 +59,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): return encoded_jwt -def get_current_user(db: Session, token: str, user_type: UserTypes): +def get_token_data(token: str, user_type: UserTypes): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail='Токен недействителен.', @@ -79,6 +79,11 @@ def get_current_user(db: Session, token: str, user_type: UserTypes): token_data = schemas_auth.TokenData(username=username) except JWTError: raise credentials_exception + return token_data + + +def get_current_user(db: Session, token: str, user_type: UserTypes): + token_data = get_token_data(token, user_type) user = None if user_type == UserTypes.player: user = crud_players.get_player(db, email=token_data.email) @@ -87,7 +92,11 @@ def get_current_user(db: Session, token: str, user_type: UserTypes): elif user_type == UserTypes.staff: user = crud_staff.get_staff(db, username=token_data.username) if user is None: - raise credentials_exception + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Пользователь не существует.', + headers={'WWW-Authenticate': 'Bearer'}, + ) return user From de425ae9d4c75aefa35acbc1514998f249999751 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Tue, 14 Mar 2023 13:49:05 +0300 Subject: [PATCH 25/81] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D1=87=D1=82=D1=8B=20=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D0=BE=D0=BA=D0=B0=20(=D0=B2=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=86=D0=B5=D1=81=D1=81=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/crud/crud_players.py | 13 +++++++++++++ backend/app/routers/router_players.py | 27 +++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/backend/app/crud/crud_players.py b/backend/app/crud/crud_players.py index b1bd269..ed104f4 100644 --- a/backend/app/crud/crud_players.py +++ b/backend/app/crud/crud_players.py @@ -21,3 +21,16 @@ def create_player(db: Session, player: schemas_players.PlayerCreate): db.commit() db.refresh(db_player) return db_player + + +def update_player( + db: Session, + email: models_players.Player.email, + new_email: EmailStr +): + db_player = db.query(models_players.Player).filter( + models_players.Player.email == email + ).first() + db_player.email = new_email + db.commit() + return db_player diff --git a/backend/app/routers/router_players.py b/backend/app/routers/router_players.py index 774da9b..af9c859 100644 --- a/backend/app/routers/router_players.py +++ b/backend/app/routers/router_players.py @@ -1,8 +1,11 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Body, HTTPException +from pydantic import EmailStr from sqlalchemy.orm import Session +from starlette import status +from app.crud import crud_players from app.database import get_db -from app.routers.utils import get_current_user, oauth2_scheme +from app.routers.utils import get_current_user, oauth2_scheme, get_token_data from app.schemas import schemas_players from app.schemas.schemas_auth import UserTypes @@ -15,3 +18,23 @@ def get_current_player( token: str = Depends(oauth2_scheme) ): return get_current_user(db, token, UserTypes.player) + + +@router.patch('/players/reset_email', response_model=schemas_players.Player) +def reset_player_email( + new_email: EmailStr = Body(embed=True), + db: Session = Depends(get_db), + token: str = Depends(oauth2_scheme), +): + email = get_token_data(token, UserTypes.player).email + player = crud_players.get_player(db, email) + if player is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Пользователь не существует.' + ) + return crud_players.update_player( + db, + player.email, + new_email + ) From 4c7ee8362f9068d8c8f6c3f3561c2a02240dfa19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 16 Mar 2023 10:56:23 +0300 Subject: [PATCH 26/81] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B5=D1=85?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=20=D0=BD=D0=B0=20Django?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 -- backend/alembic.ini | 105 ------------------------- backend/alembic/env.py | 85 --------------------- backend/alembic/script.py.mako | 24 ------ backend/app/database.py | 13 ---- backend/app/main.py | 5 -- backend/manage.py | 20 +++++ backend/{app => pub_golf}/__init__.py | 0 backend/pub_golf/asgi.py | 7 ++ backend/pub_golf/settings.py | 106 ++++++++++++++++++++++++++ backend/pub_golf/urls.py | 6 ++ backend/pub_golf/wsgi.py | 7 ++ backend/requirements.txt | 8 +- 13 files changed, 148 insertions(+), 244 deletions(-) delete mode 100644 backend/alembic.ini delete mode 100644 backend/alembic/env.py delete mode 100644 backend/alembic/script.py.mako delete mode 100644 backend/app/database.py delete mode 100644 backend/app/main.py create mode 100644 backend/manage.py rename backend/{app => pub_golf}/__init__.py (100%) create mode 100644 backend/pub_golf/asgi.py create mode 100644 backend/pub_golf/settings.py create mode 100644 backend/pub_golf/urls.py create mode 100644 backend/pub_golf/wsgi.py diff --git a/.gitignore b/.gitignore index 341257c..de99b6c 100644 --- a/.gitignore +++ b/.gitignore @@ -155,9 +155,3 @@ cython_debug/ # PyCharm .idea/ - -# Alembic -versions/ - -# db -pub_golf.db \ No newline at end of file diff --git a/backend/alembic.ini b/backend/alembic.ini deleted file mode 100644 index 081e43d..0000000 --- a/backend/alembic.ini +++ /dev/null @@ -1,105 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts -script_location = alembic - -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file -# for all available tokens -file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to alembic/versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. -# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions - -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os # Use os.pathsep. Default configuration used for new projects. - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -sqlalchemy.url = sqlite:///./pub_golf.db - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py deleted file mode 100644 index f21fa8d..0000000 --- a/backend/alembic/env.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import sys - -from logging.config import fileConfig - -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.append(BASE_DIR) - -from app.users import models -from app.main import Base - -target_metadata = Base.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako deleted file mode 100644 index 55df286..0000000 --- a/backend/alembic/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade() -> None: - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - ${downgrades if downgrades else "pass"} diff --git a/backend/app/database.py b/backend/app/database.py deleted file mode 100644 index 4e00674..0000000 --- a/backend/app/database.py +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy import create_engine, MetaData -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - -SQLALCHEMY_DATABASE_URL = "sqlite:///../pub_golf.db" -# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" - -engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} -) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -Base = declarative_base() diff --git a/backend/app/main.py b/backend/app/main.py deleted file mode 100644 index 296b314..0000000 --- a/backend/app/main.py +++ /dev/null @@ -1,5 +0,0 @@ -from fastapi import FastAPI - -from .database import Base - -app = FastAPI() 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/app/__init__.py b/backend/pub_golf/__init__.py similarity index 100% rename from backend/app/__init__.py rename to backend/pub_golf/__init__.py 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..9f99748 --- /dev/null +++ b/backend/pub_golf/settings.py @@ -0,0 +1,106 @@ +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='127.0.0.1').split() + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +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', +] + +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', + } +} + + +# 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 = 'en-us' + +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' diff --git a/backend/pub_golf/urls.py b/backend/pub_golf/urls.py new file mode 100644 index 0000000..dfc7362 --- /dev/null +++ b/backend/pub_golf/urls.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.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/requirements.txt b/backend/requirements.txt index a82e1bc..83f939e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,2 @@ -alembic==1.9.4 -fastapi==0.92.0 -pydantic==1.10.5 -python-dotenv==0.21.1 -SQLAlchemy==2.0.4 -uvicorn==0.20.0 +Django==4.1.7 +python-dotenv==1.0.0 From 331123e7fd0e69eaf8bc4bec543a9a30d5d3db44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 16 Mar 2023 10:56:23 +0300 Subject: [PATCH 27/81] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B5=D1=85?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=20=D0=BD=D0=B0=20Django?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 -- backend/alembic.ini | 105 ------------------ backend/alembic/env.py | 85 --------------- backend/alembic/script.py.mako | 24 ----- backend/app/crud/__init__.py | 0 backend/app/crud/crud_companies.py | 28 ----- backend/app/crud/crud_players.py | 23 ---- backend/app/crud/crud_staff.py | 22 ---- backend/app/crud/utils.py | 7 -- backend/app/database.py | 21 ---- backend/app/main.py | 21 ---- backend/app/models/__init__.py | 0 backend/app/models/models_companies.py | 23 ---- backend/app/models/models_players.py | 34 ------ backend/app/models/models_staff.py | 12 --- backend/app/routers/__init__.py | 0 backend/app/routers/router_auth.py | 84 --------------- backend/app/routers/router_companies.py | 17 --- backend/app/routers/router_players.py | 17 --- backend/app/routers/router_staff.py | 17 --- backend/app/routers/utils.py | 129 ----------------------- backend/app/schemas/__init__.py | 0 backend/app/schemas/schemas_auth.py | 19 ---- backend/app/schemas/schemas_companies.py | 53 ---------- backend/app/schemas/schemas_players.py | 28 ----- backend/app/schemas/schemas_staff.py | 35 ------ backend/app/schemas/utils.py | 8 -- backend/manage.py | 20 ++++ backend/{app => pub_golf}/__init__.py | 0 backend/pub_golf/asgi.py | 7 ++ backend/pub_golf/settings.py | 106 +++++++++++++++++++ backend/pub_golf/urls.py | 6 ++ backend/pub_golf/wsgi.py | 7 ++ backend/requirements.txt | 20 +--- 34 files changed, 148 insertions(+), 836 deletions(-) delete mode 100644 backend/alembic.ini delete mode 100644 backend/alembic/env.py delete mode 100644 backend/alembic/script.py.mako delete mode 100644 backend/app/crud/__init__.py delete mode 100644 backend/app/crud/crud_companies.py delete mode 100644 backend/app/crud/crud_players.py delete mode 100644 backend/app/crud/crud_staff.py delete mode 100644 backend/app/crud/utils.py delete mode 100644 backend/app/database.py delete mode 100644 backend/app/main.py delete mode 100644 backend/app/models/__init__.py delete mode 100644 backend/app/models/models_companies.py delete mode 100644 backend/app/models/models_players.py delete mode 100644 backend/app/models/models_staff.py delete mode 100644 backend/app/routers/__init__.py delete mode 100644 backend/app/routers/router_auth.py delete mode 100644 backend/app/routers/router_companies.py delete mode 100644 backend/app/routers/router_players.py delete mode 100644 backend/app/routers/router_staff.py delete mode 100644 backend/app/routers/utils.py delete mode 100644 backend/app/schemas/__init__.py delete mode 100644 backend/app/schemas/schemas_auth.py delete mode 100644 backend/app/schemas/schemas_companies.py delete mode 100644 backend/app/schemas/schemas_players.py delete mode 100644 backend/app/schemas/schemas_staff.py delete mode 100644 backend/app/schemas/utils.py create mode 100644 backend/manage.py rename backend/{app => pub_golf}/__init__.py (100%) create mode 100644 backend/pub_golf/asgi.py create mode 100644 backend/pub_golf/settings.py create mode 100644 backend/pub_golf/urls.py create mode 100644 backend/pub_golf/wsgi.py diff --git a/.gitignore b/.gitignore index 341257c..de99b6c 100644 --- a/.gitignore +++ b/.gitignore @@ -155,9 +155,3 @@ cython_debug/ # PyCharm .idea/ - -# Alembic -versions/ - -# db -pub_golf.db \ No newline at end of file diff --git a/backend/alembic.ini b/backend/alembic.ini deleted file mode 100644 index 081e43d..0000000 --- a/backend/alembic.ini +++ /dev/null @@ -1,105 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts -script_location = alembic - -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file -# for all available tokens -file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to alembic/versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. -# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions - -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os # Use os.pathsep. Default configuration used for new projects. - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -sqlalchemy.url = sqlite:///./pub_golf.db - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py deleted file mode 100644 index 42b1f8b..0000000 --- a/backend/alembic/env.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import sys - -from logging.config import fileConfig - -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.append(BASE_DIR) - -from app.database import Base -from app.models import models_players, models_companies, models_staff - -target_metadata = Base.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako deleted file mode 100644 index 55df286..0000000 --- a/backend/alembic/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade() -> None: - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - ${downgrades if downgrades else "pass"} diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/app/crud/crud_companies.py b/backend/app/crud/crud_companies.py deleted file mode 100644 index 5e30322..0000000 --- a/backend/app/crud/crud_companies.py +++ /dev/null @@ -1,28 +0,0 @@ -from pydantic import EmailStr -from sqlalchemy.orm import Session - -from app.models import models_companies -from app.crud.utils import get_password_hash -from app.schemas.schemas_companies import CompanyCreate - - -def get_company(db: Session, username: str): - return db.query(models_companies.Company).filter( - models_companies.Company.username == username - ).first() - - -def create_company(db: Session, company: CompanyCreate): - db_company = models_companies.Company( - username=company.username, - address=company.address, - photo=company.photo, - about=company.about, - email=company.email, - phone_number=company.phone_number, - hashed_password=get_password_hash(company.password) - ) - db.add(db_company) - db.commit() - db.refresh(db_company) - return db_company diff --git a/backend/app/crud/crud_players.py b/backend/app/crud/crud_players.py deleted file mode 100644 index b1bd269..0000000 --- a/backend/app/crud/crud_players.py +++ /dev/null @@ -1,23 +0,0 @@ -from pydantic import EmailStr -from sqlalchemy.orm import Session - -from app.crud.utils import get_password_hash -from app.schemas import schemas_players -from app.models import models_players - - -def get_player(db: Session, email: EmailStr): - return db.query(models_players.Player).filter( - models_players.Player.email == email - ).first() - - -def create_player(db: Session, player: schemas_players.PlayerCreate): - db_player = models_players.Player( - email=player.email, - hashed_password=get_password_hash(player.password) - ) - db.add(db_player) - db.commit() - db.refresh(db_player) - return db_player diff --git a/backend/app/crud/crud_staff.py b/backend/app/crud/crud_staff.py deleted file mode 100644 index 7759c25..0000000 --- a/backend/app/crud/crud_staff.py +++ /dev/null @@ -1,22 +0,0 @@ -from sqlalchemy.orm import Session - -from app.crud.utils import get_password_hash -from app.schemas import schemas_staff -from app.models import models_staff - - -def get_staff(db: Session, username: str): - return db.query(models_staff.Staff).filter( - models_staff.Staff.username == username - ).first() - - -def create_staff(db: Session, staff: schemas_staff.StaffCreate): - db_staff = models_staff.Staff( - username=staff.username, - hashed_password=get_password_hash(staff.password) - ) - db.add(db_staff) - db.commit() - db.refresh(db_staff) - return db_staff diff --git a/backend/app/crud/utils.py b/backend/app/crud/utils.py deleted file mode 100644 index 04287bf..0000000 --- a/backend/app/crud/utils.py +++ /dev/null @@ -1,7 +0,0 @@ -from passlib.context import CryptContext - -pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') - - -def get_password_hash(password): - return pwd_context.hash(password) diff --git a/backend/app/database.py b/backend/app/database.py deleted file mode 100644 index 100e50c..0000000 --- a/backend/app/database.py +++ /dev/null @@ -1,21 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - -SQLALCHEMY_DATABASE_URL = 'sqlite:///./pub_golf.db' -# SQLALCHEMY_DATABASE_URL = 'postgresql://user:password@postgresserver/db' - -engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={'check_same_thread': False} -) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -Base = declarative_base() - - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() diff --git a/backend/app/main.py b/backend/app/main.py deleted file mode 100644 index 31fc11b..0000000 --- a/backend/app/main.py +++ /dev/null @@ -1,21 +0,0 @@ -from fastapi import FastAPI - -from app.models import models_players, models_companies, models_staff -from app.routers import ( - router_auth, - router_players, - router_companies, - router_staff -) -from app.database import engine - -models_players.Base.metadata.create_all(bind=engine) -models_companies.Base.metadata.create_all(bind=engine) -models_staff.Base.metadata.create_all(bind=engine) - -app = FastAPI() - -app.include_router(router_auth.router) -app.include_router(router_players.router) -app.include_router(router_companies.router) -app.include_router(router_staff.router) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/app/models/models_companies.py b/backend/app/models/models_companies.py deleted file mode 100644 index c919578..0000000 --- a/backend/app/models/models_companies.py +++ /dev/null @@ -1,23 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - String, - Boolean, - Text, -) - -from app.database import Base - - -class Company(Base): - __tablename__ = 'companies' - - id = Column(Integer, primary_key=True, index=True) - username = Column(String, unique=True, index=True, nullable=False) - email = Column(String, unique=True, index=True, nullable=False) - hashed_password = Column(String, nullable=False) - address = Column(Text, nullable=False) - photo = Column(String) - about = Column(Text) - phone_number = Column(String, unique=True, nullable=False) - is_active = Column(Boolean, default=True) diff --git a/backend/app/models/models_players.py b/backend/app/models/models_players.py deleted file mode 100644 index 5684976..0000000 --- a/backend/app/models/models_players.py +++ /dev/null @@ -1,34 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - String, - Boolean, - ForeignKey, - Table -) -from sqlalchemy.orm import relationship - -from app.database import Base - - -friends = Table( - 'friends', - Base.metadata, - Column('player_id', Integer, ForeignKey('players.id')), - Column('friend_id', Integer, ForeignKey('players.id')) -) - - -class Player(Base): - __tablename__ = 'players' - - id = Column(Integer, primary_key=True, index=True) - email = Column(String, unique=True, index=True) - hashed_password = Column(String) - is_active = Column(Boolean, default=True) - - friends = relationship( - 'Player', secondary=friends, - primaryjoin=id == friends.c.player_id, - secondaryjoin=id == friends.c.friend_id - ) diff --git a/backend/app/models/models_staff.py b/backend/app/models/models_staff.py deleted file mode 100644 index b056b3c..0000000 --- a/backend/app/models/models_staff.py +++ /dev/null @@ -1,12 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean - -from app.database import Base - - -class Staff(Base): - __tablename__ = 'staff' - - id = Column(Integer, primary_key=True, index=True) - username = Column(String, unique=True, index=True, nullable=False) - hashed_password = Column(String, nullable=False) - is_active = Column(Boolean, default=True) diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/app/routers/router_auth.py b/backend/app/routers/router_auth.py deleted file mode 100644 index f7c2150..0000000 --- a/backend/app/routers/router_auth.py +++ /dev/null @@ -1,84 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session -from starlette import status - -from app.crud import crud_players, crud_companies, crud_staff -from app.schemas import ( - schemas_auth, - schemas_players, - schemas_companies, - schemas_staff -) -from app.routers.utils import sign_in_user -from app.database import get_db - -router = APIRouter() - - -@router.post('/auth/players/signup', response_model=schemas_players.Player) -def create_player( - player: schemas_players.PlayerCreate, - db: Session = Depends(get_db), -): - if crud_players.get_player(db, player.email): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Пользователь с такой почтой уже существует.' - ) - return crud_players.create_player(db, player) - - -@router.post( - '/auth/companies/signup', - response_model=schemas_companies.Company -) -def create_company( - company: schemas_companies.CompanyCreate, - db: Session = Depends(get_db), -): - if crud_companies.get_company(db, company.username): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Пользователь с таким логином уже существует.' - ) - return crud_companies.create_company(db, company) - - -@router.post( - '/auth/staff/signup', - response_model=schemas_staff.Staff -) -def create_staff( - staff: schemas_staff.StaffCreate, - db: Session = Depends(get_db), -): - if crud_staff.get_staff(db, staff.username): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Пользователь с таким логином уже существует.' - ) - return crud_staff.create_staff(db, staff) - - -@router.post('/auth/players/signin', response_model=schemas_auth.Token) -def sign_in_player( - player_data: schemas_players.PlayerLogin, - db: Session = Depends(get_db), -): - return sign_in_user(db, player_data) - - -@router.post('/auth/companies/signin', response_model=schemas_auth.Token) -def sign_in_company( - company_data: schemas_companies.CompanyLogin, - db: Session = Depends(get_db), -): - return sign_in_user(db, company_data) - - -@router.post('/auth/staff/signin', response_model=schemas_auth.Token) -def sign_in_staff( - staff_data: schemas_staff.StaffLogin, - db: Session = Depends(get_db), -): - return sign_in_user(db, staff_data) diff --git a/backend/app/routers/router_companies.py b/backend/app/routers/router_companies.py deleted file mode 100644 index 11f81d1..0000000 --- a/backend/app/routers/router_companies.py +++ /dev/null @@ -1,17 +0,0 @@ -from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session - -from app.database import get_db -from app.routers.utils import oauth2_scheme, get_current_user -from app.schemas import schemas_companies -from app.schemas.schemas_auth import UserTypes - -router = APIRouter() - - -@router.get('/companies/me', response_model=schemas_companies.Company) -def get_current_company( - db: Session = Depends(get_db), - token: str = Depends(oauth2_scheme) -): - return get_current_user(db, token, UserTypes.company) diff --git a/backend/app/routers/router_players.py b/backend/app/routers/router_players.py deleted file mode 100644 index 774da9b..0000000 --- a/backend/app/routers/router_players.py +++ /dev/null @@ -1,17 +0,0 @@ -from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session - -from app.database import get_db -from app.routers.utils import get_current_user, oauth2_scheme -from app.schemas import schemas_players -from app.schemas.schemas_auth import UserTypes - -router = APIRouter() - - -@router.get('/players/me', response_model=schemas_players.Player) -def get_current_player( - db: Session = Depends(get_db), - token: str = Depends(oauth2_scheme) -): - return get_current_user(db, token, UserTypes.player) diff --git a/backend/app/routers/router_staff.py b/backend/app/routers/router_staff.py deleted file mode 100644 index 6803b22..0000000 --- a/backend/app/routers/router_staff.py +++ /dev/null @@ -1,17 +0,0 @@ -from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session - -from app.database import get_db -from app.routers.utils import oauth2_scheme, get_current_user -from app.schemas import schemas_staff -from app.schemas.schemas_auth import UserTypes - -router = APIRouter() - - -@router.get('/staff/me', response_model=schemas_staff.Staff) -def get_current_staff( - db: Session = Depends(get_db), - token: str = Depends(oauth2_scheme) -): - return get_current_user(db, token, UserTypes.staff) diff --git a/backend/app/routers/utils.py b/backend/app/routers/utils.py deleted file mode 100644 index 4be1554..0000000 --- a/backend/app/routers/utils.py +++ /dev/null @@ -1,129 +0,0 @@ -from datetime import timedelta, datetime - -from fastapi import HTTPException -from fastapi.security import OAuth2PasswordBearer -from jose import jwt, JWTError -from pydantic import EmailStr -from sqlalchemy.orm import Session -from starlette import status - -from app.crud import crud_players, crud_companies, crud_staff -from app.crud.utils import pwd_context -from app.schemas.schemas_auth import UserTypes -from app.schemas import ( - schemas_auth, - schemas_players, - schemas_companies, - schemas_staff -) - -SECRET_KEY = '85d2725c3b40ceb35c5571e6ca947b900b0588032a9061fba4014c6f23619686' -ALGORITHM = 'HS256' - -ACCESS_TOKEN_EXPIRE_MINUTES = 30 - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token') - - -def verify_password(plain_password, hashed_password): - return pwd_context.verify(plain_password, hashed_password) - - -def authenticate_user( - db: Session, - user: schemas_players.PlayerLogin | - schemas_companies.CompanyLogin | - schemas_staff.StaffLogin -): - if isinstance(user, schemas_players.PlayerLogin): - db_user = crud_players.get_player(db, user.email) - elif isinstance(user, schemas_companies.CompanyLogin): - db_user = crud_companies.get_company(db, user.username) - elif isinstance(user, schemas_staff.StaffLogin): - db_user = crud_staff.get_staff(db, user.username) - if not db_user: - return False - if not verify_password(user.password, db_user.hashed_password): - return False - return user - - -def create_access_token(data: dict, expires_delta: timedelta | None = None): - to_encode = data.copy() - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(minutes=15) - to_encode.update({'exp': expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt - - -def get_token_data(token: str, user_type: UserTypes): - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail='Токен недействителен.', - headers={'WWW-Authenticate': 'Bearer'}, - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - if user_type == UserTypes.player: - email: EmailStr = payload.get('sub') - if email is None: - raise credentials_exception - token_data = schemas_auth.TokenData(email=email) - elif user_type == UserTypes.company or user_type == UserTypes.staff: - username: str = payload.get('sub') - if username is None: - raise credentials_exception - token_data = schemas_auth.TokenData(username=username) - except JWTError: - raise credentials_exception - return token_data - - -def get_current_user(db: Session, token: str, user_type: UserTypes): - token_data = get_token_data(token, user_type) - user = None - if user_type == UserTypes.player: - user = crud_players.get_player(db, email=token_data.email) - elif user_type == UserTypes.company: - user = crud_companies.get_company(db, username=token_data.username) - elif user_type == UserTypes.staff: - user = crud_staff.get_staff(db, username=token_data.username) - if user is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail='Пользователь не существует.', - headers={'WWW-Authenticate': 'Bearer'}, - ) - return user - - -def sign_in_user( - db: Session, - user_data: schemas_players.PlayerLogin | - schemas_companies.CompanyLogin | - schemas_staff.StaffLogin -): - user = authenticate_user(db, user_data) - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail='Неверная почта или пароль.', - headers={'WWW-Authenticate': 'Bearer'}, - ) - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - sub = '' - if type(user_data) == schemas_players.PlayerLogin: - sub = user.email - elif ( - type(user_data) == schemas_companies.CompanyLogin - or type(user_data) == schemas_staff.StaffLogin - ): - sub = user.username - access_token = create_access_token( - data={'sub': sub}, - expires_delta=access_token_expires - ) - return {'access_token': access_token, 'token_type': 'bearer'} diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/app/schemas/schemas_auth.py b/backend/app/schemas/schemas_auth.py deleted file mode 100644 index 07beae5..0000000 --- a/backend/app/schemas/schemas_auth.py +++ /dev/null @@ -1,19 +0,0 @@ -from enum import Enum - -from pydantic import BaseModel, EmailStr - - -class UserTypes(Enum): - player: str = 'player' - company: str = 'company' - staff: str = 'staff' - - -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - email: EmailStr | None = None - username: str | None = None diff --git a/backend/app/schemas/schemas_companies.py b/backend/app/schemas/schemas_companies.py deleted file mode 100644 index 050c32e..0000000 --- a/backend/app/schemas/schemas_companies.py +++ /dev/null @@ -1,53 +0,0 @@ -from pydantic import BaseModel, validator, constr -from pydantic.networks import EmailStr - -from app.schemas.utils import ( - username_regex, - password_regex, - validate_re_password -) - - -class CompanyBase(BaseModel): - username: constr( - strip_whitespace=True, - max_length=120, - regex=username_regex - ) - - -class CompanyCreate(CompanyBase): - password: constr(regex=password_regex) - re_password: str - address: str - photo: str | None = None - about: str | None = None - email: EmailStr - phone_number: constr(strip_whitespace=True) - - _validate_re_password = validator( - 're_password', allow_reuse=True)(validate_re_password) - - @validator('phone_number') - def validate_phone_number(cls, value): - if len(value) != 12 or value[0] != '+' or not value[1:].isdigit(): - raise ValueError('Неверный формат номера телефона.') - return value - - -class CompanyLogin(CompanyBase): - password: str - - -class Company(CompanyBase): - id: int - address: str - photo: str | None = None - about: str | None = None - email: EmailStr - phone_number: constr(strip_whitespace=True) - is_active: bool - # pubs: list[Pub] = [] - - class Config: - orm_mode = True diff --git a/backend/app/schemas/schemas_players.py b/backend/app/schemas/schemas_players.py deleted file mode 100644 index ff6a759..0000000 --- a/backend/app/schemas/schemas_players.py +++ /dev/null @@ -1,28 +0,0 @@ -from pydantic import BaseModel, validator, constr -from pydantic.networks import EmailStr - -from app.schemas.utils import password_regex, validate_re_password - - -class PlayerBase(BaseModel): - email: EmailStr - - -class PlayerCreate(PlayerBase): - password: constr(regex=password_regex) - re_password: str - - _validate_re_password = validator( - 're_password', allow_reuse=True)(validate_re_password) - - -class PlayerLogin(PlayerBase): - password: str - - -class Player(PlayerBase): - id: int - is_active: bool - - class Config: - orm_mode = True diff --git a/backend/app/schemas/schemas_staff.py b/backend/app/schemas/schemas_staff.py deleted file mode 100644 index 45eaa61..0000000 --- a/backend/app/schemas/schemas_staff.py +++ /dev/null @@ -1,35 +0,0 @@ -from pydantic import BaseModel, constr, validator - -from app.schemas.utils import ( - username_regex, - password_regex, - validate_re_password -) - - -class StaffBase(BaseModel): - username: constr( - strip_whitespace=True, - max_length=120, - regex=username_regex - ) - - -class StaffCreate(StaffBase): - password: constr(regex=password_regex) - re_password: str - - _validate_re_password = validator( - 're_password', allow_reuse=True)(validate_re_password) - - -class StaffLogin(StaffBase): - password: str - - -class Staff(StaffBase): - id: int - is_active: bool - - class Config: - orm_mode = True diff --git a/backend/app/schemas/utils.py b/backend/app/schemas/utils.py deleted file mode 100644 index 104d27f..0000000 --- a/backend/app/schemas/utils.py +++ /dev/null @@ -1,8 +0,0 @@ -username_regex = r'[A-Za-z0-9@#$%^&+=]' -password_regex = r'[A-Za-z0-9@#$%^&+=]{8,}' - - -def validate_re_password(value: str, values: dict) -> str: - if 'password' in values and value != values['password']: - raise ValueError('Пароли не совпадают') - return value 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/app/__init__.py b/backend/pub_golf/__init__.py similarity index 100% rename from backend/app/__init__.py rename to backend/pub_golf/__init__.py 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..9f99748 --- /dev/null +++ b/backend/pub_golf/settings.py @@ -0,0 +1,106 @@ +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='127.0.0.1').split() + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +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', +] + +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', + } +} + + +# 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 = 'en-us' + +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' diff --git a/backend/pub_golf/urls.py b/backend/pub_golf/urls.py new file mode 100644 index 0000000..dfc7362 --- /dev/null +++ b/backend/pub_golf/urls.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.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/requirements.txt b/backend/requirements.txt index 5b5dfc4..83f939e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,18 +1,2 @@ -alembic==1.9.4 -bcrypt==4.0.1 -certifi==2022.12.7 -cffi==1.15.1 -cryptography==39.0.1 -ecdsa==0.18.0 -email-validator==1.3.1 -fastapi==0.92.0 -passlib==1.7.4 -pyasn1==0.4.8 -pycparser==2.21 -pydantic==1.10.5 -python-dotenv==0.21.1 -python-jose==3.3.0 -python-multipart==0.0.5 -rsa==4.9 -SQLAlchemy==2.0.4 -uvicorn==0.20.0 +Django==4.1.7 +python-dotenv==1.0.0 From 3cbb6b600420a02a4cad3f748678552f2fb743e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 16 Mar 2023 11:10:27 +0300 Subject: [PATCH 28/81] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=20?= =?UTF-8?q?=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=20run=5Fserver.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/run_server.py | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 backend/run_server.py diff --git a/backend/run_server.py b/backend/run_server.py deleted file mode 100644 index 630eee1..0000000 --- a/backend/run_server.py +++ /dev/null @@ -1,10 +0,0 @@ -import uvicorn - -if __name__ == '__main__': - uvicorn.run( - 'app.main:app', host='127.0.0.1', - port=8000, - reload=True, - log_level='debug', - reload_dirs=['.'] - ) From cc759c084e2f28fe589cfa30958e169cc156a355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 16 Mar 2023 11:18:58 +0300 Subject: [PATCH 29/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=87=D1=82=D1=8C=20=D0=B4=D0=BB=D1=8F=20DRF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/requirements.txt b/backend/requirements.txt index 83f939e..cbe37e2 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,2 +1,3 @@ Django==4.1.7 +djangorestframework==3.14.0 python-dotenv==1.0.0 From ea99c045cbf1cf93e45c4f6d283fb9a95607c908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 16 Mar 2023 12:12:10 +0300 Subject: [PATCH 30/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pub_golf/settings.py | 4 ++ backend/users/__init__.py | 0 backend/users/admin.py | 10 +++ backend/users/apps.py | 6 ++ backend/users/migrations/0001_initial.py | 78 ++++++++++++++++++++++++ backend/users/migrations/__init__.py | 0 backend/users/models.py | 65 ++++++++++++++++++++ backend/users/validators.py | 15 +++++ 8 files changed, 178 insertions(+) create mode 100644 backend/users/__init__.py create mode 100644 backend/users/admin.py create mode 100644 backend/users/apps.py create mode 100644 backend/users/migrations/0001_initial.py create mode 100644 backend/users/migrations/__init__.py create mode 100644 backend/users/models.py create mode 100644 backend/users/validators.py diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py index 9f99748..2aed412 100644 --- a/backend/pub_golf/settings.py +++ b/backend/pub_golf/settings.py @@ -25,6 +25,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'users.apps.UsersConfig' ] MIDDLEWARE = [ @@ -68,6 +69,9 @@ } +AUTH_USER_MODEL = "users.CustomUser" + + # Password validation AUTH_PASSWORD_VALIDATORS = [ 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..5f07eb1 --- /dev/null +++ b/backend/users/migrations/0001_initial.py @@ -0,0 +1,78 @@ +# Generated by Django 4.1.7 on 2023-03-16 08:33 + +from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.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=10, unique=True)), + ('name', models.CharField(max_length=120)), + ('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': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='CustomerUser', + fields=[ + ('customuser_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('customer_id', models.TextField()), + ('address', models.TextField()), + ('time_zone', models.TextField()), + ], + options={ + 'verbose_name': 'Customer', + }, + bases=('users.customuser',), + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='StoreOwnerUser', + fields=[ + ('customuser_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('balance', models.IntegerField()), + ('stores_owned', models.TextField()), + ], + options={ + 'verbose_name': 'Store Owner', + }, + bases=('users.customuser',), + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] 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..5e9eeee --- /dev/null +++ b/backend/users/models.py @@ -0,0 +1,65 @@ +from django.contrib.auth.models import AbstractUser +from django.core.validators import RegexValidator +from django.db import models + +from users.validators import validate_username + +ROLE_CHOICES = ( + ('user', 'Аутентифицированный пользователь'), + ('company', 'Компания'), + ('admin', 'Администратор'), +) + + +class CustomUser(AbstractUser): + username = models.CharField( + max_length=150, + unique=True, + validators=[validate_username], + verbose_name='Логин', + ) + email = models.EmailField( + unique=True, + max_length=254, + verbose_name='Почта', + ) + role = models.CharField( + max_length=30, + choices=ROLE_CHOICES, + blank=True, + default='user', + verbose_name='Роль', + ) + bio = models.TextField( + blank=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( + verbose_name='Фото' + ) + + class Meta: + verbose_name = 'Пользователь' + verbose_name_plural = 'Пользователи' + + @property + def is_admin(self): + return self.is_superuser or self.role == 'admin' + + @property + def is_company(self): + return self.role == 'company' diff --git a/backend/users/validators.py b/backend/users/validators.py new file mode 100644 index 0000000..ffe3abb --- /dev/null +++ b/backend/users/validators.py @@ -0,0 +1,15 @@ +import re + +from django.core.exceptions import ValidationError + + +def validate_username(value): + if value == 'me': + raise ValidationError( + f'Использовать имя {value} в качестве username запрещено.' + ) + elif re.findall(r'[^\w.@+-]+', value): + raise ValidationError( + 'Required. 150 characters or fewer.' + 'Letters, digits and @/./+/-/_ only.' + ) \ No newline at end of file From f6dc754e52e44c29652cdb6151dedf696f131c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Thu, 16 Mar 2023 12:53:38 +0300 Subject: [PATCH 31/81] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=BD=D0=B0=20Django?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 -- .vscode/settings.json | 4 - backend/alembic.ini | 105 ------------------------- backend/alembic/env.py | 85 --------------------- backend/alembic/script.py.mako | 24 ------ backend/app/database.py | 13 ---- backend/app/main.py | 5 -- backend/app/pubs/__init__.py | 0 backend/app/pubs/models.py | 34 --------- backend/manage.py | 20 +++++ backend/{app => pub_golf}/__init__.py | 0 backend/pub_golf/asgi.py | 7 ++ backend/pub_golf/settings.py | 106 ++++++++++++++++++++++++++ backend/pub_golf/urls.py | 6 ++ backend/pub_golf/wsgi.py | 7 ++ backend/requirements.txt | 8 +- 16 files changed, 148 insertions(+), 282 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 backend/alembic.ini delete mode 100644 backend/alembic/env.py delete mode 100644 backend/alembic/script.py.mako delete mode 100644 backend/app/database.py delete mode 100644 backend/app/main.py delete mode 100644 backend/app/pubs/__init__.py delete mode 100644 backend/app/pubs/models.py create mode 100644 backend/manage.py rename backend/{app => pub_golf}/__init__.py (100%) create mode 100644 backend/pub_golf/asgi.py create mode 100644 backend/pub_golf/settings.py create mode 100644 backend/pub_golf/urls.py create mode 100644 backend/pub_golf/wsgi.py diff --git a/.gitignore b/.gitignore index 341257c..de99b6c 100644 --- a/.gitignore +++ b/.gitignore @@ -155,9 +155,3 @@ cython_debug/ # PyCharm .idea/ - -# Alembic -versions/ - -# db -pub_golf.db \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 33fe63f..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "python.linting.flake8Enabled": true, - "python.linting.enabled": true -} \ No newline at end of file diff --git a/backend/alembic.ini b/backend/alembic.ini deleted file mode 100644 index 081e43d..0000000 --- a/backend/alembic.ini +++ /dev/null @@ -1,105 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts -script_location = alembic - -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file -# for all available tokens -file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to alembic/versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. -# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions - -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os # Use os.pathsep. Default configuration used for new projects. - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -sqlalchemy.url = sqlite:///./pub_golf.db - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py deleted file mode 100644 index fc0e8cb..0000000 --- a/backend/alembic/env.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import sys - -from logging.config import fileConfig - -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.append(BASE_DIR) - -from app.pubs import models -from app.main import Base - -target_metadata = Base.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako deleted file mode 100644 index 55df286..0000000 --- a/backend/alembic/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade() -> None: - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - ${downgrades if downgrades else "pass"} diff --git a/backend/app/database.py b/backend/app/database.py deleted file mode 100644 index 4e00674..0000000 --- a/backend/app/database.py +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy import create_engine, MetaData -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - -SQLALCHEMY_DATABASE_URL = "sqlite:///../pub_golf.db" -# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" - -engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} -) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -Base = declarative_base() diff --git a/backend/app/main.py b/backend/app/main.py deleted file mode 100644 index 296b314..0000000 --- a/backend/app/main.py +++ /dev/null @@ -1,5 +0,0 @@ -from fastapi import FastAPI - -from .database import Base - -app = FastAPI() diff --git a/backend/app/pubs/__init__.py b/backend/app/pubs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/app/pubs/models.py b/backend/app/pubs/models.py deleted file mode 100644 index 14b09f1..0000000 --- a/backend/app/pubs/models.py +++ /dev/null @@ -1,34 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - String, - Boolean, - Text, - ForeignKey, - Table -) -from sqlalchemy.orm import relationship - -from ..database import Base - - -class Pub(Base): - """Модель пабов.""" - - __tablename__ = 'pubs' - - id = Column(Integer, primary_key=True, index=True) - address = Column(Text, nullable=False) - email = Column(String, nullable=False) - phone_number = Column(String, unique=True, nullable=False) - # company = ForeignKey - - -class Alcohol(Base): - """Модель алкоголя.""" - - __tablename__ = 'alcohol' - - id = Column(Integer, primary_key=True, index=True) - name = Column(Text, nullable=False) - cost = Column(Integer, nullable=False) 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/app/__init__.py b/backend/pub_golf/__init__.py similarity index 100% rename from backend/app/__init__.py rename to backend/pub_golf/__init__.py 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..9f99748 --- /dev/null +++ b/backend/pub_golf/settings.py @@ -0,0 +1,106 @@ +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='127.0.0.1').split() + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +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', +] + +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', + } +} + + +# 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 = 'en-us' + +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' diff --git a/backend/pub_golf/urls.py b/backend/pub_golf/urls.py new file mode 100644 index 0000000..dfc7362 --- /dev/null +++ b/backend/pub_golf/urls.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.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/requirements.txt b/backend/requirements.txt index a82e1bc..83f939e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,2 @@ -alembic==1.9.4 -fastapi==0.92.0 -pydantic==1.10.5 -python-dotenv==0.21.1 -SQLAlchemy==2.0.4 -uvicorn==0.20.0 +Django==4.1.7 +python-dotenv==1.0.0 From 9f08e53e138aa50edc8ae17174fe3086e957536c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Thu, 16 Mar 2023 12:55:49 +0300 Subject: [PATCH 32/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20drf=20=D0=B2=20=D0=B7=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/requirements.txt b/backend/requirements.txt index 83f939e..d83b1fe 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,2 +1,3 @@ Django==4.1.7 python-dotenv==1.0.0 +djangorestframework==3.14.0 From 8ecd8db788301b3c57be1a6547b7960a7ef0b14b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Thu, 16 Mar 2023 13:45:01 +0300 Subject: [PATCH 33/81] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20=D0=BF=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 4 +++ backend/pub_golf/settings.py | 1 + backend/pubs/__init__.py | 0 backend/pubs/admin.py | 3 +++ backend/pubs/apps.py | 6 +++++ backend/pubs/migrations/__init__.py | 0 backend/pubs/models.py | 39 +++++++++++++++++++++++++++++ backend/pubs/validators.py | 12 +++++++++ 8 files changed, 65 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 backend/pubs/__init__.py create mode 100644 backend/pubs/admin.py create mode 100644 backend/pubs/apps.py create mode 100644 backend/pubs/migrations/__init__.py create mode 100644 backend/pubs/models.py create mode 100644 backend/pubs/validators.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..33fe63f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.linting.flake8Enabled": true, + "python.linting.enabled": true +} \ No newline at end of file diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py index 9f99748..26d4f0b 100644 --- a/backend/pub_golf/settings.py +++ b/backend/pub_golf/settings.py @@ -25,6 +25,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'pubs.apps.PubsConfig', ] MIDDLEWARE = [ 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..8c38f3f --- /dev/null +++ b/backend/pubs/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. 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/__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..9498b65 --- /dev/null +++ b/backend/pubs/models.py @@ -0,0 +1,39 @@ +from django.db import models + +from pubs.validators import validate_phone + + +class Pub(models.Model): + """Модель паба.""" + + address = models.CharField( + max_length=255, + verbose_name="Адрес" + ) + phone = models.CharField( + max_length=12, + unique=True, + validators=[validate_phone], + verbose_name="Номер телефона" + ) + email = models.EmailField( + unique=True, + verbose_name="Почта" + ) + + # company = models.ForeignKey() + + class Meta: + verbose_name = "Паб" + verbose_name_plural = "Пабы" + + def __str__(self): + return self.address + + +class Alcohol(models.Model): + pass + + +class Menu(models.Model): + pass diff --git a/backend/pubs/validators.py b/backend/pubs/validators.py new file mode 100644 index 0000000..c4daf30 --- /dev/null +++ b/backend/pubs/validators.py @@ -0,0 +1,12 @@ +import re + +from django.core.exceptions import ValidationError + + +def validate_phone(value): + phone_regex = r'^\+7\d{10}$' + if not re.match(phone_regex, value): + raise ValidationError( + "Номер телефона должен быть в формате +70123456789" + ) + return value From b0f4eaa0176b20025a2bbd448cff3c6b9626d4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Thu, 16 Mar 2023 13:46:19 +0300 Subject: [PATCH 34/81] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=20gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index de99b6c..26d0b98 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,5 @@ cython_debug/ # PyCharm .idea/ + +.vscode \ No newline at end of file From 58db135bfb45b5c58c72ecda76e4f91976815ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Thu, 16 Mar 2023 13:47:09 +0300 Subject: [PATCH 35/81] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=20gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 26d0b98..1a79c65 100644 --- a/.gitignore +++ b/.gitignore @@ -156,4 +156,4 @@ cython_debug/ # PyCharm .idea/ -.vscode \ No newline at end of file +.vscode/ \ No newline at end of file From d30b3e50f2808f5411c33526d8c74556b2a91bd5 Mon Sep 17 00:00:00 2001 From: German Date: Thu, 16 Mar 2023 13:47:37 +0300 Subject: [PATCH 36/81] Delete .vscode directory --- .vscode/settings.json | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 33fe63f..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "python.linting.flake8Enabled": true, - "python.linting.enabled": true -} \ No newline at end of file From 1aab52304fdb0e14581be87d70f4490fdc96fddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Thu, 16 Mar 2023 13:51:56 +0300 Subject: [PATCH 37/81] =?UTF-8?q?=D0=9F=D0=B0=D0=B1=D1=8B=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B2=20=D0=B0?= =?UTF-8?q?=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pubs/admin.py | 7 ++++- backend/pubs/migrations/0001_initial.py | 40 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 backend/pubs/migrations/0001_initial.py diff --git a/backend/pubs/admin.py b/backend/pubs/admin.py index 8c38f3f..deef88c 100644 --- a/backend/pubs/admin.py +++ b/backend/pubs/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin -# Register your models here. +from .models import Pub + + +@admin.register(Pub) +class PubAdmin(admin.ModelAdmin): + list_display = ('pk', 'address') diff --git a/backend/pubs/migrations/0001_initial.py b/backend/pubs/migrations/0001_initial.py new file mode 100644 index 0000000..c7fa3c6 --- /dev/null +++ b/backend/pubs/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 4.1.7 on 2023-03-16 10:50 + +from django.db import migrations, models +import pubs.validators + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Alcohol', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='Menu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='Pub', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('address', models.CharField(max_length=255, verbose_name='Адрес')), + ('phone', models.CharField(max_length=12, unique=True, validators=[pubs.validators.validate_phone], verbose_name='Номер телефона')), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='Почта')), + ], + options={ + 'verbose_name': 'Паб', + 'verbose_name_plural': 'Пабы', + }, + ), + ] From 967f1d70e2f52629c713746c5c011e74238e01d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Thu, 16 Mar 2023 14:00:35 +0300 Subject: [PATCH 38/81] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20=D0=B0=D0=BB?= =?UTF-8?q?=D0=BA=D0=BE=D0=B3=D0=BE=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pubs/admin.py | 11 +++++++++-- backend/pubs/models.py | 10 +++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/pubs/admin.py b/backend/pubs/admin.py index deef88c..320a7f3 100644 --- a/backend/pubs/admin.py +++ b/backend/pubs/admin.py @@ -1,8 +1,15 @@ from django.contrib import admin -from .models import Pub +from .models import Pub, Alcohol @admin.register(Pub) class PubAdmin(admin.ModelAdmin): - list_display = ('pk', 'address') + list_display = ('pk', 'address', 'phone') + search_fields = ('address', 'phone') + + +@admin.register(Alcohol) +class AlcoholAdmin(admin.ModelAdmin): + list_display = ('name', 'cost') + search_fields = ('name') diff --git a/backend/pubs/models.py b/backend/pubs/models.py index 9498b65..80320ea 100644 --- a/backend/pubs/models.py +++ b/backend/pubs/models.py @@ -32,7 +32,15 @@ def __str__(self): class Alcohol(models.Model): - pass + """Модель алкоголя.""" + + name = models.CharField( + max_length=50, + verbose_name="Название", + ) + cost = models.PositiveIntegerField( + verbose_name="Цена", + ) class Menu(models.Model): From 871deca36f0a8956dc21a4a90bc9c2a9911ae059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Thu, 16 Mar 2023 14:46:46 +0300 Subject: [PATCH 39/81] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pubs/admin.py | 8 +++-- backend/pubs/migrations/0001_initial.py | 29 ++++++++++++----- .../0002_alter_alcohol_menu_alter_menu_pub.py | 24 ++++++++++++++ backend/pubs/models.py | 31 +++++++++++++++++-- 4 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 backend/pubs/migrations/0002_alter_alcohol_menu_alter_menu_pub.py diff --git a/backend/pubs/admin.py b/backend/pubs/admin.py index 320a7f3..289eea0 100644 --- a/backend/pubs/admin.py +++ b/backend/pubs/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Pub, Alcohol +from .models import Pub, Alcohol, Menu @admin.register(Pub) @@ -11,5 +11,7 @@ class PubAdmin(admin.ModelAdmin): @admin.register(Alcohol) class AlcoholAdmin(admin.ModelAdmin): - list_display = ('name', 'cost') - search_fields = ('name') + list_display = ('name', 'cost', 'menu') + + +admin.site.register(Menu) diff --git a/backend/pubs/migrations/0001_initial.py b/backend/pubs/migrations/0001_initial.py index c7fa3c6..57665dd 100644 --- a/backend/pubs/migrations/0001_initial.py +++ b/backend/pubs/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 4.1.7 on 2023-03-16 10:50 +# Generated by Django 4.1.7 on 2023-03-16 11:37 from django.db import migrations, models +import django.db.models.deletion import pubs.validators @@ -13,28 +14,40 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Alcohol', + name='Pub', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('address', models.CharField(max_length=255, verbose_name='Адрес')), + ('phone', models.CharField(max_length=12, unique=True, validators=[pubs.validators.validate_phone], verbose_name='Номер телефона')), + ('email', models.EmailField(max_length=254, unique=True, 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')), + ('pub', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pubs.pub', verbose_name='Паб')), ], + options={ + 'verbose_name': 'Меню', + 'verbose_name_plural': 'Меню', + }, ), migrations.CreateModel( - name='Pub', + name='Alcohol', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('address', models.CharField(max_length=255, verbose_name='Адрес')), - ('phone', models.CharField(max_length=12, unique=True, validators=[pubs.validators.validate_phone], verbose_name='Номер телефона')), - ('email', models.EmailField(max_length=254, unique=True, verbose_name='Почта')), + ('name', models.CharField(max_length=50, verbose_name='Название')), + ('cost', models.PositiveIntegerField(verbose_name='Цена')), + ('menu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pubs.menu', verbose_name='Меню')), ], options={ - 'verbose_name': 'Паб', - 'verbose_name_plural': 'Пабы', + 'verbose_name': 'Алкоголь', + 'verbose_name_plural': 'Алкоголь', }, ), ] diff --git a/backend/pubs/migrations/0002_alter_alcohol_menu_alter_menu_pub.py b/backend/pubs/migrations/0002_alter_alcohol_menu_alter_menu_pub.py new file mode 100644 index 0000000..ca8aa1f --- /dev/null +++ b/backend/pubs/migrations/0002_alter_alcohol_menu_alter_menu_pub.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.7 on 2023-03-16 11:40 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pubs', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='alcohol', + name='menu', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='alcohols', to='pubs.menu', verbose_name='Меню'), + ), + migrations.AlterField( + model_name='menu', + name='pub', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='menus', to='pubs.pub', verbose_name='Паб'), + ), + ] diff --git a/backend/pubs/models.py b/backend/pubs/models.py index 80320ea..3883734 100644 --- a/backend/pubs/models.py +++ b/backend/pubs/models.py @@ -31,6 +31,24 @@ def __str__(self): return self.address +class Menu(models.Model): + """Модель меню.""" + + pub = models.ForeignKey( + Pub, + on_delete=models.CASCADE, + verbose_name='Паб', + related_name="menus" + ) + + class Meta: + verbose_name = "Меню" + verbose_name_plural = "Меню" + + def __str__(self) -> str: + return self.pub.address + + class Alcohol(models.Model): """Модель алкоголя.""" @@ -41,7 +59,16 @@ class Alcohol(models.Model): cost = models.PositiveIntegerField( verbose_name="Цена", ) + menu = models.ForeignKey( + Menu, + on_delete=models.CASCADE, + verbose_name='Меню', + related_name="alcohols" + ) + class Meta: + verbose_name = "Алкоголь" + verbose_name_plural = "Алкоголь" -class Menu(models.Model): - pass + def __str__(self) -> str: + return self.name From c6217a03a2f48cfbd357ac6384b6fd7a1a13d204 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Wed, 22 Mar 2023 11:57:22 +0300 Subject: [PATCH 40/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20Pillow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/requirements.txt b/backend/requirements.txt index cbe37e2..c8df2ea 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,3 +1,4 @@ Django==4.1.7 djangorestframework==3.14.0 +Pillow==9.4.0 python-dotenv==1.0.0 From 84da4cfa343aede74ee0d06367f32bfea8294cb3 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Wed, 22 Mar 2023 12:46:46 +0300 Subject: [PATCH 41/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/__init__.py | 0 backend/api/apps.py | 5 +++++ backend/api/permissions.py | 0 backend/api/serializers.py | 0 backend/api/views.py | 3 +++ backend/pub_golf/settings.py | 3 ++- backend/pub_golf/urls.py | 3 ++- 7 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 backend/api/__init__.py create mode 100644 backend/api/apps.py create mode 100644 backend/api/permissions.py create mode 100644 backend/api/serializers.py create mode 100644 backend/api/views.py 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/permissions.py b/backend/api/permissions.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/serializers.py b/backend/api/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/views.py b/backend/api/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/api/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py index 2aed412..91dcaa5 100644 --- a/backend/pub_golf/settings.py +++ b/backend/pub_golf/settings.py @@ -25,7 +25,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'users.apps.UsersConfig' + 'users.apps.UsersConfig', + 'api.apps.ApiConfig' ] MIDDLEWARE = [ diff --git a/backend/pub_golf/urls.py b/backend/pub_golf/urls.py index dfc7362..bb23996 100644 --- a/backend/pub_golf/urls.py +++ b/backend/pub_golf/urls.py @@ -1,6 +1,7 @@ from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), + path('api/', include('api.urls')) ] From ff8db97abcae0c8a9ecbf2e7cec1669d0774553e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Wed, 22 Mar 2023 16:33:26 +0300 Subject: [PATCH 42/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BF=D0=B5=D1=80=D0=BC=D0=B8=D1=88=D0=BD?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/permissions.py | 32 ++++++++++ backend/api/urls.py | 0 backend/users/migrations/0001_initial.py | 78 ------------------------ 3 files changed, 32 insertions(+), 78 deletions(-) create mode 100644 backend/api/urls.py delete mode 100644 backend/users/migrations/0001_initial.py diff --git a/backend/api/permissions.py b/backend/api/permissions.py index e69de29..a3fa211 100644 --- a/backend/api/permissions.py +++ b/backend/api/permissions.py @@ -0,0 +1,32 @@ +from rest_framework import permissions + + +class AdminPermission(permissions.BasePermission): + + def has_permission(self, request, view): + return (request.user.is_authenticated + and request.user.is_admin) + + def has_object_permission(self, request, view, obj): + return (request.user.is_authenticated + and request.user.is_admin) + + +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 diff --git a/backend/api/urls.py b/backend/api/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/users/migrations/0001_initial.py b/backend/users/migrations/0001_initial.py deleted file mode 100644 index 5f07eb1..0000000 --- a/backend/users/migrations/0001_initial.py +++ /dev/null @@ -1,78 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-16 08:33 - -from django.conf import settings -import django.contrib.auth.models -import django.contrib.auth.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=10, unique=True)), - ('name', models.CharField(max_length=120)), - ('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': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='CustomerUser', - fields=[ - ('customuser_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), - ('customer_id', models.TextField()), - ('address', models.TextField()), - ('time_zone', models.TextField()), - ], - options={ - 'verbose_name': 'Customer', - }, - bases=('users.customuser',), - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='StoreOwnerUser', - fields=[ - ('customuser_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), - ('balance', models.IntegerField()), - ('stores_owned', models.TextField()), - ], - options={ - 'verbose_name': 'Store Owner', - }, - bases=('users.customuser',), - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - ] From 93f49fcee57718262d517ffad62bff160e42ad2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Wed, 22 Mar 2023 16:48:44 +0300 Subject: [PATCH 43/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20DRF=20=D0=B8=20Djoser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pub_golf/settings.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py index 91dcaa5..6a0fa75 100644 --- a/backend/pub_golf/settings.py +++ b/backend/pub_golf/settings.py @@ -93,7 +93,7 @@ # Internationalization -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = 'ru-ru' TIME_ZONE = 'UTC' @@ -109,3 +109,23 @@ # 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, + 'PERMISSIONS': { + 'user_list': ['api.permissions.ReadOnlyPermission'], + }, +} From 6a5081419d56c56eb3a2f0db1610ce303aeea2d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 23 Mar 2023 17:39:51 +0300 Subject: [PATCH 44/81] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D1=8B=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/pagination.py | 5 + backend/api/permissions.py | 11 -- backend/api/serializers.py | 138 ++++++++++++++++++ backend/api/urls.py | 24 +++ backend/api/views.py | 93 +++++++++++- backend/pub_golf/settings.py | 4 + backend/users/migrations/0001_initial.py | 63 ++++++++ ..._customuser_photo_alter_customuser_role.py | 23 +++ .../migrations/0003_alter_customuser_role.py | 18 +++ .../migrations/0004_alter_customuser_bio.py | 18 +++ backend/users/models.py | 44 ++++-- backend/users/validators.py | 15 -- 12 files changed, 412 insertions(+), 44 deletions(-) create mode 100644 backend/api/pagination.py create mode 100644 backend/users/migrations/0001_initial.py create mode 100644 backend/users/migrations/0002_alter_customuser_photo_alter_customuser_role.py create mode 100644 backend/users/migrations/0003_alter_customuser_role.py create mode 100644 backend/users/migrations/0004_alter_customuser_bio.py delete mode 100644 backend/users/validators.py 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 index a3fa211..5903f32 100644 --- a/backend/api/permissions.py +++ b/backend/api/permissions.py @@ -1,17 +1,6 @@ from rest_framework import permissions -class AdminPermission(permissions.BasePermission): - - def has_permission(self, request, view): - return (request.user.is_authenticated - and request.user.is_admin) - - def has_object_permission(self, request, view, obj): - return (request.user.is_authenticated - and request.user.is_admin) - - class CompanyPermission(permissions.BasePermission): def has_permission(self, request, view): diff --git a/backend/api/serializers.py b/backend/api/serializers.py index e69de29..8e1b163 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -0,0 +1,138 @@ +from djoser.serializers import ( + UserSerializer, + UserCreatePasswordRetypeSerializer, + PasswordRetypeSerializer, + CurrentPasswordSerializer +) +from rest_framework import serializers + +from users.models import CustomUser, Friendship + + +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): + return (self.context['request'].user.is_authenticated + and Friendship.objects.filter( + user=self.context['request'].user, + friend=obj + ).exists()) + + +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 FriendsSerializer(serializers.ModelSerializer): + id = serializers.IntegerField( + source='author.id', + read_only=True, + ) + username = serializers.CharField( + source='author.username', + read_only=True + ) + email = serializers.CharField( + source='author.email', + read_only=True + ) + is_friend = serializers.SerializerMethodField() + + class Meta: + model = Friendship + fields = ( + 'id', + 'username', + 'is_friend', + ) + + 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 Friendship.objects.filter(user=user, friend=friend).exists(): + raise serializers.ValidationError( + 'Пользователь уже есть у Вас в друзьях!') + return data + + def get_is_friend(self, obj): + return Friendship.objects.filter( + user=self.context['request'].user, + friend=obj.friend + ).exists() diff --git a/backend/api/urls.py b/backend/api/urls.py index e69de29..6bcd7c3 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -0,0 +1,24 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter + +from api.views import ( + CustomUserViewSet, + FriendsListViewSet, + FriendsCreateDestroyViewSet +) + +router_v1 = DefaultRouter() + +router_v1.register('users/friends', FriendsListViewSet, basename='friends') +router_v1.register( + r'users/(?P\d+)/friend', + FriendsCreateDestroyViewSet, + basename='add_delete_friend' +) +router_v1.register('users', CustomUserViewSet, basename='users') + +urlpatterns = [ + path('v1/auth/', include('djoser.urls.authtoken')), + path('v1/', include(router_v1.urls)), + path('v1/', include('djoser.urls.base')) +] diff --git a/backend/api/views.py b/backend/api/views.py index 91ea44a..a80e817 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,3 +1,92 @@ -from django.shortcuts import render +from django.shortcuts import get_object_or_404 +from djoser.serializers import SetUsernameSerializer +from djoser.views import UserViewSet +from rest_framework import viewsets, mixins, status, permissions +from rest_framework.decorators import action +from rest_framework.response import Response -# Create your views here. +from api.pagination import GamesAndFriendsPagination +from api.serializers import ( + FriendsSerializer, + CustomUserCreateSerializer, + CustomPasswordSerializer, + CustomUserSerializer, + CustomUserMeSerializer, + SetEmailSerializer +) +from users.models import CustomUser, Friendship + + +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 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] + 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 FriendsBaseViewSet(viewsets.GenericViewSet): + serializer_class = FriendsSerializer + + def get_queryset(self): + return self.request.user.user_friends.all() + + +class FriendsListViewSet(mixins.ListModelMixin, FriendsBaseViewSet): + pagination_class = GamesAndFriendsPagination + + +class FriendsCreateDestroyViewSet( + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + FriendsBaseViewSet +): + + def get_serializer_context(self): + context = super().get_serializer_context() + context['friend_id'] = self.kwargs.get('user_id') + return context + + def perform_create(self, serializer): + serializer.save( + user=self.request.user, + friend=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( + Friendship, + user=request.user, + friend_id=user_id + ).delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py index 6a0fa75..c00860a 100644 --- a/backend/pub_golf/settings.py +++ b/backend/pub_golf/settings.py @@ -25,6 +25,9 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework.authtoken', + 'djoser', 'users.apps.UsersConfig', 'api.apps.ApiConfig' ] @@ -125,6 +128,7 @@ DJOSER = { 'HIDE_USERS': False, + 'SET_USERNAME_RETYPE': True, 'PERMISSIONS': { 'user_list': ['api.permissions.ReadOnlyPermission'], }, diff --git a/backend/users/migrations/0001_initial.py b/backend/users/migrations/0001_initial.py new file mode 100644 index 0000000..0296f8f --- /dev/null +++ b/backend/users/migrations/0001_initial.py @@ -0,0 +1,63 @@ +# Generated by Django 4.1.7 on 2023-03-23 13:28 + +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=[('user', 'Аутентифицированный пользователь'), ('company', 'Компания'), ('admin', 'Администратор')], default='user', max_length=30, verbose_name='Роль')), + ('bio', models.TextField(blank=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(upload_to='', verbose_name='Фото')), + ('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='Friendship', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('friend', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='friend_friends', to=settings.AUTH_USER_MODEL, verbose_name='Друг')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_friends', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ], + options={ + 'verbose_name': 'Друзья', + 'verbose_name_plural': 'Друзья', + }, + ), + ] diff --git a/backend/users/migrations/0002_alter_customuser_photo_alter_customuser_role.py b/backend/users/migrations/0002_alter_customuser_photo_alter_customuser_role.py new file mode 100644 index 0000000..de8f3b3 --- /dev/null +++ b/backend/users/migrations/0002_alter_customuser_photo_alter_customuser_role.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.7 on 2023-03-23 13:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='customuser', + name='photo', + field=models.ImageField(blank=True, null=True, upload_to='', verbose_name='Фото'), + ), + migrations.AlterField( + model_name='customuser', + name='role', + field=models.CharField(blank=True, choices=[('user', 'Аутентифицированный пользователь'), ('company', 'Компания')], default='user', max_length=30, verbose_name='Роль'), + ), + ] diff --git a/backend/users/migrations/0003_alter_customuser_role.py b/backend/users/migrations/0003_alter_customuser_role.py new file mode 100644 index 0000000..09726bf --- /dev/null +++ b/backend/users/migrations/0003_alter_customuser_role.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-03-23 13:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_alter_customuser_photo_alter_customuser_role'), + ] + + operations = [ + migrations.AlterField( + model_name='customuser', + name='role', + field=models.CharField(blank=True, choices=[('player', 'Аутентифицированный пользователь'), ('company', 'Компания')], default='player', max_length=30, verbose_name='Роль'), + ), + ] diff --git a/backend/users/migrations/0004_alter_customuser_bio.py b/backend/users/migrations/0004_alter_customuser_bio.py new file mode 100644 index 0000000..ab88a9e --- /dev/null +++ b/backend/users/migrations/0004_alter_customuser_bio.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-03-23 14:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0003_alter_customuser_role'), + ] + + operations = [ + migrations.AlterField( + model_name='customuser', + name='bio', + field=models.TextField(blank=True, null=True, verbose_name='Доп. информация'), + ), + ] diff --git a/backend/users/models.py b/backend/users/models.py index 5e9eeee..b45fb80 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -2,22 +2,13 @@ from django.core.validators import RegexValidator from django.db import models -from users.validators import validate_username - ROLE_CHOICES = ( - ('user', 'Аутентифицированный пользователь'), - ('company', 'Компания'), - ('admin', 'Администратор'), + ('player', 'Аутентифицированный пользователь'), + ('company', 'Компания') ) class CustomUser(AbstractUser): - username = models.CharField( - max_length=150, - unique=True, - validators=[validate_username], - verbose_name='Логин', - ) email = models.EmailField( unique=True, max_length=254, @@ -27,11 +18,12 @@ class CustomUser(AbstractUser): max_length=30, choices=ROLE_CHOICES, blank=True, - default='user', + default='player', verbose_name='Роль', ) bio = models.TextField( blank=True, + null=True, verbose_name='Доп. информация', ) registered_office = models.CharField( @@ -49,6 +41,8 @@ class CustomUser(AbstractUser): verbose_name='Телефонный номер' ) photo = models.ImageField( + blank=True, + null=True, verbose_name='Фото' ) @@ -56,10 +50,28 @@ class Meta: verbose_name = 'Пользователь' verbose_name_plural = 'Пользователи' - @property - def is_admin(self): - return self.is_superuser or self.role == 'admin' - @property def is_company(self): return self.role == 'company' + + +class Friendship(models.Model): + user = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + related_name='user_friends', + verbose_name='Пользователь' + ) + friend = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + related_name='friend_friends', + verbose_name='Друг' + ) + + class Meta: + verbose_name = 'Друзья' + verbose_name_plural = 'Друзья' + + def __str__(self): + return f'{self.user} --- {self.friend}' diff --git a/backend/users/validators.py b/backend/users/validators.py deleted file mode 100644 index ffe3abb..0000000 --- a/backend/users/validators.py +++ /dev/null @@ -1,15 +0,0 @@ -import re - -from django.core.exceptions import ValidationError - - -def validate_username(value): - if value == 'me': - raise ValidationError( - f'Использовать имя {value} в качестве username запрещено.' - ) - elif re.findall(r'[^\w.@+-]+', value): - raise ValidationError( - 'Required. 150 characters or fewer.' - 'Letters, digits and @/./+/-/_ only.' - ) \ No newline at end of file From cd4f8fa6f66ea5f65e8086d3fddbe8a017c04086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 23 Mar 2023 17:45:00 +0300 Subject: [PATCH 45/81] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D1=8B=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/api/views.py b/backend/api/views.py index a80e817..deebbcb 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,7 +1,6 @@ from django.shortcuts import get_object_or_404 -from djoser.serializers import SetUsernameSerializer from djoser.views import UserViewSet -from rest_framework import viewsets, mixins, status, permissions +from rest_framework import viewsets, mixins, status, permissions, serializers from rest_framework.decorators import action from rest_framework.response import Response @@ -25,7 +24,7 @@ def get_serializer_class(self): elif self.action == 'set_password': return CustomPasswordSerializer elif self.action == 'set_username': - return SetUsernameSerializer + return serializers.SetUsernameSerializer elif self.action == 'set_email': return SetEmailSerializer elif self.action == 'me': @@ -35,6 +34,8 @@ def get_serializer_class(self): 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): From e0a9274242347f1bc0f27814015f4aaa87f6c6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 23 Mar 2023 21:24:07 +0300 Subject: [PATCH 46/81] =?UTF-8?q?=D0=94=D0=B6=D0=BE=D1=81=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/requirements.txt b/backend/requirements.txt index c8df2ea..aa8cc64 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,5 @@ Django==4.1.7 djangorestframework==3.14.0 Pillow==9.4.0 +djoser==2.1.0 python-dotenv==1.0.0 From 556babada1b63870fae6414c6fe0de37e56f0afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 23 Mar 2023 21:24:43 +0300 Subject: [PATCH 47/81] =?UTF-8?q?=D0=9D=D0=BE=D0=B2=D0=BE=D0=B5=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/games/__init__.py | 0 backend/games/admin.py | 3 +++ backend/games/apps.py | 6 ++++++ backend/games/migrations/__init__.py | 0 backend/games/models.py | 3 +++ backend/games/tests.py | 3 +++ backend/games/views.py | 3 +++ 7 files changed, 18 insertions(+) create mode 100644 backend/games/__init__.py create mode 100644 backend/games/admin.py create mode 100644 backend/games/apps.py create mode 100644 backend/games/migrations/__init__.py create mode 100644 backend/games/models.py create mode 100644 backend/games/tests.py create mode 100644 backend/games/views.py 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..8c38f3f --- /dev/null +++ b/backend/games/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. 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/__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..71a8362 --- /dev/null +++ b/backend/games/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/games/tests.py b/backend/games/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/games/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/games/views.py b/backend/games/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/games/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 11b76ea8ef2fcd53643c902306a6b6d8b67a67e4 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Fri, 24 Mar 2023 17:19:06 +0300 Subject: [PATCH 48/81] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=81=D0=B5=D1=80=D0=B8=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=82=D0=BE=D1=80=20=D0=B4=D0=BB=D1=8F=20=D0=B4?= =?UTF-8?q?=D1=80=D1=83=D0=B7=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/serializers.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 8e1b163..eeae028 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -99,15 +99,11 @@ def validate(self, attrs): class FriendsSerializer(serializers.ModelSerializer): id = serializers.IntegerField( - source='author.id', + source='friend.id', read_only=True, ) username = serializers.CharField( - source='author.username', - read_only=True - ) - email = serializers.CharField( - source='author.email', + source='friend.username', read_only=True ) is_friend = serializers.SerializerMethodField() From c474c12f3e9c9f46e9284397d0c89c728857b7d0 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Fri, 24 Mar 2023 17:28:25 +0300 Subject: [PATCH 49/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B0=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=20=D0=B4?= =?UTF-8?q?=D1=80=D1=83=D0=B7=D1=8C=D1=8F=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=B3=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/permissions.py | 11 +++++++++++ backend/api/serializers.py | 4 ++++ backend/api/views.py | 2 ++ 3 files changed, 17 insertions(+) diff --git a/backend/api/permissions.py b/backend/api/permissions.py index 5903f32..d6dfef7 100644 --- a/backend/api/permissions.py +++ b/backend/api/permissions.py @@ -1,6 +1,17 @@ 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): diff --git a/backend/api/serializers.py b/backend/api/serializers.py index eeae028..70607ee 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -122,6 +122,10 @@ def validate(self, data): if user == friend: raise serializers.ValidationError( 'Нельзя добавить в друзья самого себя!') + if friend.is_company: + raise serializers.ValidationError( + 'Нельзя добавить в друзья аккаунт комапнии!' + ) if Friendship.objects.filter(user=user, friend=friend).exists(): raise serializers.ValidationError( 'Пользователь уже есть у Вас в друзьях!') diff --git a/backend/api/views.py b/backend/api/views.py index deebbcb..1b900e3 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from api.pagination import GamesAndFriendsPagination +from api.permissions import PlayerPermission from api.serializers import ( FriendsSerializer, CustomUserCreateSerializer, @@ -56,6 +57,7 @@ def set_email(self, request, *args, **kwargs): class FriendsBaseViewSet(viewsets.GenericViewSet): serializer_class = FriendsSerializer + permission_classes = (PlayerPermission,) def get_queryset(self): return self.request.user.user_friends.all() From 95a330707e51d3d241d77d3ef59e5a349bfed085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8=D0=BD=20=D0=9B=D0=B5?= =?UTF-8?q?=D0=B2?= Date: Fri, 24 Mar 2023 18:15:53 +0300 Subject: [PATCH 50/81] Delete backend/games directory --- backend/games/__init__.py | 0 backend/games/admin.py | 3 --- backend/games/apps.py | 6 ------ backend/games/migrations/__init__.py | 0 backend/games/models.py | 3 --- backend/games/tests.py | 3 --- backend/games/views.py | 3 --- 7 files changed, 18 deletions(-) delete mode 100644 backend/games/__init__.py delete mode 100644 backend/games/admin.py delete mode 100644 backend/games/apps.py delete mode 100644 backend/games/migrations/__init__.py delete mode 100644 backend/games/models.py delete mode 100644 backend/games/tests.py delete mode 100644 backend/games/views.py diff --git a/backend/games/__init__.py b/backend/games/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/games/admin.py b/backend/games/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/backend/games/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/backend/games/apps.py b/backend/games/apps.py deleted file mode 100644 index 1a3efec..0000000 --- a/backend/games/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class GamesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'games' diff --git a/backend/games/migrations/__init__.py b/backend/games/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/games/models.py b/backend/games/models.py deleted file mode 100644 index 71a8362..0000000 --- a/backend/games/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/backend/games/tests.py b/backend/games/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/backend/games/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/games/views.py b/backend/games/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/backend/games/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. From 4b238bc69f6858febc2e78c4e1cf5a109070afb2 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Fri, 24 Mar 2023 19:10:20 +0300 Subject: [PATCH 51/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=B3=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/games/__init__.py | 0 backend/games/admin.py | 3 ++ backend/games/apps.py | 6 +++ backend/games/migrations/__init__.py | 0 backend/games/models.py | 70 ++++++++++++++++++++++++++++ backend/pub_golf/settings.py | 3 +- 6 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 backend/games/__init__.py create mode 100644 backend/games/admin.py create mode 100644 backend/games/apps.py create mode 100644 backend/games/migrations/__init__.py create mode 100644 backend/games/models.py 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..8c38f3f --- /dev/null +++ b/backend/games/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. 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/__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..70baa60 --- /dev/null +++ b/backend/games/models.py @@ -0,0 +1,70 @@ +from django.db import models + +from users.models import CustomUser + +DIFFICULTY_LEVELS = ( + ('', 'Аутентифицированный пользователь'), + ('company', 'Компания') +) +BUDGET_LEVELS = ( + () +) +GAME_STATUSES = ( + ('created', 'Создана комната'), + ('started', 'Игра стартовала'), + ('finished', 'Игра завершилась') +) + + +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, + verbose_name='Статус игры' + ) + start_time = models.DateTimeField( + blank=True, + null=True, + verbose_name='Время старта игры' + ) + finish_time = models.DateTimeField( + blank=True, + null=True, + 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 + ) + game = models.ForeignKey( + Game, + on_delete=models.CASCADE + ) + + def __str__(self): + return f'{self.user} --- {self.game}' diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py index d78a960..a7775c3 100644 --- a/backend/pub_golf/settings.py +++ b/backend/pub_golf/settings.py @@ -29,7 +29,8 @@ 'rest_framework.authtoken', 'djoser', 'users.apps.UsersConfig', - 'api.apps.ApiConfig' + 'api.apps.ApiConfig', + 'games.apps.GamesConfig', ] MIDDLEWARE = [ From 3877c0da4d0710c1cec118a8b90f3ed2a419dccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Sat, 25 Mar 2023 11:44:19 +0300 Subject: [PATCH 52/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20?= =?UTF-8?q?=D1=8D=D1=82=D0=B0=D0=BF=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/games/models.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/backend/games/models.py b/backend/games/models.py index 70baa60..07b24d5 100644 --- a/backend/games/models.py +++ b/backend/games/models.py @@ -3,11 +3,14 @@ from users.models import CustomUser DIFFICULTY_LEVELS = ( - ('', 'Аутентифицированный пользователь'), - ('company', 'Компания') + ('underbeerman', 'Подпивасник'), + ('', ''), + ('', '') ) BUDGET_LEVELS = ( - () + ('', ''), + ('', ''), + ('', '') ) GAME_STATUSES = ( ('created', 'Создана комната'), @@ -68,3 +71,14 @@ class GameUser(models.Model): def __str__(self): return f'{self.user} --- {self.game}' + + +class Stage(models.Model): + game = models.ForeignKey( + Game, + on_delete=models.CASCADE + ) + # pub = models.ForeignKey( + # Pub, + # on_delete=models.CASCADE + # ) From c1f41bb47d399763b84215367e1428fb2410f410 Mon Sep 17 00:00:00 2001 From: German Date: Fri, 31 Mar 2023 19:02:13 +0300 Subject: [PATCH 53/81] Update settings.py --- backend/pub_golf/settings.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py index f0c06c6..560daea 100644 --- a/backend/pub_golf/settings.py +++ b/backend/pub_golf/settings.py @@ -25,15 +25,12 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', -<<<<<<< HEAD 'rest_framework', 'rest_framework.authtoken', 'djoser', 'users.apps.UsersConfig', 'api.apps.ApiConfig' -======= 'pubs.apps.PubsConfig', ->>>>>>> origin/backend-pubs ] MIDDLEWARE = [ @@ -77,11 +74,9 @@ } -<<<<<<< HEAD + AUTH_USER_MODEL = "users.CustomUser" -======= ->>>>>>> origin/backend-pubs # Password validation AUTH_PASSWORD_VALIDATORS = [ @@ -102,11 +97,7 @@ # Internationalization -<<<<<<< HEAD LANGUAGE_CODE = 'ru-ru' -======= -LANGUAGE_CODE = 'en-us' ->>>>>>> origin/backend-pubs TIME_ZONE = 'UTC' @@ -122,7 +113,6 @@ # Default primary key field type DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -<<<<<<< HEAD # DRF @@ -144,5 +134,3 @@ 'user_list': ['api.permissions.ReadOnlyPermission'], }, } -======= ->>>>>>> origin/backend-pubs From b19c0fed6066bab106ce7e6d27df0d0c5acec76b Mon Sep 17 00:00:00 2001 From: German Date: Fri, 31 Mar 2023 19:02:40 +0300 Subject: [PATCH 54/81] Update requirements.txt --- backend/requirements.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 5947f31..aa8cc64 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,10 +1,5 @@ Django==4.1.7 -<<<<<<< HEAD djangorestframework==3.14.0 Pillow==9.4.0 djoser==2.1.0 python-dotenv==1.0.0 -======= -python-dotenv==1.0.0 -djangorestframework==3.14.0 ->>>>>>> origin/backend-pubs From 3fcec0dbed1c8da06c92953c023e06de60bdeed0 Mon Sep 17 00:00:00 2001 From: German Date: Fri, 31 Mar 2023 19:03:11 +0300 Subject: [PATCH 55/81] Update .gitignore --- .gitignore | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitignore b/.gitignore index 6205255..de99b6c 100644 --- a/.gitignore +++ b/.gitignore @@ -155,8 +155,3 @@ cython_debug/ # PyCharm .idea/ -<<<<<<< HEAD -======= - -.vscode/ ->>>>>>> origin/backend-pubs From e49c10325be20c1567cfe831182ea55009741097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Mon, 3 Apr 2023 16:09:28 +0300 Subject: [PATCH 56/81] added user in pubs --- backend/pub_golf/settings.py | 2 +- backend/pub_golf/urls.py | 7 ------- backend/pubs/admin.py | 2 +- backend/pubs/migrations/0003_pub_company.py | 22 +++++++++++++++++++++ backend/pubs/models.py | 12 ++++++++--- 5 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 backend/pubs/migrations/0003_pub_company.py diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py index 560daea..89c41a6 100644 --- a/backend/pub_golf/settings.py +++ b/backend/pub_golf/settings.py @@ -29,7 +29,7 @@ 'rest_framework.authtoken', 'djoser', 'users.apps.UsersConfig', - 'api.apps.ApiConfig' + 'api.apps.ApiConfig', 'pubs.apps.PubsConfig', ] diff --git a/backend/pub_golf/urls.py b/backend/pub_golf/urls.py index 9abb744..3c9938d 100644 --- a/backend/pub_golf/urls.py +++ b/backend/pub_golf/urls.py @@ -1,15 +1,8 @@ from django.contrib import admin -<<<<<<< HEAD from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('api.urls')) -======= -from django.urls import path - -urlpatterns = [ - path('admin/', admin.site.urls), ->>>>>>> origin/backend-pubs ] diff --git a/backend/pubs/admin.py b/backend/pubs/admin.py index 289eea0..c071430 100644 --- a/backend/pubs/admin.py +++ b/backend/pubs/admin.py @@ -5,7 +5,7 @@ @admin.register(Pub) class PubAdmin(admin.ModelAdmin): - list_display = ('pk', 'address', 'phone') + list_display = ('pk', 'address', 'phone', 'company') search_fields = ('address', 'phone') diff --git a/backend/pubs/migrations/0003_pub_company.py b/backend/pubs/migrations/0003_pub_company.py new file mode 100644 index 0000000..c5ad58f --- /dev/null +++ b/backend/pubs/migrations/0003_pub_company.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.7 on 2023-04-03 12:58 + +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), + ('pubs', '0002_alter_alcohol_menu_alter_menu_pub'), + ] + + operations = [ + migrations.AddField( + model_name='pub', + name='company', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='pubs', to=settings.AUTH_USER_MODEL, verbose_name='Компания'), + preserve_default=False, + ), + ] diff --git a/backend/pubs/models.py b/backend/pubs/models.py index 3883734..a8c0f9f 100644 --- a/backend/pubs/models.py +++ b/backend/pubs/models.py @@ -1,6 +1,7 @@ from django.db import models from pubs.validators import validate_phone +from users.models import CustomUser class Pub(models.Model): @@ -21,14 +22,19 @@ class Pub(models.Model): verbose_name="Почта" ) - # company = models.ForeignKey() + company = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + verbose_name="Компания", + related_name="pubs" + ) class Meta: verbose_name = "Паб" verbose_name_plural = "Пабы" def __str__(self): - return self.address + return self.company class Menu(models.Model): @@ -46,7 +52,7 @@ class Meta: verbose_name_plural = "Меню" def __str__(self) -> str: - return self.pub.address + return self.pub.company class Alcohol(models.Model): From 891fca7e631402cbc8434139e12058931fd7c156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Mon, 3 Apr 2023 16:40:44 +0300 Subject: [PATCH 57/81] Added Pub in Serializers --- backend/api/serializers.py | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 70607ee..4f0cff0 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -7,6 +7,7 @@ from rest_framework import serializers from users.models import CustomUser, Friendship +from pubs.models import Pub, Menu, Alcohol class CustomUserCreateSerializer(UserCreatePasswordRetypeSerializer): @@ -136,3 +137,49 @@ def get_is_friend(self, obj): user=self.context['request'].user, friend=obj.friend ).exists() + + +class PubSerializer(serializers.ModelSerializer): + company = serializers.SlugRelatedField( + slug_field='username', + read_only=True + ) + + class Meta: + model = Pub + fields = ( + 'id', + 'company', + 'address', + 'phone', + 'email' + ) + + +class MenuSerializer(serializers.ModelSerializer): + + class Meta: + model = Menu + fields = ( + 'id', + 'pub' + ) + + +class AlcoholSerializer(serializers.ModelSerializer): + + class Meta: + model = Alcohol + fields = ( + 'id', + 'name', + 'cost', + 'menu' + ) + + def validate_cost(self, cost): + if cost < 0: + raise serializers.ValidationError( + 'Цена не может быть меньше 0.' + ) + return cost From 6d97ec7a1865e5d59fff7e8aae0fa972cf204855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Tue, 11 Apr 2023 15:43:52 +0300 Subject: [PATCH 58/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B4=D1=80=D1=83=D0=B6=D0=B1=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0005_friendship_accepted.py | 18 ++++++++++++++++++ backend/users/models.py | 7 ++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 backend/users/migrations/0005_friendship_accepted.py diff --git a/backend/users/migrations/0005_friendship_accepted.py b/backend/users/migrations/0005_friendship_accepted.py new file mode 100644 index 0000000..4d41159 --- /dev/null +++ b/backend/users/migrations/0005_friendship_accepted.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-04-11 12:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0004_alter_customuser_bio'), + ] + + operations = [ + migrations.AddField( + model_name='friendship', + name='accepted', + field=models.BooleanField(blank=True, default=False, verbose_name='Статус подтверждения'), + ), + ] diff --git a/backend/users/models.py b/backend/users/models.py index b45fb80..71cdeb1 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -68,10 +68,15 @@ class Friendship(models.Model): related_name='friend_friends', verbose_name='Друг' ) + accepted = models.BooleanField( + blank=True, + default=False, + verbose_name='Статус подтверждения' + ) class Meta: verbose_name = 'Друзья' verbose_name_plural = 'Друзья' def __str__(self): - return f'{self.user} --- {self.friend}' + return f'{self.user} --- {self.friend}: {self.accepted}' From 5aabf9139fa93baa3b8dfa5c44f971b0ac7433dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Fri, 14 Apr 2023 12:21:27 +0300 Subject: [PATCH 59/81] added pubs views --- backend/api/permissions.py | 8 ++++++++ backend/api/urls.py | 4 +++- backend/api/views.py | 16 ++++++++++++++-- backend/pubs/admin.py | 4 +++- backend/pubs/models.py | 6 +++--- 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/backend/api/permissions.py b/backend/api/permissions.py index d6dfef7..d280195 100644 --- a/backend/api/permissions.py +++ b/backend/api/permissions.py @@ -30,3 +30,11 @@ def has_permission(self, request, view): 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 diff --git a/backend/api/urls.py b/backend/api/urls.py index 6bcd7c3..f007195 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -4,7 +4,8 @@ from api.views import ( CustomUserViewSet, FriendsListViewSet, - FriendsCreateDestroyViewSet + FriendsCreateDestroyViewSet, + PubViewSet, ) router_v1 = DefaultRouter() @@ -16,6 +17,7 @@ basename='add_delete_friend' ) router_v1.register('users', CustomUserViewSet, basename='users') +router_v1.register('pubs', PubViewSet, basename='pubs') urlpatterns = [ path('v1/auth/', include('djoser.urls.authtoken')), diff --git a/backend/api/views.py b/backend/api/views.py index 1b900e3..1a8987a 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -5,16 +5,18 @@ from rest_framework.response import Response from api.pagination import GamesAndFriendsPagination -from api.permissions import PlayerPermission +from api.permissions import PlayerPermission, IsOwnerOrReadOnly from api.serializers import ( FriendsSerializer, CustomUserCreateSerializer, CustomPasswordSerializer, CustomUserSerializer, CustomUserMeSerializer, - SetEmailSerializer + SetEmailSerializer, + PubSerializer ) from users.models import CustomUser, Friendship +from pubs.models import Pub, Alcohol, Menu class CustomUserViewSet(UserViewSet): @@ -93,3 +95,13 @@ def delete(self, request, user_id): friend_id=user_id ).delete() 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) diff --git a/backend/pubs/admin.py b/backend/pubs/admin.py index c071430..15d00b5 100644 --- a/backend/pubs/admin.py +++ b/backend/pubs/admin.py @@ -14,4 +14,6 @@ class AlcoholAdmin(admin.ModelAdmin): list_display = ('name', 'cost', 'menu') -admin.site.register(Menu) +@admin.register(Menu) +class MenuAdmin(admin.ModelAdmin): + list_display = ('pub',) diff --git a/backend/pubs/models.py b/backend/pubs/models.py index a8c0f9f..a5f34b4 100644 --- a/backend/pubs/models.py +++ b/backend/pubs/models.py @@ -34,7 +34,7 @@ class Meta: verbose_name_plural = "Пабы" def __str__(self): - return self.company + return self.company.username class Menu(models.Model): @@ -51,8 +51,8 @@ class Meta: verbose_name = "Меню" verbose_name_plural = "Меню" - def __str__(self) -> str: - return self.pub.company + def __str__(self): + return self.pub.company.username class Alcohol(models.Model): From 29e81bf27ee2882f22e15bb50e51eee4b8bceb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Wed, 19 Apr 2023 19:41:03 +0300 Subject: [PATCH 60/81] Created Pub and Menu viewsets --- backend/api/permissions.py | 8 +++ backend/api/serializers.py | 29 ++++++----- backend/api/urls.py | 4 ++ backend/api/views.py | 33 ++++++++++-- backend/pubs/admin.py | 9 +--- ...nu_alcohol_menu_cost_menu_name_and_more.py | 52 +++++++++++++++++++ .../migrations/0003_menu_id_alter_menu_pub.py | 25 +++++++++ backend/pubs/models.py | 31 +++-------- 8 files changed, 145 insertions(+), 46 deletions(-) create mode 100644 backend/pubs/migrations/0002_remove_menu_id_menu_alcohol_menu_cost_menu_name_and_more.py create mode 100644 backend/pubs/migrations/0003_menu_id_alter_menu_pub.py diff --git a/backend/api/permissions.py b/backend/api/permissions.py index d280195..334ca0b 100644 --- a/backend/api/permissions.py +++ b/backend/api/permissions.py @@ -38,3 +38,11 @@ def has_object_permission(self, request, view, obj): 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 index 4f0cff0..01abf68 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -7,7 +7,7 @@ from rest_framework import serializers from users.models import CustomUser, Friendship -from pubs.models import Pub, Menu, Alcohol +from pubs.models import Pub, Menu class CustomUserCreateSerializer(UserCreatePasswordRetypeSerializer): @@ -158,23 +158,19 @@ class Meta: class MenuSerializer(serializers.ModelSerializer): - class Meta: - model = Menu - fields = ( - 'id', - 'pub' - ) - - -class AlcoholSerializer(serializers.ModelSerializer): + pub = serializers.IntegerField( + source='pub.id', + read_only=True + ) class Meta: - model = Alcohol + model = Menu fields = ( 'id', + 'pub', 'name', - 'cost', - 'menu' + 'alcohol', + 'cost' ) def validate_cost(self, cost): @@ -183,3 +179,10 @@ def validate_cost(self, cost): 'Цена не может быть меньше 0.' ) return cost + + def validate_alcohol(self, alcohol): + if alcohol > 100: + raise serializers.ValidationError( + 'Процент содержания спирта не может быть больше 100%.' + ) + return alcohol diff --git a/backend/api/urls.py b/backend/api/urls.py index f007195..e036cd7 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -6,6 +6,7 @@ FriendsListViewSet, FriendsCreateDestroyViewSet, PubViewSet, + MenuViewSet, ) router_v1 = DefaultRouter() @@ -18,6 +19,9 @@ ) 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' +) urlpatterns = [ path('v1/auth/', include('djoser.urls.authtoken')), diff --git a/backend/api/views.py b/backend/api/views.py index 1a8987a..0e04581 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from api.pagination import GamesAndFriendsPagination -from api.permissions import PlayerPermission, IsOwnerOrReadOnly +from api.permissions import PlayerPermission, IsOwnerOrReadOnly, IsPubOwnerOrReadOnly from api.serializers import ( FriendsSerializer, CustomUserCreateSerializer, @@ -13,10 +13,11 @@ CustomUserSerializer, CustomUserMeSerializer, SetEmailSerializer, - PubSerializer + PubSerializer, + MenuSerializer, ) from users.models import CustomUser, Friendship -from pubs.models import Pub, Alcohol, Menu +from pubs.models import Pub, Menu class CustomUserViewSet(UserViewSet): @@ -105,3 +106,29 @@ class PubViewSet(viewsets.ModelViewSet): 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): + pub_id = self.kwargs['pub_id'] + return Menu.objects.filter(pub=pub_id) + + def retrieve(self, request, pk=None, pub_id=None): + menu = self.get_object() + serializer = self.get_serializer(menu) + return Response(serializer.data) + + def perform_create(self, serializer): + user = self.request.user + pub = Pub.objects.get(company=user) + serializer.save(pub=pub) diff --git a/backend/pubs/admin.py b/backend/pubs/admin.py index 15d00b5..b1161a4 100644 --- a/backend/pubs/admin.py +++ b/backend/pubs/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Pub, Alcohol, Menu +from .models import Pub, Menu @admin.register(Pub) @@ -9,11 +9,6 @@ class PubAdmin(admin.ModelAdmin): search_fields = ('address', 'phone') -@admin.register(Alcohol) -class AlcoholAdmin(admin.ModelAdmin): - list_display = ('name', 'cost', 'menu') - - @admin.register(Menu) class MenuAdmin(admin.ModelAdmin): - list_display = ('pub',) + list_display = ('pk', 'pub', 'name', 'cost') diff --git a/backend/pubs/migrations/0002_remove_menu_id_menu_alcohol_menu_cost_menu_name_and_more.py b/backend/pubs/migrations/0002_remove_menu_id_menu_alcohol_menu_cost_menu_name_and_more.py new file mode 100644 index 0000000..fd71296 --- /dev/null +++ b/backend/pubs/migrations/0002_remove_menu_id_menu_alcohol_menu_cost_menu_name_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.1.7 on 2023-04-19 15:08 + +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), + ('pubs', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='menu', + name='id', + ), + migrations.AddField( + model_name='menu', + name='alcohol', + field=models.PositiveIntegerField(default=20, verbose_name='Процент спирта'), + preserve_default=False, + ), + migrations.AddField( + model_name='menu', + name='cost', + field=models.PositiveIntegerField(default=100, verbose_name='Цена'), + preserve_default=False, + ), + migrations.AddField( + model_name='menu', + name='name', + field=models.CharField(default='sidr', max_length=50, verbose_name='Название'), + preserve_default=False, + ), + migrations.AddField( + model_name='pub', + name='company', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='pubs', to=settings.AUTH_USER_MODEL, verbose_name='Компания'), + preserve_default=False, + ), + migrations.AlterField( + model_name='menu', + name='pub', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='menus', serialize=False, to='pubs.pub', verbose_name='Паб'), + ), + migrations.DeleteModel( + name='Alcohol', + ), + ] diff --git a/backend/pubs/migrations/0003_menu_id_alter_menu_pub.py b/backend/pubs/migrations/0003_menu_id_alter_menu_pub.py new file mode 100644 index 0000000..b996945 --- /dev/null +++ b/backend/pubs/migrations/0003_menu_id_alter_menu_pub.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.7 on 2023-04-19 15:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pubs', '0002_remove_menu_id_menu_alcohol_menu_cost_menu_name_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='menu', + name='id', + field=models.BigAutoField(auto_created=True, default=1, primary_key=True, serialize=False, verbose_name='ID'), + preserve_default=False, + ), + migrations.AlterField( + model_name='menu', + name='pub', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='menus', to='pubs.pub', verbose_name='Паб'), + ), + ] diff --git a/backend/pubs/models.py b/backend/pubs/models.py index a5f34b4..a49676f 100644 --- a/backend/pubs/models.py +++ b/backend/pubs/models.py @@ -44,37 +44,22 @@ class Menu(models.Model): Pub, on_delete=models.CASCADE, verbose_name='Паб', - related_name="menus" + related_name="menus", ) - - class Meta: - verbose_name = "Меню" - verbose_name_plural = "Меню" - - def __str__(self): - return self.pub.company.username - - -class Alcohol(models.Model): - """Модель алкоголя.""" - name = models.CharField( max_length=50, verbose_name="Название", ) + alcohol = models.PositiveIntegerField( + verbose_name="Процент спирта", + ) cost = models.PositiveIntegerField( verbose_name="Цена", ) - menu = models.ForeignKey( - Menu, - on_delete=models.CASCADE, - verbose_name='Меню', - related_name="alcohols" - ) class Meta: - verbose_name = "Алкоголь" - verbose_name_plural = "Алкоголь" + verbose_name = "Меню" + verbose_name_plural = "Меню" - def __str__(self) -> str: - return self.name + def __str__(self): + return self.pub.company.username From f1cc3c1e9ab768a0d103d70d9b7ad1d11f6b0f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Wed, 19 Apr 2023 19:42:03 +0300 Subject: [PATCH 61/81] Created Pub and Menu viewsets --- backend/api/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/api/views.py b/backend/api/views.py index 0e04581..52f5204 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -5,7 +5,11 @@ from rest_framework.response import Response from api.pagination import GamesAndFriendsPagination -from api.permissions import PlayerPermission, IsOwnerOrReadOnly, IsPubOwnerOrReadOnly +from api.permissions import ( + PlayerPermission, + IsOwnerOrReadOnly, + IsPubOwnerOrReadOnly +) from api.serializers import ( FriendsSerializer, CustomUserCreateSerializer, @@ -116,6 +120,7 @@ class MenuViewSet( mixins.UpdateModelMixin, viewsets.GenericViewSet ): + serializer_class = MenuSerializer permission_classes = (IsPubOwnerOrReadOnly, ) From fa1557604b9f075fab804b0aa8dfe1d1ed7793ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Wed, 19 Apr 2023 19:58:00 +0300 Subject: [PATCH 62/81] added validation of name in menu --- backend/api/serializers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 01abf68..6282aa0 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -173,6 +173,16 @@ class Meta: 'cost' ) + def validate_name(self, name): + user = CustomUser.objects.get(id=self.context['request'].user.id) + pub = Pub.objects.get(company=user) + if Menu.objects.filter(pub=pub, name=name).exists(): + raise serializers.ValidationError( + 'Такой алкоголь уже есть в меню.' + ) + return name + + def validate_cost(self, cost): if cost < 0: raise serializers.ValidationError( From 158e449afa37debedb028a631aceae0f8e71abd2 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Wed, 26 Apr 2023 11:31:00 +0300 Subject: [PATCH 63/81] =?UTF-8?q?=D0=94=D0=BE=D0=BF=D0=B8=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D0=B0=D0=B1=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/serializers.py | 29 +++++----- backend/pubs/admin.py | 9 ++-- backend/pubs/migrations/0001_initial.py | 29 ++++------ .../0002_alter_alcohol_menu_alter_menu_pub.py | 24 --------- ...nu_alcohol_menu_cost_menu_name_and_more.py | 52 ------------------ .../migrations/0003_menu_id_alter_menu_pub.py | 25 --------- backend/pubs/migrations/0003_pub_company.py | 22 -------- backend/pubs/models.py | 53 ++++++++----------- backend/pubs/validators.py | 12 ----- 9 files changed, 51 insertions(+), 204 deletions(-) delete mode 100644 backend/pubs/migrations/0002_alter_alcohol_menu_alter_menu_pub.py delete mode 100644 backend/pubs/migrations/0002_remove_menu_id_menu_alcohol_menu_cost_menu_name_and_more.py delete mode 100644 backend/pubs/migrations/0003_menu_id_alter_menu_pub.py delete mode 100644 backend/pubs/migrations/0003_pub_company.py delete mode 100644 backend/pubs/validators.py diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 6282aa0..7a33557 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -149,15 +149,13 @@ class Meta: model = Pub fields = ( 'id', - 'company', - 'address', - 'phone', - 'email' + 'name', + 'pub_address', + 'company' ) class MenuSerializer(serializers.ModelSerializer): - pub = serializers.IntegerField( source='pub.id', read_only=True @@ -167,21 +165,20 @@ class Meta: model = Menu fields = ( 'id', - 'pub', - 'name', - 'alcohol', - 'cost' + 'alcohol_name', + 'alcohol_percent', + 'cost', + 'pub' ) - def validate_name(self, name): + def validate_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, name=name).exists(): + if Menu.objects.filter(pub=pub, alcohol_name=alcohol_name).exists(): raise serializers.ValidationError( 'Такой алкоголь уже есть в меню.' ) - return name - + return alcohol_name def validate_cost(self, cost): if cost < 0: @@ -190,9 +187,9 @@ def validate_cost(self, cost): ) return cost - def validate_alcohol(self, alcohol): - if alcohol > 100: + def validate_alcohol_percent(self, alcohol_percent): + if alcohol_percent > 100: raise serializers.ValidationError( 'Процент содержания спирта не может быть больше 100%.' ) - return alcohol + return alcohol_percent diff --git a/backend/pubs/admin.py b/backend/pubs/admin.py index b1161a4..5a5177a 100644 --- a/backend/pubs/admin.py +++ b/backend/pubs/admin.py @@ -5,10 +5,13 @@ @admin.register(Pub) class PubAdmin(admin.ModelAdmin): - list_display = ('pk', 'address', 'phone', 'company') - search_fields = ('address', 'phone') + list_display = ('name', 'pub_address', 'company') + search_fields = ('pub_address',) + list_filter = ('company__username',) @admin.register(Menu) class MenuAdmin(admin.ModelAdmin): - list_display = ('pk', 'pub', 'name', 'cost') + list_display = ('pk', 'alcohol_name', 'alcohol_percent', 'cost', 'pub') + search_fields = ('alcohol_name',) + list_filter = ('pub__name', 'alcohol_percent') diff --git a/backend/pubs/migrations/0001_initial.py b/backend/pubs/migrations/0001_initial.py index 57665dd..ad5fb75 100644 --- a/backend/pubs/migrations/0001_initial.py +++ b/backend/pubs/migrations/0001_initial.py @@ -1,8 +1,8 @@ -# Generated by Django 4.1.7 on 2023-03-16 11:37 +# Generated by Django 4.1.7 on 2023-04-26 08:29 +from django.conf import settings from django.db import migrations, models import django.db.models.deletion -import pubs.validators class Migration(migrations.Migration): @@ -10,6 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -17,9 +18,9 @@ class Migration(migrations.Migration): name='Pub', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('address', models.CharField(max_length=255, verbose_name='Адрес')), - ('phone', models.CharField(max_length=12, unique=True, validators=[pubs.validators.validate_phone], verbose_name='Номер телефона')), - ('email', models.EmailField(max_length=254, unique=True, verbose_name='Почта')), + ('name', models.CharField(max_length=255)), + ('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': 'Паб', @@ -30,24 +31,14 @@ class Migration(migrations.Migration): name='Menu', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('pub', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pubs.pub', verbose_name='Паб')), + ('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': 'Меню', }, ), - migrations.CreateModel( - name='Alcohol', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, verbose_name='Название')), - ('cost', models.PositiveIntegerField(verbose_name='Цена')), - ('menu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pubs.menu', verbose_name='Меню')), - ], - options={ - 'verbose_name': 'Алкоголь', - 'verbose_name_plural': 'Алкоголь', - }, - ), ] diff --git a/backend/pubs/migrations/0002_alter_alcohol_menu_alter_menu_pub.py b/backend/pubs/migrations/0002_alter_alcohol_menu_alter_menu_pub.py deleted file mode 100644 index ca8aa1f..0000000 --- a/backend/pubs/migrations/0002_alter_alcohol_menu_alter_menu_pub.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-16 11:40 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('pubs', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='alcohol', - name='menu', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='alcohols', to='pubs.menu', verbose_name='Меню'), - ), - migrations.AlterField( - model_name='menu', - name='pub', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='menus', to='pubs.pub', verbose_name='Паб'), - ), - ] diff --git a/backend/pubs/migrations/0002_remove_menu_id_menu_alcohol_menu_cost_menu_name_and_more.py b/backend/pubs/migrations/0002_remove_menu_id_menu_alcohol_menu_cost_menu_name_and_more.py deleted file mode 100644 index fd71296..0000000 --- a/backend/pubs/migrations/0002_remove_menu_id_menu_alcohol_menu_cost_menu_name_and_more.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.1.7 on 2023-04-19 15:08 - -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), - ('pubs', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='menu', - name='id', - ), - migrations.AddField( - model_name='menu', - name='alcohol', - field=models.PositiveIntegerField(default=20, verbose_name='Процент спирта'), - preserve_default=False, - ), - migrations.AddField( - model_name='menu', - name='cost', - field=models.PositiveIntegerField(default=100, verbose_name='Цена'), - preserve_default=False, - ), - migrations.AddField( - model_name='menu', - name='name', - field=models.CharField(default='sidr', max_length=50, verbose_name='Название'), - preserve_default=False, - ), - migrations.AddField( - model_name='pub', - name='company', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='pubs', to=settings.AUTH_USER_MODEL, verbose_name='Компания'), - preserve_default=False, - ), - migrations.AlterField( - model_name='menu', - name='pub', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='menus', serialize=False, to='pubs.pub', verbose_name='Паб'), - ), - migrations.DeleteModel( - name='Alcohol', - ), - ] diff --git a/backend/pubs/migrations/0003_menu_id_alter_menu_pub.py b/backend/pubs/migrations/0003_menu_id_alter_menu_pub.py deleted file mode 100644 index b996945..0000000 --- a/backend/pubs/migrations/0003_menu_id_alter_menu_pub.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.1.7 on 2023-04-19 15:11 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('pubs', '0002_remove_menu_id_menu_alcohol_menu_cost_menu_name_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='menu', - name='id', - field=models.BigAutoField(auto_created=True, default=1, primary_key=True, serialize=False, verbose_name='ID'), - preserve_default=False, - ), - migrations.AlterField( - model_name='menu', - name='pub', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='menus', to='pubs.pub', verbose_name='Паб'), - ), - ] diff --git a/backend/pubs/migrations/0003_pub_company.py b/backend/pubs/migrations/0003_pub_company.py deleted file mode 100644 index c5ad58f..0000000 --- a/backend/pubs/migrations/0003_pub_company.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 4.1.7 on 2023-04-03 12:58 - -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), - ('pubs', '0002_alter_alcohol_menu_alter_menu_pub'), - ] - - operations = [ - migrations.AddField( - model_name='pub', - name='company', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='pubs', to=settings.AUTH_USER_MODEL, verbose_name='Компания'), - preserve_default=False, - ), - ] diff --git a/backend/pubs/models.py b/backend/pubs/models.py index a49676f..8ebe406 100644 --- a/backend/pubs/models.py +++ b/backend/pubs/models.py @@ -1,37 +1,28 @@ from django.db import models -from pubs.validators import validate_phone from users.models import CustomUser class Pub(models.Model): """Модель паба.""" - address = models.CharField( + name = models.CharField( max_length=255, - verbose_name="Адрес" - ) - phone = models.CharField( - max_length=12, - unique=True, - validators=[validate_phone], - verbose_name="Номер телефона" ) - email = models.EmailField( - unique=True, - verbose_name="Почта" + pub_address = models.CharField( + max_length=255, + verbose_name='Адрес' ) - company = models.ForeignKey( CustomUser, on_delete=models.CASCADE, - verbose_name="Компания", - related_name="pubs" + verbose_name='Компания', + related_name='pubs' ) class Meta: - verbose_name = "Паб" - verbose_name_plural = "Пабы" + verbose_name = 'Паб' + verbose_name_plural = 'Пабы' def __str__(self): return self.company.username @@ -40,26 +31,26 @@ def __str__(self): class Menu(models.Model): """Модель меню.""" - pub = models.ForeignKey( - Pub, - on_delete=models.CASCADE, - verbose_name='Паб', - related_name="menus", - ) - name = models.CharField( + alcohol_name = models.CharField( max_length=50, - verbose_name="Название", + verbose_name='Название', ) - alcohol = models.PositiveIntegerField( - verbose_name="Процент спирта", + alcohol_percent = models.PositiveIntegerField( + verbose_name='Процент спирта', ) cost = models.PositiveIntegerField( - verbose_name="Цена", + verbose_name='Цена', + ) + pub = models.ForeignKey( + Pub, + on_delete=models.CASCADE, + related_name='menu', + verbose_name='Меню' ) class Meta: - verbose_name = "Меню" - verbose_name_plural = "Меню" + verbose_name = 'Меню' + verbose_name_plural = 'Меню' def __str__(self): - return self.pub.company.username + return self.alcohol_name diff --git a/backend/pubs/validators.py b/backend/pubs/validators.py deleted file mode 100644 index c4daf30..0000000 --- a/backend/pubs/validators.py +++ /dev/null @@ -1,12 +0,0 @@ -import re - -from django.core.exceptions import ValidationError - - -def validate_phone(value): - phone_regex = r'^\+7\d{10}$' - if not re.match(phone_regex, value): - raise ValidationError( - "Номер телефона должен быть в формате +70123456789" - ) - return value From 7f556fcb19aedf15e2fd124d1a8f82cfa251db52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Fri, 5 May 2023 11:58:25 +0300 Subject: [PATCH 64/81] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B8=20=D0=B4=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD=D1=8B=20=D0=BF=D0=B0=D0=B1=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/serializers.py | 6 +++++- backend/pubs/migrations/0001_initial.py | 2 +- backend/pubs/models.py | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 7a33557..1af140f 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -171,7 +171,7 @@ class Meta: 'pub' ) - def validate_name(self, alcohol_name): + 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(): @@ -192,4 +192,8 @@ def validate_alcohol_percent(self, alcohol_percent): raise serializers.ValidationError( 'Процент содержания спирта не может быть больше 100%.' ) + elif alcohol_percent < 0: + raise serializers.ValidationError( + 'Процент содержания спирта не может быть меньше 0%.' + ) return alcohol_percent diff --git a/backend/pubs/migrations/0001_initial.py b/backend/pubs/migrations/0001_initial.py index ad5fb75..8d57842 100644 --- a/backend/pubs/migrations/0001_initial.py +++ b/backend/pubs/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-04-26 08:29 +# Generated by Django 4.1.7 on 2023-05-05 08:45 from django.conf import settings from django.db import migrations, models diff --git a/backend/pubs/models.py b/backend/pubs/models.py index 8ebe406..b22e6d3 100644 --- a/backend/pubs/models.py +++ b/backend/pubs/models.py @@ -8,6 +8,7 @@ class Pub(models.Model): name = models.CharField( max_length=255, + verbose_name='Название' ) pub_address = models.CharField( max_length=255, From eab88b5d60c207ec424efb6765b4ed0f5837c3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Tue, 9 May 2023 20:46:12 +0300 Subject: [PATCH 65/81] =?UTF-8?q?=D0=9D=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BC?= =?UTF-8?q?=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/games/migrations/0001_initial.py | 48 ++++++++++++++++++++++++ backend/pubs/migrations/0001_initial.py | 4 +- 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 backend/games/migrations/0001_initial.py 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/pubs/migrations/0001_initial.py b/backend/pubs/migrations/0001_initial.py index 8d57842..af9e2cc 100644 --- a/backend/pubs/migrations/0001_initial.py +++ b/backend/pubs/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-05-05 08:45 +# Generated by Django 4.1.7 on 2023-05-09 17:44 from django.conf import settings from django.db import migrations, models @@ -18,7 +18,7 @@ class Migration(migrations.Migration): name='Pub', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), + ('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='Компания')), ], From 4023bb962aaf6a789e4782665f34480bd9b6bd3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Tue, 9 May 2023 20:50:56 +0300 Subject: [PATCH 66/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B0=D0=B2=D1=82=D0=BE=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/urls.py | 21 ++++++++++++++++++++- backend/pub_golf/settings.py | 1 + backend/requirements.txt | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/backend/api/urls.py b/backend/api/urls.py index e036cd7..321aa92 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -1,4 +1,7 @@ from django.urls import path, include +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 ( @@ -23,8 +26,24 @@ r'pubs/(?P\d+)/menu', MenuViewSet, basename='menu' ) +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')), path('v1/', include(router_v1.urls)), - path('v1/', include('djoser.urls.base')) + path('v1/', include('djoser.urls.base')), + path( + 'v1/redoc/', + schema_view.with_ui('redoc', cache_timeout=0), + name='schema-redoc' + ), ] diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py index 8b42c3f..7b5760f 100644 --- a/backend/pub_golf/settings.py +++ b/backend/pub_golf/settings.py @@ -25,6 +25,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'drf_yasg', 'rest_framework', 'rest_framework.authtoken', 'djoser', diff --git a/backend/requirements.txt b/backend/requirements.txt index aa8cc64..0cf4e69 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,3 +3,4 @@ djangorestframework==3.14.0 Pillow==9.4.0 djoser==2.1.0 python-dotenv==1.0.0 +drf-yasg==1.21.5 From af66ec0f3e68198d689eed989d84cec489b2acd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Fri, 12 May 2023 11:07:11 +0300 Subject: [PATCH 67/81] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B4=D1=80=D1=83=D0=B6=D0=B1=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/serializers.py | 95 +++++++++++----- backend/api/urls.py | 16 ++- backend/api/views.py | 101 +++++++++++++++--- ...shiprequest_customuser_friends_and_more.py | 43 ++++++++ backend/users/models.py | 57 +++++++--- 5 files changed, 248 insertions(+), 64 deletions(-) create mode 100644 backend/users/migrations/0006_friendshiprequest_customuser_friends_and_more.py diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 1af140f..7c3b69d 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -6,7 +6,7 @@ ) from rest_framework import serializers -from users.models import CustomUser, Friendship +from users.models import CustomUser, FriendshipRequest from pubs.models import Pub, Menu @@ -49,11 +49,10 @@ class Meta: ) def get_is_friend(self, obj): - return (self.context['request'].user.is_authenticated - and Friendship.objects.filter( - user=self.context['request'].user, - friend=obj - ).exists()) + request_user = self.context['request'].user + + return (request_user.is_authenticated + and obj in request_user.friends.all()) class CustomUserMeSerializer(CustomUserSerializer): @@ -98,45 +97,87 @@ def validate(self, attrs): return super().validate(attrs) -class FriendsSerializer(serializers.ModelSerializer): - id = serializers.IntegerField( - source='friend.id', - read_only=True, +class FriendshipRequestSerializer(serializers.ModelSerializer): + from_user = serializers.CharField( + source='from_user.username', + read_only=True ) - username = serializers.CharField( - source='friend.username', + to_user = serializers.CharField( + source='to_user.username', read_only=True ) - is_friend = serializers.SerializerMethodField() class Meta: - model = Friendship - fields = ( - 'id', - 'username', - 'is_friend', - ) + 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 Friendship.objects.filter(user=user, friend=friend).exists(): + + 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 - def get_is_friend(self, obj): - return Friendship.objects.filter( - user=self.context['request'].user, - friend=obj.friend - ).exists() + +# class FriendsSerializer(serializers.ModelSerializer): +# id = serializers.IntegerField( +# source='friend.id', +# read_only=True, +# ) +# username = serializers.CharField( +# source='friend.username', +# read_only=True +# ) +# is_friend = serializers.SerializerMethodField() +# +# class Meta: +# model = Friendship +# fields = ( +# 'id', +# 'username', +# 'is_friend', +# ) +# +# 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 Friendship.objects.filter(user=user, friend=friend).exists(): +# raise serializers.ValidationError( +# 'Пользователь уже есть у Вас в друзьях!') +# return data +# +# def get_is_friend(self, obj): +# return Friendship.objects.filter( +# user=self.context['request'].user, +# friend=obj.friend +# ).exists() class PubSerializer(serializers.ModelSerializer): diff --git a/backend/api/urls.py b/backend/api/urls.py index 321aa92..1c94fbf 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -6,19 +6,25 @@ from api.views import ( CustomUserViewSet, - FriendsListViewSet, - FriendsCreateDestroyViewSet, + FriendViewSet, + FriendshipRequestCreateDestroyViewSet, + FriendshipRequestViewSet, PubViewSet, MenuViewSet, ) router_v1 = DefaultRouter() -router_v1.register('users/friends', FriendsListViewSet, basename='friends') +router_v1.register('users/friends', FriendViewSet, basename='friends') router_v1.register( r'users/(?P\d+)/friend', - FriendsCreateDestroyViewSet, - basename='add_delete_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') diff --git a/backend/api/views.py b/backend/api/views.py index 52f5204..655000a 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -11,7 +11,7 @@ IsPubOwnerOrReadOnly ) from api.serializers import ( - FriendsSerializer, + FriendshipRequestSerializer, CustomUserCreateSerializer, CustomPasswordSerializer, CustomUserSerializer, @@ -20,7 +20,7 @@ PubSerializer, MenuSerializer, ) -from users.models import CustomUser, Friendship +from users.models import CustomUser, FriendshipRequest from pubs.models import Pub, Menu @@ -62,43 +62,112 @@ def set_email(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) -class FriendsBaseViewSet(viewsets.GenericViewSet): - serializer_class = FriendsSerializer +class FriendshipRequestBaseViewSet(viewsets.GenericViewSet): + serializer_class = FriendshipRequestSerializer permission_classes = (PlayerPermission,) + +class FriendshipRequestViewSet( + mixins.ListModelMixin, + FriendshipRequestBaseViewSet +): def get_queryset(self): - return self.request.user.user_friends.all() + 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 + ) -class FriendsListViewSet(mixins.ListModelMixin, FriendsBaseViewSet): - pagination_class = GamesAndFriendsPagination + return Response(status=status.HTTP_404_NOT_FOUND) -class FriendsCreateDestroyViewSet( +class FriendshipRequestCreateDestroyViewSet( mixins.CreateModelMixin, mixins.DestroyModelMixin, - FriendsBaseViewSet + 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( - user=self.request.user, - friend=get_object_or_404( - CustomUser, id=self.kwargs.get('user_id') - )) + 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( - Friendship, - user=request.user, - friend_id=user_id + 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) diff --git a/backend/users/migrations/0006_friendshiprequest_customuser_friends_and_more.py b/backend/users/migrations/0006_friendshiprequest_customuser_friends_and_more.py new file mode 100644 index 0000000..00109e9 --- /dev/null +++ b/backend/users/migrations/0006_friendshiprequest_customuser_friends_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.1.7 on 2023-05-12 07:49 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_friendship_accepted'), + ] + + operations = [ + migrations.CreateModel( + name='FriendshipRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + options={ + 'verbose_name': 'Заявка в друзья', + 'verbose_name_plural': 'Заявки в друзья', + }, + ), + migrations.AddField( + model_name='customuser', + name='friends', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL), + ), + migrations.DeleteModel( + name='Friendship', + ), + migrations.AddField( + model_name='friendshiprequest', + name='from_user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='from_me_requests', to=settings.AUTH_USER_MODEL, verbose_name='Отправитель'), + ), + migrations.AddField( + model_name='friendshiprequest', + name='to_user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='to_me_requests', to=settings.AUTH_USER_MODEL, verbose_name='Получатель'), + ), + ] diff --git a/backend/users/models.py b/backend/users/models.py index 71cdeb1..2f1575a 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -7,7 +7,6 @@ ('company', 'Компания') ) - class CustomUser(AbstractUser): email = models.EmailField( unique=True, @@ -45,6 +44,10 @@ class CustomUser(AbstractUser): null=True, verbose_name='Фото' ) + friends = models.ManyToManyField( + 'CustomUser', + blank=True + ) class Meta: verbose_name = 'Пользователь' @@ -55,28 +58,50 @@ def is_company(self): return self.role == 'company' -class Friendship(models.Model): - user = models.ForeignKey( +class FriendshipRequest(models.Model): + from_user = models.ForeignKey( CustomUser, on_delete=models.CASCADE, - related_name='user_friends', - verbose_name='Пользователь' + related_name='from_me_requests', + verbose_name='Отправитель' ) - friend = models.ForeignKey( + to_user = models.ForeignKey( CustomUser, on_delete=models.CASCADE, - related_name='friend_friends', - verbose_name='Друг' - ) - accepted = models.BooleanField( - blank=True, - default=False, - verbose_name='Статус подтверждения' + related_name='to_me_requests', + verbose_name='Получатель' ) class Meta: - verbose_name = 'Друзья' - verbose_name_plural = 'Друзья' + verbose_name = 'Заявка в друзья' + verbose_name_plural = 'Заявки в друзья' def __str__(self): - return f'{self.user} --- {self.friend}: {self.accepted}' + return f'{self.from_user} --- {self.to_user}' + + +# class Friendship(models.Model): +# user = models.ForeignKey( +# CustomUser, +# on_delete=models.CASCADE, +# related_name='user_friends', +# verbose_name='Пользователь' +# ) +# friend = models.ForeignKey( +# CustomUser, +# on_delete=models.CASCADE, +# related_name='friend_friends', +# verbose_name='Друг' +# ) +# accepted = models.BooleanField( +# blank=True, +# default=False, +# verbose_name='Статус подтверждения' +# ) +# +# class Meta: +# verbose_name = 'Друзья' +# verbose_name_plural = 'Друзья' +# +# def __str__(self): +# return f'{self.user} --- {self.friend}: {self.accepted}' From 1b4780fc8cf3d6b20430025325959164e14e35bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Mon, 15 May 2023 18:36:32 +0300 Subject: [PATCH 68/81] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=B3=D0=BB=D0=B0=D1=88=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/games/admin.py | 14 +++++- backend/games/migrations/0002_invitation.py | 28 ++++++++++++ backend/games/models.py | 24 +++++++++++ backend/users/migrations/0001_initial.py | 19 ++++---- ..._customuser_photo_alter_customuser_role.py | 23 ---------- .../migrations/0003_alter_customuser_role.py | 18 -------- .../migrations/0004_alter_customuser_bio.py | 18 -------- .../migrations/0005_friendship_accepted.py | 18 -------- ...shiprequest_customuser_friends_and_more.py | 43 ------------------- 9 files changed, 75 insertions(+), 130 deletions(-) create mode 100644 backend/games/migrations/0002_invitation.py delete mode 100644 backend/users/migrations/0002_alter_customuser_photo_alter_customuser_role.py delete mode 100644 backend/users/migrations/0003_alter_customuser_role.py delete mode 100644 backend/users/migrations/0004_alter_customuser_bio.py delete mode 100644 backend/users/migrations/0005_friendship_accepted.py delete mode 100644 backend/users/migrations/0006_friendshiprequest_customuser_friends_and_more.py diff --git a/backend/games/admin.py b/backend/games/admin.py index 8c38f3f..94e43af 100644 --- a/backend/games/admin.py +++ b/backend/games/admin.py @@ -1,3 +1,15 @@ from django.contrib import admin -# Register your models here. +from .models import Invitation, 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/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/models.py b/backend/games/models.py index 07b24d5..f84dce5 100644 --- a/backend/games/models.py +++ b/backend/games/models.py @@ -59,6 +59,30 @@ def __str__(self): return self.name +class Invitation(models.Model): + """Модель приглашения в комнату.""" + + sender = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + related_name='invitations_sent', + verbose_name='Отправитель' + ) + recipient = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + related_name='invitations_received', + verbose_name='Получатель' + ) + + class Meta: + verbose_name = 'Приглашение в комнату' + verbose_name_plural = 'Приглашения в комнату' + + def __str__(self): + return f'{self.sender} --- {self.recipient}' + + class GameUser(models.Model): user = models.ForeignKey( CustomUser, diff --git a/backend/users/migrations/0001_initial.py b/backend/users/migrations/0001_initial.py index 0296f8f..3da2756 100644 --- a/backend/users/migrations/0001_initial.py +++ b/backend/users/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-03-23 13:28 +# Generated by Django 4.1.7 on 2023-05-15 15:30 from django.conf import settings import django.contrib.auth.models @@ -32,11 +32,12 @@ class Migration(migrations.Migration): ('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=[('user', 'Аутентифицированный пользователь'), ('company', 'Компания'), ('admin', 'Администратор')], default='user', max_length=30, verbose_name='Роль')), - ('bio', models.TextField(blank=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(upload_to='', 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')), ], @@ -49,15 +50,15 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='Friendship', + name='FriendshipRequest', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('friend', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='friend_friends', to=settings.AUTH_USER_MODEL, verbose_name='Друг')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_friends', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ('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': 'Друзья', + 'verbose_name': 'Заявка в друзья', + 'verbose_name_plural': 'Заявки в друзья', }, ), ] diff --git a/backend/users/migrations/0002_alter_customuser_photo_alter_customuser_role.py b/backend/users/migrations/0002_alter_customuser_photo_alter_customuser_role.py deleted file mode 100644 index de8f3b3..0000000 --- a/backend/users/migrations/0002_alter_customuser_photo_alter_customuser_role.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-23 13:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='customuser', - name='photo', - field=models.ImageField(blank=True, null=True, upload_to='', verbose_name='Фото'), - ), - migrations.AlterField( - model_name='customuser', - name='role', - field=models.CharField(blank=True, choices=[('user', 'Аутентифицированный пользователь'), ('company', 'Компания')], default='user', max_length=30, verbose_name='Роль'), - ), - ] diff --git a/backend/users/migrations/0003_alter_customuser_role.py b/backend/users/migrations/0003_alter_customuser_role.py deleted file mode 100644 index 09726bf..0000000 --- a/backend/users/migrations/0003_alter_customuser_role.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-23 13:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0002_alter_customuser_photo_alter_customuser_role'), - ] - - operations = [ - migrations.AlterField( - model_name='customuser', - name='role', - field=models.CharField(blank=True, choices=[('player', 'Аутентифицированный пользователь'), ('company', 'Компания')], default='player', max_length=30, verbose_name='Роль'), - ), - ] diff --git a/backend/users/migrations/0004_alter_customuser_bio.py b/backend/users/migrations/0004_alter_customuser_bio.py deleted file mode 100644 index ab88a9e..0000000 --- a/backend/users/migrations/0004_alter_customuser_bio.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-23 14:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0003_alter_customuser_role'), - ] - - operations = [ - migrations.AlterField( - model_name='customuser', - name='bio', - field=models.TextField(blank=True, null=True, verbose_name='Доп. информация'), - ), - ] diff --git a/backend/users/migrations/0005_friendship_accepted.py b/backend/users/migrations/0005_friendship_accepted.py deleted file mode 100644 index 4d41159..0000000 --- a/backend/users/migrations/0005_friendship_accepted.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.7 on 2023-04-11 12:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0004_alter_customuser_bio'), - ] - - operations = [ - migrations.AddField( - model_name='friendship', - name='accepted', - field=models.BooleanField(blank=True, default=False, verbose_name='Статус подтверждения'), - ), - ] diff --git a/backend/users/migrations/0006_friendshiprequest_customuser_friends_and_more.py b/backend/users/migrations/0006_friendshiprequest_customuser_friends_and_more.py deleted file mode 100644 index 00109e9..0000000 --- a/backend/users/migrations/0006_friendshiprequest_customuser_friends_and_more.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 4.1.7 on 2023-05-12 07:49 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0005_friendship_accepted'), - ] - - operations = [ - migrations.CreateModel( - name='FriendshipRequest', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ], - options={ - 'verbose_name': 'Заявка в друзья', - 'verbose_name_plural': 'Заявки в друзья', - }, - ), - migrations.AddField( - model_name='customuser', - name='friends', - field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL), - ), - migrations.DeleteModel( - name='Friendship', - ), - migrations.AddField( - model_name='friendshiprequest', - name='from_user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='from_me_requests', to=settings.AUTH_USER_MODEL, verbose_name='Отправитель'), - ), - migrations.AddField( - model_name='friendshiprequest', - name='to_user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='to_me_requests', to=settings.AUTH_USER_MODEL, verbose_name='Получатель'), - ), - ] From fd1a47621fec440c87cac9f8475b4e1713740e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Wed, 17 May 2023 20:47:07 +0300 Subject: [PATCH 69/81] =?UTF-8?q?=D0=94=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=BD=D0=B0=D1=82=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=B8=D0=B3=D0=BB=D0=B0=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/serializers.py | 82 +++++++++++++++++++ backend/api/urls.py | 20 +++++ backend/api/views.py | 69 ++++++++++++++++ ...dget_level_alter_gameuser_game_and_more.py | 31 +++++++ .../0004_alter_game_difficulty_level.py | 18 ++++ .../games/migrations/0005_invitation_game.py | 20 +++++ backend/games/models.py | 24 ++++-- 7 files changed, 257 insertions(+), 7 deletions(-) create mode 100644 backend/games/migrations/0003_alter_game_budget_level_alter_gameuser_game_and_more.py create mode 100644 backend/games/migrations/0004_alter_game_difficulty_level.py create mode 100644 backend/games/migrations/0005_invitation_game.py diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 7c3b69d..5ff44f5 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -8,6 +8,7 @@ from users.models import CustomUser, FriendshipRequest from pubs.models import Pub, Menu +from games.models import Game, Invitation, GameUser class CustomUserCreateSerializer(UserCreatePasswordRetypeSerializer): @@ -238,3 +239,84 @@ def validate_alcohol_percent(self, alcohol_percent): 'Процент содержания спирта не может быть меньше 0%.' ) return alcohol_percent + + +class GameSerializer(serializers.ModelSerializer): + + class Meta: + model = Game + fields = ( + 'id', + 'name', + 'difficulty_level', + 'budget_level', + 'status', + 'start_time', + 'finish_time', + ) + + def validate_name(self, name): + if Game.objects.filter(name=name).exists(): + raise serializers.ValidationError( + 'Комната с таким именем уже существует.' + ) + + +class InvitationSerializer(serializers.ModelSerializer): + sender = serializers.ReadOnlyField( + source='sender.username', + ) + recipient = serializers.ReadOnlyField( + source='recipient.username', + ) + game = serializers.IntegerField( + source='game.id', + read_only=True + ) + + class Meta: + model = Invitation + fields = ( + 'id', + 'sender', + 'recipient', + 'game' + ) + + def validate(self, data): + sender = CustomUser.objects.get(id=self.context['request'].user.id) + recipient_id = self.context['view'].kwargs['user_id'] + recipient = CustomUser.objects.get(id=recipient_id) + + if sender == recipient: + raise serializers.ValidationError( + 'Нельзя пригласить самого себя!' + ) + + if recipient.is_company: + raise serializers.ValidationError( + 'Нельзя пригласить аккаунт комапнии!' + ) + + if Invitation.objects.filter( + sender=sender, + recipient=recipient + ).exists(): + raise serializers.ValidationError('Приглашение уже отправлено!') + + return data + + +class GameUserSerializer(serializers.ModelSerializer): + user = serializers.SlugRelatedField( + slug_field='username', + read_only=True + ) + game = serializers.IntegerField( + source='game.id', + read_only=True + ) + + class Meta: + model = GameUser + fields = '__all__' diff --git a/backend/api/urls.py b/backend/api/urls.py index 1c94fbf..64e7ade 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -11,6 +11,10 @@ FriendshipRequestViewSet, PubViewSet, MenuViewSet, + GameViewSet, + InvitationCreateViewSet, + InvitationReadViewSet, + GameUserViewSet ) router_v1 = DefaultRouter() @@ -31,6 +35,22 @@ router_v1.register( r'pubs/(?P\d+)/menu', MenuViewSet, basename='menu' ) +router_v1.register('party', GameViewSet, basename='party') +router_v1.register( + r'party/(?P\d+)/join', + GameUserViewSet, + basename='party_join' +) +router_v1.register( + 'users/invitation', + InvitationReadViewSet, + basename='invitation_requests' +) +router_v1.register( + r'party/(?P\d+)/users/(?P\d+)/invite', + InvitationCreateViewSet, + basename='send_invite' +) schema_view = get_schema_view( openapi.Info( diff --git a/backend/api/views.py b/backend/api/views.py index 655000a..58b63d9 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -19,9 +19,13 @@ SetEmailSerializer, PubSerializer, MenuSerializer, + GameSerializer, + GameUserSerializer, + InvitationSerializer ) from users.models import CustomUser, FriendshipRequest from pubs.models import Pub, Menu +from games.models import Game, Invitation, GameUser class CustomUserViewSet(UserViewSet): @@ -206,3 +210,68 @@ def perform_create(self, serializer): user = self.request.user pub = Pub.objects.get(company=user) serializer.save(pub=pub) + + +class GameViewSet(viewsets.ModelViewSet): + queryset = Game.objects.all() + serializer_class = GameSerializer + permission_classes = (PlayerPermission, ) + + +class InvitationCreateViewSet( + mixins.CreateModelMixin, + viewsets.GenericViewSet +): + queryset = Invitation.objects.all() + serializer_class = InvitationSerializer + permission_classes = (PlayerPermission, ) + + def perform_create(self, serializer): + game_id = self.kwargs['game_id'] + user_id = self.kwargs['user_id'] + game = Game.objects.get(id=game_id) + recipient = CustomUser.objects.get(id=user_id) + serializer.save( + sender=self.request.user, + recipient=recipient, + game=game + ) + + +class InvitationReadViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet +): + serializer_class = InvitationSerializer + permission_classes = (PlayerPermission, ) + + def get_queryset(self): + user = self.request.user + return Invitation.objects.filter(recipient=user) + + +class GameUserViewSet( + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet +): + queryset = GameUser.objects.all() + serializer_class = GameUserSerializer + permission_classes = (PlayerPermission, ) + + def perform_create(self, serializer): + game_id = self.kwargs['game_id'] + game = Game.objects.get(id=game_id) + user = self.request.user + serializer.save(user=user, game=game) + + @action(methods=['delete'], detail=True) + def delete(self, request, game_id): + get_object_or_404( + GameUser, + user=request.user, + id=game_id + ).delete() + + return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file 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/models.py b/backend/games/models.py index f84dce5..9fe21cf 100644 --- a/backend/games/models.py +++ b/backend/games/models.py @@ -4,13 +4,13 @@ DIFFICULTY_LEVELS = ( ('underbeerman', 'Подпивасник'), - ('', ''), - ('', '') + ('fan', 'Любитель'), + ('freelanholic', 'Фриланголик') ) BUDGET_LEVELS = ( - ('', ''), - ('', ''), - ('', '') + ('homeless', 'Бомж'), + ('fan', 'Любитель'), + ('major', 'Мажор') ) GAME_STATUSES = ( ('created', 'Создана комната'), @@ -20,6 +20,8 @@ class Game(models.Model): + """Модель комнаты.""" + name = models.CharField( unique=True, max_length=150, @@ -74,6 +76,12 @@ class Invitation(models.Model): related_name='invitations_received', verbose_name='Получатель' ) + game = models.ForeignKey( + Game, + on_delete=models.CASCADE, + verbose_name='Комната', + related_name='invitations' + ) class Meta: verbose_name = 'Приглашение в комнату' @@ -86,11 +94,13 @@ def __str__(self): class GameUser(models.Model): user = models.ForeignKey( CustomUser, - on_delete=models.CASCADE + on_delete=models.CASCADE, + verbose_name='Игрок' ) game = models.ForeignKey( Game, - on_delete=models.CASCADE + on_delete=models.CASCADE, + verbose_name='Комната' ) def __str__(self): From a98c9f1d7e5e0df68cf34b2cc4cde366eb1d1b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Wed, 17 May 2023 20:51:26 +0300 Subject: [PATCH 70/81] =?UTF-8?q?=D0=94=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=20validate=20=D0=B2=20GameUser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/serializers.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 5ff44f5..df9d8ca 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -320,3 +320,21 @@ class GameUserSerializer(serializers.ModelSerializer): class Meta: model = GameUser fields = '__all__' + + def validate(self, data): + user = CustomUser.objects.get(id=self.context['request'].user.id) + game_id = self.context['view'].kwargs['game_id'] + game = CustomUser.objects.get(id=game_id) + + if user.is_company: + raise serializers.ValidationError( + 'Нельзя войти в игру будучи компанией.' + ) + + if GameUser.objects.filter( + user=user, + game=game + ).exists(): + raise serializers.ValidationError('Вы уже присоединились к комнате.') + + return data From 13b6ee4bf25442b1ee543314b1f3f851504f547e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Wed, 17 May 2023 20:55:06 +0300 Subject: [PATCH 71/81] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D1=81=D1=82=D0=BE=20?= =?UTF-8?q?=D0=BE=D1=82=D1=81=D1=82=D1=83=D0=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api/views.py b/backend/api/views.py index 58b63d9..214e164 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -274,4 +274,4 @@ def delete(self, request, game_id): id=game_id ).delete() - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) From 48611c39b93c1b53517f21fb034373fb4350b3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Mon, 22 May 2023 17:41:28 +0300 Subject: [PATCH 72/81] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D0=BE=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B8=D0=B3=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/serializers.py | 163 +++++------------- backend/api/urls.py | 20 +-- backend/api/views.py | 79 ++------- backend/games/admin.py | 2 +- ...ameuser_player_status_delete_invitation.py | 21 +++ .../migrations/0007_alter_game_status.py | 18 ++ backend/games/migrations/0008_game_players.py | 20 +++ .../0009_alter_gameuser_unique_together.py | 19 ++ backend/games/models.py | 78 +++++---- ...alter_friendshiprequest_unique_together.py | 17 ++ backend/users/models.py | 1 + 11 files changed, 206 insertions(+), 232 deletions(-) create mode 100644 backend/games/migrations/0006_gameuser_player_status_delete_invitation.py create mode 100644 backend/games/migrations/0007_alter_game_status.py create mode 100644 backend/games/migrations/0008_game_players.py create mode 100644 backend/games/migrations/0009_alter_gameuser_unique_together.py create mode 100644 backend/users/migrations/0002_alter_friendshiprequest_unique_together.py diff --git a/backend/api/serializers.py b/backend/api/serializers.py index df9d8ca..39e6945 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -5,10 +5,11 @@ 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, Invitation, GameUser +from games.models import Game, GameUser class CustomUserCreateSerializer(UserCreatePasswordRetypeSerializer): @@ -140,47 +141,6 @@ def validate(self, data): return data -# class FriendsSerializer(serializers.ModelSerializer): -# id = serializers.IntegerField( -# source='friend.id', -# read_only=True, -# ) -# username = serializers.CharField( -# source='friend.username', -# read_only=True -# ) -# is_friend = serializers.SerializerMethodField() -# -# class Meta: -# model = Friendship -# fields = ( -# 'id', -# 'username', -# 'is_friend', -# ) -# -# 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 Friendship.objects.filter(user=user, friend=friend).exists(): -# raise serializers.ValidationError( -# 'Пользователь уже есть у Вас в друзьях!') -# return data -# -# def get_is_friend(self, obj): -# return Friendship.objects.filter( -# user=self.context['request'].user, -# friend=obj.friend -# ).exists() - - class PubSerializer(serializers.ModelSerializer): company = serializers.SlugRelatedField( slug_field='username', @@ -241,100 +201,63 @@ def validate_alcohol_percent(self, alcohol_percent): return alcohol_percent -class GameSerializer(serializers.ModelSerializer): +class GameCreateSerializer(serializers.ModelSerializer): + players = serializers.ListField( + child=serializers.PrimaryKeyRelatedField( + queryset=CustomUser.objects.all() + ), + write_only=True + ) class Meta: model = Game - fields = ( - 'id', + fields = '__all__' + required_fields = ( 'name', 'difficulty_level', 'budget_level', - 'status', - 'start_time', - 'finish_time', + 'players' ) - def validate_name(self, name): - if Game.objects.filter(name=name).exists(): - raise serializers.ValidationError( - 'Комната с таким именем уже существует.' - ) + 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 + ) -class InvitationSerializer(serializers.ModelSerializer): - sender = serializers.ReadOnlyField( - source='sender.username', - ) - recipient = serializers.ReadOnlyField( - source='recipient.username', - ) - game = serializers.IntegerField( - source='game.id', - read_only=True - ) + return game - class Meta: - model = Invitation - fields = ( - 'id', - 'sender', - 'recipient', - '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 ) - - def validate(self, data): - sender = CustomUser.objects.get(id=self.context['request'].user.id) - recipient_id = self.context['view'].kwargs['user_id'] - recipient = CustomUser.objects.get(id=recipient_id) - - if sender == recipient: - raise serializers.ValidationError( - 'Нельзя пригласить самого себя!' - ) - - if recipient.is_company: - raise serializers.ValidationError( - 'Нельзя пригласить аккаунт комапнии!' + 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 ) - if Invitation.objects.filter( - sender=sender, - recipient=recipient - ).exists(): - raise serializers.ValidationError('Приглашение уже отправлено!') - - return data + return instance -class GameUserSerializer(serializers.ModelSerializer): - user = serializers.SlugRelatedField( - slug_field='username', - read_only=True - ) - game = serializers.IntegerField( - source='game.id', - read_only=True - ) +class GameSerializer(serializers.ModelSerializer): class Meta: - model = GameUser + model = Game fields = '__all__' - - def validate(self, data): - user = CustomUser.objects.get(id=self.context['request'].user.id) - game_id = self.context['view'].kwargs['game_id'] - game = CustomUser.objects.get(id=game_id) - - if user.is_company: - raise serializers.ValidationError( - 'Нельзя войти в игру будучи компанией.' - ) - - if GameUser.objects.filter( - user=user, - game=game - ).exists(): - raise serializers.ValidationError('Вы уже присоединились к комнате.') - - return data diff --git a/backend/api/urls.py b/backend/api/urls.py index 64e7ade..c0d9f4e 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -12,9 +12,6 @@ PubViewSet, MenuViewSet, GameViewSet, - InvitationCreateViewSet, - InvitationReadViewSet, - GameUserViewSet ) router_v1 = DefaultRouter() @@ -35,22 +32,7 @@ router_v1.register( r'pubs/(?P\d+)/menu', MenuViewSet, basename='menu' ) -router_v1.register('party', GameViewSet, basename='party') -router_v1.register( - r'party/(?P\d+)/join', - GameUserViewSet, - basename='party_join' -) -router_v1.register( - 'users/invitation', - InvitationReadViewSet, - basename='invitation_requests' -) -router_v1.register( - r'party/(?P\d+)/users/(?P\d+)/invite', - InvitationCreateViewSet, - basename='send_invite' -) +router_v1.register('games', GameViewSet, basename='games') schema_view = get_schema_view( openapi.Info( diff --git a/backend/api/views.py b/backend/api/views.py index 214e164..f7cb541 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -2,6 +2,7 @@ from djoser.views import UserViewSet from rest_framework import viewsets, mixins, status, permissions, serializers from rest_framework.decorators import action +from rest_framework.permissions import SAFE_METHODS from rest_framework.response import Response from api.pagination import GamesAndFriendsPagination @@ -20,12 +21,11 @@ PubSerializer, MenuSerializer, GameSerializer, - GameUserSerializer, - InvitationSerializer + GameCreateSerializer, ) from users.models import CustomUser, FriendshipRequest from pubs.models import Pub, Menu -from games.models import Game, Invitation, GameUser +from games.models import Game class CustomUserViewSet(UserViewSet): @@ -198,80 +198,33 @@ class MenuViewSet( permission_classes = (IsPubOwnerOrReadOnly, ) def get_queryset(self): - pub_id = self.kwargs['pub_id'] - return Menu.objects.filter(pub=pub_id) + return Menu.objects.filter(pub=self.kwargs.get('pub_id')) def retrieve(self, request, pk=None, pub_id=None): - menu = self.get_object() - serializer = self.get_serializer(menu) + serializer = self.get_serializer(self.get_object()) return Response(serializer.data) def perform_create(self, serializer): - user = self.request.user - pub = Pub.objects.get(company=user) - serializer.save(pub=pub) + serializer.save(pub=Pub.objects.get(company=self.request.user)) class GameViewSet(viewsets.ModelViewSet): - queryset = Game.objects.all() - serializer_class = GameSerializer - permission_classes = (PlayerPermission, ) - - -class InvitationCreateViewSet( - mixins.CreateModelMixin, - viewsets.GenericViewSet -): - queryset = Invitation.objects.all() - serializer_class = InvitationSerializer - permission_classes = (PlayerPermission, ) - - def perform_create(self, serializer): - game_id = self.kwargs['game_id'] - user_id = self.kwargs['user_id'] - game = Game.objects.get(id=game_id) - recipient = CustomUser.objects.get(id=user_id) - serializer.save( - sender=self.request.user, - recipient=recipient, - game=game - ) - + serializer_class = GameCreateSerializer + permission_classes = (PlayerPermission,) -class InvitationReadViewSet( - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - viewsets.GenericViewSet -): - serializer_class = InvitationSerializer - permission_classes = (PlayerPermission, ) + def get_serializer_class(self): + if self.request.method in SAFE_METHODS: + return GameSerializer + return GameCreateSerializer def get_queryset(self): - user = self.request.user - return Invitation.objects.filter(recipient=user) - - -class GameUserViewSet( - mixins.CreateModelMixin, - mixins.DestroyModelMixin, - viewsets.GenericViewSet -): - queryset = GameUser.objects.all() - serializer_class = GameUserSerializer - permission_classes = (PlayerPermission, ) - - def perform_create(self, serializer): - game_id = self.kwargs['game_id'] - game = Game.objects.get(id=game_id) - user = self.request.user - serializer.save(user=user, game=game) + return Game.objects.filter(players=self.request.user) @action(methods=['delete'], detail=True) - def delete(self, request, game_id): + def delete(self, request, pk): get_object_or_404( - GameUser, - user=request.user, - id=game_id + Game, + id=pk ).delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/backend/games/admin.py b/backend/games/admin.py index 94e43af..3197a32 100644 --- a/backend/games/admin.py +++ b/backend/games/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Invitation, Game, GameUser +from .models import Game, GameUser @admin.register(Game) 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/models.py b/backend/games/models.py index 9fe21cf..4e61a39 100644 --- a/backend/games/models.py +++ b/backend/games/models.py @@ -17,6 +17,11 @@ ('started', 'Игра стартовала'), ('finished', 'Игра завершилась') ) +PLAYER_STATUSES = ( + ('playing', 'Играет'), + ('won', 'Выиграл'), + ('lost', 'Проиграл') +) class Game(models.Model): @@ -40,6 +45,7 @@ class Game(models.Model): status = models.CharField( max_length=50, choices=GAME_STATUSES, + default='created', verbose_name='Статус игры' ) start_time = models.DateTimeField( @@ -52,6 +58,11 @@ class Game(models.Model): null=True, verbose_name='Время завершения игры' ) + players = models.ManyToManyField( + CustomUser, + through='GameUser', + verbose_name='Игроки', + ) class Meta: verbose_name = 'Игра' @@ -61,34 +72,34 @@ def __str__(self): return self.name -class Invitation(models.Model): - """Модель приглашения в комнату.""" - - sender = models.ForeignKey( - CustomUser, - on_delete=models.CASCADE, - related_name='invitations_sent', - verbose_name='Отправитель' - ) - recipient = models.ForeignKey( - CustomUser, - on_delete=models.CASCADE, - related_name='invitations_received', - verbose_name='Получатель' - ) - game = models.ForeignKey( - Game, - on_delete=models.CASCADE, - verbose_name='Комната', - related_name='invitations' - ) - - class Meta: - verbose_name = 'Приглашение в комнату' - verbose_name_plural = 'Приглашения в комнату' - - def __str__(self): - return f'{self.sender} --- {self.recipient}' +# class Invitation(models.Model): +# """Модель приглашения в комнату.""" +# +# sender = models.ForeignKey( +# CustomUser, +# on_delete=models.CASCADE, +# related_name='invitations_sent', +# verbose_name='Отправитель' +# ) +# recipient = models.ForeignKey( +# CustomUser, +# on_delete=models.CASCADE, +# related_name='invitations_received', +# verbose_name='Получатель' +# ) +# game = models.ForeignKey( +# Game, +# on_delete=models.CASCADE, +# verbose_name='Комната', +# related_name='invitations' +# ) +# +# class Meta: +# verbose_name = 'Приглашение в комнату' +# verbose_name_plural = 'Приглашения в комнату' +# +# def __str__(self): +# return f'{self.sender} --- {self.recipient}' class GameUser(models.Model): @@ -102,9 +113,18 @@ class GameUser(models.Model): 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}' + return f'{self.user} --- {self.game}: {self.player_status}' class Stage(models.Model): 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/models.py b/backend/users/models.py index 2f1575a..d1435e7 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -73,6 +73,7 @@ class FriendshipRequest(models.Model): ) class Meta: + unique_together = ('from_user', 'to_user') verbose_name = 'Заявка в друзья' verbose_name_plural = 'Заявки в друзья' From eb89395e18d17dc79e29636f1fb1d97fe43e580b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 25 May 2023 23:53:34 +0300 Subject: [PATCH 73/81] =?UTF-8?q?=D0=A1=D1=82=D0=B0=D1=80=D1=82=20=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D1=8B=20(=D0=B2=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5?= =?UTF-8?q?=D1=81=D1=81=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/views.py | 52 ++++++++++++++++++- ...0_stage_pub_alter_stage_unique_together.py | 25 +++++++++ backend/games/models.py | 45 ++++------------ backend/users/models.py | 28 +--------- 4 files changed, 88 insertions(+), 62 deletions(-) create mode 100644 backend/games/migrations/0010_stage_pub_alter_stage_unique_together.py diff --git a/backend/api/views.py b/backend/api/views.py index f7cb541..b0f19dc 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,9 +1,12 @@ +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 from rest_framework.decorators import action from rest_framework.permissions import SAFE_METHODS from rest_framework.response import Response +from rest_framework.views import APIView from api.pagination import GamesAndFriendsPagination from api.permissions import ( @@ -25,7 +28,7 @@ ) from users.models import CustomUser, FriendshipRequest from pubs.models import Pub, Menu -from games.models import Game +from games.models import Game, Stage class CustomUserViewSet(UserViewSet): @@ -228,3 +231,50 @@ def delete(self, request, pk): ).delete() return Response(status=status.HTTP_204_NO_CONTENT) + + +class StartGameAPIView(APIView): + permission_classes = (PlayerPermission,) + + def get(self, request, game_id): + game = get_object_or_404( + Game, + id=game_id + ) + 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 = ordered_by_alcohol[:interval] + elif difficulty_level == 'fan': + drinks = ordered_by_alcohol[interval:2*interval] + elif difficulty_level == 'freelanholic': + drinks = ordered_by_alcohol[2*interval:] + + ordered_by_cost = drinks.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:] + + # drinks = drinks.order_by('pub_id') + pubs_drinks = defaultdict(list) + for drink in drinks: + pubs_drinks[drink.pub_id].append(drink.id) + + stages = [] + for pub, drinks in pubs_drinks.items(): + stages.append( + Stage.objects.create( + game=game, + pub=Pub.objects.get(id=int(pub)) + ) + ) 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/models.py b/backend/games/models.py index 4e61a39..6b96f7e 100644 --- a/backend/games/models.py +++ b/backend/games/models.py @@ -1,5 +1,6 @@ from django.db import models +from pubs.models import Pub from users.models import CustomUser DIFFICULTY_LEVELS = ( @@ -72,36 +73,6 @@ def __str__(self): return self.name -# class Invitation(models.Model): -# """Модель приглашения в комнату.""" -# -# sender = models.ForeignKey( -# CustomUser, -# on_delete=models.CASCADE, -# related_name='invitations_sent', -# verbose_name='Отправитель' -# ) -# recipient = models.ForeignKey( -# CustomUser, -# on_delete=models.CASCADE, -# related_name='invitations_received', -# verbose_name='Получатель' -# ) -# game = models.ForeignKey( -# Game, -# on_delete=models.CASCADE, -# verbose_name='Комната', -# related_name='invitations' -# ) -# -# class Meta: -# verbose_name = 'Приглашение в комнату' -# verbose_name_plural = 'Приглашения в комнату' -# -# def __str__(self): -# return f'{self.sender} --- {self.recipient}' - - class GameUser(models.Model): user = models.ForeignKey( CustomUser, @@ -132,7 +103,13 @@ class Stage(models.Model): Game, on_delete=models.CASCADE ) - # pub = models.ForeignKey( - # Pub, - # 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}' diff --git a/backend/users/models.py b/backend/users/models.py index d1435e7..1e31b0c 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -7,6 +7,7 @@ ('company', 'Компания') ) + class CustomUser(AbstractUser): email = models.EmailField( unique=True, @@ -79,30 +80,3 @@ class Meta: def __str__(self): return f'{self.from_user} --- {self.to_user}' - - -# class Friendship(models.Model): -# user = models.ForeignKey( -# CustomUser, -# on_delete=models.CASCADE, -# related_name='user_friends', -# verbose_name='Пользователь' -# ) -# friend = models.ForeignKey( -# CustomUser, -# on_delete=models.CASCADE, -# related_name='friend_friends', -# verbose_name='Друг' -# ) -# accepted = models.BooleanField( -# blank=True, -# default=False, -# verbose_name='Статус подтверждения' -# ) -# -# class Meta: -# verbose_name = 'Друзья' -# verbose_name_plural = 'Друзья' -# -# def __str__(self): -# return f'{self.user} --- {self.friend}: {self.accepted}' From 6c8f02b56111d269a09b3f16de95030f794e3c51 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Fri, 26 May 2023 13:40:25 +0300 Subject: [PATCH 74/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=81=D1=82=D0=B0=D1=80=D1=82=20=D0=B8=D0=B3?= =?UTF-8?q?=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/serializers.py | 37 ++++++++++++++++++++++++++++++++++++- backend/api/urls.py | 2 ++ backend/api/views.py | 14 +++++++++++--- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 39e6945..eedb351 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -9,7 +9,7 @@ from users.models import CustomUser, FriendshipRequest from pubs.models import Pub, Menu -from games.models import Game, GameUser +from games.models import Game, GameUser, Stage class CustomUserCreateSerializer(UserCreatePasswordRetypeSerializer): @@ -157,6 +157,16 @@ class Meta: ) +class PubInStageSerializer(serializers.ModelSerializer): + + class Meta: + model = Pub + fields = ( + 'name', + 'pub_address' + ) + + class MenuSerializer(serializers.ModelSerializer): pub = serializers.IntegerField( source='pub.id', @@ -261,3 +271,28 @@ class GameSerializer(serializers.ModelSerializer): class Meta: model = Game fields = '__all__' + + +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 = MenuInStageSerializer(read_only=True, many=True) + + class Meta: + model = Stage + fields = ( + 'id', + 'pub', + 'drinks' + ) diff --git a/backend/api/urls.py b/backend/api/urls.py index c0d9f4e..fe562f3 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -12,6 +12,7 @@ PubViewSet, MenuViewSet, GameViewSet, + StartGameAPIView ) router_v1 = DefaultRouter() @@ -48,6 +49,7 @@ urlpatterns = [ path('v1/auth/', include('djoser.urls.authtoken')), path('v1/', include(router_v1.urls)), + path('v1/start', StartGameAPIView.as_view(), name='start_game'), path('v1/', include('djoser.urls.base')), path( 'v1/redoc/', diff --git a/backend/api/views.py b/backend/api/views.py index b0f19dc..4a0a868 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -2,7 +2,8 @@ from django.shortcuts import get_object_or_404 from djoser.views import UserViewSet -from rest_framework import viewsets, mixins, status, permissions, serializers +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 @@ -24,7 +25,7 @@ PubSerializer, MenuSerializer, GameSerializer, - GameCreateSerializer, + GameCreateSerializer, StageSerializer, ) from users.models import CustomUser, FriendshipRequest from pubs.models import Pub, Menu @@ -233,7 +234,7 @@ def delete(self, request, pk): return Response(status=status.HTTP_204_NO_CONTENT) -class StartGameAPIView(APIView): +class StartGameAPIView(generics.RetrieveAPIView): permission_classes = (PlayerPermission,) def get(self, request, game_id): @@ -278,3 +279,10 @@ def get(self, request, game_id): pub=Pub.objects.get(id=int(pub)) ) ) + + serialized_stages = StageSerializer(stages, many=True).data + + return Response( + data={'id': game.id, 'stages': serialized_stages}, + status=status.HTTP_200_OK + ) From f8bb28ab21eca1229d54530d3a941ea5e123b6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Wed, 31 May 2023 11:20:11 +0300 Subject: [PATCH 75/81] =?UTF-8?q?=D0=9E=D1=82=D1=80=D0=B5=D0=B4=D0=B0?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D1=8B=20=D0=BF=D0=B0=D0=B1=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pub_golf/settings.py | 4 ++++ backend/pubs/models.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py index 7b5760f..7784e38 100644 --- a/backend/pub_golf/settings.py +++ b/backend/pub_golf/settings.py @@ -26,6 +26,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'drf_yasg', + 'corsheaders', 'rest_framework', 'rest_framework.authtoken', 'djoser', @@ -43,6 +44,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'corsheaders.middleware.CorsMiddleware', ] ROOT_URLCONF = 'pub_golf.urls' @@ -135,3 +137,5 @@ 'user_list': ['api.permissions.ReadOnlyPermission'], }, } + +CORS_ALLOW_ALL_ORIGINS = True diff --git a/backend/pubs/models.py b/backend/pubs/models.py index b22e6d3..8102c58 100644 --- a/backend/pubs/models.py +++ b/backend/pubs/models.py @@ -26,7 +26,7 @@ class Meta: verbose_name_plural = 'Пабы' def __str__(self): - return self.company.username + return f'{self.company.username} --- {self.name}' class Menu(models.Model): From 89921c0b4150fbe27758a0f21ef8f48b78f641d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D1=80=D0=BC=D0=B0=D0=BD=20=D0=9A=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=87=D0=BA=D0=BE=D0=B2?= Date: Wed, 31 May 2023 12:09:58 +0300 Subject: [PATCH 76/81] =?UTF-8?q?=D0=94=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=B9=D1=82=D0=B5=20=D0=BF=D0=B0=D0=B1=D1=8B(((?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/urls.py | 4 ++-- backend/api/views.py | 2 +- backend/pub_golf/settings.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/api/urls.py b/backend/api/urls.py index fe562f3..dd7ef55 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -1,4 +1,4 @@ -from django.urls import path, include +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 @@ -48,8 +48,8 @@ urlpatterns = [ path('v1/auth/', include('djoser.urls.authtoken')), + re_path(r'v1/games/(?P\d+)/start', StartGameAPIView.as_view(), name='start_game'), path('v1/', include(router_v1.urls)), - path('v1/start', StartGameAPIView.as_view(), name='start_game'), path('v1/', include('djoser.urls.base')), path( 'v1/redoc/', diff --git a/backend/api/views.py b/backend/api/views.py index 4a0a868..25fdb89 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -240,7 +240,7 @@ class StartGameAPIView(generics.RetrieveAPIView): def get(self, request, game_id): game = get_object_or_404( Game, - id=game_id + id=int(game_id) ) difficulty_level = game.difficulty_level budget_level = game.budget_level diff --git a/backend/pub_golf/settings.py b/backend/pub_golf/settings.py index 7784e38..798c089 100644 --- a/backend/pub_golf/settings.py +++ b/backend/pub_golf/settings.py @@ -13,7 +13,7 @@ DEBUG = True -ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', default='127.0.0.1').split() +ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', default='*').split() # Application definition From eedc3fde5717491c206926651bad311741c2eb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 1 Jun 2023 11:54:23 +0300 Subject: [PATCH 77/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/requirements.txt b/backend/requirements.txt index 0cf4e69..aed25d2 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,3 +4,4 @@ Pillow==9.4.0 djoser==2.1.0 python-dotenv==1.0.0 drf-yasg==1.21.5 +django-cors-headers==4.0.0 From c5c77e800dd09a272ab769eed3e5591d58aec70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D0=B5=D0=B2=20=D0=A5=D0=B0=D0=BB=D1=8F=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Thu, 1 Jun 2023 17:21:43 +0300 Subject: [PATCH 78/81] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=80=D1=82=20=D0=B8=D0=B3=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/serializers.py | 12 +++++- backend/api/views.py | 44 +++++++++++++++++----- backend/games/migrations/0011_stagemenu.py | 26 +++++++++++++ backend/games/models.py | 19 +++++++++- 4 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 backend/games/migrations/0011_stagemenu.py diff --git a/backend/api/serializers.py b/backend/api/serializers.py index eedb351..74705d2 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -9,7 +9,7 @@ from users.models import CustomUser, FriendshipRequest from pubs.models import Pub, Menu -from games.models import Game, GameUser, Stage +from games.models import Game, GameUser, Stage, StageMenu class CustomUserCreateSerializer(UserCreatePasswordRetypeSerializer): @@ -287,7 +287,7 @@ class Meta: class StageSerializer(serializers.ModelSerializer): pub = PubInStageSerializer(read_only=True) - drinks = MenuInStageSerializer(read_only=True, many=True) + drinks = serializers.SerializerMethodField() class Meta: model = Stage @@ -296,3 +296,11 @@ class Meta: '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 diff --git a/backend/api/views.py b/backend/api/views.py index 25fdb89..7d52103 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -29,7 +29,7 @@ ) from users.models import CustomUser, FriendshipRequest from pubs.models import Pub, Menu -from games.models import Game, Stage +from games.models import Game, Stage, StageMenu class CustomUserViewSet(UserViewSet): @@ -242,6 +242,13 @@ def get(self, request, game_id): 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 @@ -251,28 +258,36 @@ def get(self, request, game_id): ordered_by_alcohol = all_drinks.order_by('alcohol_percent') if difficulty_level == 'underbeerman': - drinks = ordered_by_alcohol[:interval] + drinks_ids = ordered_by_alcohol[ + :interval + ].values_list('id', flat=True) elif difficulty_level == 'fan': - drinks = ordered_by_alcohol[interval:2*interval] + drinks_ids = ordered_by_alcohol[ + interval:2*interval + ].values_list('id', flat=True) elif difficulty_level == 'freelanholic': - drinks = ordered_by_alcohol[2*interval:] + 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') - ordered_by_cost = drinks.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] + drinks = ordered_by_cost[interval:2 * interval] elif budget_level == 'major': - drinks = ordered_by_cost[2*interval:] + drinks = ordered_by_cost[2 * interval:] - # drinks = drinks.order_by('pub_id') pubs_drinks = defaultdict(list) for drink in drinks: pubs_drinks[drink.pub_id].append(drink.id) stages = [] - for pub, drinks in pubs_drinks.items(): + for pub, pub_drinks in pubs_drinks.items(): stages.append( Stage.objects.create( game=game, @@ -280,8 +295,19 @@ def get(self, request, game_id): ) ) + 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.status = 'started' + game.save() + return Response( data={'id': game.id, 'stages': serialized_stages}, status=status.HTTP_200_OK 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/models.py b/backend/games/models.py index 6b96f7e..170cd79 100644 --- a/backend/games/models.py +++ b/backend/games/models.py @@ -1,6 +1,6 @@ from django.db import models -from pubs.models import Pub +from pubs.models import Pub, Menu from users.models import CustomUser DIFFICULTY_LEVELS = ( @@ -113,3 +113,20 @@ class Meta: 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}' From 422929146ecb455b9d5de51ea2eecbb088855e07 Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Sun, 4 Jun 2023 12:36:27 +0300 Subject: [PATCH 79/81] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B8=D0=B3=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/serializers.py | 6 +++++ backend/api/urls.py | 14 ++++++++++-- backend/api/views.py | 47 ++++++++++++++++++++++++++++++++++---- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 74705d2..0e42d5a 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -304,3 +304,9 @@ def get_drinks(self, obj): 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') diff --git a/backend/api/urls.py b/backend/api/urls.py index dd7ef55..4344b3d 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -12,7 +12,8 @@ PubViewSet, MenuViewSet, GameViewSet, - StartGameAPIView + StartGameAPIView, + FinishGameAPIView ) router_v1 = DefaultRouter() @@ -48,7 +49,16 @@ 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+)/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( diff --git a/backend/api/views.py b/backend/api/views.py index 7d52103..9d3e9b6 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -2,12 +2,17 @@ from django.shortcuts import get_object_or_404 from djoser.views import UserViewSet -from rest_framework import viewsets, mixins, status, permissions, serializers, \ +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 rest_framework.views import APIView from api.pagination import GamesAndFriendsPagination from api.permissions import ( @@ -25,11 +30,13 @@ PubSerializer, MenuSerializer, GameSerializer, - GameCreateSerializer, StageSerializer, + GameCreateSerializer, + StageSerializer, + FinishGameSerializer, ) from users.models import CustomUser, FriendshipRequest from pubs.models import Pub, Menu -from games.models import Game, Stage, StageMenu +from games.models import Game, Stage, StageMenu, GameUser class CustomUserViewSet(UserViewSet): @@ -312,3 +319,35 @@ def get(self, request, game_id): 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.status = 'finished' + game.save() + + return Response( + data=serialized_stats, + status=status.HTTP_200_OK + ) From 1fec7b73bba769696d98d7adf6a31b43a834120b Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Sun, 4 Jun 2023 12:45:49 +0300 Subject: [PATCH 80/81] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D1=8B=20=D1=81=D1=82=D0=B0=D1=80=D1=82=20?= =?UTF-8?q?=D0=B8=20=D1=84=D0=B8=D0=BD=D0=B8=D1=88=20=D0=B8=D0=B3=D1=80?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/api/views.py b/backend/api/views.py index 9d3e9b6..ebe3a01 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,3 +1,4 @@ +import datetime as dt from collections import defaultdict from django.shortcuts import get_object_or_404 @@ -312,6 +313,7 @@ def get(self, request, game_id): serialized_stages = StageSerializer(stages, many=True).data + game.start_time = dt.datetime.now() game.status = 'started' game.save() @@ -344,6 +346,7 @@ def post(self, request, game_id): Game, id=int(game_id) ) + game.finish_time = dt.datetime.now() game.status = 'finished' game.save() From 979646a760ae6ae13eadf497c34927625c04b5fd Mon Sep 17 00:00:00 2001 From: Lev Khalyapin Date: Sun, 4 Jun 2023 12:58:51 +0300 Subject: [PATCH 81/81] =?UTF-8?q?=D0=94=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=20=D1=81=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=20=D0=B8=D0=B3=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/serializers.py | 21 ++++++++++++++------- backend/api/views.py | 1 - 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 0e42d5a..90dda9b 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -266,13 +266,6 @@ def update(self, instance, validated_data): return instance -class GameSerializer(serializers.ModelSerializer): - - class Meta: - model = Game - fields = '__all__' - - class MenuInStageSerializer(serializers.ModelSerializer): class Meta: @@ -310,3 +303,17 @@ 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/views.py b/backend/api/views.py index ebe3a01..35273bf 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -221,7 +221,6 @@ def perform_create(self, serializer): class GameViewSet(viewsets.ModelViewSet): - serializer_class = GameCreateSerializer permission_classes = (PlayerPermission,) def get_serializer_class(self):