12 Commits

Author SHA1 Message Date
48cf1022fc Added vertical stories with Swiper 2024-03-12 00:59:33 +03:00
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
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
36 changed files with 647 additions and 512 deletions

1
.gitignore vendored
View File

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

View File

@ -11,6 +11,7 @@ from . import auth_utils, orm_models, pydantic_schemas
# Загружаем стихи
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)#перейти к началу
@ -35,8 +36,11 @@ async def add_poems_to_db(async_db: AsyncSession):
author += str1
poem = orm_models.Poems(title=name, text=stixi, author=author)
# В конце каждой итерации добавляем в базу данных
async_db.add(poem)
async_db.commit()
poems.append(poem)
async_db.add_all(poems)
await async_db.commit()
# close the file
f1.close()
@ -57,6 +61,39 @@ 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))
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

View File

@ -43,66 +43,33 @@ 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])
async def announcements_list(db: Annotated[Session, Depends(auth_utils.get_session)],
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):
@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,
metro: Union[str, None] = None,category: Union[str, None] = None):
# параметры для сортировки (схема pydantic schemas.SortAnnouncements)
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)
# для каждого отфильтрованного объявления проверяем, забронировано ли оно текущим пользователем и в зависимости
# от этого флаг 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
return result
# получаем данные одного объявления
@app.get("/api/announcement", response_model=pydantic_schemas.AnnResponce)
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)]):
async def single_announcement(ann_id:int, db: Annotated[Session, Depends(auth_utils.get_session)]): # передаем индекс обявления
# Считываем данные из Body и отображаем их на странице.
# В последствии будем вставлять данные в html-форму
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")
# создаем форму 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
return announcement
# Занести объявление в базу данных
@ -131,7 +98,7 @@ async def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form(
# создаем объект 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_counter=0)
trashId=trashId, src=uploaded_name, booked_by=0)
try:
db.add(temp_ancmt) # добавляем в бд
await db.commit() # сохраняем изменения
@ -157,11 +124,11 @@ async def delete_from_db(announcement: pydantic_schemas.DelAnnouncement, db: Ann
return {"Success": True}
except:
raise HTTPException(status_code=500, detail="Problem with deleteng the announcement from the database")
raise HTTPException(status_code=500, detail="Problem with adding to database")
# Забронировать объявление
@app.put("/api/book")
@app.post("/api/book")
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
@ -175,52 +142,15 @@ async def change_book_status(data: pydantic_schemas.Book, current_user: Annotate
if current_user.id == announcement_to_change.user_id:
raise HTTPException(status_code=403, detail="A user can't book his announcement")
else:
# ищем пару с заданными id объявления и пользователя
query = await db.execute(select(orm_models.AnnouncementUser).where(
orm_models.AnnouncementUser.announcement_id == announcement_to_change.id).where(
orm_models.AnnouncementUser.booking_user_id == current_user.id))
pair_found = query.scalars().first()
# если не найдена
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)
# Инкрементируем поле booked_by на 1
announcement_to_change.booked_by += 1
# фиксируем изменения в бд
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")
# Отмена брони
@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")
# Регистрация
# reginstration
@app.post("/api/signup")
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):
@ -270,21 +200,6 @@ def read_users_me(current_user: Annotated[pydantic_schemas.User, Depends(auth_ut
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")
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)]):
@ -352,12 +267,18 @@ async def get_trashboxes(data: pydantic_schemas.TrashboxRequest = Depends()): #
'limit' : '1'
}
# Перевод категории с фронта на категорию с сайта
try:
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)
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"]:

View File

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

View File

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

View File

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

View File

@ -22,7 +22,8 @@
"react-insta-stories": "^2.6.1",
"react-leaflet": "^4.2.1",
"react-leaflet-custom-control": "^1.3.5",
"react-router-dom": "^6.14.1"
"react-router-dom": "^6.14.1",
"swiper": "^11.0.6"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",

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: {
@ -25,33 +26,47 @@ const styles = {
}
type ViewProps = {
myId: number,
announcement: Announcement,
myId: number
announcement: Announcement
}
const View = ({
myId,
announcement: { name, category, bestBy, description, lat, lng, address, metro, userId },
announcement: {
name,
category,
bestBy,
description,
lat,
lng,
address,
metro,
userId,
},
}: ViewProps) => (
<>
<h1>{name}</h1>
<span>{categoryNames[category]}</span>
<span className='m-2'>&#x2022;</span>{/* dot */}
<span className="m-2">&#x2022;</span>
{/* dot */}
<span>Годен до {bestBy}</span>
<p className='mb-0'>{description}</p>
<p className="mb-0">{description}</p>
<p className='mb-3'>
Рейтинг пользователя: <StarRating dynamic={myId !== userId} userId={userId} />
<p className="mb-3">
Рейтинг пользователя:{' '}
<StarRating dynamic={myId !== userId} userId={userId} />
</p>
<MapContainer style={styles.map} center={[lat, lng]} zoom={16} >
<MapContainer style={styles.map} center={[lat, lng]} zoom={16}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
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]}>
<Popup>
{address}
@ -64,9 +79,9 @@ const View = ({
)
type ControlProps = {
myId: number,
closeRefresh: () => void,
announcement: Announcement,
myId: number
closeRefresh: () => void
announcement: Announcement
showDispose: () => void
}
@ -83,25 +98,44 @@ function Control({
return (
<>
<p>Забронировали {bookedBy + (bookButton.disabled ? 1 : 0)} чел.</p>
{(myId === userId) ? (
<div className='m-0'>
<Button className='m-1' variant='success' onClick={showDispose}>Утилизировать</Button>
<Button className='m-1' variant='success' onClick={() => void handleRemove(id)} {...removeButton} />
{myId === userId ? (
<div className="m-0">
<Button
className="m-1"
variant="success"
onClick={showDispose}
>
Утилизировать
</Button>
<Button
className="m-1"
variant="success"
onClick={() => void handleRemove(id)}
{...removeButton}
/>
</div>
) : (
<Button variant='success' onClick={() => void handleBook(id)} {...bookButton} />
<Button
variant="success"
onClick={() => void handleBook(id)}
{...bookButton}
/>
)}
</>
)
}
type AnnouncementDetailsProps = {
close: () => void,
refresh: () => void,
announcement: Announcement,
className?: string
show: boolean
close: () => void
refresh: () => void
announcement: Announcement
}
function AnnouncementDetails({
className,
show,
close,
refresh,
announcement,
@ -116,15 +150,16 @@ function AnnouncementDetails({
const myId = useId()
return (
<div
className='modal'
style={styles.container}
<Modal
centered
show={show}
onHide={close}
keyboard
className={className}
// backdrop={false}
>
<Modal.Dialog centered className='modal-dialog'>
<Modal.Header closeButton onHide={close}>
<Modal.Title>
Подробнее
</Modal.Title>
<Modal.Title>Подробнее</Modal.Title>
</Modal.Header>
<Modal.Body>
@ -139,12 +174,14 @@ function AnnouncementDetails({
announcement={announcement}
/>
</Modal.Footer>
</Modal.Dialog>
<Modal centered show={disposeShow} onHide={() => setDisposeShow(false)} style={{ zIndex: 100000 }}>
<Modal
centered
show={disposeShow}
onHide={() => setDisposeShow(false)}
style={{ zIndex: 100000 }}
>
<Modal.Header closeButton>
<Modal.Title>
Утилизация
</Modal.Title>
<Modal.Title>Утилизация</Modal.Title>
</Modal.Header>
<SelectDisposalTrashbox
annId={announcement.id}
@ -153,7 +190,7 @@ function AnnouncementDetails({
closeRefresh={closeRefresh}
/>
</Modal>
</div>
</Modal>
)
}

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>
Я согласен с{' '}
<p>
Нажимая на кнопку, я даю своё согласие на обработку персональных данных и соглашаюсь с{' '}
<a
href={`${document.location.origin}/privacy_policy.pdf`}
target='_blank'
>условиями обработки персональных данных</a>
</Form.Check.Label>
</Form.Check>
</Form.Group>
>условиями политики конфиденциальности</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

@ -8,39 +8,68 @@ function useStoryIndex(annLength: number | undefined) {
const [searchParams, setSearchParams] = useSearchParams()
const withReset = <T>(f: SetState<T>) => (...args: Parameters<SetState<T>>) => {
const withReset =
<T>(f: SetState<T>) =>
(...args: Parameters<SetState<T>>) => {
setIndex(0)
setSearchParams(prev => ({
setSearchParams(
(prev) => ({
...Object.fromEntries(prev),
storyIndex: '0',
}), { replace: true })
}),
{ replace: true }
)
f(...args)
}
useEffect(() => {
setIndex(annLength ?
Number.parseInt(searchParams.get('storyIndex') || '0') :
0)
setIndex(
annLength
? Number.parseInt(searchParams.get('storyIndex') || '0')
: 0
)
// searchParams have actual query string at first render
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [annLength])
const increment = () => setIndex(prev => {
const increment = () =>
setIndex((prev) => {
const newIndex = (prev + 1) % (annLength || 1)
setSearchParams(prev => ({
setSearchParams(
(prev) => ({
...Object.fromEntries(prev),
storyIndex: newIndex.toString(),
}), { replace: true })
}),
{ replace: true }
)
return newIndex
})
const decrement = () => setIndex(prev => {
const newIndex = prev > 0 ? (prev - 1) : 0
setSearchParams(prev => ({
const decrement = () =>
setIndex((prev) => {
const newIndex = prev > 0 ? prev - 1 : 0
setSearchParams(
(prev) => ({
...Object.fromEntries(prev),
storyIndex: newIndex.toString(),
}), { replace: true })
}),
{ replace: true }
)
return newIndex
})
const set = (n: number) =>
setIndex(() => {
const newIndex = n % (annLength || 1)
setSearchParams(
(prev) => ({
...Object.fromEntries(prev),
storyIndex: newIndex.toString(),
}),
{ replace: true }
)
return newIndex
})
@ -50,6 +79,7 @@ function useStoryIndex(annLength: number | undefined) {
withReset,
increment,
decrement,
set,
}
}

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

@ -1,6 +1,8 @@
import { CSSProperties, useEffect, useMemo, useState } from 'react'
import Stories from 'react-insta-stories'
import { Story } from 'react-insta-stories/dist/interfaces'
// import Stories from 'react-insta-stories'
// import { Story } from 'react-insta-stories/dist/interfaces'
import { Swiper, SwiperSlide, useSwiper, useSwiperSlide } from 'swiper/react'
import { Virtual, Keyboard, Mousewheel, Zoom, Autoplay } from 'swiper/modules'
import { BottomNavBar, AnnouncementDetails, Filters } from '../components'
import { useFilters, useStoryDimensions, useStoryIndex } from '../hooks'
@ -9,77 +11,149 @@ import { Announcement } from '../api/announcement/types'
import { categoryGraphics } from '../assets/category'
import { UseFetchReturn, gotError } from '../hooks/useFetch'
import 'swiper/css'
import 'swiper/css/virtual'
import 'swiper/css/zoom'
import 'swiper/css/keyboard'
import 'swiper/css/mousewheel'
import 'swiper/css/autoplay'
import puffSpinner from '../assets/puff.svg'
function generateStories(announcements: Announcement[], refresh: () => void): Story[] {
return announcements.map(announcement => {
return ({
id: announcement.id,
url: announcement.src || categoryGraphics[announcement.category],
type: announcement.src?.endsWith('mp4') ? 'video' : undefined,
seeMore: ({ close }: { close: () => void }) => (
<AnnouncementDetails close={close} refresh={refresh} announcement={announcement} />
),
})
})
}
function fallbackGenerateStories(announcements: UseFetchReturn<Announcement[]>) {
if (announcements.loading) {
return fallbackStory()
}
if (gotError(announcements)) {
return fallbackStory(announcements.error, true)
}
if (announcements.data.length === 0) {
return [{ url: '/static/empty.png' }]
}
const stories = generateStories(announcements.data, announcements.refetch)
return stories
}
const fallbackStory = (text = '', isError = false): Story[] => [{
content: ({ action }) => {
// ESLint can't detect that it is a component
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
action('pause')
}, [action])
return (
<div className={`m-auto ${isError ? 'text-danger' : ''}`}>
{text || <img src={puffSpinner} />}
</div>
)
},
}]
import { SetState } from '../utils/types'
import { Button } from 'react-bootstrap'
import { throttle } from '../utils'
const styles = {
container: {
display: 'flex',
justifyContent: 'center',
backgroundColor: 'rgb(17, 17, 17)',
height: 'calc(100dvh - 56px)',
} as CSSProperties,
detailsContainer: {
position: 'fixed',
top: 0,
zIndex: 1000,
textAlign: 'initial',
} as CSSProperties,
cardContainer: {
height: '100%',
margin: '0 auto',
position: 'relative',
} as CSSProperties,
cardMedia: {
height: '100%',
} as CSSProperties,
cardDetails: {
position: 'absolute',
width: '100%',
bottom: 0,
textAlign: 'center',
height: '56px',
boxShadow: 'inset 0px -50px 36px -28px rgba(0, 0, 0, 0.35)',
} as CSSProperties,
progressBar: {
height: '0.5dvh',
width: 'calc(100% - 2dvh)',
left: '1dvh',
position: 'absolute',
top: '1dvh',
zIndex: 1000,
} as CSSProperties,
}
type CardProps = {
announcement: Announcement
detailsShown: boolean
percentage: number
setDetailsShown: SetState<boolean>
}
const Card = ({
announcement,
detailsShown,
percentage,
setDetailsShown,
}: CardProps) => {
const swiperSlide = useSwiperSlide()
const swiper = useSwiper()
useEffect(() => {
if (detailsShown) swiper.autoplay.stop()
else swiper.autoplay.start()
}, [detailsShown])
return (
<>
<AnnouncementDetails
className="swiper-no-swiping"
show={detailsShown && swiperSlide.isActive}
announcement={announcement}
close={() => setDetailsShown(false)}
refresh={() => {}}
/>
<div style={styles.cardContainer}>
{swiperSlide.isActive && (
<div style={styles.progressBar}>
<div
style={{
background: 'white',
height: '100%',
width: (1 - percentage) * 100 + '%',
transition: 'width 0.1s linear',
}}
></div>
</div>
)}
<div className="swiper-zoom-container">
{announcement.src?.endsWith('mp4') ||
announcement.src?.endsWith('webm') ? (
<video
style={styles.cardMedia}
src={announcement.src}
autoPlay={swiperSlide.isActive}
/>
) : (
<img
style={styles.cardMedia}
src={
announcement.src ||
categoryGraphics[announcement.category]
}
loading="lazy"
/>
)}
</div>
<div style={styles.cardDetails}>
<Button
variant="success"
onClick={() => setDetailsShown(true)}
>
Подробности
</Button>
</div>
</div>
</>
)
}
function HomePage() {
const { height, width } = useStoryDimensions(16 / 9)
const [filterShown, setFilterShown] = useState(false)
const [detailsShown, setDetailsShown] = useState(false)
const [filter, setFilter] = useFilters()
const announcements = useAnnouncements(filter)
const stories = useMemo(() => fallbackGenerateStories(announcements), [announcements])
const index = useStoryIndex(announcements.data?.length)
return (<>
const [autoplayPercentage, setAutoplayPercentage] = useState(0)
return (
<>
<Filters
filter={filter}
setFilter={index.withReset(setFilter)}
@ -87,24 +161,62 @@ function HomePage() {
setFilterShown={setFilterShown}
/>
<div style={styles.container}>
<Stories
currentIndex={index.n % stories.length}
onStoryEnd={index.increment}
onNext={index.increment}
onPrevious={index.decrement}
stories={stories}
defaultInterval={11000}
height={height}
width={width}
loop={true}
keyboardNavigation={true}
{!announcements.loading && !gotError(announcements) && (
<Swiper
autoplay={{ delay: 11000, stopOnLastSlide: true }}
direction="vertical"
keyboard
loop
modules={[
Autoplay,
Keyboard,
Mousewheel,
Virtual,
Zoom,
]}
mousewheel
onAutoplayTimeLeft={throttle(
(_: unknown, __: unknown, percentage: number) =>
setAutoplayPercentage(percentage),
100
)}
noSwiping={true}
onActiveIndexChange={(s) => index.set(s.activeIndex)}
slidesPerView={1}
spaceBetween={0}
virtual
zoom={{}}
initialSlide={index.n}
onZoomChange={(s, scale) => {
if (scale == 1) {
s.enable()
s.autoplay.start()
} else {
s.disable()
s.autoplay.stop()
}
}}
>
{announcements.data.map((announcement, i) => (
<SwiperSlide
key={announcement.id}
virtualIndex={i}
zoom
>
<Card
announcement={announcement}
detailsShown={detailsShown}
percentage={autoplayPercentage}
setDetailsShown={setDetailsShown}
/>
</SwiperSlide>
))}
</Swiper>
)}
</div>
<BottomNavBar
toggleFilters={setFilterShown}
width={width}
/>
</>)
<BottomNavBar toggleFilters={setFilterShown} width={width} />
</>
)
}
export default HomePage

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

@ -1,6 +1,5 @@
const isAborted = <T>(err: Error): err is AbortError<T> => (
const isAborted = <T>(err: Error): err is AbortError<T> =>
err.name === 'AbortError'
)
type AbortErrorMessage = 'resent' | 'unmount' | 'cancel'
@ -30,4 +29,27 @@ function handleHTTPErrors(res: Response) {
}
}
export { isAborted, AbortError, handleHTTPErrors }
const throttle = (fn: Function, wait: number = 300) => {
let inThrottle: boolean,
lastFn: ReturnType<typeof setTimeout>,
lastTime: number
return function (this: any) {
const context = this,
args = arguments
if (!inThrottle) {
fn.apply(context, args)
lastTime = Date.now()
inThrottle = true
} else {
clearTimeout(lastFn)
lastFn = setTimeout(() => {
if (Date.now() - lastTime >= wait) {
fn.apply(context, args)
lastTime = Date.now()
}
}, Math.max(wait - (Date.now() - lastTime), 0))
}
}
}
export { isAborted, AbortError, handleHTTPErrors, throttle }

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

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

View File

@ -18,6 +18,13 @@ depends_on = None
def upgrade():
# ### 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:
batch_op.add_column(sa.Column('category', sa.String(), nullable=True))
batch_op.drop_column('categories')
@ -30,4 +37,12 @@ def downgrade():
with op.batch_alter_table('trashboxes', schema=None) as batch_op:
batch_op.add_column(sa.Column('categories', sa.VARCHAR(), nullable=True))
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 ###

View File

@ -1,66 +0,0 @@
"""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

@ -1,8 +1,8 @@
"""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
"""lazy=selectin added to user table relationships
Revision ID: 19dbd9793f11
Revises: 945c70aa70e7
Create Date: 2024-08-13 15:29:21.542539
Revision ID: 8e631a2fe6b8
Revises:
Create Date: 2023-09-02 23:45:08.799366
"""
from typing import Sequence, Union
@ -12,8 +12,8 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '19dbd9793f11'
down_revision: Union[str, None] = '945c70aa70e7'
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

View File

@ -1,38 +0,0 @@
"""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,8 +18,10 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('_alembic_tmp_users')
with op.batch_alter_table('announcements', schema=None) as batch_op:
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:
batch_op.add_column(sa.Column('title', sa.String(), nullable=True))
@ -47,5 +49,16 @@ def downgrade():
batch_op.drop_column('title')
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')
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 ###

View File

@ -1,37 +0,0 @@
"""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

@ -1,30 +0,0 @@
"""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 ###