UseCaseImplementations
- изворен код на овој линк
- кратка презентација на финалниот изглед на прототипот на овој линк
Кратки инструкции како да се компајлира
docker compose up
- впишете ја адресата localhost во прелистувачот
- регистрирајте се
- логирајте се
проектот содржи README.md
Едно од поважните сценарија Search
Откако ќе се логираме, треба да притиснеме на копчето Search заокружено и означено со број 1. Тоа ќе не пренасочи на рутата !/search. Тука може да забележиме компонента за пребарување на музика и подкасти според жанра/категорија или според име.
На примерот со број 2 е означен табот со кој одбираме да пребаруваме музика. Одбираме жанра Pop означена со број 3. И на крај притискаме на копчето Search кое праќа GET барање до серверот.
Резултатот е прикажан во табелите Songs и Albums. Taбелите ги прикажуваат само песните/албумите филтрирани според жанрата што ја одбравме. Жанрата на албумот е дознаена индиректно преку кои се жанрите на песните што се дел од еден албум.
Доколку не одбереме да пребаруваме по нешто специфично се враќаат сите песни и албуми. Ова е прикажано во следните две слики.
@app.get("/albums", response_model=Union[List[schemas.Album] | schemas.Album]) def get_albums( album_id: Optional[int] = Query(None), genres: Optional[List[str]] = Query(None), search_string: Optional[str] = None, db: Session = Depends(get_db), buzzer: models.Buzzer = Depends(validate_session) ): albums_query = db.query(models.Album) if album_id: try: album = albums_query.filter(models.Album.album_id == album_id).one() except NoResultFound: raise HTTPException(status_code=404, detail="Album not found.") return album if genres: print("Filtering by genre..") albums_query = albums_query.join(models.Song, models.Album.album_id == models.Song.album_id)\ .join(models.IsOfGenre, models.IsOfGenre.creation_id == models.Song.song_id)\ .join(models.Genre, models.Genre.genre_id == models.IsOfGenre.genre_id)\ .filter(models.Genre.genre_name.in_(genres))\ .distinct() if search_string: print("Filtering by album name..") albums_query = albums_query.filter(models.Album.album_title.ilike(f"%{search_string}%")) albums = albums_query.all() return albums
Овој ендпоинт е дизајниран за да ги земе албумите од базата на податоци врз основа на различни query параметри.
(1) /albums
Ендпоинтот е пристаплив преку HTTP GET барање на "/albums" рутата.
(2) Query параметри:
- album_id: Опционален целоброен параметар кој се користи за да се земе одреден албум според неговиот идентификатор.
- genres: Опционална листа од стрингови која се користи за да се филтрираат албумите според жанри.
- search_string: Опционален стринг кој се користи за да се филтрираат албумите според нивните наслови.
(3) Зависности:
db: за сесијата на базата на податоци. buzzer: за валидација на сесијата.
(4) Функционалност:
Ако е даден album_id, се обидува да го земе албумот со тоа id и го враќа. Доколку не е пронајден, повикува 404 HTTP exception. Ако се дадени genres, филтрира албуми според тие жанрови со поврзување на Album, Song, IsOfGenre, и Genre табелите и филтрирање според имињата на жанровите. Се враќаат само различни албуми. Ако е даден search_string, филтрира албуми според насловите на албумите безразлика на големи и мали букви. На крај, ги зема сите албуми кои одговараат на специфицираните критериуми и ги враќа.
(5) Одговор:
Одговорот може да биде или еден албум (ако е даден album_id) или листа од албуми. Одговорот е серијализиран според шемата Album дефинирана во модулот schemas.
@app.get("/songs", response_model=List[schemas.Song]) def get_songs( genres: Optional[List[str]] = Query(None), search_string: Optional[str] = None, db: Session = Depends(get_db), buzzer: models.Buzzer = Depends(validate_session) ): songs_query = db.query(models.Song).join(models.Creation, models.Song.song_id == models.Creation.creation_id) if genres: songs_query = songs_query.join(models.IsOfGenre, models.IsOfGenre.creation_id == models.Song.song_id)\ .join(models.Genre, models.Genre.genre_id == models.IsOfGenre.genre_id)\ .filter(models.Genre.genre_name.in_(genres)) if search_string: songs_query = songs_query.filter(models.Song.song_title.contains(search_string)) songs = songs_query.all() result = [] for song in songs: song_data = { "song_id": song.song_id, "album_id": song.album_id, "rlabel_id": song.rlabel_id, "lyrics": song.lyrics, "song_title": song.song_title, "song_file": song.song_file, "song_duration": song.creation.creation_duration, "song_date": song.creation.creation_date, "created_by": song.creation.created_by, "genres": get_genres_for_song(song.song_id, db) } result.append(schemas.Song(**song_data)) return result
Ова е ендпоинтот "/songs" за пребарување на песни според жанра и име. На сличен начин функционира како и претходната функција.
class Song(Base): __tablename__ = 'song' __table_args__ = {'schema': 'IND0_185022'} song_id = Column(Integer, ForeignKey('IND0_185022.creation.creation_id'), primary_key=True, autoincrement=True) album_id = Column(Integer, ForeignKey('IND0_185022.album.album_id'), nullable=True) rlabel_id = Column(Integer, ForeignKey('IND0_185022.record_label.rlabel_id'), nullable=True) lyrics = Column(TEXT, nullable=True) song_title = Column(String(255)) song_file = Column(TEXT) creation= relationship("Creation", back_populates="song") genres = relationship("IsOfGenre", back_populates="song")
Ова е моделот Song кој наследува од Base моделот на SQLAlchemy библиотеката. Го дефинираме името на табелата заедно со колоните на табелата.
Исто така потребна ни е и следната шема SongBase, заедно со Song и SongCreate, за да можеме полесно да серијализираме објекти и да имаме дата валидација.
class SongBase(BaseModel): album_id: Optional[int] = None rlabel_id: Optional[int] = None lyrics: Optional[str] = None song_title: str song_file: str song_duration: int song_date: date genres: Optional[List[str]] class Song(SongBase): song_id: int class Config: orm_mode = True class SongCreate(SongBase): pass
Истото го имаме направено и за Album.
class Album(Base): __tablename__ = 'album' __table_args__ = {'schema': 'IND0_185022'} album_id = Column(Integer, primary_key=True, autoincrement=True) album_title = Column(String(255)) album_date = Column(TIMESTAMP, default=func.now()) al_created_by = Column(Integer, ForeignKey('IND0_185022.artist.user_id')) rlabel_id = Column(Integer, ForeignKey('IND0_185022.record_label.rlabel_id'))
class AlbumBase(BaseModel): album_title: str al_created_by: int rlabel_id: int class AlbumCreate(AlbumBase): pass class Album(AlbumBase): album_id: int class Config: orm_mode = True
Следува кодот кој е дел од frontend-от и тоа е компонентата Search. Импортира зависности и компоненти од надворешни датотеки/модули, вклучувајќи React hooks, API функции, контекстни провајдери, енумерации и други компоненти.
Користи useState hook за менаџирање на локалната состојба за categories и genres. Овие состојби ги чуваат податоците превземени од серверот (категории и жанрови).
Користи useContext hook за пристап до податоци поврзани со автентикацијата од контекстниот провајдер AuthContext.
Користи useEffect hook за извршување на странични ефекти (како преземање на податоци) откако компонентата ќе се рендерира. Презема категории и жанрови кога activeTab се менува или кога компонентата се монтира.
Рендира кориснички интерфејс за страницата за пребарување, вклучувајќи ја компонентата Header, компонентата SearchTab и две табели кои прикажуваат песни/албуми или епизоди/подкасти во зависност од активниот таб.
Условно рендира табели врз основа на тоа дали постоjaт податоци за creations или collections. Доколку податоците постојат, ги предава на компонентата Таble заедно со наслов кој го покажува видот на креации кои се прикажани.
Се користат CSS класи од библиотеката tailwind, за да се дефинира изгледот на различните елементи во страницата за пребарување.
import { ChangeEvent, useContext, useEffect, useState } from "react" import { get_categories, get_genres } from "./api/creation"; import { AuthContext } from "./AuthContext"; import { CreationType } from "./enums"; import Header from "./Header" import InputField from "./inputField" import { useSearchContext } from "./SearchContext"; import SearchTab from "./SearchTab"; import Table from "./Table.new" const Search = () => { const { creations, collections, activeTab } = useSearchContext() const [categories, setCategories] = useState<Category[]>([]) const [genres, setGenres] = useState<string[]>([]) const { sessionUuid } = useContext(AuthContext) useEffect(() => { if (sessionUuid) { get_categories(sessionUuid).then(res => setCategories(res)) get_genres(sessionUuid).then(res => setGenres(res)) } }, [activeTab]) return ( <div className="bg-black text-purple-300 min-h-screen h-fit p-10"> <Header /> <div> <h1 className="my-3 text-white text-bold text-5xl">Search</h1> <SearchTab categories={categories} genres={genres} /> </div> { creations ? <Table rows={creations} title={activeTab == CreationType.Song ? "Songs" : "Episodes"} /> : null } { collections ? <Table rows={collections} title={activeTab == CreationType.Song ? "Albums" : "Podcasts"} /> : null } </div> ) } export default Search;
Oва е функција која е дел од api-то што комуницира со eндпоинтот на backend-oт. Функцијата прави GET барање со две заглавја меѓу кои е и идентификаторот на сесијата. Тоа ни е потребно бидејќи рутата е заштитена и корисници кои не се логирани, не можат да пристапат до неа.
async function get_genres(sessionUuid: string) { const options = { method: "GET", headers: { "Content-Type": "application/json", "session-uuid": sessionUuid } } return fetch("http://localhost:8000/genres", options) .then((res) => res.json()) .catch((err) => { console.log(err) }) }
Oткако ќе се притисне копчето Search, кое е дел од SearchTab компонента чиј код не е прикажан тука но е дел од претходната компонента, се повикува следната функција која ги формира соодветните параметри за пребарување.
async function get_songs(genres: Set<string>, searchString: string, sessionUuid: string): Promise<SongRes[]> { let genres_query = '' if (genres) { genres_query = Array.from(genres).map(genre => `genres=${genre}&`).join("") } let query = genres_query if (searchString) { query = query + "search_string=" + searchString } return await fetch(`http://localhost:8000/songs?${query}`, { method: "GET", headers: { 'Content-Type': 'application/json', 'Session-Uuid': sessionUuid } }).then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) }
Низ апликацијата се користени и типови за олеснување на процесот на развивање на апликацијата. Еден од тие типови е SongRes.
interface SongRes { album_id?: number, rlabel_id: number, lyrics: string, song_title: string, song_file: string, song_duration: number, song_date: string | Date, genres: string[], song_id?: number }
Attachments (9)
-
Screenshot 2024-02-14 at 22.17.35 (1).jpg
(82.2 KB
) - added by 3 months ago.
search by genre
-
Screenshot 2024-02-14 at 22.34.28.jpg
(69.3 KB
) - added by 3 months ago.
search by genre 2
-
Screenshot 2024-02-14 at 22.36.54.jpg
(85.0 KB
) - added by 3 months ago.
if no genre 1
-
Screenshot 2024-02-14 at 22.37.20.jpg
(37.2 KB
) - added by 3 months ago.
every album if no genre
- get_albums.jpg (86.7 KB ) - added by 3 months ago.
- get_songs1.jpg (87.4 KB ) - added by 3 months ago.
- search.jpg (77.3 KB ) - added by 3 months ago.
- searchTab1.jpg (73.9 KB ) - added by 3 months ago.
- searchTab2.jpg (71.7 KB ) - added by 3 months ago.
Download all attachments as: .zip