From 3e4f035e37865d8afb8260f5d2a7b041f24c7c21 Mon Sep 17 00:00:00 2001 From: pavlovvitaliy Date: Mon, 22 Jan 2024 15:43:59 +0300 Subject: [PATCH 1/2] Admin panel has been added. --- .../versions/642a749be4a1_3_migration.py | 30 ++++++++++++ .../versions/9964a5033b04_2_migration.py | 30 ++++++++++++ backend/src/domain/models/meetings.py | 7 +++ backend/src/domain/models/users.py | 4 ++ backend/src/infrastructure/admin.py | 48 +++++++++++++++++++ backend/src/main.py | 2 + 6 files changed, 121 insertions(+) create mode 100644 backend/alembic/versions/642a749be4a1_3_migration.py create mode 100644 backend/alembic/versions/9964a5033b04_2_migration.py create mode 100644 backend/src/infrastructure/admin.py diff --git a/backend/alembic/versions/642a749be4a1_3_migration.py b/backend/alembic/versions/642a749be4a1_3_migration.py new file mode 100644 index 0000000..3005969 --- /dev/null +++ b/backend/alembic/versions/642a749be4a1_3_migration.py @@ -0,0 +1,30 @@ +"""3 migration + +Revision ID: 642a749be4a1 +Revises: 9964a5033b04 +Create Date: 2024-01-22 11:05:34.358319 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "642a749be4a1" +down_revision: Union[str, None] = "9964a5033b04" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/alembic/versions/9964a5033b04_2_migration.py b/backend/alembic/versions/9964a5033b04_2_migration.py new file mode 100644 index 0000000..4115099 --- /dev/null +++ b/backend/alembic/versions/9964a5033b04_2_migration.py @@ -0,0 +1,30 @@ +"""2 migration + +Revision ID: 9964a5033b04 +Revises: 5fb6698e907e +Create Date: 2024-01-22 10:26:53.306394 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "9964a5033b04" +down_revision: Union[str, None] = "5fb6698e907e" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/src/domain/models/meetings.py b/backend/src/domain/models/meetings.py index e119e9e..317ba9a 100644 --- a/backend/src/domain/models/meetings.py +++ b/backend/src/domain/models/meetings.py @@ -1,4 +1,5 @@ from datetime import datetime +from fastapi import Request from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -6,6 +7,9 @@ from src.infrastructure.db import Base +DATE_FORMAT = "%d.%m.%Y %H:%M" + + class Meeting(Base): """Модель собраний.""" @@ -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)}" diff --git a/backend/src/domain/models/users.py b/backend/src/domain/models/users.py index dc44530..aa55241 100644 --- a/backend/src/domain/models/users.py +++ b/backend/src/domain/models/users.py @@ -1,3 +1,4 @@ +from fastapi import Request from sqlalchemy import ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -18,3 +19,6 @@ 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}" diff --git a/backend/src/infrastructure/admin.py b/backend/src/infrastructure/admin.py new file mode 100644 index 0000000..58d7e2c --- /dev/null +++ b/backend/src/infrastructure/admin.py @@ -0,0 +1,48 @@ +from typing import Any, Dict + +from starlette.requests import Request +from starlette_admin.contrib.sqla import Admin +from starlette_admin.contrib.sqla.ext.pydantic import ModelView +from starlette_admin.exceptions import FormValidationError + +from src.domain.models import User, Meeting +from src.domain.schemas import UserCreate, MeetingCreate +from .db import engine + + +class UserView(ModelView): + """Модель отображения пользователя в админке.""" + + fields = ["name", "phone", "email", User.meeting] + label = "Участники" + sortable_fields = [User.meeting] + searchable_fields = ["name", "phone", "email", 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) + data.pop("meeting_id") + data["meeting"] = meeting + + +class MeetingView(ModelView): + """Модель для отображения собраний в админке.""" + fields = ["date", "is_open", "description", Meeting.users] + label = "Собрания" + sortable_fields = ["date", "is_open"] + searchable_fields = ["date"] + fields_default_sort = ["date", ("is_open", True)] + + +admin = Admin(engine, title="Проект 'Созидатели'") + +admin.add_view(UserView(User, icon="fa fa-user", pydantic_model=UserCreate)) + +admin.add_view( + MeetingView(Meeting, icon="fa fa-calendar", pydantic_model=MeetingCreate) +) diff --git a/backend/src/main.py b/backend/src/main.py index b8d5aeb..debe6a2 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -2,12 +2,14 @@ from src.api import routers from src.core import Settings +from src.infrastructure.admin import admin def create_app() -> FastAPI: """Фабрика FastAPI.""" app = FastAPI(title=Settings.app_title) + admin.mount_to(app) for router in routers: app.include_router(router) From fb25f3377ee78d02f5a23ffe3fd3eb210ff1694d Mon Sep 17 00:00:00 2001 From: pavlovvitaliy Date: Wed, 24 Jan 2024 15:29:33 +0300 Subject: [PATCH 2/2] Update admin panel. --- backend/.gitignore | 2 + ...on.py => 2e5068b24499_first_maigration.py} | 22 ++++-- .../versions/642a749be4a1_3_migration.py | 30 -------- .../versions/9964a5033b04_2_migration.py | 30 -------- backend/requirements.txt | 11 ++- backend/src/application/services/users.py | 21 ++++-- backend/src/core/settings.py | 9 ++- backend/src/domain/models/assistance.py | 9 +++ backend/src/domain/models/meetings.py | 2 +- backend/src/domain/models/users.py | 7 +- backend/src/domain/schemas/users.py | 5 ++ backend/src/infrastructure/admin.py | 64 ++++++++++++++--- backend/src/infrastructure/provider.py | 68 +++++++++++++++++++ 13 files changed, 195 insertions(+), 85 deletions(-) rename backend/alembic/versions/{5fb6698e907e_first_migration.py => 2e5068b24499_first_maigration.py} (75%) delete mode 100644 backend/alembic/versions/642a749be4a1_3_migration.py delete mode 100644 backend/alembic/versions/9964a5033b04_2_migration.py create mode 100644 backend/src/domain/models/assistance.py create mode 100644 backend/src/infrastructure/provider.py diff --git a/backend/.gitignore b/backend/.gitignore index 4b3f305..0943ca2 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -160,3 +160,5 @@ cython_debug/ .idea/ idea.py */.idea + +.isort.cfg diff --git a/backend/alembic/versions/5fb6698e907e_first_migration.py b/backend/alembic/versions/2e5068b24499_first_maigration.py similarity index 75% rename from backend/alembic/versions/5fb6698e907e_first_migration.py rename to backend/alembic/versions/2e5068b24499_first_maigration.py index e4db8e4..0738ec9 100644 --- a/backend/alembic/versions/5fb6698e907e_first_migration.py +++ b/backend/alembic/versions/2e5068b24499_first_maigration.py @@ -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 @@ -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( diff --git a/backend/alembic/versions/642a749be4a1_3_migration.py b/backend/alembic/versions/642a749be4a1_3_migration.py deleted file mode 100644 index 3005969..0000000 --- a/backend/alembic/versions/642a749be4a1_3_migration.py +++ /dev/null @@ -1,30 +0,0 @@ -"""3 migration - -Revision ID: 642a749be4a1 -Revises: 9964a5033b04 -Create Date: 2024-01-22 11:05:34.358319 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = "642a749be4a1" -down_revision: Union[str, None] = "9964a5033b04" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### diff --git a/backend/alembic/versions/9964a5033b04_2_migration.py b/backend/alembic/versions/9964a5033b04_2_migration.py deleted file mode 100644 index 4115099..0000000 --- a/backend/alembic/versions/9964a5033b04_2_migration.py +++ /dev/null @@ -1,30 +0,0 @@ -"""2 migration - -Revision ID: 9964a5033b04 -Revises: 5fb6698e907e -Create Date: 2024-01-22 10:26:53.306394 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = "9964a5033b04" -down_revision: Union[str, None] = "5fb6698e907e" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### diff --git a/backend/requirements.txt b/backend/requirements.txt index cb411f6..edf9e1e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 @@ -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 @@ -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 diff --git a/backend/src/application/services/users.py b/backend/src/application/services/users.py index b7aa704..392baa9 100644 --- a/backend/src/application/services/users.py +++ b/backend/src/application/services/users.py @@ -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 @@ -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()) @@ -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() diff --git a/backend/src/core/settings.py b/backend/src/core/settings.py index 0742cd7..34888b4 100644 --- a/backend/src/core/settings.py +++ b/backend/src/core/settings.py @@ -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") diff --git a/backend/src/domain/models/assistance.py b/backend/src/domain/models/assistance.py new file mode 100644 index 0000000..6779472 --- /dev/null +++ b/backend/src/domain/models/assistance.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class AssistanceSegment(Enum): + children_in_hospital = "Детям в больницах" + children_in_orphanages = "Детям в детских домах" + disabled_children = "Семьям с детьми-инвалидами" + auto_volunteer = "Могу автоволонтерить" + not_decide = "Еще не определился" diff --git a/backend/src/domain/models/meetings.py b/backend/src/domain/models/meetings.py index 317ba9a..35f9d2b 100644 --- a/backend/src/domain/models/meetings.py +++ b/backend/src/domain/models/meetings.py @@ -1,6 +1,6 @@ from datetime import datetime -from fastapi import Request +from fastapi import Request from sqlalchemy.orm import Mapped, mapped_column, relationship from src.domain.schemas import GetMeeting diff --git a/backend/src/domain/models/users.py b/backend/src/domain/models/users.py index aa55241..13adf2c 100644 --- a/backend/src/domain/models/users.py +++ b/backend/src/domain/models/users.py @@ -1,10 +1,12 @@ from fastapi import Request -from sqlalchemy import ForeignKey, String +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): """Модель пользователя.""" @@ -12,6 +14,9 @@ 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") diff --git a/backend/src/domain/schemas/users.py b/backend/src/domain/schemas/users.py index cbc911e..ef45a6f 100644 --- a/backend/src/domain/schemas/users.py +++ b/backend/src/domain/schemas/users.py @@ -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}$" @@ -14,6 +16,7 @@ class GetUser(BaseModel): phone: str email: EmailStr meeting_id: int + assistance_segment: AssistanceSegment class UserCreate(BaseModel): @@ -21,6 +24,7 @@ class UserCreate(BaseModel): phone: str email: EmailStr meeting_id: int + assistance_segment: AssistanceSegment | None = "not_decide" @validator("phone") def validate_phone(cls, value): @@ -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 diff --git a/backend/src/infrastructure/admin.py b/backend/src/infrastructure/admin.py index 58d7e2c..db73c3c 100644 --- a/backend/src/infrastructure/admin.py +++ b/backend/src/infrastructure/admin.py @@ -1,22 +1,50 @@ 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 src.domain.models import User, Meeting -from src.domain.schemas import UserCreate, MeetingCreate from .db import engine +from .provider import UsernameAndPasswordProvider class UserView(ModelView): """Модель отображения пользователя в админке.""" - fields = ["name", "phone", "email", User.meeting] + 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] - searchable_fields = ["name", "phone", "email", User.meeting] async def validate(self, request: Request, data: Dict[str, Any]) -> None: if data["meeting"] is None: @@ -26,23 +54,41 @@ async def validate(self, request: Request, data: Dict[str, Any]) -> None: 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): """Модель для отображения собраний в админке.""" - fields = ["date", "is_open", "description", Meeting.users] + + identity = "meeting" + + fields = [ + DateTimeField("date", label="Дата и время"), + BooleanField("is_open", label="Закрыто/Открыто"), + TextAreaField("description", label="Описание собрания"), + HasMany("users", label="Участники собрания", identity="user"), + ] label = "Собрания" sortable_fields = ["date", "is_open"] - searchable_fields = ["date"] fields_default_sort = ["date", ("is_open", True)] -admin = Admin(engine, title="Проект 'Созидатели'") +admin = Admin( + engine, + title="Проект 'Созидатели'", + i18n_config=I18nConfig(default_locale="ru"), + middlewares=[Middleware(SessionMiddleware, secret_key="1234567890")], + auth_provider=UsernameAndPasswordProvider(), +) -admin.add_view(UserView(User, icon="fa fa-user", pydantic_model=UserCreate)) +admin.add_view(UserView(User, pydantic_model=UserCreate, icon="fa fa-user")) admin.add_view( - MeetingView(Meeting, icon="fa fa-calendar", pydantic_model=MeetingCreate) + MeetingView(Meeting, pydantic_model=MeetingCreate, icon="fa fa-calendar") ) diff --git a/backend/src/infrastructure/provider.py b/backend/src/infrastructure/provider.py new file mode 100644 index 0000000..158c6e5 --- /dev/null +++ b/backend/src/infrastructure/provider.py @@ -0,0 +1,68 @@ +from starlette.requests import Request +from starlette.responses import Response +from starlette_admin.auth import AdminConfig, AdminUser, AuthProvider +from starlette_admin.exceptions import FormValidationError, LoginFailed + +from src.core.settings import Settings + + +users = { + "admin": { + "name": "SuperAdmin", + # "avatar": "admin.png", + # "company_logo_url": "admin.png", + "roles": ["read", "create", "edit", "delete", "action_make_published"], + }, +} + + +class UsernameAndPasswordProvider(AuthProvider): + async def login( + self, + username: str, + password: str, + remember_me: bool, + request: Request, + response: Response, + ) -> Response: + if len(username) < 3: + """Form data validation""" + raise FormValidationError( + {"username": "Имя пользователя длинне чем 3 символа."} + ) + if username in users and password == Settings.admin_panel_password: + request.session.update({"username": username}) + return response + + raise LoginFailed("Неправильные логин или пароль.") + + async def is_authenticated(self, request) -> bool: + if request.session.get("username", None) in users: + request.state.user = users.get(request.session["username"]) + return True + + return False + + def get_admin_config(self, request: Request) -> AdminConfig: + user = request.state.user + custom_app_title = "Привет " + user["name"] + "!" + custom_logo_url = None + if user.get("company_logo_url", None): + custom_logo_url = request.url_for( + "static", path=user["company_logo_url"] + ) + return AdminConfig( + app_title=custom_app_title, + logo_url=custom_logo_url, + ) + + def get_admin_user(self, request: Request) -> AdminUser: + user = request.state.user + photo_url = None + if user.get("avatar") is not None: + photo_url = request.url_for("static", path=user["avatar"]) + return AdminUser(username=user["name"], photo_url=photo_url) + + async def logout(self, request: Request, response: Response) -> Response: + request.session.clear() + return response