diff --git a/gramps/gen/db/base.py b/gramps/gen/db/base.py index ac1fa7606..006994a24 100644 --- a/gramps/gen/db/base.py +++ b/gramps/gen/db/base.py @@ -1448,6 +1448,7 @@ class DbReadBase(object): Place = property(lambda self: QuerySet(self, "Place")) Event = property(lambda self: QuerySet(self, "Event")) Tag = property(lambda self: QuerySet(self, "Tag")) + Media = property(lambda self: QuerySet(self, "Media")) class DbWriteBase(DbReadBase): """ diff --git a/gramps/gen/lib/citation.py b/gramps/gen/lib/citation.py index 08a7e8c1c..75a587a65 100644 --- a/gramps/gen/lib/citation.py +++ b/gramps/gen/lib/citation.py @@ -79,6 +79,46 @@ class Citation(MediaBase, NoteBase, SrcAttributeBase, IndirectCitationBase, self.confidence = Citation.CONF_NORMAL # 4 SrcAttributeBase.__init__(self) # 8 + @classmethod + def get_schema(cls): + """ + Return the schema as a dictionary for this class. + """ + from .srcattribute import SrcAttribute + from .date import Date + return { + "handle": Handle("Citation", "CITATION-HANDLE"), + "gramps_id": str, + "date": Date, + "page": str, + "confidence": str, + "source_handle": Handle("Source", "SOURCE-HANDLE"), + "note_list": [Handle("Note", "NOTE-HANDLE")], + "media_list": [Handle("Media", "MEDIA-HANDLE")], + "srcattr_list": [SrcAttribute], + "change": int, + "tag_list": [Handle("Tag", "TAG-HANDLE")], + "private": bool, + } + + @classmethod + def get_labels(cls, _): + return { + "_class": _("Citation"), + "handle": _("Handle"), + "gramps_id": _("Gramps ID"), + "date": _("Date"), + "page": _("Page"), + "confidence": _("Confidence"), + "source_handle": _("Source"), + "note_list": _("Notes"), + "media_list": _("Media"), + "srcattribute_list": _("Source Attributes"), + "change": _("Last changed"), + "tag_list": _("Tags"), + "private": _("Private"), + } + def serialize(self, no_text_date=False): """ Convert the object to a serialized tuple of data. diff --git a/gramps/gen/lib/event.py b/gramps/gen/lib/event.py index e526e08c1..5047d0f53 100644 --- a/gramps/gen/lib/event.py +++ b/gramps/gen/lib/event.py @@ -175,7 +175,7 @@ class Event(CitationBase, NoteBase, MediaBase, AttributeBase, "note_list": [Note], "media_list": [Media], "attribute_list": [Attribute], - "change": float, + "change": int, "tag_list": [Tag], "private": bool, } diff --git a/gramps/gen/lib/media.py b/gramps/gen/lib/media.py index 1243840cb..a439f8d48 100644 --- a/gramps/gen/lib/media.py +++ b/gramps/gen/lib/media.py @@ -162,10 +162,7 @@ class Media(CitationBase, NoteBase, DateBase, AttributeBase, :rtype: dict """ from .attribute import Attribute - from .citation import Citation - from .note import Note from .date import Date - from .tag import Tag return { "handle": Handle("Media", "MEDIA-HANDLE"), "gramps_id": str, @@ -174,11 +171,11 @@ class Media(CitationBase, NoteBase, DateBase, AttributeBase, "desc": str, "checksum": str, "attribute_list": [Attribute], - "citation_list": [Citation], - "note_list": [Note], - "change": float, + "citation_list": [Handle("Citation", "CITATION-HANDLE")], + "note_list": [Handle("Note", "NOTE-HANDLE")], + "change": int, "date": Date, - "tag_list": Tag, + "tag_list": [Handle("Tag", "TAG-HANDLE")], "private": bool, } diff --git a/gramps/gen/lib/place.py b/gramps/gen/lib/place.py index 1f148e605..a178358eb 100644 --- a/gramps/gen/lib/place.py +++ b/gramps/gen/lib/place.py @@ -117,6 +117,55 @@ class Place(CitationBase, NoteBase, MediaBase, UrlBase, PrimaryObject): NoteBase.serialize(self), self.change, TagBase.serialize(self), self.private) + @classmethod + def get_labels(cls, _): + return { + "handle": _("Handle"), + "gramps_id": _("Gramps ID"), + "title": _("Title"), + "long": _("Longitude"), + "lat": _("Latitude"), + "placeref_list": _("Places"), + "name": _("Name"), + "alt_names": _("Alternate Names"), + "place_type": _("Type"), + "code": _("Code"), + "alt_loc": _("Alternate Locations"), + "urls": _("URLs"), + "media_list": _("Media"), + "citation_list": _("Citations"), + "note_list": _("Notes"), + "change": _("Last changed"), + "tag_list": _("Tags"), + "private": _("Private") + } + @classmethod + def get_schema(cls): + """ + Return the schema as a dictionary for this class. + """ + from .url import Url + return { + "handle": Handle("Place", "PLACE-HANDLE"), + "gramps_id": str, + "title": str, + "long": str, + "lat": str, + "placeref_list": [PlaceRef], + "name": PlaceName, + "alt_names": [str], + "place_type": PlaceType, + "code": str, + "alt_loc": [Location], + "urls": [Url], + "media_list": [Handle("Media", "MEDIA-HANDLE")], + "citation_list": [Handle("Citation", "CITATION-HANDLE")], + "note_list": [Handle("Note", "NOTE-HANDLE")], + "change": int, + "tag_list": [Handle("Tag", "TAG-HANDLE")], + "private": bool + } + def to_struct(self): """ Convert the data held in this object to a structure (eg, diff --git a/gramps/gen/lib/primaryobj.py b/gramps/gen/lib/primaryobj.py index b89a6ddee..162fcc5fa 100644 --- a/gramps/gen/lib/primaryobj.py +++ b/gramps/gen/lib/primaryobj.py @@ -120,245 +120,6 @@ class BasicPrimaryObject(TableObject, PrivacyBase, TagBase): """ raise NotImplementedError - @classmethod - def get_labels(cls, _): - """ - Return labels. - """ - return {} - - @classmethod - def field_aliases(cls): - """ - Return dictionary of alias to full field names - for this object class. - """ - return {} - - @classmethod - def get_field_alias(cls, alias): - """ - Return full field name for an alias, if one. - """ - return cls.field_aliases().get(alias, alias) - - @classmethod - def get_schema(cls): - """ - Return schema. - """ - return {} - - @classmethod - def get_extra_secondary_fields(cls): - """ - Return a list of full field names and types for secondary - fields that are not directly listed in the schema. - """ - return [] - - @classmethod - def get_index_fields(cls): - """ - Return a list of full field names for indices. - """ - return [] - - @classmethod - def get_secondary_fields(cls): - """ - Return all seconday fields and their types - """ - from .handle import HandleClass - return ([(key.lower(), value) - for (key, value) in cls.get_schema().items() - if value in [str, int, float, bool] or - isinstance(value, HandleClass)] + - cls.get_extra_secondary_fields()) - - @classmethod - def get_label(cls, field, _): - """ - Get the associated label given a field name of this object. - No index positions allowed on lists. - """ - chain = field.split(".") - path = cls._follow_schema_path(chain[:-1]) - labels = path.get_labels(_) - if chain[-1] in labels: - return labels[chain[-1]] - else: - raise Exception("%s has no such label on %s: '%s'" % - (cls, path, field)) - - @classmethod - def get_field_type(cls, field): - """ - Get the associated label given a field name of this object. - No index positions allowed on lists. - """ - field = cls.get_field_alias(field) - chain = field.split(".") - ftype = cls._follow_schema_path(chain) - return ftype - - @classmethod - def _follow_schema_path(cls, chain): - """ - Follow a list of schema items. Return endpoint. - """ - path = cls - for part in chain: - schema = path.get_schema() - if part.isdigit(): - pass # skip over - elif part in schema.keys(): - path = schema[part] - else: - raise Exception("No such field. Valid fields are: %s" % list(schema.keys())) - if isinstance(path, (list, tuple)): - path = path[0] - return path - - def get_field(self, field, db=None, ignore_errors=False): - """ - Get the value of a field. - """ - field = self.__class__.get_field_alias(field) - chain = field.split(".") - path = self._follow_field_path(chain, db, ignore_errors) - return path - - def _follow_field_path(self, chain, db=None, ignore_errors=False): - """ - Follow a list of items. Return endpoint(s) only. - With the db argument, can do joins across tables. - self - current object - returns - None, endpoint, of recursive list of endpoints - """ - from .handle import HandleClass - # start with [self, self, chain, path_to=[]] - # results = [] - # expand when you reach multiple answers [obj, chain_left, []] - # if you get to an endpoint, put results - # go until nothing left to expand - todo = [(self, self, [], chain)] - results = [] - while todo: - parent, current, path_to, chain = todo.pop() - #print("expand:", parent.__class__.__name__, - # current.__class__.__name__, - # path_to, - # chain) - keep_going = True - p = 0 - while p < len(chain) and keep_going: - #print("while:", path_to, chain[p:]) - part = chain[p] - if hasattr(current, part): # attribute - current = getattr(current, part) - path_to.append(part) - # need to consider current+part if current is list: - elif isinstance(current, (list, tuple)): - if part.isdigit(): - # followed by index, so continue here - if int(part) < len(current): - current = current[int(part)] - path_to.append(part) - elif ignore_errors: - current = None - keeping_going = False - else: - raise Exception("invalid index position") - else: # else branch! in middle, split paths - for i in range(len(current)): - #print("split list:", self.__class__.__name__, - # current.__class__.__name__, - # path_to[:], - # [str(i)] + chain[p:]) - todo.append([self, current, path_to[:], [str(i)] + chain[p:]]) - current = None - keep_going = False - else: # part not found on this self - # current is a handle - # part is something on joined object - if parent: - ptype = parent.__class__.get_field_type(".".join(path_to)) - if isinstance(ptype, HandleClass): - if db: - # start over here: - obj = None - if current: - obj = ptype.join(db, current) - if part == "self": - current = obj - path_to = [] - #print("split self:", obj.__class__.__name__, - # current.__class__.__name__, - # path_to, - # chain[p + 1:]) - todo.append([obj, current, path_to, chain[p + 1:]]) - elif obj: - current = getattr(obj, part) - #print("split :", obj.__class__.__name__, - # current.__class__.__name__, - # [part], - # chain[p + 1:]) - todo.append([obj, current, [part], chain[p + 1:]]) - current = None - keep_going = False - else: - raise Exception("Can't join without database") - elif part == "self": - pass - elif ignore_errors: - pass - else: - raise Exception("%s is not a valid field of %s; use %s" % - (part, current, dir(current))) - current = None - keep_going = False - p += 1 - if keep_going: - results.append(current) - if len(results) == 1: - return results[0] - elif len(results) == 0: - return None - else: - return results - - def set_field(self, field, value, db=None, ignore_errors=False): - """ - Set the value of a basic field (str, int, float, or bool). - value can be a string or actual value. - Returns number of items changed. - """ - field = self.__class__.get_field_alias(field) - chain = field.split(".") - path = self._follow_field_path(chain[:-1], db, ignore_errors) - ftype = self.get_field_type(field) - # ftype is str, bool, float, or int - value = (value in ['True', True]) if ftype is bool else value - return self._set_fields(path, chain[-1], value, ftype) - - def _set_fields(self, path, attr, value, ftype): - """ - Helper function to handle recursive lists of items. - """ - from .handle import HandleClass - if isinstance(path, (list, tuple)): - count = 0 - for item in path: - count += self._set_fields(item, attr, value, ftype) - elif isinstance(ftype, HandleClass): - setattr(path, attr, value) - count = 1 - else: - setattr(path, attr, ftype(value)) - count = 1 - return count - def set_gramps_id(self, gramps_id): """ Set the Gramps ID for the primary object. diff --git a/gramps/gen/lib/repo.py b/gramps/gen/lib/repo.py index ece338a31..e0a3edcd2 100644 --- a/gramps/gen/lib/repo.py +++ b/gramps/gen/lib/repo.py @@ -69,6 +69,41 @@ class Repository(NoteBase, AddressBase, UrlBase, IndirectCitationBase, UrlBase.serialize(self), self.change, TagBase.serialize(self), self.private) + @classmethod + def get_labels(cls, _): + return { + "handle": _("Handle"), + "gramps_id": _("Gramps ID"), + "type": _("Type"), + "name": _("Name"), + "note_list": _("Notes"), + "address_list": _("Addresses"), + "urls": _("URLs"), + "change": _("Last changed"), + "tag_list": _("Tags"), + "private": _("Private") + } + + @classmethod + def get_schema(cls): + """ + Return the schema as a dictionary for this class. + """ + from .address import Address + from .url import Url + return { + "handle": Handle("Repository", "REPOSITORY-HANDLE"), + "gramps_id": str, + "type": RepositoryType, + "name": str, + "note_list": [Handle("Note", "NOTE-HANDLE")], + "address_list": [Address], + "urls": [Url], + "change": int, + "tag_list": [Handle("Tag", "TAG-HANDLE")], + "private": bool + } + def to_struct(self): """ Convert the data held in this object to a structure (eg, diff --git a/gramps/gen/lib/src.py b/gramps/gen/lib/src.py index 7002970ee..b6dc9417b 100644 --- a/gramps/gen/lib/src.py +++ b/gramps/gen/lib/src.py @@ -79,6 +79,48 @@ class Source(MediaBase, NoteBase, SrcAttributeBase, IndirectCitationBase, TagBase.serialize(self), # 11 self.private) # 12 + @classmethod + def get_labels(cls, _): + return { + "handle": _("Handle"), + "gramps_id": _("Gramps ID"), + "title": _("Title"), + "author": _("Author"), + "pubinfo": _("Publication info"), + "note_list": _("Notes"), + "media_list": _("Media"), + "abbrev": _("Abbreviation"), + "change": _("Last changed"), + "srcattr_list": _("Source Attributes"), + "reporef_list": _("Repositories"), + "tag_list": _("Tags"), + "private": _("Private") + } + + @classmethod + def get_schema(cls): + """ + Return the schema as a dictionary for this class. + """ + from .srcattribute import SrcAttribute + from .reporef import RepoRef + from .url import Url + return { + "handle": Handle("Source", "SOURCE-HANDLE"), + "gramps_id": str, + "title": str, + "author": str, + "pubinfo": str, + "note_list": [Handle("Note", "NOTE-HANDLE")], + "media_list": [Handle("Media", "MEDIA-HANDLE")], + "abbrev": str, + "change": int, + "srcattr_list": [SrcAttribute], + "reporef_list": [RepoRef], + "tag_list": [Handle("Tag", "")], + "private": bool + } + def to_struct(self): """ Convert the data held in this object to a structure (eg, diff --git a/gramps/gen/lib/tableobj.py b/gramps/gen/lib/tableobj.py index 90411b054..d203d9151 100644 --- a/gramps/gen/lib/tableobj.py +++ b/gramps/gen/lib/tableobj.py @@ -182,3 +182,243 @@ class TableObject(BaseObject): :rtype: str """ return self.handle + + @classmethod + def get_labels(cls, _): + """ + Return labels. + """ + return {} + + @classmethod + def field_aliases(cls): + """ + Return dictionary of alias to full field names + for this object class. + """ + return {} + + @classmethod + def get_field_alias(cls, alias): + """ + Return full field name for an alias, if one. + """ + return cls.field_aliases().get(alias, alias) + + @classmethod + def get_schema(cls): + """ + Return schema. + """ + return {} + + @classmethod + def get_extra_secondary_fields(cls): + """ + Return a list of full field names and types for secondary + fields that are not directly listed in the schema. + """ + return [] + + @classmethod + def get_index_fields(cls): + """ + Return a list of full field names for indices. + """ + return [] + + @classmethod + def get_secondary_fields(cls): + """ + Return all seconday fields and their types + """ + from .handle import HandleClass + return ([(key.lower(), value) + for (key, value) in cls.get_schema().items() + if value in [str, int, float, bool] or + isinstance(value, HandleClass)] + + cls.get_extra_secondary_fields()) + + @classmethod + def get_label(cls, field, _): + """ + Get the associated label given a field name of this object. + No index positions allowed on lists. + """ + chain = field.split(".") + path = cls._follow_schema_path(chain[:-1]) + labels = path.get_labels(_) + if chain[-1] in labels: + return labels[chain[-1]] + else: + raise Exception("%s has no such label on %s: '%s'" % + (cls, path, field)) + + @classmethod + def get_field_type(cls, field): + """ + Get the associated label given a field name of this object. + No index positions allowed on lists. + """ + field = cls.get_field_alias(field) + chain = field.split(".") + ftype = cls._follow_schema_path(chain) + return ftype + + @classmethod + def _follow_schema_path(cls, chain): + """ + Follow a list of schema items. Return endpoint. + """ + path = cls + for part in chain: + schema = path.get_schema() + if part.isdigit(): + pass # skip over + elif part in schema.keys(): + path = schema[part] + else: + raise Exception("No such field. Valid fields are: %s" % list(schema.keys())) + if isinstance(path, (list, tuple)): + path = path[0] + return path + + def get_field(self, field, db=None, ignore_errors=False): + """ + Get the value of a field. + """ + field = self.__class__.get_field_alias(field) + chain = field.split(".") + path = self._follow_field_path(chain, db, ignore_errors) + return path + + def _follow_field_path(self, chain, db=None, ignore_errors=False): + """ + Follow a list of items. Return endpoint(s) only. + With the db argument, can do joins across tables. + self - current object + returns - None, endpoint, of recursive list of endpoints + """ + from .handle import HandleClass + # start with [self, self, chain, path_to=[]] + # results = [] + # expand when you reach multiple answers [obj, chain_left, []] + # if you get to an endpoint, put results + # go until nothing left to expand + todo = [(self, self, [], chain)] + results = [] + while todo: + parent, current, path_to, chain = todo.pop() + #print("expand:", parent.__class__.__name__, + # current.__class__.__name__, + # path_to, + # chain) + keep_going = True + p = 0 + while p < len(chain) and keep_going: + #print("while:", path_to, chain[p:]) + part = chain[p] + if hasattr(current, part): # attribute + current = getattr(current, part) + path_to.append(part) + # need to consider current+part if current is list: + elif isinstance(current, (list, tuple)): + if part.isdigit(): + # followed by index, so continue here + if int(part) < len(current): + current = current[int(part)] + path_to.append(part) + elif ignore_errors: + current = None + keeping_going = False + else: + raise Exception("invalid index position") + else: # else branch! in middle, split paths + for i in range(len(current)): + #print("split list:", self.__class__.__name__, + # current.__class__.__name__, + # path_to[:], + # [str(i)] + chain[p:]) + todo.append([self, current, path_to[:], [str(i)] + chain[p:]]) + current = None + keep_going = False + else: # part not found on this self + # current is a handle + # part is something on joined object + if parent: + ptype = parent.__class__.get_field_type(".".join(path_to)) + if isinstance(ptype, HandleClass): + if db: + # start over here: + obj = None + if current: + obj = ptype.join(db, current) + if part == "self": + current = obj + path_to = [] + #print("split self:", obj.__class__.__name__, + # current.__class__.__name__, + # path_to, + # chain[p + 1:]) + todo.append([obj, current, path_to, chain[p + 1:]]) + elif obj: + current = getattr(obj, part) + #print("split :", obj.__class__.__name__, + # current.__class__.__name__, + # [part], + # chain[p + 1:]) + todo.append([obj, current, [part], chain[p + 1:]]) + current = None + keep_going = False + else: + raise Exception("Can't join without database") + elif part == "self": + pass + elif ignore_errors: + pass + else: + raise Exception("%s is not a valid field of %s; use %s" % + (part, current, dir(current))) + current = None + keep_going = False + p += 1 + if keep_going: + results.append(current) + if len(results) == 1: + return results[0] + elif len(results) == 0: + return None + else: + return results + + def set_field(self, field, value, db=None, ignore_errors=False): + """ + Set the value of a basic field (str, int, float, or bool). + value can be a string or actual value. + Returns number of items changed. + """ + field = self.__class__.get_field_alias(field) + chain = field.split(".") + path = self._follow_field_path(chain[:-1], db, ignore_errors) + ftype = self.get_field_type(field) + # ftype is str, bool, float, or int + value = (value in ['True', True]) if ftype is bool else value + return self._set_fields(path, chain[-1], value, ftype) + + def _set_fields(self, path, attr, value, ftype): + """ + Helper function to handle recursive lists of items. + """ + from .handle import HandleClass + if isinstance(path, (list, tuple)): + count = 0 + for item in path: + count += self._set_fields(item, attr, value, ftype) + elif isinstance(ftype, HandleClass): + setattr(path, attr, value) + count = 1 + else: + setattr(path, attr, ftype(value)) + count = 1 + return count + diff --git a/gramps/gen/lib/tag.py b/gramps/gen/lib/tag.py index ad02fee47..2b0f992fe 100644 --- a/gramps/gen/lib/tag.py +++ b/gramps/gen/lib/tag.py @@ -114,6 +114,19 @@ class Tag(TableObject): "change": int, } + @classmethod + def get_labels(cls, _): + """ + Return the label for fields + """ + return { + "handle": _("Handle"), + "name": _("Name"), + "color": _("Color"), + "priority": _("Priority"), + "change": _("Last changed"), + } + def get_text_data_list(self): """ Return the list of all textual attributes of the object. diff --git a/gramps/plugins/database/dbapi.py b/gramps/plugins/database/dbapi.py index 26e3d1fa5..76f7e3b03 100644 --- a/gramps/plugins/database/dbapi.py +++ b/gramps/plugins/database/dbapi.py @@ -1114,7 +1114,7 @@ class DBAPI(DbGeneric): # else, use Python sorts if order_by: secondary_fields = class_.get_secondary_fields() - if not self.check_order_by_fields(class_.__name__, order_by, secondary_fields): + if not self._check_order_by_fields(class_.__name__, order_by, secondary_fields): for item in self.iter_items_order_by_python(order_by, class_): yield item return