Added support for making multiple changes in a single commit, using the flask dict "session" (keys used are "modified_categories" and "deleted_categories") + Added a view for editing categories that allows the user to see what he has added, deleted or edited + Made it so an unauthenticated user should not generate any Github api request (you have to be logged to see the categories now).
authorNicolas DURAND <nicolas.durand@iri.centrepompidou.fr>
Mon, 29 Dec 2014 18:01:09 +0100
changeset 14 fc63b1a3d2ef
parent 13 ea5f985156b1
child 15 4f08f1a107b3
Added support for making multiple changes in a single commit, using the flask dict "session" (keys used are "modified_categories" and "deleted_categories") + Added a view for editing categories that allows the user to see what he has added, deleted or edited + Made it so an unauthenticated user should not generate any Github api request (you have to be logged to see the categories now).
src/catedit/__init__.py
src/catedit/api.py
src/catedit/main.py
src/catedit/models.py
src/catedit/persistence.py
src/catedit/static/css/style.css
src/catedit/static/img/catedit_brand.png
src/catedit/static/js/property_functions.js
src/catedit/templates/cateditor.html
src/catedit/templates/catmodifs.html
src/catedit/templates/catrecap.html
src/catedit/views.py
--- a/src/catedit/__init__.py	Thu Dec 18 12:07:03 2014 +0100
+++ b/src/catedit/__init__.py	Mon Dec 29 18:01:09 2014 +0100
@@ -8,6 +8,7 @@
 from settings import AppSettings
 from config import AppConfig
 from logging import FileHandler, Formatter
+from os import urandom
 
 # set up app and database
 app = Flask(__name__)
@@ -36,4 +37,4 @@
 
 # session management
 
-app.secret_key = "extremely_secure_and_very_secret_key"
+app.secret_key = urandom(24)
--- a/src/catedit/api.py	Thu Dec 18 12:07:03 2014 +0100
+++ b/src/catedit/api.py	Mon Dec 29 18:01:09 2014 +0100
@@ -9,19 +9,19 @@
 from flask import request
 from catedit import app, cache
 from catedit.models import Category, CategoryManager
-
+import catedit.persistence
 
 api = Api(app)
 
-LOGGER = app.logger
+logger = app.logger
 
-CAT_PARSER = reqparse.RequestParser()
-CAT_PARSER.add_argument('label', type=str)
-CAT_PARSER.add_argument('description', type=str)
-CAT_PARSER.add_argument('commit_message', type=str)
-CAT_PARSER.add_argument('property_predicate', type=str, action="append")
-CAT_PARSER.add_argument('property_object', type=str, action="append")
-CAT_PARSER.add_argument('delete_message', type=str)
+cat_parser = reqparse.RequestParser()
+cat_parser.add_argument('label', type=str)
+cat_parser.add_argument('description', type=str)
+cat_parser.add_argument('commit_message', type=str)
+cat_parser.add_argument('property_predicate', type=str, action="append")
+cat_parser.add_argument('property_object', type=str, action="append")
+cat_parser.add_argument('delete_message', type=str)
 
 
 class CategoryAPI(Resource):
@@ -36,7 +36,10 @@
             API to get the category of id cat_id, or if cat_id is None,
             get the list of category
         """
-        cat_manager_instance = CategoryManager()
+        cat_manager_instance = CategoryManager(
+                                   getattr(catedit.persistence,
+                                           app.config["PERSISTENCE_METHOD"])()
+                                               )
         if cat_id is not None:
             cat = cat_manager_instance.load_cat(cat_id)
             return cat.cat_graph.serialize(format='turtle').decode("utf-8")
@@ -49,53 +52,59 @@
 
     # update category cat_id
     @classmethod
-    def put(cls, cat_id):
+    def put(cls, cat_id=None):
         """
             API to edit the category of id cat_id
+            * If None and persistence support change sets, will submit all
+            changes to category list
         """
-        args = CAT_PARSER.parse_args()
-        cat_manager_instance = CategoryManager()
-
-        new_property_list = []
-
-        LOGGER.debug(args["property_predicate"])
-        LOGGER.debug(args["property_object"])
-        if args["property_predicate"] is not None and \
-           args["property_object"] is not None:
-            for property_predicate, property_object in zip(
-                    args["property_predicate"],
-                    args["property_object"]):
-                if property_object:
-                    property_object_to_append = property_object
-                    # if URIRef category, we must prefix id with namespace
-                    if (app.config["PROPERTY_LIST"]
-                                  [property_predicate]
-                                  ["object_type"]) == "uriref-category":
-                        property_object_to_append = \
-                            app.config["CATEGORY_NAMESPACE"] + property_object
-                        LOGGER.debug(property_object_to_append)
-                    new_property_list.append((property_predicate,
-                                              property_object_to_append))
-        LOGGER.debug(new_property_list)
-        cat = cat_manager_instance.load_cat(cat_id)
-        cat.edit_category(new_description=args["description"],
-                          new_label=args["label"],
-                          new_other_properties=new_property_list)
-        cat_manager_instance.save_cat(cat, message=args["commit_message"])
-        LOGGER.debug("put id: "+cat.cat_id)
+        cat_manager_instance = CategoryManager(
+            getattr(catedit.persistence, app.config["PERSISTENCE_METHOD"])()
+        )
+        args = cat_parser.parse_args()
+        if (cat_id is None and
+            cat_manager_instance.persistence.session_compliant is True):
+            cat_manager_instance.save_cat(message=args["commit_message"])
+        else:
+            new_property_list = []
+            logger.debug(args["property_predicate"])
+            logger.debug(args["property_object"])
+            if args["property_predicate"] is not None and \
+               args["property_object"] is not None:
+                for property_predicate, property_object in zip(
+                        args["property_predicate"],
+                        args["property_object"]):
+                    if property_object:
+                        property_object_to_append = property_object
+                        # if URIRef category, we must prefix id with namespace
+                        if (app.config["PROPERTY_LIST"]
+                                      [property_predicate]
+                                      ["object_type"]) == "uriref-category":
+                            property_object_to_append = \
+                                app.config["CATEGORY_NAMESPACE"] \
+                                + property_object
+                            logger.debug(property_object_to_append)
+                        new_property_list.append((property_predicate,
+                                                  property_object_to_append))
+            logger.debug(new_property_list)
+            cat = cat_manager_instance.load_cat(cat_id)
+            cat.edit_category(new_description=args["description"],
+                              new_label=args["label"],
+                              new_other_properties=new_property_list)
+            cat_manager_instance.save_cat(cat, message=args["commit_message"])
+            logger.debug("put id: "+cat.cat_id)
         cache.clear()
-        return cat.cat_graph.serialize(format='turtle').decode("utf-8"), 200
-        # Maybe not send the whole cat back, see if it's worth it
+        return 204
 
     @classmethod
     def post(cls):
         """
             API to create a new category
         """
-        args = CAT_PARSER.parse_args()
+        args = cat_parser.parse_args()
         property_list = []
-        LOGGER.debug(args["property_predicate"])
-        LOGGER.debug(args["property_object"])
+        logger.debug(args["property_predicate"])
+        logger.debug(args["property_object"])
         for property_predicate, property_object in zip(
                 request.form.getlist('property_predicate'),
                 request.form.getlist('property_object')):
@@ -109,13 +118,16 @@
                 else:
                     property_list.append((property_predicate,
                                           property_object))
-        LOGGER.debug(property_list)
+        logger.debug(property_list)
         cat = Category(label=args["label"],
                        description=args["description"],
                        other_properties=property_list)
-        cat_manager_instance = CategoryManager()
+        cat_manager_instance = CategoryManager(
+                                   getattr(catedit.persistence,
+                                           app.config["PERSISTENCE_METHOD"])()
+                                               )
         cat_manager_instance.save_cat(cat, message=args["commit_message"])
-        LOGGER.debug("post id: "+cat.cat_id)
+        logger.debug("post id: "+cat.cat_id)
         cache.clear()
         return cat.cat_graph.serialize(format='turtle').decode("utf-8"), 201
 
@@ -124,14 +136,17 @@
         """
             API to delete the category of id cat_id
         """
-        args = CAT_PARSER.parse_args()
-        cat_manager_instance = CategoryManager()
+        args = cat_parser.parse_args()
+        cat_manager_instance = CategoryManager(
+                                   getattr(catedit.persistence,
+                                           app.config["PERSISTENCE_METHOD"])()
+                                               )
         if args["delete_message"] is None:
             message = "Deleting category "+cat_id
         else:
             message = args["delete_message"]
         cat_manager_instance.delete_cat(cat_id, message=message)
-        LOGGER.debug("delete id: "+cat_id)
+        logger.debug("delete id: "+cat_id)
         cache.clear()
         return 204
 
--- a/src/catedit/main.py	Thu Dec 18 12:07:03 2014 +0100
+++ b/src/catedit/main.py	Mon Dec 29 18:01:09 2014 +0100
@@ -2,7 +2,6 @@
 main.py:
 script that is used to boot up the application
 """
-
 from catedit import app
 from catedit.api import api
 from catedit.views import cat_editor, cat_recap, github_login, \
--- a/src/catedit/models.py	Thu Dec 18 12:07:03 2014 +0100
+++ b/src/catedit/models.py	Mon Dec 29 18:01:09 2014 +0100
@@ -6,14 +6,15 @@
 """
 
 from rdflib import Graph, RDF, RDFS, Literal, URIRef
-# from uuid import uuid4
+from rdflib.namespace import SKOS
+from flask import session
+from uuid import uuid4
 from io import StringIO
 from slugify import slugify
 from catedit import app
-import catedit.persistence
 
 
-LOGGER = app.logger
+logger = app.logger
 
 
 class Category(object):
@@ -35,8 +36,8 @@
     def __init__(self, label=None, description=None,
                  other_properties=None, graph=None):
         if not graph:
-            # cat_id = uuid4().hex - Alternate method of generating ids
-            cat_id = "category_id_"+slugify(label)
+            # cat_id = .hex - Alternate method of generating ids
+            cat_id = str(uuid4())[:8]+"_"+slugify(label)
             self.cat_graph = Graph()
             self.this_category = URIRef(app.config["CATEGORY_NAMESPACE"] +
                                         cat_id)
@@ -112,22 +113,20 @@
                 property_list.append((key, obj.toPython()))
         return property_list
 
-    def edit_category(self, new_label=False,
-                      new_description=False,
-                      new_other_properties=False):
+    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
 
-            Every argument is optional and defaults to False for consistency,
-            as giving a property list that is None means we want
-            to delete all properties
+            new_other_properties default is None because it can't be [], as
+            that would mean we want to delete all properties
         """
 
-        if (new_label is not False) and \
-           (new_label is not None) and \
+        if (new_label != "") and \
            (new_label != self.label):
             self.cat_graph.remove((self.this_category,
                                    RDFS.label,
@@ -136,8 +135,7 @@
                                 RDFS.label,
                                 Literal(new_label)))
 
-        if (new_description is not False) and \
-           (new_description is not None) and \
+        if (new_description != "") and \
            (new_description != self.description):
             self.cat_graph.remove((self.this_category,
                                   RDF.Description,
@@ -147,23 +145,19 @@
                                RDF.Description,
                                Literal(new_description)))
 
-        if new_other_properties is not False or \
-            (new_other_properties is None and
-             self.properties is not None):
-            LOGGER.debug("before suppressing properties: ")
-            LOGGER.debug(self.properties)
-            LOGGER.debug("will replace with: ")
-            LOGGER.debug(new_other_properties)
+        logger.debug("current properties: "
+                     +str(self.properties)
+                     +" new properties: "
+                     +str(new_other_properties))
+
+        if new_other_properties is not None and \
+           (new_other_properties != self.properties):
             for key in app.config["PROPERTY_LIST"]:
                 self.cat_graph.remove((self.this_category,
                                        app.config["PROPERTY_LIST"]
                                                  [key]
                                                  ["rdflib_class"],
                                        None))
-            LOGGER.debug("now properties are: ")
-            LOGGER.debug(self.properties)
-            LOGGER.debug("making new properties: ")
-            LOGGER.debug(new_other_properties)
             for (predicate, obj) in new_other_properties:
                 self.cat_graph.add((self.this_category,
                                     app.config["PROPERTY_LIST"]
@@ -178,68 +172,128 @@
     """
         CategoryManager class
         This class deals with creation and loading of Category objects
-        Persistence method is set in config.py and used to save
-        and load categories
     """
+    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_cat(self, cat_id):
         """
             Loads a category from its id
         """
-        persistence = getattr(catedit.persistence,
-                              app.config["PERSISTENCE_METHOD"])()
-        cat_serial = persistence.load(name=cat_id)
+        cat_serial = self.persistence.load(name=cat_id)
         loaded_cat_graph = Graph()
         loaded_cat_graph.parse(source=StringIO(cat_serial), format='turtle')
         cat = Category(graph=loaded_cat_graph)
         return cat
 
-    def save_cat(self, cat, message=None):
-        """
-            Saves a category, message serves as commit message
-            if github is used as a persistence method
+    def save_cat(self, cat=None, message=None):
         """
-        persistence = getattr(catedit.persistence,
-                              app.config["PERSISTENCE_METHOD"])()
-        persistence.save(content=cat.cat_graph.serialize(format='turtle'),
-                         name=cat.cat_id, message=message)
+            Saves a category, message serves as commit message if github
+            is used as a persistence method
+
+            * If cat is None and the persistence method supports submitting
+            changes to multiple files in one go, then it will call
+            submit_changes method
+        """
+        if cat is None:
+            self.persistence.submit_changes(message=message)
+        else:
+            self.persistence.save(content=cat.cat_graph
+                                             .serialize(format='turtle'),
+                                  name=cat.cat_id,
+                                  message=message)
 
     def delete_cat(self, deleted_cat_id, message=None):
         """
-            Deletes a category from its id, message serves
-            as commit message if github is used as a persistence method
+            Deletes a category from its id, message serves as commit message if
+            github is used as a persistence method
         """
+        self.persistence.delete(name=deleted_cat_id, message=message)
+        # Now we must clean up the categories that reference the deleted cat
         cat_list = self.list_cat()
-        for cat in cat_list:
-            if cat.cat_id != app.config["CATEGORY_NAMESPACE"]+deleted_cat_id:
-                new_property_list_for_cat = []
-                for (predicate, obj) in cat.properties:
-                    if not (
-                        (app.config["PROPERTY_LIST"]
-                                   [predicate]
-                                   ["object_type"] == "uriref-category") and
-                        (obj == (app.config["CATEGORY_NAMESPACE"] +
-                                 deleted_cat_id))
-                    ):
-                        new_property_list_for_cat.append((predicate, obj))
-                cat.edit_category(
-                    new_other_properties=new_property_list_for_cat
-                )
-                self.save_cat(
-                    cat,
-                    message=message+", cleaning up other properties"
-                )
-        persistence = getattr(catedit.persistence,
-                              app.config["PERSISTENCE_METHOD"])()
-        persistence.delete(name=deleted_cat_id, message=message)
+        # first we edit what was already modified before the deletion
+        logger.debug(session["modified_categories"])
+        element_list = list(session["modified_categories"])
+        if deleted_cat_id in [element["name"] for \
+                element in session["deleted_categories"]]:
+            for element in element_list:
+                logger.debug(str(element))
+                modified_cat_graph = Graph()
+                modified_cat_graph.parse(source=StringIO(element["content"]),
+                                         format="turtle")
+                modified_cat = Category(graph=modified_cat_graph)
+                if (modified_cat.cat_id != app.config["CATEGORY_NAMESPACE"]
+                                           +deleted_cat_id):
+                    new_property_list_for_modified_cat = []
+                    for (predicate, obj) in modified_cat.properties:
+                        if not (
+                            (app.config["PROPERTY_LIST"]
+                                       [predicate]
+                                       ["object_type"] == "uriref-category")
+                            and
+                            (obj == (app.config["CATEGORY_NAMESPACE"] +
+                                     deleted_cat_id))
+                        ):
+                            new_property_list_for_modified_cat.append(
+                                (predicate, obj)
+                            )
+                    if (new_property_list_for_modified_cat
+                            != modified_cat.properties):
+                        logger.debug("Modifying modified category")
+                        modified_cat.edit_category(
+                            new_other_properties=\
+                                new_property_list_for_modified_cat
+                        )
+                        self.save_cat(
+                            modified_cat,
+                            message=message+", cleaning up other properties"
+                        )
+            # now we check if an unmodified category reference the deleted
+            # category
+            for cat in cat_list:
+                if cat.cat_id not in [element["name"] for \
+                        element in session["modified_categories"]] and \
+                   cat.cat_id not in [element["name"] for \
+                        element in session["deleted_categories"]]:
+                    new_property_list_for_cat = []
+                    for (predicate, obj) in cat.properties:
+                        if not (
+                            (app.config["PROPERTY_LIST"]
+                                       [predicate]
+                                       ["object_type"] == "uriref-category")
+                            and
+                            (obj == (app.config["CATEGORY_NAMESPACE"] +
+                                     deleted_cat_id))
+                        ):
+                            new_property_list_for_cat.append((predicate, obj))
+                    if(new_property_list_for_cat!=cat.properties):
+                        logger.debug("Modifying untouched category")
+                        cat.edit_category(
+                            new_other_properties=new_property_list_for_cat
+                        )
+                        self.save_cat(
+                            cat,
+                            message=message+", cleaning up other properties"
+                        )
+
 
     def list_cat(self):
         """
             Lists all categories available
         """
-        persistence = getattr(catedit.persistence,
-                              app.config["PERSISTENCE_METHOD"])()
-        cat_serial_list = persistence.list()
-        # LOGGER.debug(cat_serial_list)
+        cat_serial_list = self.persistence.list()
+        # logger.debug(cat_serial_list)
         cat_list = []
         for cat_serial in cat_serial_list:
             loaded_cat_graph = Graph()
--- a/src/catedit/persistence.py	Thu Dec 18 12:07:03 2014 +0100
+++ b/src/catedit/persistence.py	Mon Dec 29 18:01:09 2014 +0100
@@ -11,11 +11,12 @@
 from abc import ABCMeta, abstractmethod
 from catedit import app, github
 from base64 import b64encode, b64decode
+from flask import session
 from flask.ext.github import GitHubError
 import os
 import json
 
-LOGGER = app.logger
+logger = app.logger
 
 
 class Persistence:
@@ -25,6 +26,21 @@
     __metaclass__ = ABCMeta
 
     @abstractmethod
+    def session_compliant(self):
+        """
+            Abstract - Can this persistence submit a changeset?
+        """
+        return
+
+    @abstractmethod
+    def submit_changes(self, **kwargs):
+        """
+            Abstract - Submit all saved objects, only useful if persistence
+            method can submit a changeset
+        """
+        return
+
+    @abstractmethod
     def save(self, **kwargs):
         """
             Abstract - Saves object
@@ -62,6 +78,19 @@
         * name : name of the file to write in/read from
         * content : desired content of the file when writing
     """
+    @property
+    def session_compliant(self):
+        """
+            Not session compliant: each modification is submitted
+        """
+        return False
+
+    def submit_changes(self, **kwargs):
+        """
+            As each modification is submitted, this is where we save to a file
+        """
+
+
     def save(self, **kwargs):
         """
             Saves to a file
@@ -101,7 +130,7 @@
             file_content = path_to_load.read()
             path_to_load.close()
             file_content_list.append(file_content)
-        # LOGGER.debug(file_content_list)
+        # logger.debug(file_content_list)
         return file_content_list
 
 
@@ -116,50 +145,197 @@
         * message : when saving or deleting, commit message to be saved
         to Github
     """
+    @property
+    def session_compliant(self):
+        """
+            Not session compliant: each modification is comitted
+        """
+        return True
+
+    def submit_changes(self, **kwargs):
+        """
+            Saves all the recorded files in the session dict to a Github
+            repository
+
+            Expected kwargs is:
+            * message: the commit message to document the changes
+
+            IMPORTANT: To save to a file Github, we have to use Git
+            internal mechanics:
+            1) Get the current reference for master branch
+            2) Get the latest commit on that reference
+            3) Get the tree associated to this commit
+            4) Create a new tree using the old tree and the session dict that
+            contains all the changes (keys are "modified_categories" and
+            "deleted_categories")
+                4-1) This requires creating new blobs for new files
+            5) Create a new commit referencing the previous commit as parent
+            and the new tree we just created
+            6) Update the master branch reference so it points on this new
+            commit
+
+            About point 4):
+            We have 3 list of elements: files already in repo, modified files,
+            and deleted_files
+            First, we loop on the files already in repo: this allows us to
+            update it (if it's in both original file list and modified files
+            list) or delete it (if it's in both original file list and deleted
+            files list)
+            Then, we loop on the modified files list. If it isn't in the
+            original file list, then it's a new file and we append it to the
+            tree
+        """
+
+        # point 1
+        ref_master = github.get("repos/"
+                                + app.config["REPOSITORY_OWNER"] + "/"
+                                + app.config["REPOSITORY_NAME"]
+                                + "/git/refs/heads/master")
+        logger.debug(str(ref_master))
+        # point 2
+        last_commit_master = github.get("repos/"
+                                        + app.config["REPOSITORY_OWNER"] + "/"
+                                        + app.config["REPOSITORY_NAME"]
+                                        + "/git/commits/"
+                                        + ref_master["object"]["sha"])
+
+        logger.debug(str(last_commit_master))
+        # point 3
+        last_commit_tree = github.get("repos/"
+                                      + app.config["REPOSITORY_OWNER"] + "/"
+                                      + app.config["REPOSITORY_NAME"]
+                                      + "/git/trees/"
+                                      + last_commit_master["tree"]["sha"]
+                                      + "?recursive=1")
+
+        logger.debug(str(last_commit_tree))
+        # point 4
+        new_tree_data = {"tree": []}
+        for element in last_commit_tree["tree"]:
+            if element["type"] == "blob":
+                # test if element is in deleted categories, if it is,
+                # no point doing anything, the file won't be in the new tree
+                if not(element["path"] in [(app.config["CATEGORIES_PATH"]
+                                           + category["name"])
+                                           for category in
+                                           session["deleted_categories"]]):
+
+                    # the element is either modified or untouched so in both
+                    # case we'll need a blob sha
+                    blob_sha = ""
+
+                    # test if element is in modified categories
+                    if element["path"] in [(app.config["CATEGORIES_PATH"]
+                                            + category["name"])
+                                            for category in
+                                            session["modified_categories"]]:
+                        # find element in modified categories
+                        for category in session["modified_categories"]:
+                            if element["path"] == (
+                                            app.config["CATEGORIES_PATH"]
+                                            + category["name"]
+                                                   ):
+                                # 4-1 for edited files
+                                new_blob_data = {
+                                            "content": category["content"],
+                                            "encoding": "utf-8"
+                                                 }
+                                new_blob = github.post(
+                                            "repos/"
+                                            + app.config["REPOSITORY_OWNER"]
+                                            +"/"
+                                            + app.config["REPOSITORY_NAME"]
+                                            + "/git/blobs",
+                                            data=new_blob_data
+                                                       )
+                                blob_sha = new_blob["sha"]
+                                break
+
+                    # this means element is an untouched file
+                    else:
+                        blob_sha = element["sha"]
+                    new_tree_data["tree"].append({"path": element["path"],
+                                                  "mode": element["mode"],
+                                                  "type": "blob",
+                                                  "sha": blob_sha })
+        logger.debug(str(new_tree_data["tree"]))
+        # Now we'll add new files in the tree
+        for category in session["modified_categories"]:
+            logger.debug(app.config["CATEGORIES_PATH"]+category["name"]
+                         + " should not be in "
+                         + str([ file["path"] for
+                                 file in last_commit_tree["tree"]]))
+            if (app.config["CATEGORIES_PATH"]+category["name"] not in
+                [ file["path"] for file in last_commit_tree["tree"]]):
+
+                # 4-1 for added files
+                new_blob_data={"content": category["content"],
+                               "encoding": "utf-8"}
+                new_blob = github.post("repos/"
+                                       + app.config["REPOSITORY_OWNER"]+"/"
+                                       + app.config["REPOSITORY_NAME"]
+                                       + "/git/blobs",
+                                       data=new_blob_data)
+                new_tree_data["tree"].append({
+                                    "path": app.config["CATEGORIES_PATH"]
+                                            + category["name"],
+                                    "mode": "100644",
+                                    "type": "blob",
+                                    "sha": new_blob["sha"] })
+        logger.debug(str(new_tree_data))
+
+        # Finally, we post the new tree
+        new_tree_response = github.post("repos/"
+                                        + app.config["REPOSITORY_OWNER"]+"/"
+                                        + app.config["REPOSITORY_NAME"]
+                                        + "/git/trees",
+                                        data=new_tree_data)
+
+        # point 5
+        new_commit_data = {"message": kwargs["message"],
+                           "parents": [last_commit_master["sha"]],
+                           "tree": new_tree_response["sha"]}
+        logger.debug(str(new_commit_data))
+        new_commit = github.post("repos/"
+                                 + app.config["REPOSITORY_OWNER"]+"/"
+                                 + app.config["REPOSITORY_NAME"]
+                                 + "/git/commits",
+                                 data=new_commit_data)
+        logger.debug(str(new_commit))
+
+        # point 6
+        new_head_data = {"sha": new_commit["sha"], "force": "true"}
+        logger.debug(str(new_head_data))
+        new_head = github.patch("repos/"
+                                + app.config["REPOSITORY_OWNER"] + "/"
+                                + app.config["REPOSITORY_NAME"]
+                                + "/git/refs/heads/master",
+                                data=json.dumps(new_head_data))
+        logger.debug(str(new_head))
+        session["deleted_categories"] = []
+        session["modified_categories"] = []
+
     def save(self, **kwargs):
         """
-            Saves to a Github repository
+            Saves given file to the session dict
 
-            IMPORTANT: To save to a file Github, we use a PUT request, and
-            if the file already exists we must put its sha in the request data.
-            The first try-except block actually expects an error if creating a
-            new file because we need to check if the file already exists
+            Expected kwargs should be:
+            * name: the name of the file to save
+            * content: the content of the file to save
         """
-        # LOGGER.debug(kwargs["content"])
-        request_data = {"content": str(b64encode(kwargs["content"]), "ascii"),
-                        "message": kwargs["message"]}
-        try:
-            filedict = github.get("repos/"
-                                  + app.config["REPOSITORY_OWNER"]+"/"
-                                  + app.config["REPOSITORY_NAME"]
-                                  + "/contents/"
-                                  + app.config["CATEGORIES_PATH"]
-                                  + kwargs["name"])
-            request_data["sha"] = filedict["sha"]
-        except GitHubError as ghe:
-            LOGGER.debug("Github sent an error, either: \
-                         1- You're trying to create a new file named : "
-                         + kwargs["name"] + " OR \
-                         2- You're trying to edit a file named : "
-                         + kwargs["name"] + ", \
-                         in which case something went wrong \
-                         trying to get its sha")
-            LOGGER.debug(ghe.response.json())
-        # LOGGER.debug(json.dumps(request_data))
-        try:
-            github.request('PUT',
-                           "repos/"
-                           + app.config["REPOSITORY_OWNER"]+"/"
-                           + app.config["REPOSITORY_NAME"]
-                           + "/contents/"
-                           + app.config["CATEGORIES_PATH"]
-                           + kwargs["name"],
-                           data=json.dumps(request_data))
-        except GitHubError as ghe:
-            LOGGER.debug("Github Error trying to update file: "+kwargs["name"])
-            LOGGER.debug("Github sent an error, if 404, either you may not \
-                         have access to the repository or it doesn't exist ")
-            LOGGER.debug(ghe.response.json())
+        session["modified_categories"][:] = [elt \
+            for elt in session["modified_categories"] \
+            if elt["name"] != kwargs["name"]]
+        session["modified_categories"].append(
+            {"name": kwargs["name"],
+             "content": str(kwargs["content"],"utf-8")}
+        )
+        # Now we must clean the deleted categories list in case the modified
+        # category was deleted before being edited
+
+        for element in session["deleted_categories"]:
+            if element["name"] == kwargs["name"]:
+                session["deleted_categories"].remove(element)
 
     def load(self, **kwargs):
         """
@@ -174,40 +350,31 @@
                                   + kwargs["name"])
             file_content = str(b64decode(filedict["content"]), "utf-8")
         except GitHubError as ghe:
-            LOGGER.debug("Github Error trying to get file: "+kwargs["name"])
-            LOGGER.debug("Github sent an error, if 404, either you may not \
+            logger.debug("Github Error trying to get file: "+kwargs["name"])
+            logger.debug("Github sent an error, if 404, either you may not \
                          have access to the repository or it doesn't exist ")
-            LOGGER.debug(ghe.response.text)
+            logger.debug(ghe.response.text)
         return file_content
 
     def delete(self, **kwargs):
         """
             Deletes from a Github repository
+            Calling delete for a file already deleted will restore it
+            (by deleting it from the delete files list)
         """
-        request_data = {"message": kwargs["message"]}
-        try:
-            filedict = github.get("repos/"
-                                  + app.config["REPOSITORY_OWNER"]+"/"
-                                  + app.config["REPOSITORY_NAME"]
-                                  + "/contents/"
-                                  + app.config["CATEGORIES_PATH"]
-                                  + kwargs["name"])
-            request_data["sha"] = filedict["sha"]
-        except GitHubError as ghe:
-            LOGGER.debug("Github Error trying to get sha for \
-                         file: "+kwargs["name"])
-            LOGGER.debug(ghe.response.text)
+        if (kwargs["name"] in [element["name"] for \
+                              element in session["deleted_categories"]]):
+            session["deleted_categories"].remove({"name": kwargs["name"]})
+            # warning, not safe if 2 files share the same name (or category id)
+            # but that shouldn't happen
+        else:
+            session["deleted_categories"].append({"name": kwargs["name"]})
+            # now we must clean the modified categories list in case the
+            # deleted category was modified before
+            for element in session["modified_categories"]:
+                if element["name"]==kwargs["name"]:
+                    session["modified_categories"].remove(element)
 
-        try:
-            github.request('DELETE',
-                           "repos/catedit-system/"
-                           + app.config["REPOSITORY_NAME"]
-                           + "/contents/categories/"
-                           + kwargs["name"],
-                           data=json.dumps(request_data))
-        except GitHubError as ghe:
-            LOGGER.debug("Github Error trying to delete file: "+kwargs["name"])
-            LOGGER.debug(ghe.response.text)
 
     def list(self, **kwargs):
         """
@@ -222,14 +389,14 @@
                                        + app.config["CATEGORIES_PATH"])
             filenames_list = [github_file["name"]
                               for github_file in files_in_repo]
-            # LOGGER.debug(filenames_list)
+            # logger.debug(filenames_list)
         except GitHubError as ghe:
-            LOGGER.debug("Github Error trying to get the file list in the \
+            logger.debug("Github Error trying to get the file list in the \
                          category repository")
-            LOGGER.debug("NOTE: Github sent an error, if 404 either you \
+            logger.debug("NOTE: Github sent an error, if 404 either you \
                          may not have access to the repository or it doesn't \
                          exist or there isn't any files in it")
-            LOGGER.debug(ghe.response.text)
+            logger.debug(ghe.response.text)
 
         file_content_list = []
         for filename in filenames_list:
@@ -243,7 +410,53 @@
                 file_content_list.append(str(b64decode(filedict["content"]),
                                          "utf-8"))
             except GitHubError as ghe:
-                LOGGER.debug("Github Error trying to get file: "+filename)
-                LOGGER.debug(ghe.response.text)
-        LOGGER.debug(file_content_list)
+                logger.debug("Github Error trying to get file: "+filename)
+                logger.debug(ghe.response.text)
+        #logger.debug(file_content_list)
         return file_content_list
+
+
+class PersistenceToGist(Persistence):
+    """
+        This persistence class saves to Github gists
+
+        It uses the Flask session object and an optional param in the save
+        method to submit a set of changes instead of one change at a time
+
+        Expected kwargs for saving to/loading from a Github repository are:
+        * name : name of the file to write in/read from
+        * content : desired content of the file when writing
+        * message : when saving and/or deleting, commit message to be saved
+        to Github
+    """
+    @property
+    def session_compliant(self):
+        """
+            Session compliant: each modification is stored in flask session
+            object and every stacked changes can be submitted at once
+        """
+        return True
+
+    def save(self, **kwargs):
+        """
+            TODO : Docstring
+        """
+        return
+
+    def delete(self, **kwargs):
+        """
+            TODO : Docstring
+        """
+        return
+
+    def load(self, **kwargs):
+        """
+            TODO : Docstring
+        """
+        return
+
+    def list(self, **kwargs):
+        """
+            TODO : Docstring
+        """
+        return
--- a/src/catedit/static/css/style.css	Thu Dec 18 12:07:03 2014 +0100
+++ b/src/catedit/static/css/style.css	Mon Dec 29 18:01:09 2014 +0100
@@ -14,3 +14,8 @@
 .hidden{
   display:none;
 }
+
+.navbar-brand
+{
+  padding:8px;
+}
Binary file src/catedit/static/img/catedit_brand.png has changed
--- a/src/catedit/static/js/property_functions.js	Thu Dec 18 12:07:03 2014 +0100
+++ b/src/catedit/static/js/property_functions.js	Mon Dec 29 18:01:09 2014 +0100
@@ -86,26 +86,31 @@
         case "property_type_default":
           document.getElementById("literal-field").className = "hidden form-control";
           document.getElementById("uriref-category-field").className = "hidden form-control";
+          document.getElementById("uriref-category-field-text").className = "hidden";
           document.getElementById("uriref-link-field").className = "hidden form-control";
           break;
         case "literal":
           document.getElementById("literal-field").className = "visible form-control";
           document.getElementById("uriref-category-field").className = "hidden form-control";
+          document.getElementById("uriref-category-field-text").className = "hidden";
           document.getElementById("uriref-link-field").className = "hidden form-control";
           break;
         case "uriref-category":
           document.getElementById("literal-field").className = "hidden form-control";
           document.getElementById("uriref-category-field").className = "visible form-control";
+          document.getElementById("uriref-category-field-text").className = "visible";
           document.getElementById("uriref-link-field").className = "hidden form-control";
           break;
         case "uriref-link":
           document.getElementById("literal-field").className = "hidden form-control";
           document.getElementById("uriref-category-field").className = "hidden form-control";
+          document.getElementById("uriref-category-field-text").className = "hidden";
           document.getElementById("uriref-link-field").className = "visible form-control";
           break;
         default:
           document.getElementById("literal-field").className = "hidden form-control";
           document.getElementById("uriref-category-field").className = "hidden form-control";
+          document.getElementById("uriref-category-field-text").className = "hidden";
           document.getElementById("uriref-link-field").className = "hidden form-control";
       }
     }
--- a/src/catedit/templates/cateditor.html	Thu Dec 18 12:07:03 2014 +0100
+++ b/src/catedit/templates/cateditor.html	Mon Dec 29 18:01:09 2014 +0100
@@ -17,18 +17,24 @@
   <div class="navbar navbar-inverse" role="navigation">
       <div class="container">
         <div class="navbar-header">
+          <a class="navbar-brand" href="{{ url_for('cat_recap') }}">
+            <img alt="Brand" src="{{ url_for('static', filename='img/catedit_brand.png') }}" class="navbar-img" width="32" height="32">
+          </a>
           <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
             <span class="sr-only">Toggle navigation</span>
             <span class="icon-bar"></span>
             <span class="icon-bar"></span>
             <span class="icon-bar"></span>
           </button>
-          <a class="navbar-brand" id="logo" href="{{ url_for('cat_recap') }}"><span class="glyphicon glyphicon-picture"/></a>
         </div>
         <div class="collapse navbar-collapse">
           <ul class="nav navbar-nav">
-            <li><a href="{{ url_for('cat_recap') }}">Liste des catégories</a></li>
-            <li class="active"><a>Editeur de catégorie: {% if cat_id: %} Edition {% else %} Création {% endif %}</a></li>
+            <li><a href="#">Page d'accueil</a></li>
+            {% if session.get("user_logged", None) %}
+            <li><a href="{{ url_for('cat_recap') }}">Vue d'ensemble</a></li>
+            <li class="active"><a>{% if cat_id: %}Editer{% else %}Créer{% endif %} une catégorie</a></li>
+            <li><a href="{{ url_for('cat_modifs') }}">Mes modifications</a></li>
+            {% endif %}
           </ul>
           <div class="navbar-text navbar-right">
             {% if not session.get("user_logged", None)%} Non authentifié - <a href="{{ url_for('github_login') }}" class="navbar-link">S'authentifier</a>
@@ -51,7 +57,7 @@
       <span class="sr-only">Attention:</span>
       Vous devez être authentifié pour modifier les catégories.
     </div> {% endif %}
-    {% if form.label.errors or form.description.errors or form.commit_message.errors %}
+    {% if form.label.errors or form.description.errors %}
     <div class="alert alert-danger">
       <strong>
         <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
@@ -70,9 +76,6 @@
         {% if form.description.errors %}
           {% set description_placeholder="Champ obligatoire" %}
         {% endif %}
-        {% if form.commit_message.errors %}
-          {% set commit_placeholder="Champ obligatoire" %}
-        {% endif %}
         {{ form.label.label }} <br> {{ form.label(size=40, class="form-control", readonly=readonly, placeholder=label_placeholder) }} <br>
         {{ form.description.label }} <br> {{ form.description(size=150, class="form-control", readonly=readonly, placeholder=description_placeholder) }} <br>
         <label>Propriétés </label>
@@ -88,12 +91,16 @@
           </select>
           <input type="text" id="literal-field" class="hidden form-control">
           <input type="text" id="uriref-link-field" placeholder="http://my-example.com" class="hidden form-control">
-          <select id="uriref-category-field" class="hidden form-control">
+          <select class="hidden form-control" id="uriref-category-field">
             <option value="default" selected> Liste des catégories </option>
             {% for cat in cat_list %}
-            <option value="{{ cat.cat_id }}"> {{ cat.cat_label }} </option>
+              <option value="{{ cat.cat_id }}"> {{ cat.cat_label }} </option>
             {% endfor %}
           </select>
+          <div class="alert alert-warning hidden" role="alert" id="uriref-category-field-text">
+            <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+            Les catégories actuellement en suppression (voir onglet "Mes modifications") ne sont pas référençables
+          </div>
         </div>
         <div id="properties" class="row hidden">
           <div class="row">
@@ -107,17 +114,19 @@
                       {% if config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-category" %}
                         {% for cat in cat_list %}
                           {% if object == config["CATEGORY_NAMESPACE"]+cat.cat_id %}
-                            <input type="hidden" id="property_predicate{{ property_count }}" name="property_predicate" value="{{predicate}}"/>
-                            <input type="hidden" id="property_object{{ property_count }}" name="property_object" value="{{cat.cat_id}}"/>
-                            <td id="predicate_td{{ property_count }}">
-                              <strong>{{ config["PROPERTY_LIST"][predicate]["descriptive_label_fr"] }}</strong>
-                            </td>
-                            <td id="object_td{{ property_count }}">
-                              {{ cat.cat_label }}
-                            </td>
-                            <td id="delete_button_td{{ property_count }}">
-                              <input type="button" id="property_delete_button{{ property_count }}" class="btn btn-default property-delete-button" onClick="CatEditScripts.removeProperty({{ property_count }}, 'properties')" value="Supprimer">
-                            </td>
+                            {% if cat.cat_id not in deleted_cat_list %}
+                              <input type="hidden" id="property_predicate{{ property_count }}" name="property_predicate" value="{{predicate}}"/>
+                              <input type="hidden" id="property_object{{ property_count }}" name="property_object" value="{{cat.cat_id}}"/>
+                              <td id="predicate_td{{ property_count }}">
+                                <strong>{{ config["PROPERTY_LIST"][predicate]["descriptive_label_fr"] }}</strong>
+                              </td>
+                              <td id="object_td{{ property_count }}">
+                                {{ cat.cat_label }}
+                              </td>
+                              <td id="delete_button_td{{ property_count }}">
+                                <input type="button" id="property_delete_button{{ property_count }}" class="btn btn-default property-delete-button" onClick="CatEditScripts.removeProperty({{ property_count }}, 'properties')" value="Supprimer">
+                              </td>
+                            {% endif %}
                           {% endif %}
                         {% endfor %}
                       {% else %}
@@ -145,7 +154,6 @@
             </div>
           </div>
         </div><br>
-        {{ form.commit_message.label }} <br> {{ form.commit_message(size=150, class="form-control", readonly=readonly, placeholder=description_placeholder) }} <br>
         <br><input type="submit" value="Sauvegarder" class="btn btn-default">
     </form>
     {% if readonly %} </fieldset> {% endif %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/catedit/templates/catmodifs.html	Mon Dec 29 18:01:09 2014 +0100
@@ -0,0 +1,348 @@
+{% if not session["user_logged"] or not session["user_can_edit"] %}
+  {% set readonly="readonly" %}
+{% else %}
+  {% set readonly=False %}
+{% endif %}
+<!DOCTYPE html>
+<html lang="fr">
+  <head>
+    <meta charset="utf-8">
+      <meta http-equiv="X-UA-Compatible" content="IE=edge">
+      <meta name="viewport" content="width=device-width, initial-scale=1">
+      <title>Mes modifications</title>
+      <link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
+      <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
+      <script src="{{ url_for('static', filename='js/jquery-1.11.1.min.js') }}" language="Javascript" type="text/javascript"></script>
+      <script>
+        $(document).ready(function(){
+          {% for cat in cat_list %}
+            $("#properties_{{cat.cat_id}}").hide();
+            $("#info_button_{{cat.cat_id}}").click(function(){
+              $("#properties_{{cat.cat_id}}").slideToggle();
+            });
+            $("#delete_cat_{{cat.cat_id}}").hide();
+            $("#delete_button_{{cat.cat_id}}").click(function(){
+              $("#delete_cat_{{cat.cat_id}}").slideToggle();
+            });
+          {% endfor %}
+          {% for cat in modified_cat_list %}
+            $("#properties_modified_{{cat.cat_id}}").hide();
+            $("#info_button_modified_{{cat.cat_id}}").click(function(){
+              $("#properties_modified_{{cat.cat_id}}").slideToggle();
+            });
+            $("#delete_modified_{{cat.cat_id}}").hide();
+            $("#remove_modifs_modified_{{cat.cat_id}}").click(function(){
+              $("#delete_modified_{{cat.cat_id}}").slideToggle();
+            });
+          {% endfor %}
+          {% for cat in created_cat_list %}
+            $("#properties_created_{{cat.cat_id}}").hide();
+            $("#info_button_created_{{cat.cat_id}}").click(function(){
+              $("#properties_created_{{cat.cat_id}}").slideToggle();
+            });
+            $("#delete_created_{{cat.cat_id}}").hide();
+            $("#remove_modifs_created_{{cat.cat_id}}").click(function(){
+              $("#delete_created_{{cat.cat_id}}").slideToggle();
+            });
+          {% endfor %}
+        });
+      </script>
+  </head>
+  <body>
+    <div class="navbar navbar-inverse" role="navigation">
+        <div class="container">
+          <div class="navbar-header">
+            <a class="navbar-brand" href="{{ url_for('cat_recap') }}">
+              <img alt="Brand" src="{{ url_for('static', filename='img/catedit_brand.png') }}" class="navbar-img" width="32" height="32">
+            </a>
+            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+              <span class="sr-only">Toggle navigation</span>
+              <span class="icon-bar"></span>
+              <span class="icon-bar"></span>
+              <span class="icon-bar"></span>
+            </button>
+          </div>
+          <div class="collapse navbar-collapse">
+            <ul class="nav navbar-nav">
+              <li><a href="#">Page d'accueil</a></li>
+              {% if session.get("user_logged", None) %}
+              <li><a href="{{ url_for('cat_recap') }}">Vue d'ensemble</a></li>
+              <li><a href="{{ url_for('cat_editor') }}">Créer une catégorie</a></li>
+              <li class="active"><a>Mes modifications</a></li>
+              {% endif %}
+            </ul>
+            <div class="navbar-text navbar-right">
+              {% if not session.get("user_logged", None)%}Non authentifié - <a href="{{ url_for('github_login') }}" class="navbar-link">S'authentifier</a>
+              {% else %} Authentifié: {{ session["user_login"] }} - <a href="{{ url_for('logout') }}" class="navbar-link">Quitter</a>{% endif %}
+            </div>
+          </div>
+        </div>
+    </div>
+    <div class="container">
+      <h2> Créer une catégorie <a href="{{url_for('cat_editor')}}" class="btn btn-default {% if readonly %}disabled{% endif %}"><span class="glyphicon glyphicon-plus"/></a></h2>
+      <h2> Editer ou supprimer une catégorie </h2>
+      <table class="table table-bordered table-condensed">
+        <thead>
+          <tr class="active">
+            <th class="col-md-2"><b>Nom de la catégorie</b></th>
+            <th class="col-md-10" colspan="2"><b>Description de la catégorie</b></th>
+          </tr>
+        </thead>
+        <tbody>
+        {% if not session["user_logged"] %}
+        <tr>
+          <td class="col-md-12" colspan="3">
+            <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+            <span class="sr-only">Attention:</span>
+            Veuillez vous identifier pour visualiser les catégories
+          </td>
+        </tr>
+        {% else %}
+          {% if cat_list|length == 0 %}
+            <tr>
+              <td class="col-md-12" colspan="3">Aucune catégorie n'a été créée pour l'instant. {% if not readonly %}<a href="{{ url_for('cat_editor') }}">Créer une catégorie</a>{% endif %}</td>
+            </tr>
+          {% else %}
+            {% for cat in cat_list %}
+              <tr>
+                <td class="col-md-2">{{ cat.cat_label }}</td>
+                <td class="col-md-8">{{ cat.cat_description}}</td>
+                <td class="col-md-2 text-center">
+                  <button class="btn btn-default" id="info_button_{{ cat.cat_id }}"><span class="glyphicon glyphicon glyphicon-plus-sign"/></button>
+                  {% if cat.cat_id not in deleted_cat_list %}
+                  <a href="{{ url_for('cat_editor', cat_id=cat.cat_id)}}" class="btn btn-default"><span class="glyphicon glyphicon glyphicon-pencil"/></a>
+                  <button class="btn btn-default" id="delete_button_{{cat.cat_id}}"><span class="glyphicon glyphicon-trash"/></a>
+                  {% endif %}
+                </td>
+              </tr>
+                <tr>
+                  <td colspan="3">
+                    <div id="properties_{{cat.cat_id}}">
+                      <dl class="dl-horizontal">
+                      {% if cat.cat_properties|length == 0 %} <dt></dt><dd>Aucune autre propriété</dd>
+                      {% else %}
+                        {% for (predicate, object) in cat.cat_properties %}
+                          <dt>{{ config["PROPERTY_LIST"][predicate]["descriptive_label_fr"] }}</dt>
+                          <dd>
+                            {% if config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-category" %}
+                              {% for cat in cat_list %}
+                                {% if object == config["CATEGORY_NAMESPACE"]+cat.cat_id %}
+                                  {{ cat.cat_label }}
+                                {% endif %}
+                              {% endfor %}
+                            {% elif config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-link" %}
+                              <a href="{{ object }}">{{ object }}</a>
+                            {% else %}
+                              {{ object }}
+                            {% endif %}
+                          </dd>
+                        {% endfor %}
+                      {% endif %}
+                      </dl>
+                    </div>
+                    <div id="delete_cat_{{cat.cat_id}}">
+                      <form method="POST" action="{{ url_for('cat_modifs', delete_cat_id=cat.cat_id) }}" class="form-inline align-center">
+                        <fieldset {% if readonly %}disabled{% endif %}>
+                          <div class="input-group">
+                            <div class="input-group-addon">
+                              Vous devrez soumettre les changements pour appliquer la suppression. Supprimer une catégorie édite automatiquement les autres catégories qui y font référence.
+                            </div>
+                            <input type="submit" class="btn btn-default" value="Supprimer">
+                          </div>
+                        </fieldset>
+                      </form>
+                    </div>
+                  </td>
+                </tr>
+            {% endfor %}
+          {% endif %}
+        {% endif %}
+        </tbody>
+      </table>
+
+      <h2> Mes modifications </h2>
+      <table class="table table-bordered table-condensed">
+        <thead>
+          <tr class="active">
+            <th class="col-md-2"><b>Nom de la catégorie</b></th>
+            <th class="col-md-10" colspan="2"><b>Description de la catégorie</b></th>
+          </tr>
+        </thead>
+        <tbody>
+        {% if not session["user_logged"] %}
+        <tr>
+          <td class="col-md-12" colspan="3">
+            <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+            <span class="sr-only">Attention:</span>
+            Veuillez vous identifier pour visualiser les modifications
+          </td>
+        </tr>
+        {% else %}
+          <tr class="success">
+            <td class="col-md-12" colspan="3">
+              <b> Catégories ajoutées</b>
+            </td>
+          </tr>
+          {% if created_cat_list|length == 0 %}
+            <tr>
+              <td class="col-md-12" colspan="3">Aucune catégorie n'a été ajoutée pour l'instant.</td>
+            </tr>
+          {% else %}
+            {% for cat in created_cat_list %}
+              <tr class="success">
+                <td class="col-md-2">{{ cat.cat_label }}</td>
+                <td class="col-md-9">{{ cat.cat_description}}</td>
+                <td class="col-md-1 text-center">
+                  <button class="btn btn-default" id="info_button_created_{{ cat.cat_id }}"><span class="glyphicon glyphicon-plus-sign"/></button>
+                  <button class="btn btn-default" id="remove_modifs_created_{{ cat.cat_id }}"><span class="glyphicon glyphicon-remove-sign"/></button>
+                </td>
+              </tr>
+              <tr class="success">
+                <td colspan="3">
+                  <div id="properties_created_{{cat.cat_id}}">
+                    <dl class="dl-horizontal">
+                    {% if cat.cat_properties|length == 0 %} <dt></dt><dd>Aucune autre propriété</dd>
+                    {% else %}
+                      {% for (predicate, object) in cat.cat_properties %}
+                        <dt>{{ config["PROPERTY_LIST"][predicate]["descriptive_label_fr"] }}</dt>
+                        <dd>
+                          {% if config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-category" %}
+                            {% for cat in cat_list %}
+                              {% if object == config["CATEGORY_NAMESPACE"]+cat.cat_id %}
+                                {{ cat.cat_label }}
+                              {% endif %}
+                            {% endfor %}
+                          {% elif config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-link" %}
+                            <a href="{{ object }}">{{ object }}</a>
+                          {% else %}
+                            {{ object }}
+                          {% endif %}
+                        </dd>
+                      {% endfor %}
+                    {% endif %}
+                    </dl>
+                  </div>
+                  <div id="delete_created_{{cat.cat_id}}">
+                    <form method="POST" action="{{ url_for('cat_modifs', delete_modifs_id=cat.cat_id) }}" class="form-inline align-center">
+                      <fieldset {% if readonly %}disabled{% endif %}>
+                        <div class="input-group">
+                          <div class="input-group-addon">
+                            Vous allez supprimer les changements faits sur cette catégorie.
+                          </div>
+                          <input type="submit" class="btn btn-default" value="Supprimer">
+                        </div>
+                      </fieldset>
+                    </form>
+                  </div>
+                </td>
+              </tr>
+            {% endfor %}
+          {% endif %}
+          <tr class="warning">
+            <td class="col-md-12" colspan="3">
+              <b> Catégories modifiées</b>
+            </td>
+          </tr>
+          {% if modified_cat_list|length == 0 %}
+          <tr>
+            <td class="col-md-12" colspan="3">Aucune catégorie n'a été modifiée pour l'instant.</td>
+          </tr>
+          {% else %}
+            {% for cat in modified_cat_list %}
+              <tr class="warning">
+                <td class="col-md-2">{{ cat.cat_label }}</td>
+                <td class="col-md-9">{{ cat.cat_description}}</td>
+                <td class="col-md-1 text-center">
+                  <button class="btn btn-default" id="info_button_modified_{{ cat.cat_id }}"><span class="glyphicon glyphicon-plus-sign"/></button>
+                  <button class="btn btn-default" id="remove_modifs_modified_{{ cat.cat_id }}"><span class="glyphicon glyphicon-remove-sign"/></button>
+                </td>
+              </tr>
+              <tr class="warning">
+                <td colspan="3">
+                  <div id="properties_modified_{{cat.cat_id}}">
+                    <dl class="dl-horizontal">
+                    {% if cat.cat_properties|length == 0 %} <dt></dt><dd>Aucune autre propriété</dd>
+                    {% else %}
+                      {% for (predicate, object) in cat.cat_properties %}
+                        <dt>{{ config["PROPERTY_LIST"][predicate]["descriptive_label_fr"] }}</dt>
+                        <dd>
+                          {% if config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-category" %}
+                            {% for cat in cat_list %}
+                              {% if object == config["CATEGORY_NAMESPACE"]+cat.cat_id %}
+                                {{ cat.cat_label }}
+                              {% endif %}
+                            {% endfor %}
+                          {% elif config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-link" %}
+                            <a href="{{ object }}">{{ object }}</a>
+                          {% else %}
+                            {{ object }}
+                          {% endif %}
+                        </dd>
+                      {% endfor %}
+                    {% endif %}
+                    </dl>
+                  </div>
+                  <div id="delete_modified_{{cat.cat_id}}">
+                    <form method="POST" action="{{ url_for('cat_modifs', delete_modifs_id=cat.cat_id) }}" class="form-inline align-center">
+                      <fieldset {% if readonly %}disabled{% endif %}>
+                        <div class="input-group">
+                          <div class="input-group-addon">
+                            Vous allez supprimer les changements faits sur cette catégorie.
+                          </div>
+                          <input type="submit" class="btn btn-default" value="Supprimer">
+                        </div>
+                      </fieldset>
+                    </form>
+                  </div>
+                </td>
+              </tr>
+            {% endfor %}
+          {% endif %}
+
+          <tr class="danger">
+            <td class="col-md-12" colspan="3">
+              <b> Catégories supprimées</b>
+            </td>
+          </tr>
+          {% if deleted_cat_list|length == 0 %}
+          <tr>
+            <td class="col-md-12" colspan="3">Aucune catégorie n'a été supprimée pour l'instant.</td>
+          </tr>
+          {% else %}
+            {% for deleted_cat in deleted_cat_list %}
+              {% for existing_cat in cat_list %}
+                {% if existing_cat.cat_id == deleted_cat %}
+                  <tr class="danger">
+                    <td class="col-md-2">{{ existing_cat.cat_label }}</td>
+                    <td class="col-md-9"><i>Cette catégorie va être supprimée quand vous soumettrez vos modifications.</i></td>
+                    <td class="col-md-1 text-center">
+                      <form method="POST" action="{{ url_for('cat_modifs', delete_cat_id=deleted_cat) }}">
+                        <fieldset {% if readonly %}disabled{% endif %}>
+                          <input type="submit" class="btn btn-default" value="Restaurer">
+                        </fieldset>
+                      </form>
+                    </td>
+                  </tr>
+                {% endif %}
+              {% endfor %}
+            {% endfor %}
+          {% endif %}
+        {% endif %}
+        </tbody>
+      </table>
+      <h2> Soumettre mes changements </h2>
+      <div class="col-md-12">
+        <form method="POST" action="{{ url_for('cat_modifs')}}">
+          <fieldset {% if readonly %}disabled{% endif %}>
+            {{ commit_form.hidden_tag() }}
+            <div class="form-group">
+              {{ commit_form.commit_message.label }}
+              {{ commit_form.commit_message(size=40, class="form-control", readonly=readonly) }}
+            </div>
+            <button type="submit" class="btn btn-default">Soumettre modifications</button>
+          </fieldset>
+        </form><br>
+      </div>
+    </div>
+  </body>
+</html>
--- a/src/catedit/templates/catrecap.html	Thu Dec 18 12:07:03 2014 +0100
+++ b/src/catedit/templates/catrecap.html	Mon Dec 29 18:01:09 2014 +0100
@@ -9,7 +9,7 @@
     <meta charset="utf-8">
       <meta http-equiv="X-UA-Compatible" content="IE=edge">
       <meta name="viewport" content="width=device-width, initial-scale=1">
-      <title>Liste des catégories</title>
+      <title>Vue d'ensemble</title>
       <link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
       <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
       <script src="{{ url_for('static', filename='js/jquery-1.11.1.min.js') }}" language="Javascript" type="text/javascript"></script>
@@ -20,10 +20,6 @@
           $("#info_button_{{cat.cat_id}}").click(function(){
             $("#properties_{{cat.cat_id}}").slideToggle();
           });
-          $("#delete_cat_{{cat.cat_id}}").hide();
-          $("#delete_button_{{cat.cat_id}}").click(function(){
-            $("#delete_cat_{{cat.cat_id}}").slideToggle();
-          });
           {% endfor %}
         });
       </script>
@@ -32,20 +28,24 @@
     <div class="navbar navbar-inverse" role="navigation">
         <div class="container">
           <div class="navbar-header">
+            <a class="navbar-brand" href="{{ url_for('cat_recap') }}">
+              <img alt="Brand" src="{{ url_for('static', filename='img/catedit_brand.png') }}" class="navbar-img" width="32" height="32">
+            </a>
             <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
               <span class="sr-only">Toggle navigation</span>
               <span class="icon-bar"></span>
               <span class="icon-bar"></span>
               <span class="icon-bar"></span>
             </button>
-            <a class="navbar-brand" id="logo" href="{{ url_for('cat_recap') }}">
-              <span class="glyphicon glyphicon-picture"/>
-            </a>
           </div>
           <div class="collapse navbar-collapse">
             <ul class="nav navbar-nav">
-              <li class="active"><a>Liste des catégories</a></li>
-              <li><a href="{{ url_for('cat_editor') }}">Editeur de catégorie: Création</a></li>
+              <li><a href="#">Page d'accueil</a></li>
+              {% if session.get("user_logged", None) %}
+              <li class="active"><a>Vue d'ensemble</a></li>
+              <li><a href="{{ url_for('cat_editor') }}">Créer une catégorie</a></li>
+              <li><a href="{{ url_for('cat_modifs') }}">Mes modifications</a></li>
+              {% endif %}
             </ul>
             <div class="navbar-text navbar-right">
               {% if not session.get("user_logged", None)%}Non authentifié - <a href="{{ url_for('github_login') }}" class="navbar-link">S'authentifier</a>
@@ -66,63 +66,68 @@
       <table class="table table-striped table-bordered table-condensed">
         <thead>
           <tr>
-            <th class="col-md-2"><b>Category Label</b></th>
-            <th class="col-md-10" colspan="2"><b>Category Description</b></th>
+            <th class="col-md-2"><b>Nom de la catégorie</b></th>
+            <th class="col-md-10" colspan="2"><b>Description de la catégorie</b></th>
           </tr>
         </thead>
         <tbody>
-        {% if cat_list|length == 0 %}
-          <tr>
-            <td class="col-md-12" colspan="3">Aucune catégorie n'a été créée pour l'instant. {% if not readonly %}<a href="{{ url_for('cat_editor') }}">Créer une catégorie</a>{% endif %}</td>
-          </tr>
+        {% if not session["user_logged"] %}
+        <tr>
+          <td class="col-md-12" colspan="3">
+            <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+            <span class="sr-only">Attention:</span>
+            Veuillez vous identifier pour visualiser les catégories
+          </td>
+        </tr>
         {% else %}
-          {% for cat in cat_list %}
+          {% if cat_list|length == 0 %}
             <tr>
-              <td class="col-md-2">{{ cat.cat_label }}</td>
-              <td class="col-md-8">{{ cat.cat_description}}</td>
-              <td class="col-md-2 text-right">
-                <button class="btn btn-default" id="info_button_{{ cat.cat_id }}"><span class="glyphicon glyphicon-plus"/></button>
-                <a href="{{ url_for('cat_editor', cat_id=cat.cat_id)}}" class="btn btn-default"><span class="glyphicon glyphicon glyphicon-pencil"/></a>
-                <button class="btn btn-default" id="delete_button_{{cat.cat_id}}"><span class="glyphicon glyphicon-trash"/></a>
-              </td>
+              <td class="col-md-12" colspan="3">Aucune catégorie n'a été créée pour l'instant. {% if not readonly %}<a href="{{ url_for('cat_editor') }}">Créer une catégorie</a>{% endif %}</td>
             </tr>
+          {% else %}
+            {% for cat in cat_list %}
               <tr>
-                <td colspan="3">
-                  <div id="properties_{{cat.cat_id}}">
-                    <dl class="dl-horizontal">
-                    {% if cat.cat_properties|length == 0 %} <dt></dt><dd>Aucune autre propriété</dd>
-                    {% else %}
-                      {% for (predicate, object) in cat.cat_properties %}
-                        <dt>{{ config["PROPERTY_LIST"][predicate]["descriptive_label_fr"] }}</dt>
-                        <dd>
-                          {% if config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-category" %}
-                            {% for cat in cat_list %}
-                              {% if object == config["CATEGORY_NAMESPACE"]+cat.cat_id %}
-                                {{ cat.cat_label }}
-                              {% endif %}
-                            {% endfor %}
-                          {% elif config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-link" %}
-                            <a href="{{ object }}">{{ object }}</a>
-                          {% else %}
-                            {{ object }}
-                          {% endif %}
-                        </dd>
-                      {% endfor %}
-                    {% endif %}
-                    </dl>
-                  </div>
-                  <div id="delete_cat_{{cat.cat_id}}">
-                    <form method="POST" action="{{ url_for('cat_recap', delete_cat_id=cat.cat_id) }}" class="form-inline align-center">
-                      <input type="text" style="width: 90%" name="delete_message" class="form-control" placeholder="Message de suppression">
-                      <input type="submit" class="btn btn-default" value="Supprimer">
-                    </form>
-                  </div>
+                <td class="col-md-2">{{ cat.cat_label }}</td>
+                <td class="col-md-9">{{ cat.cat_description}}</td>
+                <td class="col-md-1 text-center">
+                  <button class="btn btn-default" id="info_button_{{ cat.cat_id }}"><span class="glyphicon glyphicon-plus-sign"/></button>
                 </td>
               </tr>
-          {% endfor %}
+                <tr>
+                  <td colspan="3">
+                    <div id="properties_{{cat.cat_id}}">
+                      <dl class="dl-horizontal">
+                      {% if cat.cat_properties|length == 0 %} <dt></dt><dd>Aucune autre propriété</dd>
+                      {% else %}
+                        {% for (predicate, object) in cat.cat_properties %}
+                          <dt>{{ config["PROPERTY_LIST"][predicate]["descriptive_label_fr"] }}</dt>
+                          <dd>
+                            {% if config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-category" %}
+                              {% for cat in cat_list %}
+                                {% if object == config["CATEGORY_NAMESPACE"]+cat.cat_id %}
+                                  {{ cat.cat_label }}
+                                {% endif %}
+                              {% endfor %}
+                            {% elif config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-link" %}
+                              <a href="{{ object }}">{{ object }}</a>
+                            {% else %}
+                              {{ object }}
+                            {% endif %}
+                          </dd>
+                        {% endfor %}
+                      {% endif %}
+                      </dl>
+                    </div>
+                  </td>
+                </tr>
+            {% endfor %}
+          {% endif %}
         {% endif %}
         </tbody>
-      </table><br>
+      </table>
+      {% if session.get("user_logged") %}
+        <h3> Consulter l'historique des modifications : <a href="https://github.com/{{config['REPOSITORY_OWNER']}}/{{config['REPOSITORY_NAME']}}/commits/master" class="btn btn-default"><span class="glyphicon glyphicon-list"/></a></h3>
+      {% endif %}
     </div>
   </body>
 </html>
--- a/src/catedit/views.py	Thu Dec 18 12:07:03 2014 +0100
+++ b/src/catedit/views.py	Mon Dec 29 18:01:09 2014 +0100
@@ -5,6 +5,7 @@
 
 from catedit import app, github
 from catedit.models import Category
+import catedit.persistence
 from flask import render_template, request, redirect, url_for, session
 from flask.ext.github import GitHubError
 from flask_wtf import Form
@@ -14,7 +15,7 @@
 from rdflib import Graph
 from io import StringIO
 
-LOGGER = app.logger
+logger = app.logger
 
 
 class NewCategoryMinimalForm(Form):
@@ -30,16 +31,19 @@
         "Description de la categorie (obligatoire)",
         validators=[DataRequired()]
         )
+
+class CommitForm(Form):
+    """
+        Custom form class for commiting changes
+    """
     commit_message = StringField(
-        "Message de soumission (Obligatoire)",
+        "Message de soumission (obligatoire)",
         validators=[DataRequired()]
         )
 
-
-@app.route('/catrecap/delete-<delete_cat_id>', methods=['POST'])
-@app.route('/', defaults={'delete_cat_id': None}, methods=['GET'])
-@app.route('/catrecap', defaults={'delete_cat_id': None}, methods=['GET'])
-def cat_recap(delete_cat_id):
+@app.route('/', methods=['GET'])
+@app.route('/catrecap', methods=['GET'])
+def cat_recap():
     """
         View that has a list of all categories available. Template is
         catrecap.html, located in src/templates/
@@ -47,40 +51,10 @@
         Note: it also handles category deletion from the same page.
     """
     cat_api_instance = CategoryAPI()
-    if delete_cat_id is None:
-        LOGGER.debug("Category to delete is None")
+    serialized_cat_list = []
+    if session.get("user_logged", None) is not None:
         serialized_cat_list = cat_api_instance.get()
-        # LOGGER.debug(serialized_cat_list)
-        cat_list = []
-        for serialized_cat in serialized_cat_list:
-            cat_rdf_graph = Graph()
-            cat_rdf_graph.parse(source=StringIO(serialized_cat),
-                                format='turtle')
-            cat = Category(graph=cat_rdf_graph)
-
-            cat_list.append({"cat_label": cat.label,
-                             "cat_description": cat.description,
-                             "cat_id": cat.cat_id,
-                             "cat_properties": cat.properties})
-            # LOGGER.debug(c.properties)
-        return render_template('catrecap.html',
-                               cat_list=cat_list)
-    else:
-        LOGGER.debug("Category "+delete_cat_id+" will be deleted.")
-        cat_api_instance.delete(delete_cat_id)
-        return redirect(url_for('cat_recap'))
-
-
-@app.route('/cateditor', methods=['GET', 'POST'])
-@app.route('/cateditor/<string:cat_id>', methods=['GET', 'POST'])
-def cat_editor(cat_id=None):
-    """
-        View that handles creation and edition of categories. Template is
-        cateditor.html, located in src/templates
-    """
-    cat_api_instance = CategoryAPI()
-
-    serialized_cat_list = cat_api_instance.get()
+    # logger.debug(serialized_cat_list)
     cat_list = []
     for serialized_cat in serialized_cat_list:
         cat_rdf_graph = Graph()
@@ -92,6 +66,36 @@
                          "cat_description": cat.description,
                          "cat_id": cat.cat_id,
                          "cat_properties": cat.properties})
+        # logger.debug(c.properties)
+    return render_template('catrecap.html',
+                           cat_list=cat_list)
+
+
+@app.route('/cateditor', methods=['GET', 'POST'])
+@app.route('/cateditor/<string:cat_id>', methods=['GET', 'POST'])
+def cat_editor(cat_id=None):
+    """
+        View that handles creation and edition of categories. Template is
+        cateditor.html, located in src/templates
+    """
+    cat_api_instance = CategoryAPI()
+    cat_list = []
+    deleted_cat_list=[]
+    if (session.get("user_logged",None) is not None):
+        serialized_cat_list = cat_api_instance.get()
+        deleted_cat_list = [element["name"] for \
+                            element in session["deleted_categories"]]
+    for serialized_cat in serialized_cat_list:
+        cat_rdf_graph = Graph()
+        cat_rdf_graph.parse(source=StringIO(serialized_cat),
+                            format='turtle')
+        cat = Category(graph=cat_rdf_graph)
+        if cat.cat_id not in [element["name"] for element \
+                              in session["deleted_categories"]]:
+            cat_list.append({"cat_label": cat.label,
+                             "cat_description": cat.description,
+                             "cat_id": cat.cat_id,
+                             "cat_properties": cat.properties})
 
     if cat_id is not None:
         specific_serialized_cat = cat_api_instance.get(cat_id)
@@ -111,7 +115,7 @@
                 TextAreaField("Description de la categorie",
                               validators=[DataRequired()],
                               default=cat.description))
-        LOGGER.debug("CatForm fields preset to "
+        logger.debug("CatForm fields preset to "
                      + cat.label + " and "
                      + cat.description)
 
@@ -123,18 +127,25 @@
                                    cat_id=cat.cat_id,
                                    cat_properties=cat.properties,
                                    form=cat_form,
-                                   cat_list=cat_list)
+                                   cat_list=cat_list,
+                                   deleted_cat_list=deleted_cat_list)
 
-        # PUT + cat_id = Submit edited cat
-        if cat_form.validate_on_submit():
-            cat_api_instance.put(cat_id)
-            return redirect(url_for('cat_recap'))
+        # PUT + cat_id = Submit edited cat, only if cat is not already
+        # in deleted categories
+        if cat_form.validate_on_submit() and \
+                cat.cat_id not in [element["name"] for element \
+                in session["deleted_categories"]]:
+            if (session.get("user_logged", None) is not None and
+                session.get("user_can_edit", False) is not False):
+                cat_api_instance.put(cat_id)
+            return redirect(url_for('cat_modifs'))
         else:
             return render_template('cateditor.html',
                                    cat_id=cat_id,
                                    cat_properties=cat.properties,
                                    form=cat_form,
-                                   cat_list=cat_list)
+                                   cat_list=cat_list,
+                                   deleted_cat_list=deleted_cat_list)
 
     else:
         setattr(NewCategoryMinimalForm,
@@ -152,17 +163,104 @@
         if request.method == 'GET':
             return render_template('cateditor.html',
                                    form=cat_form,
-                                   cat_list=cat_list)
+                                   cat_list=cat_list,
+                                   deleted_cat_list=deleted_cat_list)
 
         # POST seul = Submit created cat
         if cat_form.validate_on_submit():
-            cat_api_instance.post()
-            return redirect(url_for('cat_recap'))
+            if (session.get("user_logged", None) is not None and
+                session.get("user_can_edit", False) is not False):
+                cat_api_instance.post()
+            return redirect(url_for('cat_modifs'))
         else:
             return render_template('cateditor.html',
                                    form=cat_form,
-                                   cat_list=cat_list)
+                                   cat_list=cat_list,
+                                   deleted_cat_list=deleted_cat_list)
+
+@app.route('/catrecap/delete-modifs-<delete_modifs_id>',
+           defaults={'delete_cat_id': None},
+           methods=['POST'])
+@app.route('/catrecap/delete-<delete_cat_id>',
+           defaults={'delete_modifs_id': None},
+           methods=['POST'])
+@app.route('/catmodifs',
+           defaults={'delete_cat_id': None, 'delete_modifs_id': None},
+           methods=['GET', 'POST'])
+def cat_modifs(delete_cat_id, delete_modifs_id):
+    cat_list = []
+    modified_cat_list = []
+    deleted_cat_list = []
+    created_cat_list = []
+    cat_api_instance = CategoryAPI()
+    if delete_cat_id is None and delete_modifs_id is None:
+        commit_form = CommitForm(request.form)
+        if request.method=="GET":
+            if session.get("user_logged", None) is not None:
+                serialized_cat_list = cat_api_instance.get()
+                for serialized_cat in serialized_cat_list:
+                    cat_rdf_graph = Graph()
+                    cat_rdf_graph.parse(source=StringIO(serialized_cat),
+                                        format='turtle')
+                    cat = Category(graph=cat_rdf_graph)
 
+                    cat_list.append({"cat_label": cat.label,
+                                     "cat_description": cat.description,
+                                     "cat_id": cat.cat_id,
+                                     "cat_properties": cat.properties})
+                for modified_category in session["modified_categories"]:
+                    modified_cat_rdf_graph = Graph()
+                    modified_cat_rdf_graph.parse(
+                        source=StringIO(
+                            modified_category["content"]
+                        ),
+                        format='turtle'
+                    )
+                    modified_cat = Category(graph=modified_cat_rdf_graph)
+                    if modified_cat.cat_id in [existing_cat["cat_id"] \
+                            for existing_cat in cat_list]:
+                        modified_cat_list.append(
+                            {"cat_label": modified_cat.label,
+                             "cat_description": modified_cat.description,
+                             "cat_id": modified_cat.cat_id,
+                             "cat_properties": modified_cat.properties}
+                         )
+                    else:
+                        created_cat_list.append(
+                            {"cat_label": modified_cat.label,
+                             "cat_description": modified_cat.description,
+                             "cat_id": modified_cat.cat_id,
+                             "cat_properties": modified_cat.properties}
+                        )
+                deleted_cat_list = [element["name"] for element \
+                                   in session["deleted_categories"]]
+            return render_template('catmodifs.html',
+                                   cat_list=cat_list,
+                                   created_cat_list=created_cat_list,
+                                   modified_cat_list=modified_cat_list,
+                                   deleted_cat_list=deleted_cat_list,
+                                   commit_form=commit_form)
+        elif request.method=="POST":
+            logger.debug("Submitting changes")
+            if commit_form.validate_on_submit():
+                logger.debug("Form validates")
+                cat_api_instance.put()
+            return redirect(url_for('cat_modifs'))
+    else:
+        if request.method=="POST":
+            if delete_modifs_id is not None:
+                for element in session["modified_categories"]:
+                    if element["name"] == delete_modifs_id:
+                        logger.debug("Modification for "
+                                     + delete_modifs_id
+                                     + " will be deleted.")
+                        session["modified_categories"].remove(element)
+            if delete_cat_id is not None:
+                logger.debug("Category "+delete_cat_id+" will be deleted.")
+                if (session.get("user_logged", None) is not None and
+                    session.get("user_can_edit", False) is not False):
+                    cat_api_instance.delete(delete_cat_id)
+        return redirect(url_for('cat_modifs'))
 
 @app.route('/catedit-github-login')
 def github_login():
@@ -173,6 +271,11 @@
         in local files, used for debugging), creates a mock user named
         "FileEditUser"
     """
+    if getattr(catedit.persistence,
+               app.config["PERSISTENCE_METHOD"])().session_compliant is True:
+        session["save_user_changes"] = True
+        session["modified_categories"] = []
+        session["deleted_categories"] = []
     if app.config["PERSISTENCE_METHOD"] == "PersistenceToGithub":
         return github.authorize(scope="repo")
     elif app.config["PERSISTENCE_METHOD"] == "PersistenceToFile":
@@ -196,16 +299,16 @@
         repo_list = []
         repo_list = github.get("user/repos")
         for repo in repo_list:
-            LOGGER.debug(repo["name"])
+            logger.debug(repo["name"])
         session["user_can_edit"] = True
         if not any(repo["name"] == app.config["REPOSITORY_NAME"]
                    for repo in repo_list):
             session["user_can_edit"] = False
-        LOGGER.debug(session["user_can_edit"])
+        logger.debug(session["user_can_edit"])
     except GitHubError:
-        LOGGER.debug("error getting repos!")
+        logger.debug("error getting repos!")
 
-    LOGGER.debug(session["user_login"])
+    logger.debug(session["user_login"])
     return redirect(url_for('cat_recap'))
 
 
@@ -216,7 +319,7 @@
         making authenticated requests
     """
     if session.get("user_logged", None):
-        # LOGGER.debug("I made an authentified request")
+        # logger.debug("I made an authentified request")
         return session["user_code"]
 
 
@@ -233,4 +336,7 @@
     session["user_logged"] = None
     session["user_login"] = None
     session["user_can_edit"] = None
+    session["save_user_changes"] = None
+    session["modified_categories"] = []
+    session["deleted_categories"] = []
     return redirect(url_for('cat_recap'))