Skip to content

Commit

Permalink
Merge pull request #5 from Studio-Yandex-Practicum/admin-panel
Browse files Browse the repository at this point in the history
Add an admin panel
  • Loading branch information
WolfMTK authored Jan 24, 2024
2 parents c3196f3 + fb25f33 commit 4d05193
Show file tree
Hide file tree
Showing 12 changed files with 246 additions and 15 deletions.
2 changes: 2 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,5 @@ cython_debug/
.idea/
idea.py
*/.idea

.isort.cfg
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
"""First migration.
"""first maigration
Revision ID: 5fb6698e907e
Revision ID: 2e5068b24499
Revises:
Create Date: 2024-01-18 11:23:45.547254
Create Date: 2024-01-24 09:49:57.440908
"""
from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "5fb6698e907e"
revision: str = "2e5068b24499"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
Expand All @@ -33,6 +33,18 @@ def upgrade() -> None:
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("phone", sa.String(), nullable=False),
sa.Column("email", sa.String(), nullable=False),
sa.Column(
"assistance_segment",
sa.Enum(
"children_in_hospital",
"children_in_orphanages",
"disabled_children",
"auto_volunteer",
"not_decide",
name="assistancesegment",
),
nullable=False,
),
sa.Column("meeting_id", sa.Integer(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
Expand Down
11 changes: 10 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
aiofiles==23.2.1
aiogram==3.3.0
aiogram-forms==1.1.1
aiohttp==3.9.1
aiosignal==1.3.1
aiosqlite==0.19.0
alembic==1.13.1
annotated-types==0.6.0
anyio==4.2.0
async-timeout==4.0.3
attrs==23.2.0
Babel==2.14.0
black==23.12.1
cachetools==5.3.2
certifi==2023.11.17
click==8.1.7
dependency-injector==4.41.0
dnspython==2.4.2
email-validator==2.1.0.post1
fastapi==0.109.0
Expand All @@ -20,6 +23,8 @@ gunicorn==21.2.0
h11==0.14.0
idna==3.6
isort==5.13.2
itsdangerous==2.1.2
Jinja2==3.1.3
magic-filter==1.0.12
Mako==1.3.0
MarkupSafe==2.1.3
Expand All @@ -28,12 +33,16 @@ mypy-extensions==1.0.0
packaging==23.2
pathspec==0.12.1
platformdirs==4.1.0
psycopg==3.1.17
pydantic==2.5.3
pydantic_core==2.14.6
python-dotenv==1.0.0
python-multipart==0.0.6
six==1.16.0
sniffio==1.3.0
SQLAlchemy==2.0.25
starlette==0.35.1
starlette-admin==0.13.1
typing_extensions==4.9.0
uvicorn==0.25.0
yarl==1.9.4
21 changes: 14 additions & 7 deletions backend/src/application/services/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class UserService:
async def validate_user_exists(self, uow: UoW, search_params):
"""Проверка уникальности пользователя."""
search_params = {
key: value for key, value in search_params if value != None
key: value for key, value in search_params if value is not None
}
if not search_params:
return
Expand Down Expand Up @@ -51,8 +51,11 @@ async def create_user(self, uow: UoW, schema: UserCreate) -> GetUser:
meeting_id = schema.meeting_id
await self.validate_meeting_exists(uow, meeting_id)
del schema.meeting_id
assistance_segment = schema.assistance_segment
del schema.assistance_segment
await self.validate_user_exists(uow, schema)
schema.meeting_id = meeting_id
schema.assistance_segment = assistance_segment
await self.validate_meeting_is_open(uow, meeting_id)
async with uow:
user = await uow.users.add_one(**schema.model_dump())
Expand All @@ -65,13 +68,17 @@ async def update_user(
"""Обновить инфо о пользователе."""
meeting_id = schema.meeting_id
del schema.meeting_id
await self.validate_user_exists(uow, schema)
if assistance_segment := schema.assistance_segment:
del schema.assistance_segment
await self.validate_user_exists(uow, schema)
schema.assistance_segment = assistance_segment
schema.meeting_id = meeting_id
if schema.meeting_id > 0:
await self.validate_meeting_exists(uow, schema.meeting_id)
await self.validate_meeting_is_open(uow, schema.meeting_id)
else:
raise ObjectIsNoneException
if meeting_id:
if meeting_id > 0:
await self.validate_meeting_exists(uow, meeting_id)
await self.validate_meeting_is_open(uow, meeting_id)
else:
raise ObjectIsNoneException
async with uow:
user = await uow.users.update_one(id=id, **schema.model_dump())
await uow.commit()
Expand Down
9 changes: 8 additions & 1 deletion backend/src/core/settings.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import os
from dataclasses import dataclass

from dotenv import load_dotenv


load_dotenv()


@dataclass
class Settings:
"""Настройки проекта."""

db_url: str = os.getenv("DB_URL") or "sqlite+aiosqlite:///sqlite.db"
app_title = "API для админки проекта 'Созидатели'."
app_title = "API для проекта 'Созидатели'."
admin_panel_user = os.getenv("ADMIN_PANEL_USER")
admin_panel_password = os.getenv("ADMIN_PANEL_PASSWORD")
9 changes: 9 additions & 0 deletions backend/src/domain/models/assistance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from enum import Enum


class AssistanceSegment(Enum):
children_in_hospital = "Детям в больницах"
children_in_orphanages = "Детям в детских домах"
disabled_children = "Семьям с детьми-инвалидами"
auto_volunteer = "Могу автоволонтерить"
not_decide = "Еще не определился"
7 changes: 7 additions & 0 deletions backend/src/domain/models/meetings.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from datetime import datetime

from fastapi import Request
from sqlalchemy.orm import Mapped, mapped_column, relationship

from src.domain.schemas import GetMeeting
from src.infrastructure.db import Base


DATE_FORMAT = "%d.%m.%Y %H:%M"


class Meeting(Base):
"""Модель собраний."""

Expand All @@ -18,3 +22,6 @@ def to_read_model(self) -> GetMeeting:
attrs = self.__dict__.copy()
attrs.pop("_sa_instance_state", None)
return GetMeeting(**attrs)

async def __admin_repr__(self, request: Request):
return f"{self.date.strftime(DATE_FORMAT)}"
11 changes: 10 additions & 1 deletion backend/src/domain/models/users.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
from sqlalchemy import ForeignKey, String
from fastapi import Request
from sqlalchemy import Enum, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from src.domain.schemas import GetUser
from src.infrastructure.db import Base

from .assistance import AssistanceSegment


class User(Base):
"""Модель пользователя."""

name: Mapped[str] = mapped_column(String(255), nullable=False)
phone: Mapped[str] = mapped_column(unique=True)
email: Mapped[str] = mapped_column(unique=True, nullable=False)
assistance_segment: Mapped[AssistanceSegment] = mapped_column(
Enum(AssistanceSegment), default=AssistanceSegment.not_decide
)
meeting_id: Mapped[int] = mapped_column(ForeignKey("meeting.id"))
meeting = relationship("Meeting", back_populates="users")

def to_read_model(self) -> GetUser:
attrs = self.__dict__.copy()
attrs.pop("_sa_instance_state", None)
return GetUser(**attrs)

async def __admin_repr__(self, request: Request):
return f"{self.name}"
5 changes: 5 additions & 0 deletions backend/src/domain/schemas/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from pydantic import BaseModel, ConfigDict, EmailStr, Field, validator

from src.domain.models.assistance import AssistanceSegment


NUMBER_RE = r"^(\+7|8)?[\s\-]?\(?\d{3}\)?[\s\-]?\d{3}[\s\-]?\d{2}[\s\-]?\d{2}$"

Expand All @@ -14,13 +16,15 @@ class GetUser(BaseModel):
phone: str
email: EmailStr
meeting_id: int
assistance_segment: AssistanceSegment


class UserCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
phone: str
email: EmailStr
meeting_id: int
assistance_segment: AssistanceSegment | None = "not_decide"

@validator("phone")
def validate_phone(cls, value):
Expand All @@ -36,3 +40,4 @@ class UserUpdate(BaseModel):
phone: str | None = None
email: EmailStr | None = None
meeting_id: int | None = None
assistance_segment: AssistanceSegment | None = None
94 changes: 94 additions & 0 deletions backend/src/infrastructure/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from typing import Any, Dict

from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.requests import Request
from starlette_admin import (
BooleanField,
DateTimeField,
EmailField,
EnumField,
HasMany,
HasOne,
PhoneField,
StringField,
TextAreaField,
)
from starlette_admin.contrib.sqla import Admin
from starlette_admin.contrib.sqla.ext.pydantic import ModelView
from starlette_admin.exceptions import FormValidationError
from starlette_admin.i18n import I18nConfig

from src.domain.models import Meeting, User
from src.domain.models.assistance import AssistanceSegment
from src.domain.schemas import MeetingCreate, UserCreate

from .db import engine
from .provider import UsernameAndPasswordProvider


class UserView(ModelView):
"""Модель отображения пользователя в админке."""

identity = "user"

fields = [
StringField("name", label="Имя", required=True),
PhoneField("phone", label="Номер телефона", required=True),
EmailField("email", required=True),
EnumField(
"assistance_segment",
label="Направление помощи",
enum=AssistanceSegment,
),
HasOne("meeting", label="Собрание", identity="meeting", required=True),
]
label = "Участники"
sortable_fields = [User.meeting]

async def validate(self, request: Request, data: Dict[str, Any]) -> None:
if data["meeting"] is None:
raise FormValidationError(
{"meeting": "Надо выбрать дату собрания."}
)
meeting = data.pop("meeting")
data["meeting_id"] = meeting.id
await super().validate(request, data)
if assistance_segment_value := data.get("assistance_segment"):
assistance_segment_key = AssistanceSegment(
assistance_segment_value
).name
data["assistance_segment"] = assistance_segment_key
data.pop("meeting_id")
data["meeting"] = meeting


class MeetingView(ModelView):
"""Модель для отображения собраний в админке."""

identity = "meeting"

fields = [
DateTimeField("date", label="Дата и время"),
BooleanField("is_open", label="Закрыто/Открыто"),
TextAreaField("description", label="Описание собрания"),
HasMany("users", label="Участники собрания", identity="user"),
]
label = "Собрания"
sortable_fields = ["date", "is_open"]
fields_default_sort = ["date", ("is_open", True)]


admin = Admin(
engine,
title="Проект 'Созидатели'",
i18n_config=I18nConfig(default_locale="ru"),
middlewares=[Middleware(SessionMiddleware, secret_key="1234567890")],
auth_provider=UsernameAndPasswordProvider(),
)

admin.add_view(UserView(User, pydantic_model=UserCreate, icon="fa fa-user"))

admin.add_view(
MeetingView(Meeting, pydantic_model=MeetingCreate, icon="fa fa-calendar")
)
Loading

0 comments on commit 4d05193

Please sign in to comment.