"""
models.py:
contains the "principal" objects that will be manipulated by the application:
* categories
* helper classes to manage category life cycle
"""
from catedit import app
from io import StringIO
import logging
from uuid import uuid4
from rdflib import Graph, RDF, RDFS, Literal, URIRef
from rdflib.compare import to_isomorphic, graph_diff
from slugify import slugify
logger = logging.getLogger(__name__)
class Category(object):
"""
Category Class:
Init:
* label is the rdf label of the category, unique and non-empty
* description is the description of the category, unique and non-empty
* other_properties is a dictionnary containing every other supported
property as defined in the PROPERTY_LIST dict in settings.py
* graph is used if we want to create the Category from a turtle
rdf graph
Additional info:
* cat_id is a generated, hidden id that uniquely identify
a given category
"""
def __init__(self, label=None, description=None,
other_properties=None, graph=None):
if not graph:
# cat_id = .hex - Alternate method of generating ids
cat_id = slugify(label)+"_"+str(uuid4())[:8]
self.cat_graph = Graph()
self.this_category = app.config["CATEGORY_NAMESPACE"][cat_id]
self.cat_graph.add((self.this_category, URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#ID"), Literal(cat_id)))
if label:
self.cat_graph.add(
(self.this_category, RDFS.label, Literal(label))
)
if description:
self.cat_graph.add(
(self.this_category, URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#Description"), Literal(description))
)
if other_properties:
for (predicate, obj) in other_properties:
self.cat_graph.add((self.this_category, predicate, obj))
else:
self.cat_graph = graph
# Warning: not foolproof, if loading a Graph with multiple IDs (should not happen)
self.this_category = next(self.cat_graph.subjects(predicate=URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#ID")))
@property
def label(self):
"""
Returns category label
"""
return_value = self.cat_graph.value(self.this_category, RDFS.label)
if return_value is None:
return None
else:
return return_value.toPython()
@property
def description(self):
"""
Returns category description
"""
return_value = \
self.cat_graph.value(self.this_category, URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#Description"))
if return_value is None:
return None
else:
return return_value.toPython()
@property
def cat_id(self):
"""
Returns category id
"""
return self.cat_graph.value(self.this_category, URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#ID")).toPython()
@property
def properties(self):
"""
Returns category property list
"""
return [
predicate_object_tuple for predicate_object_tuple in self.cat_graph.predicate_objects()
if (
predicate_object_tuple[0]!=URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#ID") and
predicate_object_tuple[0]!=RDFS.label and
predicate_object_tuple[0]!=URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#Description")
)
]
def edit_category(self, new_label="",
new_description="",
new_other_properties=None):
"""
Edits category
* new_label is the new label of the category
* new_description is the new description of the category
* new_other_property is the new property list of the category
new_other_properties default is None because it can't be [], as
that would mean we want to delete all properties
"""
if (new_label != "") and \
(new_label != self.label):
self.cat_graph.remove((self.this_category,
RDFS.label,
self.label))
self.cat_graph.add((self.this_category,
RDFS.label,
Literal(new_label)))
if (new_description != "") and \
(new_description != self.description):
self.cat_graph.remove((self.this_category,
URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#Description"),
self.cat_graph.value(self.this_category,
URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#Description"))))
self.cat_graph.add((self.this_category,
URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#Description"),
Literal(new_description)))
if new_other_properties is not None and \
(new_other_properties != self.properties):
for (predicate, object) in self.cat_graph.predicate_objects():
if predicate not in [URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#ID"), URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#Description"), RDFS.label]:
self.cat_graph.remove(
(self.this_category, predicate, None)
)
for (predicate, obj) in new_other_properties:
self.cat_graph.add(
(self.this_category, predicate, obj)
)
class CategoryManager(object):
"""
CategoryManager class
This class deals with creation and loading of Category objects
* load_category returns a category from its id
* list_categories returns all categories
* save_changes will take a list of modified categories and a list of
deleted categories and save the changes using the persistence method
* delete_category will delete a single category from its id
"""
def __init__(self, persistence_method):
"""
persistence_method is a class that must have 4 methods:
* save
* load
* delete
* list
It must save and return RDF graph serializations.
See persistence.py for details
"""
self.persistence = persistence_method
def load_category(self, cat_id):
"""
Loads a category from its id
"""
cat_serial = self.persistence.load(name=cat_id)
cat = None
if cat_serial != "":
loaded_cat_graph = Graph()
loaded_cat_graph.parse(source=StringIO(cat_serial), format='turtle')
cat = Category(graph=loaded_cat_graph)
return cat
def save_changes(self,
deleted_cat_dict=None,
modified_cat_dict=None,
message=""):
"""
Saves all changes to categories
* deleted_cat_list must be a list of dict describing deleted
categories, each dict of the format :
{"name": category_id}
* modified_cat_list must be a list of dict describing modified
categories, each dict of the format :
{"name": category_id, "content": category turtle serialization}
* message is used as commit message when applicable (github)
"""
if modified_cat_dict is None:
modified_cat_dict = {}
if deleted_cat_dict is None:
deleted_cat_dict = {}
self.persistence.save(deletion_dict=deleted_cat_dict,
modification_dict=modified_cat_dict,
message=message)
def delete_category(self, deleted_cat_id):
"""
Deletes a category from its id
* message serves as commit message if github is used as a
persistence method
NOTE: This will not auto-update existing categories to delete
references to deleted category. This is handled in the api as such
operations apply to the intermediary persistence (changeset)
"""
self.persistence.delete(name=deleted_cat_id, message="Category deleted")
# Now we must clean up the categories that reference the deleted cat
def list_categories(self):
"""
Lists all categories available
"""
cat_serial_list = self.persistence.list()
# logger.debug(cat_serial_list)
cat_list = []
for cat_serial in cat_serial_list:
loaded_cat_graph = Graph()
loaded_cat_graph.parse(
source=StringIO(cat_serial),
format='turtle'
)
cat = Category(graph=loaded_cat_graph)
cat_list.append(cat)
return cat_list