34 Commits

Author SHA1 Message Date
e2a7f73804 Added link to telegram channel
Fixes #40
2023-09-14 22:23:48 +03:00
fc8f2b527b Replaced checkbox with text agreements
Fixes #47
2023-09-14 22:18:14 +03:00
a2b0b25233 Fixed unknown category trashboxes
Added specific error for trashboxes token expiration
2023-09-14 01:30:51 +03:00
1f7f69e933 Changed poems adding to packet add_all 2023-09-14 01:30:08 +03:00
860ea43091 Added rounded corners for poem pic 2023-09-14 01:29:20 +03:00
32d1b1b0e6 Added tipical student locations 2023-09-14 00:34:37 +03:00
7ecfe6faa4 Merge pull request 'asynchronous-porridger' (#49) from asynchronous-porridger into main
Reviewed-on: dm1sh/porridger#49
2023-09-13 22:46:16 +03:00
7a4b3978a7 Fixed stories preview buttons color 2023-09-13 22:43:17 +03:00
18a7a0cbb9 Renamed Answer to Success return fields on front 2023-09-13 22:41:53 +03:00
2b001579c5 Fixed front ann book and delete err handling
Button no longer pre-updates on err
File uploading fix
2023-09-13 22:26:53 +03:00
a60ff39c43 postgres related error fixed in filter_ann 2023-09-12 21:29:55 +03:00
761f48c56f HTTP exceptions added instead of True, False responces 2023-09-12 00:35:24 +03:00
f74199b064 HTTP exceptions added instead of True, False responces 2023-09-12 00:31:53 +03:00
e6b34d684a HTTP exceptions added to endpoints (instead of just True, False responces) 2023-09-12 00:22:36 +03:00
60e5463028 HTTP exceptions added (intead of just True or False answers) 2023-09-12 00:19:52 +03:00
558922dcf4 Removed useless files 2023-09-08 19:40:46 +03:00
64a84d7c70 Switched obsolete checking period to daily 2023-09-08 19:40:44 +03:00
acd0a8fbf7 Fixed initial tables creation 2023-09-08 19:40:41 +03:00
22dc21bda1 Refactored trash category convertion 2023-09-08 19:40:38 +03:00
543b7b0c46 Fixed assets mounting in app and container 2023-09-08 19:40:33 +03:00
0df1d50612 Added config loading from .env file 2023-09-08 19:40:29 +03:00
c922c8611e Fixed async-await bugs 2023-09-08 19:40:23 +03:00
f2de7c419e Humanized form captions
Related to #42
2023-09-05 08:42:19 +03:00
e9bf7eabaf Made 'Петроградская' default metro station
Related to #44
2023-09-05 08:35:45 +03:00
74f89ae7cb Fixed poetry image width 2023-09-05 08:26:35 +03:00
7453a60eee async completely fixed 2023-09-03 00:29:17 +03:00
37d219c516 filter_ann almost fixed 2023-09-02 19:04:30 +03:00
f744cce713 fixing ann_filters function 2023-09-02 15:32:20 +03:00
2c870ee983 making async api - 2 2023-08-31 23:41:48 +03:00
4326c70dbc trying to make api work - 1 2023-08-31 22:32:50 +03:00
93d2e2713e add_poems_to_db function made async type 2023-08-31 21:37:18 +03:00
834b0f27bb add_poems_to_db function made async type 2023-08-31 21:34:42 +03:00
ee79b9d4c5 наработки Вовы и Димы 2023-08-31 21:08:23 +03:00
c43814ccd4 Наработки вовы и димы 2023-08-31 21:05:38 +03:00
46 changed files with 502 additions and 2921 deletions

View File

@@ -29,4 +29,7 @@ dist-ssr
uploads/
.env
poems.txt
poem_pic/
__pycache__

3
.gitignore vendored
View File

@@ -28,5 +28,8 @@ dist-ssr
*.db
uploads/
.env
poem_pic/
poem_pic/
__pycache__

View File

@@ -35,5 +35,5 @@ Only docker/podman are required
```sh
docker build . -t porridger:build
docker run --name porridger -p 8000:8000 -v ./sql_app.db:/srv/sql_app.db -v uploads:/srv/uploads -v poem_pic:/srv/poem_pic porridger:build
docker run --name porridger -p 8000:8000 -v ./sql_app.db:/srv/sql_app.db -v ./poems.txt:/srv/poems.txt -v ./poem_pic:/srv/poem_pic -v uploads:/srv/uploads porridger:build
```

View File

@@ -4,8 +4,9 @@
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# 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
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
@@ -48,11 +49,16 @@ prepend_sys_path = .
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# sqlalchemy.url = driver://user:pass@localhost/dbname
; sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
@@ -66,6 +72,12 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

View File

@@ -1,18 +1,18 @@
from sqlalchemy.orm import Session
from sqlalchemy.sql import text, literal_column
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated
from fastapi import Depends
from . import auth_utils, orm_models, pydantic_schemas
from sqlalchemy import select, or_, and_
import datetime
# Переменные для получения данных о мусорках с внешнего API
# url API
BASE_URL='https://geointelect2.gate.petersburg.ru'#адрес сайта и мой токин
# токен для получения данных
my_token='eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhU1RaZm42bHpTdURYcUttRkg1SzN5UDFhT0FxUkhTNm9OendMUExaTXhFIn0.eyJleHAiOjE3ODM3ODk4NjgsImlhdCI6MTY4OTA5NTQ2OCwianRpIjoiNDUzNjQzZTgtYTkyMi00NTI4LWIzYmMtYWJiYTNmYjkyNTkxIiwiaXNzIjoiaHR0cHM6Ly9rYy5wZXRlcnNidXJnLnJ1L3JlYWxtcy9lZ3MtYXBpIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImJjYjQ2NzljLTU3ZGItNDU5ZC1iNWUxLWRlOGI4Yzg5MTMwMyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLXJlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImM2ZDJiOTZhLWMxNjMtNDAxZS05ZjMzLTI0MmE0NDcxMDY5OCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZWdzLWFwaSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiJjNmQyYjk2YS1jMTYzLTQwMWUtOWYzMy0yNDJhNDQ3MTA2OTgiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiLQktC70LDQtNC40LzQuNGAINCv0LrQvtCy0LvQtdCyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZTBmYzc2OGRhOTA4MjNiODgwZGQzOGVhMDJjMmQ5NTciLCJnaXZlbl9uYW1lIjoi0JLQu9Cw0LTQuNC80LjRgCIsImZhbWlseV9uYW1lIjoi0K_QutC-0LLQu9C10LIifQ.E2bW0B-c6W5Lj63eP_G8eI453NlDMnW05l11TZT0GSsAtGayXGaolHtWrmI90D5Yxz7v9FGkkCmcUZYy1ywAdO9dDt_XrtFEJWFpG-3csavuMjXmqfQQ9SmPwDw-3toO64NuZVv6qVqoUlPPj57sLx4bLtVbB4pdqgyJYcrDHg7sgwz4d1Z3tAeUfSpum9s5ZfELequfpLoZMXn6CaYZhePaoK-CxeU3KPBPTPOVPKZZ19s7QY10VdkxLULknqf9opdvLs4j8NMimtwoIiHNBFlgQz10Cr7bhDKWugfvSRsICouniIiBJo76wrj5T92s-ztf1FShJuqnQcKE_QLd2A'
from . import auth_utils, orm_models, pydantic_schemas
# Загружаем стихи
def add_poems_to_db(db: Session):
f1 = open('text121.txt', encoding='utf-8', mode='r')#открыть фаил для чтения на русском
async def add_poems_to_db(async_db: AsyncSession):
poems = []
f1 = open('poems.txt', encoding='utf-8', mode='r')#открыть фаил для чтения на русском
for a in range(1, 110):
f1.seek(0)#перейти к началу
i=0
@@ -36,40 +36,81 @@ def add_poems_to_db(db: Session):
author += str1
poem = orm_models.Poems(title=name, text=stixi, author=author)
# В конце каждой итерации добавляем в базу данных
db.add(poem)
db.commit()
# db.refresh(poem)
poems.append(poem)
async_db.add_all(poems)
await async_db.commit()
# close the file
f1.close()
def filter_ann(schema: pydantic_schemas.SortAnnouncements, db: Annotated[Session, Depends(auth_utils.get_session)]):
async def filter_ann(schema: pydantic_schemas.SortAnnouncements, db: AsyncSession):
"""Функция для последовательного применения различных фильтров (через схему SortAnnouncements)"""
res = db.query(orm_models.Announcement)
fields = schema.__dict__ # параметры передоваемой схемы SortAnnouncements (ключи и значения)
# проходим по названиям фильтров и их значениям
for name, filt in fields.items():
# выбираем все строки
query = await db.execute(select(orm_models.Announcement))
res = set(query.scalars().all())
for name, filt_val in fields.items():
# res = await db.execute(statement)
# если фильтр задан
if filt is not None:
d = {name: filt}
# фильтруем
res = res.filter_by(**d)
if filt_val is not None:
if name == "obsolete":
filt_val = bool(filt_val)
filter_query = await db.execute(select(orm_models.Announcement).where(literal_column(f"announcements.{name}") == filt_val))
filtered = set(filter_query.scalars().all())
res = res.intersection(filtered)
# # отфильтровываем подходящие объявления
# res = await db.execute(
# select(orm_models.Announcement).where(
# ((schema.obsolete == None) | ((schema.obsolete != None) & (orm_models.Announcement.obsolete == schema.obsolete)))
# & ((schema.user_id == None) | ((schema.user_id != None) & (orm_models.Announcement.user_id == schema.user_id)))
# & ((schema.metro == None) | ((schema.metro != None) & (orm_models.Announcement.metro == schema.metro)))
# & ((schema.category == None) | ((schema.category != None) & (orm_models.Announcement.category == schema.category)))
# )
# )
# .where(schema.user_id != None and orm_models.Announcement.user_id == schema.user_id)
# .where(schema.metro != None and orm_models.Announcement.metro == schema.metro)
# .where(schema.category != None and orm_models.Announcement.category == schema.category)
# statement = text("SELECT * FROM announcements "
# "WHERE announcements.obsolete = :obsolete "
# "INTERSECT"
# "SELECT * FROM announcements "
# "WHERE announcements.user_id == :user_id "
# "INTERSECT"
# "SELECT * FROM announcements "
# "WHERE announcements.metro == :metro "
# "INTERSECT"
# "SELECT * FROM announcements "
# "WHERE announcements.category == :category")
# res = await db.execute(statement,
# {"obsolete": schema.obsolete,
# "user_id": schema.user_id,
# "metro": schema.metro,
# "category": schema.category}
# )
# возвращаем все подходящие объявления
return res.all()
return res
def check_obsolete(db: Annotated[Session, Depends(auth_utils.get_session)], current_date: datetime.date):
async def check_obsolete(db: AsyncSession, current_date: datetime.date):
"""
Функция участвует в процессе обновления поля obsolete у всех объявлений раз в сутки
"""
# обращаемся ко всем объявлениям бд
announcements = db.query(orm_models.Announcement).all()
query_announcements = await db.execute(select(orm_models.Announcement))
announcements = query_announcements.scalars().all()
# для каждого объявления
for ann in announcements:
# если просрочено
if ann.best_by < current_date:
ann.obsolete = True
db.commit()
db.refresh(ann) # обновляем состояние объекта
await db.commit()
await db.refresh(ann) # обновляем состояние объекта

View File

@@ -10,6 +10,7 @@ from fastapi.requests import Request
from typing import Any, Annotated, List, Union
from starlette.staticfiles import StaticFiles
from sqlalchemy.orm import Session
from sqlalchemy import select
import requests
from uuid import uuid4
@@ -24,6 +25,8 @@ import os
from . import add_poems_and_filters, auth_utils, orm_models, pydantic_schemas
from .config import TRASHBOXES_BASE_URL, TRASHBOXES_TOKEN
# создаем приложение Fastapi
app = FastAPI()
@@ -40,6 +43,10 @@ if not os.path.exists("./uploads"):
# создаем эндпоинт для хранения файлов пользователя
app.mount("/uploads", StaticFiles(directory = "./uploads"))
# эндпоинт для возвращения согласия в pdf
@app.get("/privacy_policy.pdf")
async def privacy_policy():
return FileResponse("./privacy_policy.pdf")
# получение списка объявлений
@app.get("/api/announcements", response_model=List[pydantic_schemas.Announcement])#адрес объявлений
@@ -48,7 +55,7 @@ async def announcements_list(db: Annotated[Session, Depends(auth_utils.get_sessi
# параметры для сортировки (схема pydantic schemas.SortAnnouncements)
params_to_sort = pydantic_schemas.SortAnnouncements(obsolete=obsolete, user_id=user_id, metro=metro, category=category)
# получаем результат
result = add_poems_and_filters.filter_ann(db=db, schema=params_to_sort)
result = await add_poems_and_filters.filter_ann(db=db, schema=params_to_sort)
return result
@@ -58,11 +65,11 @@ async def announcements_list(db: Annotated[Session, Depends(auth_utils.get_sessi
async def single_announcement(ann_id:int, db: Annotated[Session, Depends(auth_utils.get_session)]): # передаем индекс обявления
# Считываем данные из Body и отображаем их на странице.
# В последствии будем вставлять данные в html-форму
try:
announcement = db.get(orm_models.Announcement, ann_id)
return announcement
except:
return {"Answer" : False} #если неуданый доступ, то сообщаем об этом
announcement = await db.get(orm_models.Announcement, ann_id)
#announcement = await db.execute(select(orm_models.Announcement)).scalars().all()
if not announcement:
raise HTTPException(status_code=404, detail="Item not found")
return announcement
# Занести объявление в базу данных
@@ -71,46 +78,53 @@ async def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form(
address: Annotated[str, Form()], longtitude: Annotated[float, Form()], latitude: Annotated[float, Form()],
description: Annotated[str, Form()], metro: Annotated[str, Form()], current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_active_user)],
db: Annotated[Session, Depends(auth_utils.get_session)], src: Union[UploadFile, None] = None, trashId: Annotated[int, Form()] = None):
# имя загруженного файла по умолчанию - пустая строка
uploaded_name = ""
# если пользователь загрузил картинку
if src:
# процесс сохранения картинки
f = src.file
f.seek(0, os.SEEK_END)
if f.tell() > 0:
f.seek(0)
destination = pathlib.Path("./uploads/" + str(hash(f)) + pathlib.Path(src.filename).suffix.lower())
with destination.open('wb') as buffer:
shutil.copyfileobj(f, buffer)
# изменяем название директории загруженного файла
uploaded_name = "/uploads/" + destination.name
# создаем объект Announcement
temp_ancmt = orm_models.Announcement(user_id=current_user.id, name=name, category=category, best_by=bestBy,
address=address, longtitude=longtitude, latitude=latitude, description=description, metro=metro,
trashId=trashId, src=uploaded_name, booked_by=0)
try:
# имя загруженного файла по умолчанию - пустая строка
uploaded_name = ""
# если пользователь загрузил картинку
if src:
# процесс сохранения картинки
f = src.file
f.seek(0, os.SEEK_END)
if f.tell() > 0:
f.seek(0)
destination = pathlib.Path("./uploads/" + str(hash(f)) + pathlib.Path(src.filename).suffix.lower())
with destination.open('wb') as buffer:
shutil.copyfileobj(f, buffer)
# изменяем название директории загруженного файла
uploaded_name = "/uploads/" + destination.name
# создаем объект Announcement
temp_ancmt = orm_models.Announcement(user_id=current_user.id, name=name, category=category, best_by=bestBy,
address=address, longtitude=longtitude, latitude=latitude, description=description, metro=metro,
trashId=trashId, src=uploaded_name, booked_by=0)
db.add(temp_ancmt) # добавляем в бд
db.commit() # сохраняем изменения
db.refresh(temp_ancmt) # обновляем состояние объекта
return {"Answer" : True}
await db.commit() # сохраняем изменения
await db.refresh(temp_ancmt) # обновляем состояние объекта
return {"Success": True}
except:
return {"Answer" : False}
raise HTTPException(status_code=500, detail="problem with adding object to db")
# Удалить объявления из базы
@app.delete("/api/announcement") #адрес объявления
async def delete_from_db(announcement: pydantic_schemas.DelAnnouncement, db: Annotated[Session, Depends(auth_utils.get_session)]): # функция удаления объекта из БД
# находим объект с заданным id в бд
#to_delete = db.query(orm_models.Announcement).filter(orm_models.Announcement.id==announcement.id).first()
query = await db.execute(select(orm_models.Announcement).where(orm_models.Announcement.id==announcement.id))
to_delete = query.scalars().first()
if not to_delete:
raise HTTPException(status_code=404, detail="Item not found. Can't delete")
try:
# находим объект с заданным id в бд
to_delete = db.query(orm_models.Announcement).filter(orm_models.Announcement.id==announcement.id).first()
db.delete(to_delete) # удаление из БД
db.commit() # сохраняем изменения
return {"Answer" : True}
await db.delete(to_delete) # удаление из БД
await db.commit() # сохраняем изменения
return {"Success": True}
except:
return {"Answer" : False}
raise HTTPException(status_code=500, detail="Problem with adding to database")
# Забронировать объявление
@@ -118,7 +132,9 @@ async def delete_from_db(announcement: pydantic_schemas.DelAnnouncement, db: Ann
async def change_book_status(data: pydantic_schemas.Book, current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_user)],
db: Annotated[Session, Depends(auth_utils.get_session)]):
# Находим объявление по данному id
announcement_to_change = db.query(orm_models.Announcement).filter(orm_models.Announcement.id == data.id).first()
#announcement_to_change = db.query(orm_models.Announcement).filter(orm_models.Announcement.id == data.id).first()
query = await db.execute(select(orm_models.Announcement).where(orm_models.Announcement.id == data.id))
announcement_to_change = query.scalars().first()
# Проверяем, что объявление с данным id существует
if not announcement_to_change:
raise HTTPException(status_code=404, detail="Item not found")
@@ -129,8 +145,8 @@ async def change_book_status(data: pydantic_schemas.Book, current_user: Annotate
# Инкрементируем поле booked_by на 1
announcement_to_change.booked_by += 1
# фиксируем изменения в бд
db.commit()
db.refresh(announcement_to_change)
await db.commit()
await db.refresh(announcement_to_change)
return {"Success": True}
@@ -140,14 +156,17 @@ async def create_user(nickname: Annotated[str, Form()], password: Annotated[str,
name: Annotated[str, Form()]=None, surname: Annotated[str, Form()]=None, avatar: Annotated[UploadFile, Form()]=None):
# проверяем, что юзера с введенным никнеймом не существует в бд
if db.query(orm_models.User).filter(orm_models.User.nickname == nickname).first() == None:
#if db.query(orm_models.User).filter(orm_models.User.nickname == nickname).first() == None:
query_user = await db.execute(select(orm_models.User).where(orm_models.User.nickname == nickname))
user_with_entered_nick = query_user.scalars().first()
if user_with_entered_nick == None:
# создаем нового юзера
new_user = orm_models.User(nickname=nickname, hashed_password=auth_utils.get_password_hash(password),
name=name, surname=surname, reg_date=datetime.date.today())
# добавляем в бд
db.add(new_user)
db.commit()
db.refresh(new_user) # обновляем состояние объекта
await db.commit()
await db.refresh(new_user) # обновляем состояние объекта
return {"Success": True}
return {"Success": False, "Message": "Пользователь с таким email уже зарегестрирован"}
@@ -158,7 +177,7 @@ async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: Annotated[Session, Depends(auth_utils.get_session)]
):
# пробуем найти юзера в бд по введенным паролю и никнейму
user = auth_utils.authenticate_user(db, form_data.username, form_data.password)
user = await auth_utils.authenticate_user(db, form_data.username, form_data.password)
# если не нашли - кидаем ошибку
if not user:
raise HTTPException(
@@ -177,7 +196,7 @@ async def login_for_access_token(
# получаем данные успешно вошедшего пользователя
@app.get("/api/users/me", response_model=pydantic_schemas.User) #
async def read_users_me(current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_active_user)]):
def read_users_me(current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_active_user)]):
return current_user
@@ -186,20 +205,20 @@ async def read_users_me(current_user: Annotated[pydantic_schemas.User, Depends(a
async def add_points(data: pydantic_schemas.AddRating, current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_user)], db: Annotated[Session, Depends(auth_utils.get_session)]):
# проверяем,
if current_user.id != data.user_id:
user = auth_utils.get_user_by_id(db, data.user_id)
user = await auth_utils.get_user_by_id(db, data.user_id)
if not user:
raise HTTPException(status_code=404, detail="Item not found")
user.rating = (user.rating*user.num_of_ratings + data.rate)/(user.num_of_ratings + 1)
user.num_of_ratings += 1
db.commit()
db.refresh(user) # обновляем состояние объекта
await db.commit()
await db.refresh(user) # обновляем состояние объекта
return {"Success": True}
# получаем рейтинг пользователя
@app.get("/api/user/rating")
async def add_points(user_id: int, db: Annotated[Session, Depends(auth_utils.get_session)]):
user = auth_utils.get_user_by_id(db, user_id=user_id)
user = await auth_utils.get_user_by_id(db, user_id=user_id)
if not user:
raise HTTPException(status_code=404, detail="Item not found")
return {"rating": user.rating}
@@ -208,24 +227,39 @@ async def add_points(user_id: int, db: Annotated[Session, Depends(auth_utils.get
# Отправляем стихи
@app.get("/api/user/poem", response_model=pydantic_schemas.Poem)
async def poems_to_front(db: Annotated[Session, Depends(auth_utils.get_session)]):
num_of_poems = db.query(orm_models.Poems).count() # определяем кол-во стихов в бд
#num_of_poems = db.query(orm_models.Poems).count() # определяем кол-во стихов в бд
query = await db.execute(select(orm_models.Poems)) # определяем кол-во стихов в бд
num_of_poems = len(query.scalars().all())
# если стихов в бд нет
if num_of_poems < 1:
add_poems_and_filters.add_poems_to_db(db) # добавляем поэмы в базу данных
num_of_poems = db.query(orm_models.Poems).count() # определяем кол-во стихов в бд
await add_poems_and_filters.add_poems_to_db(db) # добавляем поэмы в базу данных
# после добавления стихов снова определяем кол-во стихов в бд
query = await db.execute(select(orm_models.Poems))
num_of_poems = len(query.scalars().all())
rand_id = random.randint(1, num_of_poems) # генерируем номер стихотворения
poem = db.query(orm_models.Poems).filter(orm_models.Poems.id == rand_id).first() # находим стих в бд
#poem = db.query(orm_models.Poems).filter(orm_models.Poems.id == rand_id).first() # находим стих в бд
query_poem = await db.execute(select(orm_models.Poems).where(orm_models.Poems.id == rand_id)) # находим стих в бд
poem = query_poem.scalars().first()
if not poem:
raise HTTPException(status_code=404, detail="Poem not found")
return poem
trashboxes_category = {
"PORRIDGE": ["Опасные отходы", "Иное"],
"conspects": ["Бумага"],
"milk": ["Стекло", "Тетра Пак", "Иное"],
"bred": ["Пластик", "Иное"],
"wathing": ["Пластик", "Опасные отходы", "Иное"],
"cloth": ["Одежда"],
"fruits_vegatables": ["Иное"],
"other_things": ["Металл", "Бумага", "Стекло", "Иное", "Тетра Пак", "Батарейки", "Крышечки", "Шины",
"Опасные отходы", "Лампочки", "Пластик"]
}
@app.get("/api/trashbox", response_model=List[pydantic_schemas.TrashboxResponse])
async def get_trashboxes(data: pydantic_schemas.TrashboxRequest = Depends()):#крутая функция для работы с api
async def get_trashboxes(data: pydantic_schemas.TrashboxRequest = Depends()): #крутая функция для работы с api
# json, передаваемый стороннему API
BASE_URL= "https://geointelect2.gate.petersburg.ru"
my_token="eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhU1RaZm42bHpTdURYcUttRkg1SzN5UDFhT0FxUkhTNm9OendMUExaTXhFIn0.eyJleHAiOjE3ODYyMjUzMzMsImlhdCI6MTY5MTUzMDkzMywianRpIjoiYjU0MmU3MTQtYzJkMS00NTY2LWJkY2MtYmQ5NzA0ODY1ZjgzIiwiaXNzIjoiaHR0cHM6Ly9rYy5wZXRlcnNidXJnLnJ1L3JlYWxtcy9lZ3MtYXBpIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImJjYjQ2NzljLTU3ZGItNDU5ZC1iNWUxLWRlOGI4Yzg5MTMwMyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLXJlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjJhOTgwMzUyLTY1M2QtNGZlZC1iMDI1LWQ1N2U0NDRjZmM3NiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZWdzLWFwaSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiIyYTk4MDM1Mi02NTNkLTRmZWQtYjAyNS1kNTdlNDQ0Y2ZjNzYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiLQktC70LDQtNC40LzQuNGAINCv0LrQvtCy0LvQtdCyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZTBmYzc2OGRhOTA4MjNiODgwZGQzOGVhMDJjMmQ5NTciLCJnaXZlbl9uYW1lIjoi0JLQu9Cw0LTQuNC80LjRgCIsImZhbWlseV9uYW1lIjoi0K_QutC-0LLQu9C10LIifQ.FTKiC1hpWcOkmSW9QZpC-RY7Ko50jw1mDMfXIWYxlQ-zehLm2CLmOnHvYoOoI39k2OzeCIAB9ZdRrrGZc6G9Z1eFELUjNGEqKxSC1Phj9ATemKgbOKEttk-OGc-rFr9VPA8_SnfvLts6wTI2YK33YBIxCF5nCbnr4Qj3LeEQ0d6Hy8PO4ATrBF5EOeuAZRprvIEjXe_f8N9ONKckCPB-xFB4P2pZlVXGoCNoewGEcY3zXH4khezN6zcVr6tpc6G8dBv9EqT_v92IDSg-aXQk6ysA0cO0-6x5w1-_qU0iHGIAPsLNV9IKBoFbjc0JH6cWabldPRH12NP1trvYfqKDGQ"
head = {'Authorization': 'Bearer {}'.format(my_token)}
head = {'Authorization': 'Bearer ' + TRASHBOXES_TOKEN}
# Данные пользователя (местоположение, количество мусорок, которое пользователь хочет видеть)
my_data={
'x' : f"{data.Lng}",
@@ -233,27 +267,18 @@ async def get_trashboxes(data: pydantic_schemas.TrashboxRequest = Depends()):#к
'limit' : '1'
}
# Перевод категории с фронта на категорию с сайта
match data.Category:
case "PORRIDGE":
list_of_category = ["Опасные отходы", "Иное"]
case "conspects":
list_of_category = ["Бумага"]
case "milk":
list_of_category = ["Стекло", "Тетра Пак", "Иное"]
case "bred":
list_of_category = ["Пластик", "Иное"]
case "wathing":
list_of_category = ["Пластик", "Опасные отходы", "Иное"]
case "cloth":
list_of_category = ["Одежда"]
case "fruits_vegatables":
list_of_category = ["Иное"]
case "other_things":
list_of_category = ["Металл", "Бумага", "Стекло", "Иное", "Тетра Пак", "Батарейки", "Крышечки", "Шины",
"Опасные отходы", "Лампочки", "Пластик"]
try:
list_of_category = trashboxes_category[data.Category]
except:
list_of_category = trashboxes_category['other_things']
# Получение ответа от стороннего апи
response = requests.post(f"{BASE_URL}/nearest_recycling/get", headers=head, data=my_data, timeout=10)
response = requests.post(TRASHBOXES_BASE_URL + "/nearest_recycling/get", headers=head, data=my_data, timeout=10)
infos = response.json()
if 'error' in infos and infos['error_description'] == 'Invalid bearer token':
raise HTTPException(status_code=502, detail="Invalid trashboxes token")
# Чтение ответа
trashboxes = []
for trashbox in infos["results"]:
@@ -283,7 +308,7 @@ async def react_app(req: Request, rest_of_path: str):
async def dispose(data: pydantic_schemas.DisposeRequest, current_user_schema: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_user)],
db: Annotated[Session, Depends(auth_utils.get_session)]):
# Находим в бд текущего юзера
current_user = auth_utils.get_user_by_id(db, current_user_schema.id)
current_user = await auth_utils.get_user_by_id(db, current_user_schema.id)
# Начисляем баллы пользователю за утилизацию
current_user.points += 60
# В полученном json переходим к данным мусорки
@@ -295,12 +320,14 @@ async def dispose(data: pydantic_schemas.DisposeRequest, current_user_schema: An
db.add(new_trashox)
# в соответствии с логикой api, после утилизации объявление пользователя удаляется
# находим объявление с айди data.ann_id
ann_to_del = db.query(orm_models.Announcement).filter(orm_models.Announcement.id == data.ann_id).first() # находим стих в бд
#ann_to_del = db.query(orm_models.Announcement).filter(orm_models.Announcement.id == data.ann_id).first() #
query_ann = await db.execute(select(orm_models.Announcement).where(orm_models.Announcement.id == data.ann_id)) # находим объявление в бд
ann_to_del = query_ann.scalars().first()
if not ann_to_del:
raise HTTPException(status_code=404, detail="Announcement not found")
# удаляем объявление из бд
db.delete(ann_to_del)
db.commit()
db.refresh(new_trashox) # обновляем состояние объекта
await db.delete(ann_to_del)
await db.commit()
await db.refresh(new_trashox) # обновляем состояние объекта
return {"Success": True}

View File

@@ -1,6 +1,5 @@
from datetime import datetime, timedelta
from typing import Annotated, Union
import os
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
@@ -9,15 +8,12 @@ from passlib.context import CryptContext
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.ext.asyncio import AsyncSession
from dotenv import load_dotenv
from .db import SessionLocal
from . import orm_models, pydantic_schemas
from .config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
load_dotenv("unimportant.env")
SECRET_KEY = os.getenv("SECRET_KEY")
ALGORITHM = os.getenv("ALGORITHM")
ACCESS_TOKEN_EXPIRE_MINUTES = os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES")
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
@@ -27,30 +23,32 @@ async def get_session() -> AsyncSession:
yield session
async def verify_password(plain_password, hashed_password):
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
async def get_password_hash(password):
def get_password_hash(password):
return pwd_context.hash(password)
async def get_user_by_nickname(db: Annotated[AsyncSession, Depends(get_session)], nickname: str):
user_with_required_id = db.select(orm_models.User).where(orm_models.User.nickname == nickname).first()
if user_with_required_id:
return user_with_required_id
query = await db.execute(select(orm_models.User).where(orm_models.User.nickname == nickname))
user_with_required_nickname = query.scalars().first()
if user_with_required_nickname:
return user_with_required_nickname
return None
async def get_user_by_id(db: Annotated[AsyncSession, Depends(get_session)], user_id: int):
user_with_required_id = db.select(orm_models.User).where(orm_models.User.id == user_id).first()
query = await db.execute(select(orm_models.User).where(orm_models.User.id == user_id))
user_with_required_id = query.scalars().first()
if user_with_required_id:
return user_with_required_id
return None
async def authenticate_user(db: Annotated[AsyncSession, Depends(get_session)], nickname: str, password: str):
user = get_user_by_nickname(db=db, nickname=nickname)
user = await get_user_by_nickname(db=db, nickname=nickname)
if not user:
return False
if not verify_password(password, user.hashed_password):
@@ -58,7 +56,7 @@ async def authenticate_user(db: Annotated[AsyncSession, Depends(get_session)], n
return user
async def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
@@ -83,14 +81,14 @@ async def get_current_user(db: Annotated[AsyncSession, Depends(get_session)], to
token_data = pydantic_schemas.TokenData(user_id=user_id)
except JWTError:
raise credentials_exception
user = get_user_by_id(db, user_id=token_data.user_id)
user = await get_user_by_id(db, user_id=token_data.user_id)
if user is None:
raise credentials_exception
return pydantic_schemas.User(id=user.id, nickname=user.nickname, name=user.name, surname=user.surname,
disabled=user.disabled, items=user.announcements, reg_date=user.reg_date, points=user.points)
async def get_current_active_user(
def get_current_active_user(
current_user: Annotated[pydantic_schemas.User, Depends(get_current_user)]
):
if current_user.disabled:

13
back/config.py Normal file
View File

@@ -0,0 +1,13 @@
import os
from dotenv import load_dotenv
load_dotenv('.env')
TRASHBOXES_TOKEN = os.environ.get("TRASHBOXES_TOKEN")
TRASHBOXES_BASE_URL = os.environ.get("TRASHBOXES_BASE_URL")
SECRET_KEY = os.environ.get("SECRET_KEY")
ALGORITHM = os.environ.get("ALGORITHM")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES"))
SQLALCHEMY_DATABASE_URL = os.environ.get("SQLALCHEMY_DATABASE_URL")

View File

@@ -3,19 +3,18 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_scoped_session, create_as
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from .config import SQLALCHEMY_DATABASE_URL
SQLALCHEMY_DATABASE_URL = 'postgresql+asyncpg://postgres:D560c34V112Ak@localhost/porridger'
engine = create_async_engine(SQLALCHEMY_DATABASE_URL, echo=True)
engine = create_async_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
async_session = async_scoped_session(SessionLocal, scopefunc=current_task)
async_session = SessionLocal()
# async_session = async_scoped_session(SessionLocal, scopefunc=current_task)
Base = declarative_base()
# Создаем таблицы
async def init_models():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)

View File

@@ -3,6 +3,8 @@ import uvicorn
from .api import app as app_fastapi
from .scheduler import app as app_rocketry
from .db import init_models
class Server(uvicorn.Server):
"""Customized uvicorn.Server
@@ -16,6 +18,9 @@ class Server(uvicorn.Server):
async def main():
"Run scheduler and the API"
await init_models()
server = Server(config=uvicorn.Config(app_fastapi, workers=1, loop="asyncio", host="0.0.0.0"))
api = asyncio.create_task(server.serve())

View File

@@ -1,7 +1,8 @@
from sqlalchemy import Column, Integer, String, Boolean, Float, Date, ForeignKey
from .db import Base, engine
from sqlalchemy.orm import relationship
from .db import Base, engine
class User(Base):#класс пользователя
__tablename__ = "users"
@@ -17,8 +18,8 @@ class User(Base):#класс пользователя
num_of_ratings = Column(Integer, default=0) # количество оценок (т.е. то, сколько раз другие пользователи оценили текущего)
reg_date = Column(Date) # дата регистрации
announcements = relationship("Announcement", back_populates="user")
trashboxes_chosen = relationship("Trashbox", back_populates="user")
announcements = relationship("Announcement", back_populates="user", lazy='selectin')
trashboxes_chosen = relationship("Trashbox", back_populates="user", lazy='selectin')
class Announcement(Base): #класс объявления
__tablename__ = "announcements"

View File

@@ -2,13 +2,13 @@ from . import add_poems_and_filters
from rocketry import Rocketry
from rocketry.conds import daily
import datetime
from .db import database
from .db import async_session
app = Rocketry(execution="async")
# Create task:
@app.task('minutely')
@app.task('daily')
async def daily_check():
# Фильтруем по сроку годности
add_poems_and_filters.check_obsolete(database, current_date=datetime.date.today())
await add_poems_and_filters.check_obsolete(async_session, current_date=datetime.date.today())

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhU1RaZm42bHpTdURYcUttRkg1SzN5UDFhT0FxUkhTNm9OendMUExaTXhFIn0.eyJleHAiOjE3ODYyMjUzMzMsImlhdCI6MTY5MTUzMDkzMywianRpIjoiYjU0MmU3MTQtYzJkMS00NTY2LWJkY2MtYmQ5NzA0ODY1ZjgzIiwiaXNzIjoiaHR0cHM6Ly9rYy5wZXRlcnNidXJnLnJ1L3JlYWxtcy9lZ3MtYXBpIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImJjYjQ2NzljLTU3ZGItNDU5ZC1iNWUxLWRlOGI4Yzg5MTMwMyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLXJlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjJhOTgwMzUyLTY1M2QtNGZlZC1iMDI1LWQ1N2U0NDRjZmM3NiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZWdzLWFwaSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiIyYTk4MDM1Mi02NTNkLTRmZWQtYjAyNS1kNTdlNDQ0Y2ZjNzYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiLQktC70LDQtNC40LzQuNGAINCv0LrQvtCy0LvQtdCyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZTBmYzc2OGRhOTA4MjNiODgwZGQzOGVhMDJjMmQ5NTciLCJnaXZlbl9uYW1lIjoi0JLQu9Cw0LTQuNC80LjRgCIsImZhbWlseV9uYW1lIjoi0K_QutC-0LLQu9C10LIifQ.FTKiC1hpWcOkmSW9QZpC-RY7Ko50jw1mDMfXIWYxlQ-zehLm2CLmOnHvYoOoI39k2OzeCIAB9ZdRrrGZc6G9Z1eFELUjNGEqKxSC1Phj9ATemKgbOKEttk-OGc-rFr9VPA8_SnfvLts6wTI2YK33YBIxCF5nCbnr4Qj3LeEQ0d6Hy8PO4ATrBF5EOeuAZRprvIEjXe_f8N9ONKckCPB-xFB4P2pZlVXGoCNoewGEcY3zXH4khezN6zcVr6tpc6G8dBv9EqT_v92IDSg-aXQk6ysA0cO0-6x5w1-_qU0iHGIAPsLNV9IKBoFbjc0JH6cWabldPRH12NP1trvYfqKDGQ"
DOMAIN = "https://geointelect2.gate.petersburg.ru"
SECRET_KEY = "651a52941cf5de14d48ef5d7af115709"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 1440

View File

@@ -6,7 +6,7 @@ const composePutAnnouncementURL = () => (
)
const processPutAnnouncement = (data: PutAnnouncementResponse): PutAnnouncement => {
return data.Answer
return data.Success
}
export { composePutAnnouncementURL, processPutAnnouncement }

View File

@@ -1,12 +1,12 @@
import { isObject } from '../../utils/types'
type PutAnnouncementResponse = {
Answer: boolean,
Success: boolean,
}
const isPutAnnouncementResponse = (obj: unknown): obj is PutAnnouncementResponse => (
isObject(obj, {
'Answer': 'boolean',
'Success': 'boolean',
})
)

View File

@@ -6,11 +6,11 @@ const composeRemoveAnnouncementURL = () => (
)
function processRemoveAnnouncement(data: RemoveAnnouncementResponse): RemoveAnnouncement {
if (!data.Answer) {
if (!data.Success) {
throw new Error('Не удалось закрыть объявление')
}
return data.Answer
return data.Success
}
export { composeRemoveAnnouncementURL, processRemoveAnnouncement }

View File

@@ -1,12 +1,12 @@
import { isObject } from '../../utils/types'
type RemoveAnnouncementResponse = {
Answer: boolean,
Success: boolean,
}
const isRemoveAnnouncementResponse = (obj: unknown): obj is RemoveAnnouncementResponse => (
isObject(obj, {
'Answer': 'boolean',
'Success': 'boolean',
})
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -105,5 +105,7 @@ const lineByName = (name: string) => (
lines.find(line => stations[line].has(name))
)
const DEFAULT_LINE = 'Петроградская'
export type { Lines }
export { lines, stations, colors, lineNames, lineByName }
export { lines, stations, colors, lineNames, lineByName, DEFAULT_LINE }

View File

@@ -0,0 +1,73 @@
import { iconDormitory, iconITMO, iconLETI } from '../utils/markerIcons'
type LocationType = 'dormitory' | 'leti' | 'itmo'
const studentLocations: {
name: string,
position: [number, number],
type: LocationType
}[] = [
{
name: 'Первое, второе, третье общежития',
position: [59.987299, 30.330672],
type: 'dormitory',
},
{
name: 'Четвертое общежитие',
position: [59.985620, 30.331319],
type: 'dormitory',
},
{
name: 'Шестое общежитие',
position: [59.969713, 30.299851],
type: 'dormitory',
},
{
name: 'Седьмое общежитие',
position: [60.003723, 30.287616],
type: 'dormitory',
},
{
name: 'Восьмое общежитие',
position: [59.991115, 30.318752],
type: 'dormitory',
},
{
name: 'Общежития Межвузовского студенческого городка',
position: [59.871053, 30.307154],
type: 'dormitory',
},
{
name: 'Одиннадцатое общежитие',
position: [59.877962, 30.242889],
type: 'dormitory',
},
{
name: 'Общежитие Академии транспортных технологий',
position: [59.870375, 30.308646],
type: 'dormitory',
},
{
name: 'ЛЭТИ шестой корпус',
position: [59.971578, 30.296653],
type: 'leti',
},
{
name: 'ЛЭТИ Первый и другие корпуса',
position: [59.971947, 30.324303],
type: 'leti',
},
{
name: 'ИТМО',
position: [59.956363, 30.310029],
type: 'itmo',
},
]
const locationsIcons: Record<LocationType, L.Icon> = {
dormitory: iconDormitory,
itmo: iconITMO,
leti: iconLETI,
}
export { studentLocations, locationsIcons }

View File

@@ -11,6 +11,7 @@ import { iconItem } from '../utils/markerIcons'
import { useId } from '../hooks'
import SelectDisposalTrashbox from './SelectDisposalTrashbox'
import StarRating from './StarRating'
import StudentLocations from './StudentLocations'
const styles = {
container: {
@@ -52,6 +53,8 @@ const View = ({
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
<StudentLocations />
<Marker icon={iconItem} position={[lat, lng]}>
<Popup>
{address}

View File

@@ -47,18 +47,13 @@ const AuthForm = ({ goBack }: AuthFormProps) => {
<Form.Control placeholder='Пароль' name='password' type='password' required />
</Form.Group>
<Form.Group className='mb-3' controlId='privacyPolicyConsent'>
<Form.Check>
<Form.Check.Input type='checkbox' required />
<Form.Check.Label>
Я согласен с{' '}
<a
href={`${document.location.origin}/privacy_policy.pdf`}
target='_blank'
>условиями обработки персональных данных</a>
</Form.Check.Label>
</Form.Check>
</Form.Group>
<p>
Нажимая на кнопку, я даю своё согласие на обработку персональных данных и соглашаюсь с{' '}
<a
href={`${document.location.origin}/privacy_policy.pdf`}
target='_blank'
>условиями политики конфиденциальности</a>
</p>
<ButtonGroup className='d-flex'>
<Button

View File

@@ -77,7 +77,7 @@ function Filters({ filter, setFilter, filterShown, setFilterShown }: FiltersProp
</Form.Group>
<Button variant='success' type='submit'>
Отправить
Выбрать
</Button>
</Form>
</Modal.Body>

View File

@@ -24,9 +24,10 @@ function LocationMarker({ address, position, setPosition }: LocationMarkerProps)
})
return (
<Marker icon={iconItem} position={position}>
<Marker icon={iconItem} position={position} zIndexOffset={1000}>
<Popup>
{address}
<br />
{position.lat.toFixed(4)}, {position.lng.toFixed(4)}
</Popup>
</Marker>

View File

@@ -26,7 +26,7 @@ function Poetry() {
}}
/>
<p><em>{poetry.data.author}</em></p>
<img src={`/poem_pic/${poetry.data.id}.jpg`} alt='Иллюстрация' />
<img className={styles.image} src={`/poem_pic/${poetry.data.id}.jpg`} alt='Иллюстрация' />
</>
)
) : (

View File

@@ -1,6 +1,5 @@
import { Link } from 'react-router-dom'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { Button } from 'react-bootstrap'
import { UserCategory, composeUserCategoriesFilters, userCategoriesInfos } from '../assets/userCategories'
import { Announcement } from '../api/announcement/types'
@@ -96,9 +95,9 @@ function StoriesPreviewCarousel({ announcements, category }: StoriesPreviewProps
return <div className={styles.container}>
{showScrollButtons.left &&
<Button onClick={doScroll(false)} className={`${styles.scrollButton} ${styles.leftScrollButton}`}>
<button onClick={doScroll(false)} className={`${styles.scrollButton} ${styles.leftScrollButton}`}>
<img src={rightAngleIcon} alt='Показать ещё' />
</Button>
</button>
}
<ul className={styles.list} ref={ulElement}>
@@ -106,9 +105,9 @@ function StoriesPreviewCarousel({ announcements, category }: StoriesPreviewProps
</ul>
{showScrollButtons.right &&
<Button onClick={doScroll(true)} className={`${styles.scrollButton} ${styles.rightScrollButton}`}>
<button onClick={doScroll(true)} className={`${styles.scrollButton} ${styles.rightScrollButton}`}>
<img src={rightAngleIcon} alt='Показать ещё' />
</Button>
</button>
}
</div>
}

View File

@@ -0,0 +1,36 @@
import { Marker, Tooltip, useMap } from 'react-leaflet'
import { LatLng } from 'leaflet'
import { locationsIcons, studentLocations } from '../assets/studentLocations'
type StudentLocationsProps = {
setPosition?: (pos: LatLng) => void
}
function StudentLocations({ setPosition }: StudentLocationsProps) {
const map = useMap()
return (
<>{
studentLocations.map((el) =>
<Marker
icon={locationsIcons[el.type]}
position={el.position}
eventHandlers={{
click: setPosition && (() => {
setPosition(new LatLng(...el.position))
map.setView(el.position)
}),
}}
>
<Tooltip>
{el.name}
</Tooltip>
</Marker>
)
}
</>
)
}
export default StudentLocations

View File

@@ -17,3 +17,4 @@ export { default as SelectDisposalTrashbox } from './SelectDisposalTrashbox'
export { default as StarRating } from './StarRating'
export { default as CardLayout } from './CardLayout'
export { default as LocateButton } from './LocateButton'
export { default as StudentLocations } from './StudentLocations'

View File

@@ -14,8 +14,8 @@ function useAddAnnouncement() {
processPutAnnouncement
)
function handleAdd(formData: FormData) {
void doSend({}, {
async function handleAdd(formData: FormData) {
await doSend({}, {
body: formData,
})
}

View File

@@ -15,8 +15,8 @@ function useBook() {
processBook,
)
const handleBook = useCallback((id: number) => {
void doSend({}, {
const handleBook = useCallback(async (id: number) => {
await doSend({}, {
body: JSON.stringify({
id,
}),

View File

@@ -12,7 +12,7 @@ function useSendButtonCaption(
const [title, setTitle] = useState(initial)
const update = useCallback(<T extends NonNullable<unknown>>(data: T | null | undefined) => {
if (data !== undefined) { // not loading
if (data !== undefined && data !== null) { // not loading or error
setCaption(result)
setTitle('Отправить ещё раз')

View File

@@ -1,13 +1,13 @@
import { CSSProperties, FormEventHandler, useEffect, useState } from 'react'
import { CSSProperties, FormEventHandler, useEffect, useMemo, useState } from 'react'
import { Form, Button } from 'react-bootstrap'
import { MapContainer, TileLayer } from 'react-leaflet'
import { latLng } from 'leaflet'
import { useNavigate } from 'react-router-dom'
import { MapClickHandler, LocationMarker, CardLayout, LocateButton } from '../components'
import { MapClickHandler, LocationMarker, CardLayout, LocateButton, StudentLocations } from '../components'
import { useAddAnnouncement } from '../hooks/api'
import { categories, categoryNames } from '../assets/category'
import { stations, lines, lineNames } from '../assets/metro'
import { stations, lines, lineNames, DEFAULT_LINE } from '../assets/metro'
import { fallbackError, gotResponse } from '../hooks/useFetch'
import { useOsmAddresses } from '../hooks/api'
@@ -38,7 +38,7 @@ function AddPage() {
formData.append('address', address.data || '') // if address.error
formData.set('bestBy', new Date((formData.get('bestBy') as number | null) || 0).getTime().toString())
handleAdd(formData)
void handleAdd(formData)
}
useEffect(() => {
@@ -51,15 +51,15 @@ function AddPage() {
<CardLayout text='Опубликовать объявление'>
<Form onSubmit={handleSubmit}>
<Form.Group className='mb-3' controlId='name'>
<Form.Label>Заголовок объявления</Form.Label>
<Form.Label>Как ошляпите объявление?</Form.Label>
<Form.Control type='text' required name='name' />
</Form.Group>
<Form.Group className='mb-3' controlId='category'>
<Form.Label>Категория</Form.Label>
<Form.Label>Какая категория?</Form.Label>
<Form.Select required name='category'>
<option value='' hidden>
Выберите категорию
Выберите категорию предмета
</option>
{categories.map(category =>
<option key={category} value={category}>{categoryNames[category]}</option>
@@ -68,12 +68,12 @@ function AddPage() {
</Form.Group>
<Form.Group className='mb-3' controlId='bestBy'>
<Form.Label>Срок годности</Form.Label>
<Form.Label>Когда закрыть объявление?</Form.Label>
<Form.Control type='date' required name='bestBy' />
</Form.Group>
<Form.Group className='mb-3' controlId='address'>
<Form.Label>Адрес выдачи</Form.Label>
<Form.Label>Где забирать?</Form.Label>
<div className='mb-3'>
<MapContainer
scrollWheelZoom={false}
@@ -86,6 +86,8 @@ function AddPage() {
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
{useMemo(() => <StudentLocations setPosition={setAddressPosition} />, [])}
<LocationMarker
address={gotResponse(address) ? fallbackError(address) : 'Загрузка...'}
position={addressPosition}
@@ -103,7 +105,7 @@ function AddPage() {
</Form.Group>
<Form.Group className='mb-3' controlId='description'>
<Form.Label>Описание</Form.Label>
<Form.Label>Пару слов о вас и предмете?</Form.Label>
<Form.Control
as='textarea'
name='description'
@@ -113,7 +115,7 @@ function AddPage() {
</Form.Group>
<Form.Group className='mb-3' controlId='src'>
<Form.Label>Иллюстрация (фото или видео)</Form.Label>
<Form.Label>Загрузите гляделки? (фото или видео)</Form.Label>
<Form.Control
type='file'
name='src'
@@ -124,12 +126,9 @@ function AddPage() {
<Form.Group className='mb-3' controlId='metro'>
<Form.Label>
Станция метро
Где метро поближе?
</Form.Label>
<Form.Select name='metro'>
<option value=''>
Укажите ближайщую станцию метро
</option>
<Form.Select name='metro' defaultValue={DEFAULT_LINE}>
{lines.map(
line =>
<optgroup key={line} label={lineNames[line]}>

View File

@@ -48,6 +48,11 @@ function UserPage() {
<div className='mb-3'>
<Poetry />
</div>
<div className='mb-3'>
<h4>Связаться с разработчиками</h4>
Канал в телеграме: <a href='https://t.me/porridger'>Porridger</a>
</div>
</Container>
)
}

View File

@@ -5,3 +5,8 @@
.text {
white-space: pre-wrap;
}
.image {
width: 100%;
border-radius: 12px;
}

View File

@@ -42,6 +42,7 @@
top: 0;
z-index: 100;
background: linear-gradient(to right, rgba(17, 17, 17, 0) 0%, rgba(17, 17, 17, 255) 100%);
background-color: transparent;
display: block;
height: 100%;
width: 10%;

View File

@@ -2,6 +2,10 @@ import L from 'leaflet'
import itemMarker from '../assets/itemMarker.png'
import trashMarker from '../assets/trashMarker.png'
import dormitoryMarker from '../assets/dormitoryMarker.png'
import letiMarker from '../assets/letiMarker.png'
import itmoMarker from '../assets/itmoMarker.png'
const iconItem = new L.Icon({
iconUrl: itemMarker,
@@ -17,4 +21,25 @@ const iconTrash = new L.Icon({
iconSize: [34, 41],
})
export { iconItem, iconTrash }
const iconDormitory = new L.Icon({
iconUrl: dormitoryMarker,
iconRetinaUrl: dormitoryMarker,
popupAnchor: [0, 0],
iconSize: [41, 41],
})
const iconLETI = new L.Icon({
iconUrl: letiMarker,
iconRetinaUrl: letiMarker,
popupAnchor: [0, 0],
iconSize: [41, 41],
})
const iconITMO = new L.Icon({
iconUrl: itmoMarker,
iconRetinaUrl: itmoMarker,
popupAnchor: [0, 0],
iconSize: [41, 41],
})
export { iconItem, iconTrash, iconDormitory, iconLETI, iconITMO }

View File

@@ -1 +1 @@
Generic single-database configuration.
Generic single-database configuration with an async dbapi.

View File

@@ -1,11 +1,12 @@
import asyncio
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
from back import auxiliary_for_alembic, db
# this is the Alembic Config object, which provides
@@ -17,6 +18,10 @@ config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = auxiliary_for_alembic.Base.metadata
# other values from the config, defined by the needs of env.py,
@@ -25,7 +30,7 @@ target_metadata = auxiliary_for_alembic.Base.metadata
# ... etc.
def run_migrations_offline():
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
@@ -37,44 +42,49 @@ def run_migrations_offline():
script output.
"""
# url = config.get_main_option("sqlalchemy.url")
url = config.get_main_option(db.SQLALCHEMY_DATABASE_URL)
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
render_as_batch=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
def do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)
In this scenario we need to create an Engine
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
"""In this scenario we need to create an Engine
and associate a connection with the context.
"""
configuration = config.get_section(config.config_ini_section)
configuration['sqlalchemy.url'] = db.SQLALCHEMY_DATABASE_URL
connectable = engine_from_config(
connectable = async_engine_from_config(
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=True
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
with context.begin_transaction():
context.run_migrations()
await connectable.dispose()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
asyncio.run(run_async_migrations())
if context.is_offline_mode():

View File

@@ -5,20 +5,22 @@ Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
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)}
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade():
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade():
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,30 @@
"""lazy=selectin added to user table relationships
Revision ID: 8e631a2fe6b8
Revises:
Create Date: 2023-09-02 23:45:08.799366
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '8e631a2fe6b8'
down_revision: Union[str, None] = None
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 ###

BIN
privacy_policy.pdf Normal file

Binary file not shown.

View File

@@ -1,5 +1,7 @@
aiosqlite==0.19.0
annotated-types==0.5.0
anyio==3.7.1
asyncpg==0.28.0
certifi==2023.7.22
charset-normalizer==3.2.0
click==8.1.6
@@ -15,6 +17,7 @@ pyasn1==0.5.0
pydantic==1.10.10
pydantic_core==2.4.0
python-dateutil==2.8.2
python-dotenv==1.0.0
python-jose==3.3.0
python-multipart==0.0.6
redbird==0.7.1