Compare commits

..

7 Commits

32 changed files with 359 additions and 295 deletions

1
.gitignore vendored
View File

@ -28,7 +28,6 @@ dist-ssr
*.db *.db
uploads/ uploads/
.env .env
poem_pic/
poem_pic/ poem_pic/

View File

@ -11,7 +11,6 @@ from . import auth_utils, orm_models, pydantic_schemas
# Загружаем стихи # Загружаем стихи
async def add_poems_to_db(async_db: AsyncSession): async def add_poems_to_db(async_db: AsyncSession):
poems = []
f1 = open('poems.txt', encoding='utf-8', mode='r')#открыть фаил для чтения на русском f1 = open('poems.txt', encoding='utf-8', mode='r')#открыть фаил для чтения на русском
for a in range(1, 110): for a in range(1, 110):
f1.seek(0)#перейти к началу f1.seek(0)#перейти к началу
@ -36,11 +35,8 @@ async def add_poems_to_db(async_db: AsyncSession):
author += str1 author += str1
poem = orm_models.Poems(title=name, text=stixi, author=author) poem = orm_models.Poems(title=name, text=stixi, author=author)
# В конце каждой итерации добавляем в базу данных # В конце каждой итерации добавляем в базу данных
poems.append(poem) async_db.add(poem)
async_db.commit()
async_db.add_all(poems)
await async_db.commit()
# close the file # close the file
f1.close() f1.close()
@ -61,39 +57,6 @@ async def filter_ann(schema: pydantic_schemas.SortAnnouncements, db: AsyncSessio
filter_query = await db.execute(select(orm_models.Announcement).where(literal_column(f"announcements.{name}") == 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()) filtered = set(filter_query.scalars().all())
res = res.intersection(filtered) 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 return res

View File

@ -43,33 +43,66 @@ if not os.path.exists("./uploads"):
# создаем эндпоинт для хранения файлов пользователя # создаем эндпоинт для хранения файлов пользователя
app.mount("/uploads", StaticFiles(directory = "./uploads")) app.mount("/uploads", StaticFiles(directory = "./uploads"))
# эндпоинт для возвращения согласия в pdf # эндпоинт для возвращения согласия в pdf
@app.get("/privacy_policy.pdf") @app.get("/privacy_policy.pdf")
async def privacy_policy(): async def privacy_policy():
return FileResponse("./privacy_policy.pdf") return FileResponse("./privacy_policy.pdf")
# получение списка объявлений # получение списка объявлений
@app.get("/api/announcements", response_model=List[pydantic_schemas.Announcement])#адрес объявлений @app.get("/api/announcements", response_model=List[pydantic_schemas.Announcement])
async def announcements_list(db: Annotated[Session, Depends(auth_utils.get_session)], obsolete: Union[bool, None] = False, user_id: Union[int, None] = None, async def announcements_list(db: Annotated[Session, Depends(auth_utils.get_session)],
metro: Union[str, None] = None,category: Union[str, None] = None): current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_active_user)],
obsolete: Union[bool, None] = False, user_id: Union[int, None] = None, metro: Union[str, None] = None,
category: Union[str, None] = None):
# параметры для сортировки (схема pydantic schemas.SortAnnouncements) # параметры для сортировки (схема pydantic schemas.SortAnnouncements)
params_to_sort = pydantic_schemas.SortAnnouncements(obsolete=obsolete, user_id=user_id, metro=metro, category=category) params_to_sort = pydantic_schemas.SortAnnouncements(obsolete=obsolete, user_id=user_id, metro=metro, category=category)
# получаем результат # получаем результат (значения с определенными полями obsolete, user_id, metro, category).
# user_id - id пользователя, которому принадлежат объявления
result = await 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 # для каждого отфильтрованного объявления проверяем, забронировано ли оно текущим пользователем и в зависимости
# от этого флаг booked_by_current_user устанавливаем в 0 или в 1
check_if_booked = [pydantic_schemas.Announcement.from_orm(elem) for elem in result]
for an in check_if_booked:
# ищем пару с заданными id объявления и id текущего пользователя
query = await db.execute(select(orm_models.AnnouncementUser).where(
orm_models.AnnouncementUser.announcement_id == an.id).where(
orm_models.AnnouncementUser.booking_user_id == current_user.id))
pair_found = query.scalars().first()
if pair_found:
an.booked_by_current_user = True
else:
an.booked_by_current_user = False
return check_if_booked
# получаем данные одного объявления # получаем данные одного объявления
@app.get("/api/announcement", response_model=pydantic_schemas.AnnResponce) @app.get("/api/announcement", response_model=pydantic_schemas.AnnResponce)
async def single_announcement(ann_id:int, db: Annotated[Session, Depends(auth_utils.get_session)]): # передаем индекс обявления async def single_announcement(ann_id:int, db: Annotated[Session, Depends(auth_utils.get_session)],
current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_active_user)]):
# Считываем данные из Body и отображаем их на странице. # Считываем данные из Body и отображаем их на странице.
# В последствии будем вставлять данные в html-форму # В последствии будем вставлять данные в html-форму
announcement = await db.get(orm_models.Announcement, ann_id) announcement = await db.get(orm_models.Announcement, ann_id)
#announcement = await db.execute(select(orm_models.Announcement)).scalars().all() #announcement = await db.execute(select(orm_models.Announcement)).scalars().all()
if not announcement: if not announcement:
raise HTTPException(status_code=404, detail="Item not found") raise HTTPException(status_code=404, detail="Item not found")
return announcement
# создаем форму pydantic_schemas.AnnResponce из ORM-объекта announcement
announcement_model = pydantic_schemas.AnnResponce.from_orm(announcement)
# ищем пару с заданными id объявления и id текущего пользователя
query = await db.execute(select(orm_models.AnnouncementUser).where(
orm_models.AnnouncementUser.announcement_id == announcement.id).where(
orm_models.AnnouncementUser.booking_user_id == current_user.id))
pair_found = query.scalars().first()
# если такая пара найдена, записываем в поле booked_by_current_user True
if pair_found:
announcement_model.booked_by_current_user = True
return announcement_model
# Занести объявление в базу данных # Занести объявление в базу данных
@ -98,7 +131,7 @@ async def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form(
# создаем объект Announcement # создаем объект Announcement
temp_ancmt = orm_models.Announcement(user_id=current_user.id, name=name, category=category, best_by=bestBy, 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, address=address, longtitude=longtitude, latitude=latitude, description=description, metro=metro,
trashId=trashId, src=uploaded_name, booked_by=0) trashId=trashId, src=uploaded_name, booked_counter=0)
try: try:
db.add(temp_ancmt) # добавляем в бд db.add(temp_ancmt) # добавляем в бд
await db.commit() # сохраняем изменения await db.commit() # сохраняем изменения
@ -124,11 +157,11 @@ async def delete_from_db(announcement: pydantic_schemas.DelAnnouncement, db: Ann
return {"Success": True} return {"Success": True}
except: except:
raise HTTPException(status_code=500, detail="Problem with adding to database") raise HTTPException(status_code=500, detail="Problem with deleteng the announcement from the database")
# Забронировать объявление # Забронировать объявление
@app.post("/api/book") @app.put("/api/book")
async def change_book_status(data: pydantic_schemas.Book, current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_user)], 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)]): db: Annotated[Session, Depends(auth_utils.get_session)]):
# Находим объявление по данному id # Находим объявление по данному id
@ -142,15 +175,52 @@ async def change_book_status(data: pydantic_schemas.Book, current_user: Annotate
if current_user.id == announcement_to_change.user_id: if current_user.id == announcement_to_change.user_id:
raise HTTPException(status_code=403, detail="A user can't book his announcement") raise HTTPException(status_code=403, detail="A user can't book his announcement")
else: else:
# Инкрементируем поле booked_by на 1 # ищем пару с заданными id объявления и пользователя
announcement_to_change.booked_by += 1 query = await db.execute(select(orm_models.AnnouncementUser).where(
# фиксируем изменения в бд orm_models.AnnouncementUser.announcement_id == announcement_to_change.id).where(
await db.commit() orm_models.AnnouncementUser.booking_user_id == current_user.id))
await db.refresh(announcement_to_change) pair_found = query.scalars().first()
return {"Success": True} # если не найдена
if not pair_found:
# создаем новый объект таблицы AnnouncementUser
new_pair = orm_models.AnnouncementUser(announcement_to_change.id, current_user.id)
# Инкрементируем поле booked_counter на 1
announcement_to_change.booked_counter += 1
# добавляем запись в таблицу announcementuser
db.add(new_pair)
# фиксируем изменения в бд
await db.commit()
await db.refresh(announcement_to_change)
return {"Success": True}
raise HTTPException(status_code=403, detail="The announcement is already booked by this user")
# reginstration # Отмена брони
@app.delete("/api/book")
async def cancel_booking(data: pydantic_schemas.CancelBooking, current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_user)],
db: Annotated[Session, Depends(auth_utils.get_session)]):
# ищем пару с заданными id объявления и пользователя
query = await db.execute(select(orm_models.AnnouncementUser).where(
orm_models.AnnouncementUser.announcement_id == data.announcement_id).where(
orm_models.AnnouncementUser.booking_user_id == current_user.id))
pair_found = query.scalars().first()
# если не найдена
if not pair_found:
raise HTTPException(status_code=404, detail="Item not found")
# если найдена пытаемся удалить из бд
else:
try:
await db.delete(pair_found) # удаление из БД
await db.commit() # сохраняем изменения
return {"Success": True}
except:
raise HTTPException(status_code=500, detail="Problem with deleteng pair 'booked_announcement_id:user_id' from the database")
# Регистрация
@app.post("/api/signup") @app.post("/api/signup")
async def create_user(nickname: Annotated[str, Form()], password: Annotated[str, Form()], db: Annotated[Session, Depends(auth_utils.get_session)], async def create_user(nickname: Annotated[str, Form()], password: Annotated[str, Form()], db: Annotated[Session, Depends(auth_utils.get_session)],
name: Annotated[str, Form()]=None, surname: Annotated[str, Form()]=None, avatar: Annotated[UploadFile, Form()]=None): name: Annotated[str, Form()]=None, surname: Annotated[str, Form()]=None, avatar: Annotated[UploadFile, Form()]=None):
@ -200,6 +270,21 @@ def read_users_me(current_user: Annotated[pydantic_schemas.User, Depends(auth_ut
return current_user return current_user
# ендпоинт для генерации refresh token. Генерируется при каждом входе юзера
# на сайт
@app.post("/api/token/refresh", response_model=pydantic_schemas.Token)
async def generate_refresh_token(
current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_active_user)]
):
# задаем временной интервал, в течение которого токен можно использовать
access_token_expires = auth_utils.timedelta(minutes=auth_utils.ACCESS_TOKEN_EXPIRE_MINUTES)
# создаем новый токен
access_token = auth_utils.create_access_token(
data={"user_id": current_user.id}, expires_delta=access_token_expires
)
return {"access_token":access_token}
# изменяем рейтинг пользователя # изменяем рейтинг пользователя
@app.post("/api/user/rating") @app.post("/api/user/rating")
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)]): 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)]):
@ -267,18 +352,12 @@ async def get_trashboxes(data: pydantic_schemas.TrashboxRequest = Depends()): #
'limit' : '1' 'limit' : '1'
} }
# Перевод категории с фронта на категорию с сайта # Перевод категории с фронта на категорию с сайта
try: list_of_category = trashboxes_category[data.Category]
list_of_category = trashboxes_category[data.Category]
except:
list_of_category = trashboxes_category['other_things']
# Получение ответа от стороннего апи # Получение ответа от стороннего апи
response = requests.post(TRASHBOXES_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() infos = response.json()
if 'error' in infos and infos['error_description'] == 'Invalid bearer token':
raise HTTPException(status_code=502, detail="Invalid trashboxes token")
# Чтение ответа # Чтение ответа
trashboxes = [] trashboxes = []
for trashbox in infos["results"]: for trashbox in infos["results"]:

View File

@ -21,7 +21,7 @@ async def main():
await init_models() await init_models()
server = Server(config=uvicorn.Config(app_fastapi, workers=1, loop="asyncio", host="0.0.0.0")) server = Server(config=uvicorn.Config(app_fastapi, workers=1, loop="asyncio", host="127.0.0.1"))
api = asyncio.create_task(server.serve()) api = asyncio.create_task(server.serve())
sched = asyncio.create_task(app_rocketry.serve()) sched = asyncio.create_task(app_rocketry.serve())

View File

@ -1,9 +1,10 @@
from sqlalchemy import Column, Integer, String, Boolean, Float, Date, ForeignKey from sqlalchemy import Column, Integer, String, Boolean, Float, Date, ForeignKey, ForeignKeyConstraint
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship, mapped_column, composite, Mapped
from sqlalchemy.dialects import postgresql
import dataclasses
from .db import Base, engine from .db import Base, engine
class User(Base):#класс пользователя class User(Base):#класс пользователя
__tablename__ = "users" __tablename__ = "users"
@ -18,7 +19,7 @@ class User(Base):#класс пользователя
num_of_ratings = Column(Integer, default=0) # количество оценок (т.е. то, сколько раз другие пользователи оценили текущего) num_of_ratings = Column(Integer, default=0) # количество оценок (т.е. то, сколько раз другие пользователи оценили текущего)
reg_date = Column(Date) # дата регистрации reg_date = Column(Date) # дата регистрации
announcements = relationship("Announcement", back_populates="user", lazy='selectin') announcements = relationship("Announcement", secondary="announcementuser", back_populates="user", lazy='selectin')
trashboxes_chosen = relationship("Trashbox", back_populates="user", lazy='selectin') trashboxes_chosen = relationship("Trashbox", back_populates="user", lazy='selectin')
class Announcement(Base): #класс объявления class Announcement(Base): #класс объявления
@ -36,11 +37,35 @@ class Announcement(Base): #класс объявления
src = Column(String, nullable=True) #изображение продукта в объявлении src = Column(String, nullable=True) #изображение продукта в объявлении
metro = Column(String) #ближайщее метро от адреса нахождения продукта metro = Column(String) #ближайщее метро от адреса нахождения продукта
trashId = Column(Integer, nullable=True) trashId = Column(Integer, nullable=True)
booked_by = Column(Integer) #количество забронировавших (0 - никто не забронировал) booked_counter = Column(Integer, nullable=True) #количество забронировавших (0 - никто не забронировал)
# state = Column(Enum(State), default=State.published) # состояние объявления (опубликовано, забронировано, устарело)
obsolete = Column(Boolean, default=False) # состояние объявления (по-умолчанию считаем его актуальным) obsolete = Column(Boolean, default=False) # состояние объявления (по-умолчанию считаем его актуальным)
user = relationship("User", back_populates="announcements") user = relationship("User", secondary="announcementuser", back_populates="announcements")
class AnnouncementUser(Base):
__tablename__ = "announcementuser"
def __init__(self, an_id, b_u_id):
self.announcement_id = an_id
self.booking_user_id = b_u_id
announcement_id = Column(Integer, ForeignKey("announcements.id"), primary_key=True) # id забронированного объявления
booking_user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) # id пользователя, забронировавшего объявление
class Ratings(Base):
__tablename__ = "ratings"
def __init__(self, rated_user_id, user_giving_rating_id, rating_val):
self.rated_user_id = rated_user_id
self.user_giving_rating_id = user_giving_rating_id
self.rating_value = rating_val
rated_user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) # id пользователя, оставившего оценку
user_giving_rating_id = Column(Integer, ForeignKey("users.id"), primary_key=True) # id оцениваемого пользователя
rating_value = Column(Integer, primary_key=True) # оценка
class Trashbox(Base): #класс мусорных баков class Trashbox(Base): #класс мусорных баков

View File

@ -1,13 +1,15 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Annotated, Union from typing import Union
from datetime import date from datetime import date
from typing import List from typing import List
from fastapi import UploadFile, Form
class Book(BaseModel): class Book(BaseModel):
id: int id: int
class CancelBooking(BaseModel):
announcement_id: int
class DelAnnouncement(BaseModel): class DelAnnouncement(BaseModel):
id: int id: int
@ -25,7 +27,7 @@ class Announcement(BaseModel):
src: Union[str, None] = None #изображение продукта в объявлении src: Union[str, None] = None #изображение продукта в объявлении
metro: str #ближайщее метро от адреса нахождения продукта metro: str #ближайщее метро от адреса нахождения продукта
trashId: Union[int, None] = None trashId: Union[int, None] = None
booked_by: Union[int, None] = 0 #статус бронирования (либо 0, либо айди бронирующего) booked_by_current_user: Union[bool, None] = False
obsolete: bool obsolete: bool
class Config: class Config:
@ -47,7 +49,7 @@ class AnnResponce(BaseModel):
src: Union[str, None] = None #изображение продукта в объявлении src: Union[str, None] = None #изображение продукта в объявлении
metro: str #ближайщее метро от адреса нахождения продукта metro: str #ближайщее метро от адреса нахождения продукта
trashId: Union[int, None] = None trashId: Union[int, None] = None
booked_by: Union[int, None] = 0 #статус бронирования (либо 0, либо айди бронирующего) booked_by_current_user: Union[bool, None] = False
class Config: class Config:
orm_mode = True orm_mode = True

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,36 +0,0 @@
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,4 +17,3 @@ export { default as SelectDisposalTrashbox } from './SelectDisposalTrashbox'
export { default as StarRating } from './StarRating' export { default as StarRating } from './StarRating'
export { default as CardLayout } from './CardLayout' export { default as CardLayout } from './CardLayout'
export { default as LocateButton } from './LocateButton' export { default as LocateButton } from './LocateButton'
export { default as StudentLocations } from './StudentLocations'

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,6 @@ import L from 'leaflet'
import itemMarker from '../assets/itemMarker.png' import itemMarker from '../assets/itemMarker.png'
import trashMarker from '../assets/trashMarker.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({ const iconItem = new L.Icon({
iconUrl: itemMarker, iconUrl: itemMarker,
@ -21,25 +17,4 @@ const iconTrash = new L.Icon({
iconSize: [34, 41], iconSize: [34, 41],
}) })
const iconDormitory = new L.Icon({ export { iconItem, iconTrash }
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,8 +1,8 @@
"""lazy=selectin added to user table relationships """create constructor to make object (if leave as is, with two primary keys, says that one argument required, but two were given). Booked_counter can be nullable
Revision ID: 8e631a2fe6b8 Revision ID: 19dbd9793f11
Revises: Revises: 945c70aa70e7
Create Date: 2023-09-02 23:45:08.799366 Create Date: 2024-08-13 15:29:21.542539
""" """
from typing import Sequence, Union from typing import Sequence, Union
@ -12,8 +12,8 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '8e631a2fe6b8' revision: str = '19dbd9793f11'
down_revision: Union[str, None] = None down_revision: Union[str, None] = '945c70aa70e7'
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None

View File

@ -20,7 +20,6 @@ def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('announcements', schema=None) as batch_op: with op.batch_alter_table('announcements', schema=None) as batch_op:
batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True))
batch_op.add_column(sa.Column('state', sa.Enum('published', 'taken', 'obsolete', name='state'), nullable=True))
# batch_op.drop_constraint(None, type_='foreignkey') # batch_op.drop_constraint(None, type_='foreignkey')
batch_op.create_foreign_key('fk_users_id', 'users', ['user_id'], ['id']) batch_op.create_foreign_key('fk_users_id', 'users', ['user_id'], ['id'])
batch_op.drop_column('owner_id') batch_op.drop_column('owner_id')
@ -42,7 +41,6 @@ def downgrade():
batch_op.add_column(sa.Column('owner_id', sa.INTEGER(), nullable=True)) batch_op.add_column(sa.Column('owner_id', sa.INTEGER(), nullable=True))
# batch_op.drop_constraint('fk_users_id', type_='foreignkey') # batch_op.drop_constraint('fk_users_id', type_='foreignkey')
batch_op.create_foreign_key(None, 'users', ['owner_id'], ['id']) batch_op.create_foreign_key(None, 'users', ['owner_id'], ['id'])
batch_op.drop_column('state')
batch_op.drop_column('user_id') batch_op.drop_column('user_id')
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -18,13 +18,6 @@ depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('poems', schema=None) as batch_op:
batch_op.add_column(sa.Column('title', sa.String(), nullable=True))
batch_op.add_column(sa.Column('text', sa.String(), nullable=True))
batch_op.add_column(sa.Column('author', sa.String(), nullable=True))
batch_op.drop_column('poem_name')
batch_op.drop_column('poem_text')
with op.batch_alter_table('trashboxes', schema=None) as batch_op: with op.batch_alter_table('trashboxes', schema=None) as batch_op:
batch_op.add_column(sa.Column('category', sa.String(), nullable=True)) batch_op.add_column(sa.Column('category', sa.String(), nullable=True))
batch_op.drop_column('categories') batch_op.drop_column('categories')
@ -37,12 +30,4 @@ def downgrade():
with op.batch_alter_table('trashboxes', schema=None) as batch_op: with op.batch_alter_table('trashboxes', schema=None) as batch_op:
batch_op.add_column(sa.Column('categories', sa.VARCHAR(), nullable=True)) batch_op.add_column(sa.Column('categories', sa.VARCHAR(), nullable=True))
batch_op.drop_column('category') batch_op.drop_column('category')
with op.batch_alter_table('poems', schema=None) as batch_op:
batch_op.add_column(sa.Column('poem_text', sa.VARCHAR(), nullable=True))
batch_op.add_column(sa.Column('poem_name', sa.VARCHAR(), nullable=True))
batch_op.drop_column('author')
batch_op.drop_column('text')
batch_op.drop_column('title')
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -0,0 +1,66 @@
"""Added booked_by column which contains id of users who booked the announcement old booked_by renamed to booked_counter
Revision ID: 5a8105ac1a4f
Revises: 547f860f21a7
Create Date: 2024-08-09 15:07:51.386406
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '5a8105ac1a4f'
down_revision: Union[str, None] = '547f860f21a7'
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! ###
op.add_column('announcements', sa.Column('booked_counter', sa.Integer(), nullable=True))
op.alter_column('announcements', 'longtitude',
existing_type=sa.INTEGER(),
type_=sa.Float(),
existing_nullable=True)
op.alter_column('announcements', 'latitude',
existing_type=sa.INTEGER(),
type_=sa.Float(),
existing_nullable=True)
op.drop_column('announcements', 'booked_by')
op.alter_column('trashboxes', 'latitude',
existing_type=sa.INTEGER(),
type_=sa.Float(),
existing_nullable=True)
op.alter_column('trashboxes', 'longtitude',
existing_type=sa.INTEGER(),
type_=sa.Float(),
existing_nullable=True)
op.create_foreign_key(None, 'trashboxes', 'users', ['user_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'trashboxes', type_='foreignkey')
op.alter_column('trashboxes', 'longtitude',
existing_type=sa.Float(),
type_=sa.INTEGER(),
existing_nullable=True)
op.alter_column('trashboxes', 'latitude',
existing_type=sa.Float(),
type_=sa.INTEGER(),
existing_nullable=True)
op.add_column('announcements', sa.Column('booked_by', sa.INTEGER(), autoincrement=False, nullable=True))
op.alter_column('announcements', 'latitude',
existing_type=sa.Float(),
type_=sa.INTEGER(),
existing_nullable=True)
op.alter_column('announcements', 'longtitude',
existing_type=sa.Float(),
type_=sa.INTEGER(),
existing_nullable=True)
op.drop_column('announcements', 'booked_counter')
# ### end Alembic commands ###

View File

@ -0,0 +1,38 @@
"""booked_by colomn changed to Integer, added new table AnnouncementUser implementing one to many relationships
Revision ID: 945c70aa70e7
Revises: 5a8105ac1a4f
Create Date: 2024-08-12 20:36:08.669574
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '945c70aa70e7'
down_revision: Union[str, None] = '5a8105ac1a4f'
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! ###
op.create_table('announcementuser',
sa.Column('announcement_id', sa.Integer(), nullable=False),
sa.Column('booking_user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['announcement_id'], ['announcements.id'], ),
sa.ForeignKeyConstraint(['booking_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('announcement_id', 'booking_user_id')
)
op.add_column('announcements', sa.Column('booked_by', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('announcements', 'booked_by')
op.drop_table('announcementuser')
# ### end Alembic commands ###

View File

@ -18,10 +18,8 @@ depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('_alembic_tmp_users')
with op.batch_alter_table('announcements', schema=None) as batch_op: with op.batch_alter_table('announcements', schema=None) as batch_op:
batch_op.add_column(sa.Column('obsolete', sa.Boolean(), nullable=True)) batch_op.add_column(sa.Column('obsolete', sa.Boolean(), nullable=True))
batch_op.drop_column('state')
with op.batch_alter_table('poems', schema=None) as batch_op: with op.batch_alter_table('poems', schema=None) as batch_op:
batch_op.add_column(sa.Column('title', sa.String(), nullable=True)) batch_op.add_column(sa.Column('title', sa.String(), nullable=True))
@ -49,16 +47,5 @@ def downgrade():
batch_op.drop_column('title') batch_op.drop_column('title')
with op.batch_alter_table('announcements', schema=None) as batch_op: with op.batch_alter_table('announcements', schema=None) as batch_op:
batch_op.add_column(sa.Column('state', sa.VARCHAR(length=9), nullable=True))
batch_op.drop_column('obsolete') batch_op.drop_column('obsolete')
op.create_table('_alembic_tmp_users',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('email', sa.VARCHAR(), nullable=True),
sa.Column('hashed_password', sa.VARCHAR(), nullable=True),
sa.Column('name', sa.VARCHAR(), nullable=True),
sa.Column('surname', sa.VARCHAR(), nullable=True),
sa.Column('disabled', sa.BOOLEAN(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -0,0 +1,37 @@
"""Many to many relationship Rating added
Revision ID: a309e6ee6307
Revises: cf0525fd49a8
Create Date: 2024-08-28 22:08:08.399530
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'a309e6ee6307'
down_revision: Union[str, None] = 'cf0525fd49a8'
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! ###
op.create_table('ratings',
sa.Column('rated_user_id', sa.Integer(), nullable=False),
sa.Column('user_giving_rating_id', sa.Integer(), nullable=False),
sa.Column('rating_value', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['rated_user_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['user_giving_rating_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('rated_user_id', 'user_giving_rating_id', 'rating_value')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('ratings')
# ### end Alembic commands ###

View File

@ -0,0 +1,30 @@
"""booked_by column removed in Announcements table
Revision ID: cf0525fd49a8
Revises: 19dbd9793f11
Create Date: 2024-08-28 21:59:23.787732
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'cf0525fd49a8'
down_revision: Union[str, None] = '19dbd9793f11'
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! ###
op.drop_column('announcements', 'booked_by')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('announcements', sa.Column('booked_by', sa.INTEGER(), autoincrement=False, nullable=True))
# ### end Alembic commands ###