diff --git a/database/database.py b/database/database.py index 29496ba..bb718c5 100644 --- a/database/database.py +++ b/database/database.py @@ -7,7 +7,7 @@ import sqlalchemy from sqlalchemy.orm import declarative_base, relationship, load_only from database.models import Art, ArtnoID, Artist, Presence, TagCategory, TagCategorynoID, TagNoID, Tag, \ - Topic, TopicNoId, Collection, CollectionNoID + Topic, TopicNoId, Collection, CollectionNoID, Art2CollRelationNoID, Art2CollRelation # TODO read sensitive data from environment file or similar SQLALCHEMY_DATABASE_URL = "postgresql://artnet_editor:G606Rm9sFEXe6wfTLxVu@[::1]/test_artnet" @@ -23,7 +23,7 @@ art_to_tag_table = Table('art_to_tag', Base.metadata, art_to_art_collection = Table('art_to_art_collection', Base.metadata, Column('collection_id', Integer, ForeignKey('art_collection.id')), Column('art_id', Integer, ForeignKey('art.id')), - Column('ranking', String)) + Column('ranking', String))""" artist_to_topic_table = Table('artist_to_topic', Base.metadata, Column('artist_id', Integer, ForeignKey('artist.id')), @@ -68,7 +68,7 @@ class DBArt(Base): description = Column(String, nullable=True) tags = relationship("DBTag", secondary=art_to_tag_table, back_populates="art", viewonly=True) - collections = relationship("DBCollection", secondary=art_to_art_collection, back_populates="art") + collections = relationship("DBCollection", secondary="art_to_art_collection", back_populates="art") presences = relationship("DBPresence", secondary="art_to_presence", back_populates="arts") @@ -104,7 +104,15 @@ class DBCollection(Base): name = Column(String, unique=True, nullable=False) description = Column(String) - art = relationship("DBArt", secondary=art_to_art_collection, back_populates="collections") + art = relationship("DBArt", secondary="art_to_art_collection", back_populates="collections") + + +class DBCollection2Art(Base): + __tablename__ = "art_to_art_collection" + + collection_id = Column(Integer, ForeignKey(DBCollection.id, ondelete="CASCADE"), primary_key=True) + art_id = Column(Integer, ForeignKey(DBArt.id, ondelete="CASCADE"), primary_key=True) + ranking = Column(String) class DBTopic(Base): # as of now unused @@ -155,7 +163,6 @@ class Database: try: return Database.__db finally: - ##Database.__db.close() pass def __del__(self): @@ -188,6 +195,8 @@ class Database: if art.presences is not None: for presence in art.presences: + if self.get_presence(presence.name, presence.domain) is None: + raise ValueError("Used presence did not exist!") db_art_presence = DBArt2Presence(presence_name=presence.name, presence_domain=presence.domain, art_id=db_art.id) db.add(db_art_presence) @@ -213,6 +222,14 @@ class Database: db_art.path = art.path if art.path is not None else db_art.path db_art.description = art.description if art.description is not None else db_art.description + if art.presences is not None: + for presence in art.presences: + db_art.presences.append(self.get_presence(presence.name, presence.domain)) + + if art.collections is not None: + for collection in art.collections: + db_art.collections.append(self.get_collection_by_id(collection.id)) + self.__get_db().commit() def delete_art_by_id(self, art_id: int): @@ -616,4 +633,43 @@ class Database: self.__get_db().delete(db_collection) self.__get_db().commit() + # Art -> Collection + def get_collection_art(self, collection_id: int): + return self.__get_db().query(DBCollection2Art).filter(DBCollection2Art.collection_id == collection_id).all() + # Art <-> Collection + + def get_art_collection_relation(self, art_id: int, collection_id: int) -> DBCollection2Art: + return self.__get_db().query(DBCollection2Art).filter(DBCollection2Art.collection_id == collection_id, + DBCollection2Art.art_id == art_id).first() + + def create_art_collection_relation(self, art2collection: Art2CollRelation): + db_art2collection = DBCollection2Art(art_id=art2collection.art_id, + collection_id=art2collection.collection_id, + ranking=art2collection.ranking) + + self.__get_db().add(db_art2collection) + self.__get_db().commit() + + def update_art_collection_relation_by_model(self, art_collection_relation: Art2CollRelation): + db_art2collection = self.get_art_collection_relation(art_id=art_collection_relation.art_id, + collection_id=art_collection_relation.collection_id) + + db_art2collection.ranking = art_collection_relation.ranking + self.__get_db().commit() + + def delete_art_collection_relation(self, art_id: int, collection_id: int): + db_art2collection = self.get_art_collection_relation(art_id=art_id, collection_id=collection_id) + if db_art2collection is None: + raise ValueError("Could not find art-collection relation with these IDs!") + + self.__get_db().delete(db_art2collection) + self.__get_db().commit() + + # Collection -> Art + def get_art_collections(self, art_id: int): + return self.__get_db().query(DBCollection2Art).filter( + DBCollection2Art.art_id == art_id).all() + # TODO throws error + # TODO SAWarning: SELECT statement has a cartesian product between FROM element(s) "art_to_art_collection" and FROM element "art_collection". Apply join condition(s) between each element to resolve. + # return self.__get_db().query(DBCollection).filter(DBCollection2Art.art_id == art_id).all() diff --git a/database/models.py b/database/models.py index 6d84f50..aec1082 100644 --- a/database/models.py +++ b/database/models.py @@ -78,8 +78,10 @@ class ArtnoID(BaseModel): title: Optional[str] = None link: Optional[str] = None description: Optional[str] + presences: Optional[List[Presence]] tags: Optional[List[Tag]] + collections: Optional[List[Collection]] class Config: orm_mode = True @@ -92,16 +94,17 @@ class Art(ArtnoID): orm_mode = True -class CollectionNoID(BaseModel): - name: str - description: Optional[str] +class Art2CollRelationNoID(BaseModel): + ranking: Optional[str] class Config: orm_mode = True -class Collection(CollectionNoID): - id: int +class Art2CollRelation(Art2CollRelationNoID): + collection_id: int + art_id: int class Config: orm_mode = True + diff --git a/main.py b/main.py index d750cf3..a6671bb 100644 --- a/main.py +++ b/main.py @@ -4,16 +4,16 @@ import uvicorn from typing import List, Tuple from database.database import Database -from database.database import DBPresence, DBArt2Presence +from database.database import DBPresence, DBArt2Presence, DBCollection2Art from database.models import Art, ArtnoID, Presence, ArtistNoId, Tag, TagNoID, TagCategory, TagCategorynoID, TopicNoId, \ - Topic, CollectionNoID, Collection + Topic, CollectionNoID, Collection, Art2CollRelationNoID, Art2CollRelation app = FastAPI() db = Database() @app.get("/artnet/metadata/art") -async def art(id: int = None, hash: str = None, tag_id: int = None): +async def art(id: int = None, hash: str = None, tag_id: int = None, collection_id: int = None): hash = unquote(hash) if hash is not None else None print(f"Received GET on /artnet/metadata/art (id={id}, hash={hash}, tag_id={tag_id})") @@ -24,6 +24,8 @@ async def art(id: int = None, hash: str = None, tag_id: int = None): result = db.get_art_by_hash(hash) elif tag_id is not None: result = db.get_tag_art(tag_id) + elif collection_id is not None: + result = db.get_collection_art(collection_id) if id is None and hash is None and tag_id is None: # TODO fix against listing enormous amounts of art (because "all" could be large) result = db.get_art_list() @@ -60,6 +62,9 @@ async def art(art: ArtnoID, id: int = None): if art.presences is not None: updated_art.presences = art.presences + if art.collections is not None: + updated_art.collections = art.collections + db.update_art_by_model(updated_art) return True @@ -350,10 +355,10 @@ def category(id: int): @app.get("/artnet/metadata/collection") -def collection(id: int = None, name: str = None): +def collection(id: int = None, name: str = None, art_id: int = None): print(f"Received GET on /artnet/metadata/collection (id={id}, name={name})") result = None - if id is None and name is None: + if id is None and name is None and art_id is None: result = db.get_collection_list() if id is not None and name is not None: @@ -365,9 +370,14 @@ def collection(id: int = None, name: str = None): if name is not None: result = db.get_collections_by_name(name) + if art_id is not None: + result = db.get_art_collections(art_id) + if result is not None: if isinstance(result, list): for i in range(len(result)): + if isinstance(result[i], DBCollection2Art): + continue result[i].name = result[i].name.strip() else: result.name = result.name.strip() @@ -396,5 +406,41 @@ async def collection(id: int): raise HTTPException(status_code=404, detail=str(e)) +@app.get("/artnet/metadata/collection_entry") +async def collection_entry(art_id: int, collection_id: int): + print(f"Received GET on /artnet/metadata/collection_entry (art_id={art_id}, collection_id={collection_id})") + result = db.get_art_collection_relation(art_id=art_id, collection_id=collection_id) + + if result is None: + raise HTTPException(status_code=404, detail="Can not find a art-collection-relation with these IDs!") + + return result + + +@app.post("/artnet/metadata/collection_entry") +async def collection_entry(art2collection: Art2CollRelationNoID, art_id: int = None, collection_id: int = None): + print(f"Received POST on /artnet/metadata/collection_entry") + + if art_id is None and collection_id is None: # create new relation, impossible! + raise HTTPException(status_code=406, detail="You can not create a relation without IDs!") + + elif art_id is not None and collection_id is not None: + art2collection_id = Art2CollRelation(art_id=art_id, collection_id=collection_id) + art2collection_id.ranking = art2collection.ranking + db.update_art_collection_relation_by_model(art2collection_id) + return + + raise HTTPException(status_code=406, detail="You can not only specify only one ID!") + + +@app.delete("/artnet/metadata/collection_entry") +async def collection_entry(art_id: int, collection_id: int): + print(f"Received DELETE on /artnet/metadata/collection_entry (art_id={art_id}, collection_id={collection_id})") + try: + db.delete_art_collection_relation(art_id=art_id, collection_id=collection_id) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + + if __name__ == "__main__": uvicorn.run(app, host="127.0.0.1", port=8000) diff --git a/tests/createAPI/create_delete_art.py b/tests/createAPI/create_delete_art.py index da3bb18..2562569 100644 --- a/tests/createAPI/create_delete_art.py +++ b/tests/createAPI/create_delete_art.py @@ -10,13 +10,13 @@ from tests.createAPI.create_delete_presence import test_presence_entries, list_p test_art_entries = [ {"hash": "hash1", "path": "artist1/image1", "title": "Image Title 1", "link": "http://localhost/artist1/image1.png", "description": "description Nr. 1", - "presences": [(test_presence_entries[0]["name"], test_presence_entries[0]["domain"])]}, + "presences": [(test_presence_entries[2]["name"], test_presence_entries[2]["domain"])]}, {"hash": "hash2", "path": "artist1/image2", "title": "Image Title 2", "link": "http://localhost/artist1/image2.png", "description": "description Nr. 2", - "presences": [(test_presence_entries[1]["name"], test_presence_entries[1]["domain"])]}, + "presences": [(test_presence_entries[2]["name"], test_presence_entries[2]["domain"])]}, {"hash": "hash3", "path": "artist2/image1", "title": "Image Title 3", "link": "http://localhost/artist2/image3.png", "description": "description Nr. 3", - "presences": [(test_presence_entries[0]["name"], test_presence_entries[0]["domain"]), + "presences": [(test_presence_entries[2]["name"], test_presence_entries[2]["domain"]), (test_presence_entries[1]["name"], test_presence_entries[1]["domain"])]}, ] @@ -35,6 +35,15 @@ def list_art(url: str, port: int): return art +def get_art(url: str, port: int, id: int): + r = requests.get(f"http://{url}:{port}/artnet/metadata/art") + if r.status_code != 200: + print(f"Failed to query for art! Status: {r.status_code} Reason: {r.text}") + raise Exception("Failed querying for art!") + art = json.loads(r.text) + return art + + def create_art_entries(url: str, port: int): """ Creates many art entries for testing purposes diff --git a/tests/relationAPI/relate_art.py b/tests/relationAPI/relate_art.py index 8dd36c9..ecb7631 100644 --- a/tests/relationAPI/relate_art.py +++ b/tests/relationAPI/relate_art.py @@ -105,7 +105,7 @@ def delete_art_presence_relations(url: str, port: int): def delete_art_presence_relation(url: str, port: int, presence_name: str, presence_domain: str, art_id: int): return requests.delete(f"http://{url}:{port}/artnet/metadata/art?id={art_id}&presence_name={presence_name}" - f"&presence_domain={presence_domain}") + f"&presence_domain={presence_domain}") def run_art_presence_relation_test(url, port): diff --git a/tests/relationAPI/relate_art_to_collection.py b/tests/relationAPI/relate_art_to_collection.py new file mode 100644 index 0000000..9e764a8 --- /dev/null +++ b/tests/relationAPI/relate_art_to_collection.py @@ -0,0 +1,128 @@ +import requests +import json + +from tests.createAPI.create_delete_art import get_art, list_art, create_art_entries, delete_art_entries, \ + test_art_entries +from tests.createAPI.create_delete_presence import create_presence_entries +from tests.createAPI.create_delete_artist import create_artist_entries, delete_artist_entries +from tests.createAPI.create_delete_collection import get_collection_by_id, list_collections, create_collection_entries, \ + delete_collection_entries, test_collection_entries + +test_art_2_collection_entries = [{"art_id": 0, "collection_id": 0, "ranking": "aa"}, + {"art_id": 1, "collection_id": 0, "ranking": "ab"}, + {"art_id": 2, "collection_id": 1, "ranking": "aaa"}, + {"art_id": 1, "collection_id": 1, "ranking": "ac"}] + + +def create_art2collections(url: str, port: int): + for relation in test_art_2_collection_entries: + add_art_to_collection(url, port, art_id=test_art_entries[relation["art_id"]]["ID"], + collection_id=test_collection_entries[relation["collection_id"]]["id"]) + + get_art2collection(url, port, art_id=test_art_entries[relation["art_id"]]["ID"], + collection_id=test_collection_entries[relation["collection_id"]]["id"]) + + return True + + +def update_art2collections(url: str, port: int): + for relation in test_art_2_collection_entries: + update_art2collection(url, port, art_id=test_art_entries[relation["art_id"]]["ID"], + collection_id=test_collection_entries[relation["collection_id"]]["id"], + ranking=relation["ranking"]) + + c = get_art2collection(url, port, art_id=test_art_entries[relation["art_id"]]["ID"], + collection_id=test_collection_entries[relation["collection_id"]]["id"]) + if c["ranking"] != relation["ranking"]: + print(f"Failed to update the ARt2Collection relation! Got: {c['ranking']} Expected: {relation['ranking']}") + raise Exception("Failed to update the ranking in Art2Collection") + return True + + +def delete_art2collections(url: str, port: int): + for relation in test_art_2_collection_entries: + delete_art2collection(url, port, art_id=test_art_entries[relation["art_id"]]["ID"], + collection_id=test_collection_entries[relation["collection_id"]]["id"]) + return True + + +def add_art_to_collection(url: str, port: int, art_id: int, collection_id: int): + prev_colls = get_art2collections_by_art_id(url, port, art_id) + r = requests.post(f"http://{url}:{port}/artnet/metadata/art?id={art_id}", + json={"collections": [{"id": c["collection_id"]} for c in prev_colls] + [{"id": collection_id}]}) + + if r.status_code != 200: + print(f"Failed to set Art2Collection. Status: {r.status_code} Reason: {r.text}") + raise Exception("Failed to set Art2Collection relation!") + + +def update_art2collection(url: str, port: int, art_id: int, collection_id: int, ranking: str): + r = requests.post(f"http://{url}:{port}/artnet/metadata/collection_entry?art_id={art_id}" + f"&collection_id={collection_id}", + json={"ranking": ranking}) + + if r.status_code != 200: + print(f"Failed to update Art2Collection relation! Status: {r.status_code} Reason: {r.text}") + raise Exception("Failed to update Art2Collection relation!") + + +def get_art2collection(url: str, port: int, art_id: int, collection_id: int): + r = requests.get(f"http://{url}:{port}/artnet/metadata/collection_entry?art_id={art_id}" + f"&collection_id={collection_id}") + + if r.status_code != 200: + print(f"Failed to fetch specific art-collection relation! Status: {r.status_code} Reason: {r.text}") + raise Exception("Failed to fetch Art2Collection!") + result = json.loads(r.text) + if result["art_id"] != art_id or result["collection_id"] != collection_id: + print(f"Got: ({result['art_id']}, {result['collection_id']}) Expected: ({art_id}, {collection_id})") + raise Exception("The returned relation does not fit the requested one!") + + return result + + +def delete_art2collection(url: str, port: int, art_id: int, collection_id: int): + r = requests.delete(f"http://{url}:{port}/artnet/metadata/collection_entry?art_id={art_id}" + f"&collection_id={collection_id}") + + if r.status_code != 200: + print(f"Failed to delete art-collection relation! Status: {r.status_code} Reason: {r.text}") + raise Exception("Failed to delete art-collection relation!") + + +def get_art2collections_by_art_id(url: str, port: int, art_id: int): + r = requests.get(f"http://{url}:{port}/artnet/metadata/collection?art_id={art_id}") + if r.status_code != 200: + print(f"Failed to query for Art2Collection relation via art_id. Status: {r.status_code} Reason: {r.text}") + raise Exception("Failed to query Art2Collection relations!") + + return json.loads(r.text) + + +def run_art2collections_tests(url: str, port: int): + print() + print("----------------") + print(f"Starting art2collection test with ({len(list_collections(url, port))}) collections and " + f"({len(list_art(url, port))}) art!") + print("Setting up art, presence, artists and collections for the tests ...") + create_collection_entries(url, port) + create_artist_entries(url, port) + create_presence_entries(url, port) + create_art_entries(url, port) + print(f"Done with the setup! Now have ({len(list_collections(url, port))}) collections and " + f"({len(list_art(url, port))}) art!") + + print("Adding art2collection relation ...") + create_result = create_art2collections(url, port) + print("Updating rankings ...") + update_result = update_art2collections(url, port) + print("Removing relations ...") + delete_result = delete_art2collections(url, port) + print("Done with the art2collection relation test!") + + print("Cleaning up the setup ...") + delete_collection_entries(url, port) + delete_artist_entries(url, port) + delete_art_entries(url, port) + + return create_result, update_result, delete_result diff --git a/tests/run_tests.py b/tests/run_tests.py index 5cc1253..42ee44d 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -8,6 +8,7 @@ from tests.createAPI.create_delete_topic import list_topics, run_topic_tests, de from tests.createAPI.create_delete_collection import run_collection_tests from tests.relationAPI.relate_art import run_art_presence_relation_test +from tests.relationAPI.relate_art_to_collection import run_art2collections_tests def run_tests(url: str, port: int): @@ -38,6 +39,8 @@ def run_tests(url: str, port: int): create_tag_result, update_tag_result, delete_tag_result = run_tag_test(url, port) create_topic_result, update_topic_result, delete_topic_result = run_topic_tests(url, port) create_collection_result, update_collection_result, delete_collection_result = run_collection_tests(url, port) + create_art2collection_result, update_art2collection_result, delete_art2collection_result = \ + run_art2collections_tests(url, port) print() print("-------- Test Results ---------") @@ -73,7 +76,8 @@ def run_tests(url: str, port: int): f"\tDelete: {delete_topic_result} \t(Direct)") print(f"Art Collection: \t\tCreate: {create_collection_result} \tUpdate: {update_collection_result} " f"\tDelete: {delete_collection_result} \t(Direct)") - print(f"Art2Art Collection: \tN/A") + print(f"Art2Art Collection: \tCreate: {create_art2collection_result} \tUpdate: {update_art2collection_result}" + f"\tDelete: {delete_art2collection_result} \t(Direct)") print(f"Artist2Topic: \t\t\tN/A") print(f"Art2Tag: \t\t\t\tN/A") print(f"Tag Alias: \t\t\t\tN/A")