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()