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"))
Event = property(lambda self: QuerySet(self, "Event"))
Tag = property(lambda self: QuerySet(self, "Tag"))
Media = property(lambda self: QuerySet(self, "Media"))
class DbWriteBase(DbReadBase):
"""

View File

@ -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.

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,

View File

@ -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.

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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.

View File

@ -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