src/catedit/models.py
author ymh <ymh.work@gmail.com>
Wed, 14 Aug 2024 22:08:14 +0200
changeset 142 640fb0f13022
parent 103 ef02353dff20
permissions -rw-r--r--
server and docker migration

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