From b37a21d7853b89cf403772fa7a631c654f109c59 Mon Sep 17 00:00:00 2001 From: Peery Date: Fri, 9 Dec 2022 12:30:52 +0100 Subject: [PATCH] Half-done collections and UI changes Minor tweaks and refactors. A half-baked collection support and several UI tweaks. Just committing these are they are "old" changes and the current state turns out to be pretty stable. --- ArtNet/artnet_manager.py | 30 +- ArtNet/db/db_adapter.py | 332 +++++++++++++++--- ArtNet/file/file_reader.py | 2 + ArtNet/gui/browse_window.py | 2 +- .../artist_modify_dialog.py | 14 +- .../artist_modify_dialog.ui | 18 +- .../image_select_dialog/image_sel_dialog.py | 247 +++++++++++++ ArtNet/gui/importer_window.py | 137 +++++++- ArtNet/gui/windows/artnet_mainwindow.py | 2 +- ArtNet/singleton_manager.py | 13 + DB | 2 +- __main__.py | 10 +- 12 files changed, 730 insertions(+), 79 deletions(-) create mode 100644 ArtNet/gui/dialogs/image_select_dialog/image_sel_dialog.py create mode 100644 ArtNet/singleton_manager.py diff --git a/ArtNet/artnet_manager.py b/ArtNet/artnet_manager.py index d76bb5e..f2220f5 100644 --- a/ArtNet/artnet_manager.py +++ b/ArtNet/artnet_manager.py @@ -14,19 +14,22 @@ from ArtNet.gui.importer_window import ImporterWindow from ArtNet.gui.browse_window import BrowseWindow from ArtNet.gui.dialogs.db_connection_dialog.db_dialog import DBDialog from ArtNet.web.link_generator import LinkGenerator +from ArtNet.singleton_manager import SingletonManager class ArtNetManager: LOG_FOLDER = "log" def __init__(self, config_location: str = "."): + SingletonManager.set_instance(self) + if not os.path.isdir(ArtNetManager.LOG_FOLDER): os.mkdir(ArtNetManager.LOG_FOLDER) file_name = "artnet-" + str(datetime.datetime.now().strftime("%Y-%m-%d")) + ".log" logging.basicConfig(filename=os.path.join(ArtNetManager.LOG_FOLDER, file_name), encoding='utf-8', level=logging.DEBUG) logFormatter = logging.Formatter(fmt="%(asctime)s.%(msecs)03d [%(threadName)-12.12s] [%(levelname)-5.5s] " - "%(message)s", + "%(message)s", datefmt="%Y-%m-%d %H:%M:%S") logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) @@ -182,7 +185,7 @@ class ArtNetManager: if tag in already_applied_tags: # tag is already applied continue - result = self.db_connection.get_tag_impliers(tag) + result = self.db_connection.get_tag_impliers_by_name(tag) if len(result) != 0: # tag is implied by some other tag skip = False for implier_tag_id in result: @@ -389,10 +392,12 @@ class ArtNetManager: s = self.all_images[self.curr_image_index].split(os.path.sep) if image_db_result is not None: image_title = image_db_result["title"] if isinstance(image_db_result["title"], str) and \ - len(image_db_result["title"]) > 0 else self.import_window.UNKNOWN_TITLE + len(image_db_result[ + "title"]) > 0 else self.import_window.UNKNOWN_TITLE image_author = self.db_connection.get_authors_of_art_by_ID(image_db_result["ID"]) image_link = image_db_result["link"] image_description = image_db_result["description"] + collections = self.db_connection.get_collections_of_art_by_ID(image_db_result["ID"]) art_ID = image_db_result["ID"] if image_author is None or len(image_author) == 0: image_author = [(self.all_images[self.curr_image_index].split(os.path.sep)[0], "(Not in Database)")] @@ -401,13 +406,16 @@ class ArtNetManager: image_author = [(s[0], "(Not in Database)")] image_title = s[-1] + " (Not in Database)" image_link = "(Unknown)" + collections = [] image_description = None logging.info(f"Displaying #{self.curr_image_index} \"{self.all_images[self.curr_image_index]}\"") self.curr_active_window.display_image(image_title, image_author, - os.path.join(self.config.data["file_root"], self.all_images[self.curr_image_index]), - self.all_images[self.curr_image_index], - art_ID, image_link, file_name=s[-1], description=image_description) + os.path.join(self.config.data["file_root"], + self.all_images[self.curr_image_index]), + self.all_images[self.curr_image_index], + art_ID, image_link, file_name=s[-1], description=image_description, + collections=collections) self.curr_active_window.set_tag_list( [self.db_connection.get_tag_by_ID(x)[0][1].strip() for x in self.db_connection.get_art_tags_by_ID(art_ID)]) @@ -431,7 +439,7 @@ class ArtNetManager: """ if len(path) == 0: exit(0) - logging.info("Changing root to", path) + logging.info(f"Changing root to {path}") self.config.data["file_root"] = path self.config.update_config() self.__file_reader = FileReader(self.config.data["file_root"]) @@ -442,6 +450,12 @@ class ArtNetManager: return self.config.data["db"] def change_db_connection(self, host: str, port: int, database: str, user: str, password: str): + try: + self.db_connection = self.create_db_connection(host, port, database, user, password) + except ValueError as e: + logging.error(f"Error encountered during change_db_connection(): {e}") + raise e + self.config.data["db"]["user"] = user self.config.data["db"]["password"] = password self.config.data["db"]["host"] = host @@ -449,5 +463,3 @@ class ArtNetManager: self.config.data["db"]["port"] = str(port) self.config.update_config() - - self.db_connection = self.create_db_connection(host, port, database, user, password) diff --git a/ArtNet/db/db_adapter.py b/ArtNet/db/db_adapter.py index 3d35413..ecd9401 100644 --- a/ArtNet/db/db_adapter.py +++ b/ArtNet/db/db_adapter.py @@ -1,10 +1,11 @@ import logging +from typing import List, Tuple + import psycopg2 from psycopg2.errorcodes import UNIQUE_VIOLATION class DBAdapter: - EXPECTED_TABLES = ['art', 'artist', 'tag', 'topic', 'presence', 'artist_to_topic', 'art_to_presence', 'art_to_tag', 'tag_alias', 'tag_implication', 'tag_category', 'art_collection', 'art_to_art_collection'] @@ -30,8 +31,13 @@ class DBAdapter: Check if all the expected tables are present in the connected database. :return: """ - self.db_cursor.execute("select relname from pg_class where relkind='r' and relname !~ '^(pg_|sql_)';") - present_tables = self.db_cursor.fetchall() + try: + self.db_cursor.execute("select relname from pg_class where relkind='r' and relname !~ '^(pg_|sql_)';") + present_tables = self.db_cursor.fetchall() + except psycopg2.DatabaseError as e: + logging.error(f"Database error occured on check_tables(): {e}") + self.db.rollback() + return False unknown_tables = [] missing = [x for x in DBAdapter.EXPECTED_TABLES] for table in present_tables: @@ -46,11 +52,13 @@ class DBAdapter: return False if len(unknown_tables) > 0: - logging.error("The following tables are unknown and not expected inside the database: {0}".format(unknown_tables)) + logging.error( + "The following tables are unknown and not expected inside the database: {0}".format(unknown_tables)) return True - def save_image(self, ID: str, title: str, authors: list, path: str, tags: list, link: str, md5_hash: str, desc: str): + def save_image(self, ID: str, title: str, authors: list, path: str, tags: list, link: str, md5_hash: str, + desc: str, collections: list): """ Updates or saves the given image data to the DB :param ID: @@ -61,30 +69,34 @@ class DBAdapter: :param link: :param md5_hash: md5 hash as a hex digest :param desc: image description or None for empty + :param collections: list of collections included by the image :return: """ logging.debug("Saving Image {0}:{1} authors: {2} path: {3} tags: {4} link: {5} hash:{6} desc:{7}" - .format(ID, title, authors, path, tags, link, md5_hash, desc)) - d = {"title": title, "path": path, "id": ID, "link": link, "hash": md5_hash, "desc": desc} + .format(ID, title, authors, path, tags, link, md5_hash, desc)) + d = {"title": title, "path": path, "id": ID, "link": link, "hash": md5_hash, "desc": desc} # TODO implement try/except for database errors and rollback! if self.get_art_by_path(path) is None: if ID is None: self.db_cursor.execute( "INSERT INTO art (path, title, link, md5_hash, description) " "VALUES (%(path)s, %(title)s, %(link)s, %(hash)s, %(desc)s)", d) + self.db.commit() ID = self.get_art_by_path(path)["ID"] d["id"] = ID elif self.get_tag_by_ID(ID) is None: self.db_cursor.execute( "INSERT INTO art (path, title, link, md5_hash, description) " "VALUES (%(path)s, %(title)s, %(link)s, %(hash)s, %(desc)s)", d) + self.db.commit() else: self.db_cursor.execute("UPDATE art SET path = %(path)s, title = %(title)s, link = %(link)s, " + "md5_hash = %(hash)s, description = %(desc)s WHERE id = %(id)s", d) + self.db.commit() if ID is None: ID = self.get_art_by_path(path)["ID"] - assert(ID != None) # was checked before, should never fail + assert (ID != None) # was checked before, should never fail old_tags = [self.get_tag_by_ID(x)[0][1].strip() for x in self.get_art_tags_by_ID(ID)] for tag in tags: @@ -93,10 +105,12 @@ class DBAdapter: d = {"id": ID, "tag": self.get_tag_ID(tag)} try: self.db_cursor.execute("INSERT INTO art_to_tag (art_id, tag_ID) VALUES (%(id)s, %(tag)s)", d) + self.db.commit() except psycopg2.Error as e: - if e.pgcode == UNIQUE_VIOLATION: + if e.pgcode == UNIQUE_VIOLATION: # tag was already set logging.debug(e) logging.info("Skipping Unique Violation ...") + self.db.rollback() else: raise e @@ -111,6 +125,7 @@ class DBAdapter: d = {"id": ID, "author_name": author[0], "author_domain": author[1]} self.db_cursor.execute("INSERT INTO art_to_presence (presence_name, presence_domain, art_ID) " + "VALUES (%(author_name)s, %(author_domain)s, %(id)s)", d) + self.db.commit() for old_author_name, old_author_domain in old_authors: if (old_author_name, old_author_domain) not in authors: # need to remove old author self.remove_artist_from_image(art_ID=ID, presence_name=old_author_name, @@ -130,16 +145,15 @@ class DBAdapter: art_ID = image_data["ID"] logging.debug(f"Deleting image #{art_ID} {image_data['title']}") tags = self.get_art_tags_by_ID(art_ID) - for tag_ID in tags: - pass - #self.remove_tag_from_image(art_ID=art_ID, tag_ID=tag_ID) + # for tag_ID in tags: + # pass + # self.remove_tag_from_image(art_ID=art_ID, tag_ID=tag_ID) authors = self.get_authors_of_art_by_ID(art_ID) - for presence_name, presence_domain in authors: - pass - #self.remove_artist_from_image(art_ID=art_ID, presence_name=presence_name, presence_domain=presence_domain) - - #self.db_cursor.execute("DELETE FROM art WHERE md5_hash = %(hash)s", {"hash": hash}) + # for presence_name, presence_domain in authors: + # pass + # self.remove_artist_from_image(art_ID=art_ID, presence_name=presence_name, presence_domain=presence_domain) + # self.db_cursor.execute("DELETE FROM art WHERE md5_hash = %(hash)s", {"hash": hash}) def remove_artist_from_image(self, art_ID, presence_name, presence_domain): """ @@ -340,6 +354,29 @@ class DBAdapter: """ self.db_cursor.execute("SELECT id, name FROM artist") + def get_art_by_ID(self, ID: int): + """ + Queries the database for the art via its ID and returns it if available. + """ + d = {"ID": ID} + self.db_cursor.execute("SELECT id, path, title, link, md5_hash, description FROM art WHERE id = %(ID)s", d) + + result = self.db_cursor.fetchall() + if len(result) == 0: + return None + else: + result = result[0] + image_data = { + "ID": result[0], + "path": result[1], + "title": result[2], + "link": result[3], + "md5_hash": result[4], + "description": result[5], + } + return image_data + + def get_art_by_hash(self, file_hash: str) -> dict: """ Queries the database for the art via its hash and returns it if available. @@ -349,7 +386,8 @@ class DBAdapter: :return: """ d = {"hash": file_hash} - self.db_cursor.execute("SELECT id, path, title, link, md5_hash, description FROM art where md5_hash = %(hash)s", d) + self.db_cursor.execute("SELECT id, path, title, link, md5_hash, description FROM art where md5_hash = %(hash)s", + d) result = self.db_cursor.fetchall() @@ -449,6 +487,36 @@ class DBAdapter: new_rows.append(row[0]) return new_rows + def search_art(self, tags: list, presences: list, resolve_tags: bool = True): + """ + Search with the tags and given presences for fitting art. + :param tags: List[int] list of tag ids that are to be present in the art. See resolve_tags for implication. + :param presences: List[Tuple(str, str)] list of presences to be present in the art. + :param resolve_tags: flag if tag implications should be resolved fully. + """ + d = {"tags": tags, "presences": presences} + l = list() + for i in range(len(presences)): # converting for psycopg2 + name, domain = presences[i] + casted = [name, domain] + l.append(casted) + d["presences"] = l + + search_query = "SELECT art.id FROM art " \ + "INNER JOIN art_to_tag ON art_to_tag.art_id = art.id " \ + "INNER JOIN tag ON art_to_tag.tag_id = tag.id " \ + "INNER JOIN art_to_presence as AtP ON art.id = AtP.art_id " \ + "GROUP BY art.id " \ + "HAVING array_agg(tag.name) @> %(tags)s::varchar[] AND " \ + "array_agg(array[AtP.presence_name, AtP.presence_domain]) @> %(presences)s::varchar[]" + + # 1. Join all related tables + # 2. Group them over art.id + # 3. check which rows contain the searched tags or presences in the value arrays + self.db_cursor.execute(search_query, d) + result = self.db_cursor.fetchall() + return result + def search_fuzzy_presence(self, name: str, domain: str, all_if_empty: bool = False): """ Search a list of presences fitting the (name, domain) fuzzy. @@ -460,7 +528,7 @@ class DBAdapter: if all_if_empty and (name is None or len(name) == 0) and (domain is None or len(domain) == 0): self.db_cursor.execute("SELECT name, domain, artist_id FROM presence") else: - d = {"name":"%"+name+"%", "domain": "%"+domain+"%"} + d = {"name": "%" + name + "%", "domain": "%" + domain + "%"} self.db_cursor.execute("SELECT name, domain, artist_id FROM presence WHERE LOWER(name) LIKE " "LOWER(%(name)s) AND domain LIKE %(domain)s", d) rows = self.db_cursor.fetchall() @@ -502,7 +570,7 @@ class DBAdapter: :param search: :return: """ - d = {"search": "%"+search+"%"} + d = {"search": "%" + search + "%"} self.db_cursor.execute("SELECT name FROM tag_category WHERE LOWER(name) LIKE LOWER(%(search)s)", d) rows = [] @@ -519,10 +587,11 @@ class DBAdapter: :return: """ if all_if_empty and len(name) == 0: - self.db_cursor.execute("SELECT name, description, category_id FROM tag") + self.db_cursor.execute("SELECT name, description, category_id, ID FROM tag") else: - d = {"name": "%"+name+"%"} - self.db_cursor.execute("SELECT name, description, category_id FROM tag WHERE LOWER(name) LIKE LOWER(%(name)s)", d) + d = {"name": "%" + name + "%"} + self.db_cursor.execute( + "SELECT name, description, category_id, ID FROM tag WHERE LOWER(name) LIKE LOWER(%(name)s)", d) rows = self.db_cursor.fetchall() new_rows = [] @@ -553,7 +622,7 @@ class DBAdapter: elif all_if_empty and ID is None and len(name) == 0: self.db_cursor.execute("SELECT id, name FROM artist") else: - d = {"name": "%"+name+"%"} + d = {"name": "%" + name + "%"} self.db_cursor.execute("SELECT id, name FROM artist WHERE LOWER(name) LIKE LOWER(%(name)s)", d) return self.db_cursor.fetchall() @@ -600,7 +669,10 @@ class DBAdapter: "category_id": category_id, } - self.db_cursor.execute("UPDATE tag SET description = %(description)s, category_id = %(category_id)s, name = %(name)s WHERE tag_ID = %(id)s", d) + self.db_cursor.execute( + "UPDATE tag SET description = %(description)s, category_id = %(category_id)s, name = %(name)s " + "WHERE ID = %(id)s", + d) if aliases is not None: old_aliases = self.get_tag_aliases_by_ID(tag_id) @@ -684,7 +756,6 @@ class DBAdapter: d) self.db.commit() - def add_implication_by_name(self, name: str, implicant: str): """ Add the implication to the database @@ -725,7 +796,7 @@ class DBAdapter: :return: """ d = {"tag": tag} - self.db_cursor.execute("DELETE FROM tag WHERE tag_id = %(tag)s", d) + self.db_cursor.execute("DELETE FROM tag WHERE id = %(tag)s", d) self.db.commit() def remove_tag_by_name(self, name: str): @@ -743,11 +814,15 @@ class DBAdapter: :return: """ d = {"name": name} - self.db_cursor.execute("SELECT tag_ID FROM tag where LOWER(name) = LOWER(%(name)s)", d) + try: + self.db_cursor.execute("SELECT ID FROM tag where LOWER(name) = LOWER(%(name)s)", d) - rows = [] - for row in self.db_cursor.fetchall(): - rows.append(row[0]) + rows = [] + for row in self.db_cursor.fetchall(): + rows.append(row[0]) + except psycopg2.DatabaseError as e: + + return None if len(rows) > 1: raise Exception("Found multiple tags by the same name!") @@ -765,7 +840,8 @@ class DBAdapter: :return: Returns the tag's name, description, category, tag ID """ d = {"name": name} - self.db_cursor.execute("SELECT name, description, category_id, tag_ID FROM tag where LOWER(name) = LOWER(%(name)s)", d) + self.db_cursor.execute( + "SELECT name, description, category_id, ID FROM tag where LOWER(name) = LOWER(%(name)s)", d) rows = [] for row in self.db_cursor.fetchall(): @@ -789,13 +865,13 @@ class DBAdapter: :return: Returns the tag's ID, name, description, category """ d = {"ID": ID} - self.db_cursor.execute("SELECT tag_ID, name, description, category_id FROM tag where tag_ID = %(ID)s", d) + self.db_cursor.execute("SELECT ID, name, description, category_id FROM tag where ID = %(ID)s", d) return self.db_cursor.fetchall() def get_tag_aliases_by_name(self, name: str) -> list: """ - Search for the tag's aliases and the tag's aliases + Search for the tag's aliases and the alias's aliases :param name: :return: List of tag Names that are aliases of this one """ @@ -803,7 +879,7 @@ class DBAdapter: result = [] for alias in aliases: tag_data = self.get_tag_by_ID(alias) - if len(tag_data) > 0: + if len(tag_data) > 0: # tag exists result.append(tag_data[0][1].strip()) return result @@ -848,7 +924,8 @@ class DBAdapter: collected_tags.append(found) to_search.append(found) - collected_tags.remove(root_ID) + if root_ID in collected_tags: + collected_tags.remove(root_ID) result = [] for tag_ID in collected_tags: @@ -857,6 +934,31 @@ class DBAdapter: result.append(tag_data[0][1].strip()) return result + def get_all_tag_impliers_by_ID(self, ID: int) -> list: + """ + Search for all tags that imply this one or imply a tag that implies this one. + :param ID: + :return: + """ + collected_tags = self.get_tag_impliers_by_ID(ID) + to_search = self.get_tag_impliers_by_ID(ID) + collected_tags.append(ID) + + while len(to_search) != 0: + curr_tag = to_search.pop() + found_impliers = self.get_tag_impliers_by_ID(curr_tag) + for found in found_impliers: + if found in collected_tags: + continue + else: + collected_tags.append(found) + to_search.append(found) + + if ID in collected_tags: + collected_tags.remove(ID) + + return collected_tags + def get_tag_implications_by_ID(self, ID: int) -> list: """ Search for the tag's implications @@ -908,20 +1010,29 @@ class DBAdapter: return r - def get_tag_impliers(self, name: str) -> list: + def get_tag_impliers_by_ID(self, ID: int): + """ + Search for tags that imply this one. + :param ID: + :return: + """ + d = {"ID": ID} + self.db_cursor.execute("SELECT root_tag FROM Tag_Implication WHERE implicate = %(ID)s", d) + + rows = self.db_cursor.fetchall() + return rows + + def get_tag_impliers_by_name(self, name: str) -> list: """ Search for tags that imply this one. :param name: :return: """ d = {"ID": self.get_tag_ID(name)} - self.db_cursor.execute("SELECT root_tag, implicate FROM Tag_Implication WHERE implicate = %(ID)s", d) + self.db_cursor.execute("SELECT root_tag FROM Tag_Implication WHERE implicate = %(ID)s", d) rows = self.db_cursor.fetchall() - r = [] - for row in rows: - r.append(row[0]) - return r + return rows def __get_tag_aliases_no_recurse(self, tag_id: int) -> list: """ @@ -946,3 +1057,140 @@ class DBAdapter: raise Exception("Something went terribly wrong!") return r + + def create_collection(self, collection_name: str, description: str) -> int: + """ + Create the collection with name and description as given. + """ + d = {"name": collection_name, "description": description} + + try: + self.db_cursor.execute("INSERT INTO art_collection (name, description) VALUES (%(name)s, %(description)s) " + "RETURNING id", d) + self.db.commit() + except psycopg2.DatabaseError as e: + logging.error(f"Encountered database error on create_collection() with collection_name: {collection_name} " + f"description: {description}. Rolling it back now ...") + logging.error(e) + self.db.rollback() + return None + + return self.db_cursor.fetchall()[0] + + def delete_collection(self, collection_id: int): + """ + Delete the collection given by the id. + """ + d = {"ID": collection_id} + + try: + self.db_cursor.execute("DELETE FROM art_collection WHERE ID = %(ID)s", d) + self.db.commit() + except psycopg2.DatabaseError as e: + logging.error(f"Encountered database error on delete_collection() ID: {collection_id}. " + f"Rolling it back now ...") + logging.error(e) + self.db.rollback() + + def get_collections_of_art_by_ID(self, art_id: int): + """ + Queries the database for collections that include the given art_id + + :return: [(ID, name, description)] + """ + d = {"art_id": art_id} + + self.db_cursor.execute("SELECT art_collection.id, art_collection.name, art_collection.description " + "FROM art_collection " + "INNER JOIN art_to_art_collection as atac ON art_collection.id = atac.collection_id " + "WHERE atac.art_id = %(art_id)s", d) + + return self.db_cursor.fetchall() + + def get_collection(self, collection_id: int): + """ + Returns the collection given by the id. + """ + d = {"ID": collection_id} + + self.db_cursor.execute("SELECT collection.id, collection.name, collection.description, array_agg(art.id), " + "array_agg(art.title), array_agg(art.path), array_agg(atac.ranking) " + "FROM art_collection as collection " + "LEFT OUTER JOIN art_to_art_collection as atac ON atac.collection_id = collection.id " + "LEFT OUTER JOIN art ON atac.art_id = art.id " + "WHERE collection.ID = %(ID)s " + "GROUP BY collection.id", d) + r = self.db_cursor.fetchall() + if len(r) > 0: + r = r[0] + collection = dict() + collection["id"] = r[0] + collection["name"] = r[1] + collection["description"] = r[2] + collection["entries"] = [] + for i in range(len(r[3])): + art_data = {"id": r[3][i], "title": r[4][i], "path": r[5][i], "ranking": r[6][i]} + if art_data["id"] is None: # skipping non-existing art entries + continue # might happen once when the collection has no entries + collection["entries"].append(art_data) + + return collection + + def search_collections_by_name(self, name: str) -> List[Tuple[int, str]]: + """ + Returns the collection by the given name + """ + d = {"name": name} + + try: + self.db_cursor.execute("SELECT art_collection.id, art_collection.name " + "FROM art_collection " + "WHERE art_collection.name = %(name)s", d) + except psycopg2.DatabaseError as e: + logging.error(f"Encountered database error on get_collection_by_name() name: {name}." + f"Rolling it back now ...") + logging.error(e) + self.db.rollback() + return None + + return self.db_cursor.fetchall() + + def search_collections(self, collection_name: str = None, presence_name: str = None, + presence_domain: str = None, art_name: str = None): + """ + Search for collections that contain the given parameters. + + Search is always fuzzy. + :param collection_name: + :param presence_name: + :param presence_domain: + :param art_name: Search for collections containing art with this title + :result: List[collection_id, collection_name, collection_desc, List[art_id]] + """ + d = {"collection_name": "%"+collection_name+"%", "presence_name": "%"+presence_name+"%", + "presence_domain": "%"+presence_domain+"%", "art_name": "%"+art_name+"%"} + + try: + self.db_cursor.execute("SELECT " + "(art_collection.id) as collection_id, " + "(art_collection.name) as collection_name, " + "(art_collection.description) as collection_desc, " + "array_agg(art.id) as art_ID, array_agg(art.title) as art_titles, " + "array_agg(art_to_presence.presence_name) as presence_names, " + "array_agg(art_to_presence.presence_domain) as presence_domains " + "FROM art_collection " + "LEFT OUTER JOIN art_to_art_collection On art_to_art_collection.collection_id = art_collection.id " + "LEFT OUTER JOIN art ON art_to_art_collection.art_id = art.id " + "LEFT OUTER JOIN art_to_presence ON art_to_presence.art_ID = art.id " + "WHERE LOWER(art_collection.name) LIKE LOWER(%(collection_name)s) " + "GROUP BY art_collection.id " + "HAVING LOWER(array_to_string(array_agg(art_to_presence.presence_name), '|', '')) LIKE LOWER(%(presence_name)s) " + "AND LOWER(array_to_string(array_agg(art_to_presence.presence_domain), '|', '')) LIKE LOWER(%(presence_domain)s) " + "AND LOWER(array_to_string(array_agg(art.title), '|', '')) LIKE LOWER(%(art_name)s);", + d) + except psycopg2.DatabaseError as e: + logging.error(f"Encountered database error on search_collections(): {e}") + self.db.rollback() + raise e + + return self.db_cursor.fetchall() diff --git a/ArtNet/file/file_reader.py b/ArtNet/file/file_reader.py index 61a1185..e49eec6 100644 --- a/ArtNet/file/file_reader.py +++ b/ArtNet/file/file_reader.py @@ -32,6 +32,8 @@ class FileReader: while len(dirs) != 0: curr_dir = dirs.pop() curr_full_dir = os.path.join(root, curr_dir) + if not os.path.isdir(curr_full_dir): + continue l += [os.path.join(curr_dir, f) for f in os.listdir(curr_full_dir) if os.path.isfile(os.path.join(curr_full_dir, f))] for d in [x for x in os.listdir(curr_full_dir) if os.path.isdir(os.path.join(curr_full_dir, x))]: dirs.append(os.path.join(curr_dir, d)) diff --git a/ArtNet/gui/browse_window.py b/ArtNet/gui/browse_window.py index dfdc39f..7b08841 100644 --- a/ArtNet/gui/browse_window.py +++ b/ArtNet/gui/browse_window.py @@ -41,7 +41,7 @@ class BrowseWindow(ArtnetMainWindow): self.ui.image_info_button.clicked.connect(self.on_image_info_clicked) def display_image(self, image_title: str, image_authors: list, full_path: str, relative_path: str, art_ID: int, - link: str, file_name: str, description: str): + link: str, file_name: str, description: str, collections: list): """ Display the described image in the GraphicsView """ diff --git a/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.py b/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.py index 3ed320f..baa3738 100644 --- a/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.py +++ b/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.py @@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Artist_Mod_Dialog(object): def setupUi(self, Artist_Mod_Dialog): Artist_Mod_Dialog.setObjectName("Artist_Mod_Dialog") - Artist_Mod_Dialog.resize(311, 205) + Artist_Mod_Dialog.resize(311, 253) self.verticalLayout = QtWidgets.QVBoxLayout(Artist_Mod_Dialog) self.verticalLayout.setObjectName("verticalLayout") self.dialog_frame = QtWidgets.QFrame(Artist_Mod_Dialog) @@ -49,6 +49,10 @@ class Ui_Artist_Mod_Dialog(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.id_label.sizePolicy().hasHeightForWidth()) self.id_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.id_label.setFont(font) self.id_label.setObjectName("id_label") self.verticalLayout_5.addWidget(self.id_label) self.id_layout = QtWidgets.QVBoxLayout() @@ -65,6 +69,10 @@ class Ui_Artist_Mod_Dialog(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.description_label.sizePolicy().hasHeightForWidth()) self.description_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.description_label.setFont(font) self.description_label.setObjectName("description_label") self.verticalLayout_5.addWidget(self.description_label) self.description_line = QtWidgets.QLineEdit(self.frame) @@ -87,8 +95,8 @@ class Ui_Artist_Mod_Dialog(object): _translate = QtCore.QCoreApplication.translate Artist_Mod_Dialog.setWindowTitle(_translate("Artist_Mod_Dialog", "Dialog")) self.dialog_topic.setText(_translate("Artist_Mod_Dialog", "Artist Topic")) - self.id_label.setText(_translate("Artist_Mod_Dialog", "Artist ID:")) - self.description_label.setText(_translate("Artist_Mod_Dialog", "Artist Description:")) + self.id_label.setText(_translate("Artist_Mod_Dialog", "Artist ID")) + self.description_label.setText(_translate("Artist_Mod_Dialog", "Artist Name")) if __name__ == "__main__": diff --git a/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.ui b/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.ui index 85cf331..4f77729 100644 --- a/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.ui +++ b/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.ui @@ -7,7 +7,7 @@ 0 0 311 - 205 + 253 @@ -63,8 +63,14 @@ 0 + + + 75 + true + + - Artist ID: + Artist ID @@ -90,8 +96,14 @@ 0 + + + 75 + true + + - Artist Description: + Artist Name diff --git a/ArtNet/gui/dialogs/image_select_dialog/image_sel_dialog.py b/ArtNet/gui/dialogs/image_select_dialog/image_sel_dialog.py new file mode 100644 index 0000000..05aefa1 --- /dev/null +++ b/ArtNet/gui/dialogs/image_select_dialog/image_sel_dialog.py @@ -0,0 +1,247 @@ +import logging +import os.path + +from PyQt5 import QtWidgets +from PyQt5.QtCore import Qt, QSize, QModelIndex +from PyQt5.QtGui import QStandardItem, QStandardItemModel, QIcon + +from ArtNet.gui.dialogs.image_select_dialog.image_select_dialog import Ui_Dialog +from ArtNet.db.db_adapter import DBAdapter +from ArtNet.singleton_manager import SingletonManager + + +class ImageSelectDialog(QtWidgets.QDialog): + + def __init__(self, db_connection, parent=None): + super().__init__(parent) + self.parent = parent + self.__config = SingletonManager.get_manager().config.data + self.__file_root = self.__config["file_root"] + self.db_connection: DBAdapter = db_connection + + self.data: dict = None # data to be returned by this dialog to the parent + + self.__tag_search_result = list() + self.__presence_search_result = list() + self.__art_list_dict = dict() + self.curr_selected_tags = list() + self.curr_selected_presences = list() + + self.setWindowTitle("Select Art") + + self.ui = Ui_Dialog() + self.ui.setupUi(self) + + self.ui.search_tag_line.textChanged.connect(self.on_tag_search_changed) + self.ui.search_presence_name_line.textChanged.connect(self.on_presence_search_changed) + self.ui.search_presence_domain_line.textChanged.connect(self.on_presence_search_changed) + + self.ui.art_selection_list.hide() + self.ui.art_selection_list.doubleClicked.connect(self.on_art_doubleclicked) + + self.on_presence_search_changed("") + self.on_tag_search_changed("") + + def set_tag_search_result_list(self, tags: list): + """ + Set the tag search result list given the tags from the search + """ + item_model = QStandardItemModel(self.ui.search_tag_list) + + self.__tag_search_result = tags + + for tag in tags: + item = QStandardItem(tag) + flags = Qt.ItemIsEnabled + + item.setData(Qt.Unchecked, Qt.CheckStateRole) + flags |= Qt.ItemIsUserCheckable + if self.curr_selected_tags is not None and tag in self.curr_selected_tags: + # already selected, implied or aliased tags + item.setCheckState(Qt.Checked) + item.setFlags(flags) + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_tag_search_item_changed) + self.ui.search_tag_list.setModel(item_model) + + def refresh_selected_tags_list(self): + """ + Refreshes the selected tags list according to self.curr_selected_tags + """ + item_model = QStandardItemModel(self.ui.selected_tag_list) + + for tag in self.curr_selected_tags: + item = QStandardItem(tag) + flags = Qt.ItemIsEnabled + + item.setData(Qt.Unchecked, Qt.CheckStateRole) + flags |= Qt.ItemIsUserCheckable + item.setCheckState(Qt.Checked) + item.setFlags(flags) + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_tag_selected_item_changed) + self.ui.selected_tag_list.setModel(item_model) + + self.refresh_image_selection() + + def refresh_selected_presences_list(self): + """ + Refreshes the selected presences list according to self.curr_selected_presences + """ + item_model = QStandardItemModel(self.ui.selected_presence_list) + + for name, domain in self.curr_selected_presences: + item = QStandardItem(f"{name}:{domain}") + flags = Qt.ItemIsEnabled + + item.setData(Qt.Unchecked, Qt.CheckStateRole) + flags |= Qt.ItemIsUserCheckable + item.setCheckState(Qt.Checked) + item.setFlags(flags) + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_presence_selected_item_changed) + self.ui.selected_presence_list.setModel(item_model) + + self.refresh_image_selection() + + def refresh_image_selection(self): + """ + Refreshes the images available for selection by using self.curr_selected_presences and self.curr_selected_tags + """ + logging.debug(f"Refreshing info selection with tags {self.curr_selected_tags} " + f"and presences {self.curr_selected_presences}") + arts = self.db_connection.search_art(tags=self.curr_selected_tags, resolve_tags=True, + presences=self.curr_selected_presences) + #print("Result:", arts) + + self.ui.art_placeholder_label.hide() + self.ui.art_selection_list.show() + + self.ui.art_selection_list.setViewMode(QtWidgets.QListView.IconMode) + + self.__art_list_dict = dict() + item_model = QStandardItemModel(self.ui.art_selection_list) + items = list() + if len(arts) > 100: + arts = arts[:100] + for art in arts: + art_data = self.db_connection.get_art_by_ID(art) + full_path = os.path.join(self.__file_root, art_data["path"]) + if not os.path.isfile(full_path): + continue + + icon = QIcon() + icon.addFile(full_path) + flags = Qt.ItemIsEnabled + + item = QStandardItem( + art_data["title"] if art_data["title"] is not None else os.path.basename(art_data["path"])) + item.setData(icon, role=Qt.DecorationRole) + item.setData(art_data["ID"], role=Qt.UserRole) # allows the item to be identified again + + presences = self.db_connection.get_authors_of_art_by_ID(art_data["ID"]) + + p = "" + for presence in presences: + p += f"{presence[0]}:{presence[1]}|" + p = p[:-1] + + item.setData(f"{art_data['title']} \nby \n{p} \n\nDescription:\n{art_data['description']}", role=Qt.ToolTipRole) # tooltip for the item + + item.setFlags(flags) + items.append(item) + self.__art_list_dict[art_data["ID"]] = art_data + + #print("Adding to last row", items) + item_model.appendColumn(items) + + self.ui.art_selection_list.setIconSize(QSize(150, 150)) + self.ui.art_selection_list.setModel(item_model) + + def set_presence_search_result_list(self, presences: list): + """ + Set the presence search result list as given by presences. + """ + item_model = QStandardItemModel(self.ui.search_presence_list) + + self.__presence_search_result = presences + for name, domain in presences: + item = QStandardItem(f"{name}:{domain}") + flags = Qt.ItemIsEnabled + + item.setData(Qt.Unchecked, Qt.CheckStateRole) + flags |= Qt.ItemIsUserCheckable + + item.setFlags(flags) + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_presence_search_item_changed) + self.ui.search_presence_list.setModel(item_model) + + def on_art_doubleclicked(self, index: QModelIndex): + art_id = index.model().itemData(index)[Qt.UserRole] + print(f"Double-clicked: {self.__art_list_dict[art_id]}") + self.data = self.__art_list_dict[art_id] + self.done(QtWidgets.QDialog.Accepted) + + def on_tag_search_changed(self, v: str): + result = self.db_connection.search_fuzzy_tag(v, all_if_empty=True) + + self.set_tag_search_result_list([r[0] for r in result]) + + def on_presence_search_changed(self, v: str): + presence_name = self.ui.search_presence_name_line.text() + presence_domain = self.ui.search_presence_domain_line.text() + + result = self.db_connection.search_fuzzy_presence(name=presence_name, domain=presence_domain, all_if_empty=True) + + presences = list() + for name, domain in result: + presences.append((name, domain)) + self.set_presence_search_result_list(presences) + + def on_tag_search_item_changed(self, item: QStandardItem): + logging.info(f"Tag search item {item.text()} has changed!") + if item.checkState() == Qt.Checked: + self.curr_selected_tags.append(item.text()) + if item.checkState() == Qt.Unchecked: + self.curr_selected_tags.remove(item.text()) + + self.refresh_selected_tags_list() + + def on_tag_selected_item_changed(self, item: QStandardItem): + logging.info(f"Tag select item {item.text()} has changed!") + self.curr_selected_tags.remove(item.text()) + + self.refresh_selected_tags_list() + self.set_tag_search_result_list(self.__tag_search_result) + + def on_presence_search_item_changed(self, item: QStandardItem): + logging.info(f"Presence search item {item.text()} has changed!") + name, domain = item.text().split(":") + if item.checkState() == Qt.Checked: + self.curr_selected_presences.append((name, domain)) + elif item.checkState() == Qt.Unchecked: + self.curr_selected_presences.remove((name, domain)) + + self.refresh_selected_presences_list() + + def on_presence_selected_item_changed(self, item: QStandardItem): + logging.info(f"Presence select item {item.text()} has changed!") + name, domain = item.text().split(":") + self.curr_selected_presences.remove((name, domain)) + + self.set_presence_search_result_list(self.__presence_search_result) + self.refresh_selected_presences_list() + + def exec_(self) -> dict: + if super(ImageSelectDialog, self).exec_() == QtWidgets.QDialog.Rejected: + return None + + if self.data is None: + self.done(QtWidgets.QDialog.Rejected) + + return self.data diff --git a/ArtNet/gui/importer_window.py b/ArtNet/gui/importer_window.py index 59176cb..610f8fc 100644 --- a/ArtNet/gui/importer_window.py +++ b/ArtNet/gui/importer_window.py @@ -20,6 +20,8 @@ from ArtNet.gui.dockers.presence.presence_dock import PresenceDocker from ArtNet.gui.dialogs.category_modify_dialog.category_mod_dialog import CategoryModDialog from ArtNet.gui.dialogs.tag_import_dialog.tag_imp_dialog import TagImportDialog from ArtNet.gui.dialogs.link_input_dialog.link_dialog import LinkInputDialog +from ArtNet.gui.dialogs.collection_select_dialog.collection_sel_dialog import CollectionSelectDialog +from ArtNet.gui.dialogs.collection_modify_dialog.collection_dialog import CollectionModDialog from ArtNet.web.link_generator import LinkGenerator @@ -28,6 +30,7 @@ class ImporterWindow(ArtnetMainWindow): UNKNOWN_TITLE = "-Title Unknown-" UNKNOWN_PRESENCE = "(Not in Database)" UNKNOWN_LINK = "No Source Available" + NO_COLLECTION = "No Collections" def __init__(self, main): super().__init__(main) @@ -52,6 +55,7 @@ class ImporterWindow(ArtnetMainWindow): self.curr_tags: list = list() self.curr_implied_tags: list = list() self.curr_tag_aliases: list = list() + self.curr_collections: list = list() self.__data_changed: bool = False @@ -154,6 +158,7 @@ class ImporterWindow(ArtnetMainWindow): "link": self.curr_link, "md5_hash": self.main.get_md5_of_image(self.curr_art_path), "description": new_description, + "collections": self.curr_collections, } for presence in self.curr_presences: @@ -166,10 +171,19 @@ class ImporterWindow(ArtnetMainWindow): msg.exec_() return False + for coll_id, coll_name in self.curr_collections: + msg = QtWidgets.QMessageBox() + msg.setWindowTitle("Could not save collection!") + msg.setInformativeText("The saving and proper selection of collections is not supported as of now!") + msg.setIcon(QtWidgets.QMessageBox.Warning) + msg.exec_() + break + self.main.db_connection.save_image(ID=image_data["ID"], title=image_data["title"], authors=image_data["authors"], path=image_data["path"], tags=image_data["tags"], link=image_data["link"], - md5_hash=image_data["md5_hash"], desc=image_data["description"]) + md5_hash=image_data["md5_hash"], desc=image_data["description"], + collections=image_data["collections"]) self.set_temporary_status_message("Saved {0} ({1}) to ArtNet DB!" .format(image_data["title"], image_data["ID"]), 5000) @@ -318,6 +332,30 @@ class ImporterWindow(ArtnetMainWindow): self.data_changed = True + def get_current_collections(self) -> list: + """ + Get the collections currently associated with the current art + """ + return self.curr_collections + + def set_current_collections(self, collections: list): + """ + Set the collections associated with the current art + :param collections: List[Tuple[coll_id, coll_name]] + """ + self.curr_collections = collections + if len(collections) == 0: + self.ui.collections_label.setText(ImporterWindow.NO_COLLECTION) + else: + s = "" + for coll_id, coll_name, coll_desc in collections: + s += "{1}".format("", f"#{coll_id}:{coll_name}") + # currently not linking to anything + s += "|" + s = s[:-1] + self.ui.collections_label.setText(s) + self.data_changed = True + def get_categories(self, search: str, all_if_empty: bool = False): """ Fuzzy Query for categories in the database. @@ -332,6 +370,35 @@ class ImporterWindow(ArtnetMainWindow): return self.main.db_connection.search_fuzzy_categories(search) + def create_collection(self, collection_name: str, description: str = None) -> int: + """ + Create a new collection with the given name and description + + Returns the assigned ID of the collection + """ + return self.main.db_connection.create_collection(collection_name, description) + + def get_collection(self, collection_id: int): + """ + Fetch the collection given by the id. If not present None is returned. + """ + return self.main.db_connection.get_collection(collection_id) + + def delete_collection(self, collection_id: int): + """ + Delete the collection given by the id from the database. + """ + self.main.db_connection.delete_collection(collection_id) + + def search_collections(self, collection_name: str = None, presence_name: str = None, + presence_domain: str = None, art_name: str = None): + """ + Search for collections that fit the given parameters. All parameters are searched fuzzy. + """ + return self.main.db_connection.search_collections(collection_name=collection_name, + presence_name=presence_name, presence_domain=presence_domain, + art_name=art_name) + def get_image_link_from_label(self) -> str: """ Gets the image link from the label if it is a valid link. @@ -432,17 +499,18 @@ class ImporterWindow(ArtnetMainWindow): :param tags: :return: """ + tags.sort() item_model = QStandardItemModel(self.ui.search_result_list) for tag in tags: item = QStandardItem(tag) flags = Qt.ItemIsEnabled - if tag not in self.curr_imply_tags + self.curr_tag_aliases and tag not in self.curr_tags: + if tag not in self.curr_implied_tags: # new tag and not implied yet item.setData(Qt.Unchecked, Qt.CheckStateRole) flags |= Qt.ItemIsUserCheckable - if self.curr_tags is not None and tag in (self.curr_tags + self.curr_imply_tags + self.curr_tag_aliases): + if self.curr_tags is not None and tag in (self.curr_tags + self.curr_implied_tags + self.curr_tag_aliases): # already selected, implied or aliased tags item.setCheckState(Qt.Checked) item.setFlags(flags) @@ -463,21 +531,37 @@ class ImporterWindow(ArtnetMainWindow): Also updates the tag implication list if no_implication is False :param tags: :param set_checked: - :param no_implication: bool indicating if the implication list should also be updated + :param no_implication: not used :return: """ + tags.sort() self.curr_tags = tags item_model = QStandardItemModel(self.ui.tag_list) + implied_tags = [] + for x in self.curr_tags: + # collect all implied tags into a list + implied_tags += self.main.db_connection.get_all_tag_implications_by_name(x) + + self.set_implied_list(implied_tags) + for tag in tags: item = QStandardItem(tag) - item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - item.setData(Qt.Unchecked, Qt.CheckStateRole) - item_model.appendRow(item) + flags = Qt.ItemIsEnabled + if tag not in self.curr_implied_tags: + # new tag and not implied yet + item.setData(Qt.Unchecked, Qt.CheckStateRole) + flags |= Qt.ItemIsUserCheckable if set_checked: - item.setCheckState(Qt.Checked) + if tag not in self.curr_implied_tags: + item.setCheckState(Qt.Checked) + else: + item.setCheckState(Qt.Unchecked) + + item.setFlags(flags) + item_model.appendRow(item) item_model.itemChanged.connect(self.on_tag_item_changed) self.ui.tag_list.setModel(item_model) @@ -490,7 +574,7 @@ class ImporterWindow(ArtnetMainWindow): :param tags: :return: """ - self.curr_imply_tags = tags + self.curr_implied_tags = tags item_model = QStandardItemModel(self.ui.implied_tag_list) done = [] for tag in tags: @@ -506,7 +590,7 @@ class ImporterWindow(ArtnetMainWindow): self.data_changed = True def display_image(self, image_title: str, image_authors: list, full_path: str, relative_path: str, art_ID: int, - link: str, file_name: str, description: str): + link: str, file_name: str, description: str, collections: list): """ Display an image in the central widget :param image_authors: @@ -517,6 +601,7 @@ class ImporterWindow(ArtnetMainWindow): :param link: :param file_name: :param description: + :param collections: :return: """ self.curr_art_id = art_ID @@ -524,6 +609,8 @@ class ImporterWindow(ArtnetMainWindow): self.curr_image_title = image_title self.curr_file_name = os.path.basename(full_path) self.curr_link = link + + self.set_current_collections(collections) self.set_current_presences(image_authors) file_ending = relative_path.split(".")[-1] @@ -856,6 +943,11 @@ class ImporterWindow(ArtnetMainWindow): category=tag_data["category"]) self.on_tag_search_change() + implied_tags = [] # refresh implied tags, the edit/addition might have changed smth + for x in self.curr_tags: + # collect all implied tags into a list + implied_tags += self.main.db_connection.get_all_tag_implications_by_name(x) + def on_tag_deletion_clicked(self): logging.info("Clicked Tag Deletion!") dialog = TagSelectDialog(self, delete_tag=True) @@ -946,17 +1038,38 @@ class ImporterWindow(ArtnetMainWindow): def on_tag_search_item_changed(self, item: QStandardItem): if item.checkState() == Qt.Checked: self.curr_tags.append(item.text()) + + aliases = self.main.db_connection.get_tag_aliases_by_name(item.text()) + for alias in aliases: + self.curr_tags.append(alias) + implications = self.main.db_connection.get_all_tag_implications_by_name(item.text()) + for implication in implications: + self.curr_tags.append(implication) if item.checkState() == Qt.Unchecked: if item.text() in self.curr_tags: self.curr_tags.remove(item.text()) + aliases = self.main.db_connection.get_tag_aliases_by_name(item.text()) + for alias in aliases: + if alias in self.curr_tags: + self.curr_tags.remove(alias) + implications = self.main.db_connection.get_all_tag_implications_by_name(item.text()) + for implication in implications: + self.curr_tags.remove(implication) else: - return + raise Exception("Something went terribly wrong!") + self.set_tag_list(self.curr_tags) + self.on_tag_search_change() def on_tag_item_changed(self, item: QStandardItem): logging.debug("Item {0} has changed!".format(item.text())) if item.checkState() == Qt.Unchecked: if item.text() in self.curr_tags: + aliases = self.main.db_connection.get_tag_aliases_by_name(item.text()) + if len(aliases) > 0: # tag has aliases, might need to also remove them + for alias in aliases: + self.curr_tags.remove(alias) + self.curr_tags.remove(item.text()) self.set_tag_list(self.curr_tags) self.on_tag_search_change() @@ -967,7 +1080,7 @@ class ImporterWindow(ArtnetMainWindow): tags = self.main.db_connection.search_fuzzy_tag(self.ui.tag_search_bar.text(), all_if_empty=True) result = [] - for tag_name, tag_desc, tag_category in tags: + for tag_name, tag_desc, tag_category, tag_id in tags: result.append(tag_name) self.set_tag_search_result_list(result) diff --git a/ArtNet/gui/windows/artnet_mainwindow.py b/ArtNet/gui/windows/artnet_mainwindow.py index e6deb05..5ae69fc 100644 --- a/ArtNet/gui/windows/artnet_mainwindow.py +++ b/ArtNet/gui/windows/artnet_mainwindow.py @@ -29,7 +29,7 @@ class ArtnetMainWindow(QtWidgets.QMainWindow): self.setting_up_data = False def display_image(self, image_title: str, image_authors: list, full_path: str, relative_path: str, art_ID: int, - link: str, file_name: str, description: str): + link: str, file_name: str, description: str, collections: list): """ Display an image in the central widget :param image_authors: diff --git a/ArtNet/singleton_manager.py b/ArtNet/singleton_manager.py new file mode 100644 index 0000000..8f2b24b --- /dev/null +++ b/ArtNet/singleton_manager.py @@ -0,0 +1,13 @@ + +class SingletonManager: + + __instance = None + + @staticmethod + def set_instance(instance): + SingletonManager.__instance = instance + + @staticmethod + def get_manager(): + if SingletonManager.__instance is not None: + return SingletonManager.__instance diff --git a/DB b/DB index 7a0b0df..4d9059b 160000 --- a/DB +++ b/DB @@ -1 +1 @@ -Subproject commit 7a0b0dfc88e46f1ec31842a34c18d408f23f041d +Subproject commit 4d9059b55790d9e758c129fbbf3c706266e999a4 diff --git a/__main__.py b/__main__.py index b5e47ea..3cf14d6 100644 --- a/__main__.py +++ b/__main__.py @@ -1,13 +1,9 @@ from ArtNet.artnet_manager import ArtNetManager -# TODO fix DB bug -# TODO 1. Open known image -# TODO 2. edit a tag on the current image & save the edit -# TODO 3. attempt to save the current image +# TODO features: +# TODO 2. Add creation/editing/deletion of Topics (Needs Dialogs) +# TODO 3. add changing/adding Topics to Artists (See dockers/topic/artist_topic_docker.py) -# TODO fix bugs: -# TODO 1. import tags on #237 failed to catch all tags -# TODO 2. pressing prev. Unknown from ~#238 returns wrong unknowns! (always #136, #132, #127, then correct) if __name__ == "__main__": am = ArtNetManager()