Fixes for field-based schema and labels

* moved methods to TableObject from PrimaryObj (to include Tag)
* added missing scheme, labels to citation, place, repo, and source
* minor bug fixes
This commit is contained in:
Doug Blank 2016-05-02 14:32:50 -04:00
parent 0bbf52b4aa
commit 7e570b6724
11 changed files with 426 additions and 248 deletions

View File

@ -1448,6 +1448,7 @@ class DbReadBase(object):
Place = property(lambda self: QuerySet(self, "Place")) Place = property(lambda self: QuerySet(self, "Place"))
Event = property(lambda self: QuerySet(self, "Event")) Event = property(lambda self: QuerySet(self, "Event"))
Tag = property(lambda self: QuerySet(self, "Tag")) Tag = property(lambda self: QuerySet(self, "Tag"))
Media = property(lambda self: QuerySet(self, "Media"))
class DbWriteBase(DbReadBase): class DbWriteBase(DbReadBase):
""" """

View File

@ -79,6 +79,46 @@ class Citation(MediaBase, NoteBase, SrcAttributeBase, IndirectCitationBase,
self.confidence = Citation.CONF_NORMAL # 4 self.confidence = Citation.CONF_NORMAL # 4
SrcAttributeBase.__init__(self) # 8 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): def serialize(self, no_text_date=False):
""" """
Convert the object to a serialized tuple of data. Convert the object to a serialized tuple of data.

View File

@ -175,7 +175,7 @@ class Event(CitationBase, NoteBase, MediaBase, AttributeBase,
"note_list": [Note], "note_list": [Note],
"media_list": [Media], "media_list": [Media],
"attribute_list": [Attribute], "attribute_list": [Attribute],
"change": float, "change": int,
"tag_list": [Tag], "tag_list": [Tag],
"private": bool, "private": bool,
} }

View File

@ -162,10 +162,7 @@ class Media(CitationBase, NoteBase, DateBase, AttributeBase,
:rtype: dict :rtype: dict
""" """
from .attribute import Attribute from .attribute import Attribute
from .citation import Citation
from .note import Note
from .date import Date from .date import Date
from .tag import Tag
return { return {
"handle": Handle("Media", "MEDIA-HANDLE"), "handle": Handle("Media", "MEDIA-HANDLE"),
"gramps_id": str, "gramps_id": str,
@ -174,11 +171,11 @@ class Media(CitationBase, NoteBase, DateBase, AttributeBase,
"desc": str, "desc": str,
"checksum": str, "checksum": str,
"attribute_list": [Attribute], "attribute_list": [Attribute],
"citation_list": [Citation], "citation_list": [Handle("Citation", "CITATION-HANDLE")],
"note_list": [Note], "note_list": [Handle("Note", "NOTE-HANDLE")],
"change": float, "change": int,
"date": Date, "date": Date,
"tag_list": Tag, "tag_list": [Handle("Tag", "TAG-HANDLE")],
"private": bool, "private": bool,
} }

View File

@ -117,6 +117,55 @@ class Place(CitationBase, NoteBase, MediaBase, UrlBase, PrimaryObject):
NoteBase.serialize(self), NoteBase.serialize(self),
self.change, TagBase.serialize(self), self.private) 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): def to_struct(self):
""" """
Convert the data held in this object to a structure (eg, Convert the data held in this object to a structure (eg,

View File

@ -120,245 +120,6 @@ class BasicPrimaryObject(TableObject, PrivacyBase, TagBase):
""" """
raise NotImplementedError 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): def set_gramps_id(self, gramps_id):
""" """
Set the Gramps ID for the primary object. Set the Gramps ID for the primary object.

View File

@ -69,6 +69,41 @@ class Repository(NoteBase, AddressBase, UrlBase, IndirectCitationBase,
UrlBase.serialize(self), UrlBase.serialize(self),
self.change, TagBase.serialize(self), self.private) 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): def to_struct(self):
""" """
Convert the data held in this object to a structure (eg, Convert the data held in this object to a structure (eg,

View File

@ -79,6 +79,48 @@ class Source(MediaBase, NoteBase, SrcAttributeBase, IndirectCitationBase,
TagBase.serialize(self), # 11 TagBase.serialize(self), # 11
self.private) # 12 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): def to_struct(self):
""" """
Convert the data held in this object to a structure (eg, Convert the data held in this object to a structure (eg,

View File

@ -182,3 +182,243 @@ class TableObject(BaseObject):
:rtype: str :rtype: str
""" """
return self.handle 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

View File

@ -114,6 +114,19 @@ class Tag(TableObject):
"change": int, "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): def get_text_data_list(self):
""" """
Return the list of all textual attributes of the object. Return the list of all textual attributes of the object.

View File

@ -1114,7 +1114,7 @@ class DBAPI(DbGeneric):
# else, use Python sorts # else, use Python sorts
if order_by: if order_by:
secondary_fields = class_.get_secondary_fields() 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_): for item in self.iter_items_order_by_python(order_by, class_):
yield item yield item
return return