# HG changeset patch # User Nicolas DURAND # Date 1419872469 -3600 # Node ID fc63b1a3d2ef140c9d57b7ac0fefbbcd793b3092 # Parent ea5f985156b148cae63007738d59cfc58e129331 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). diff -r ea5f985156b1 -r fc63b1a3d2ef src/catedit/__init__.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) diff -r ea5f985156b1 -r fc63b1a3d2ef src/catedit/api.py --- 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 diff -r ea5f985156b1 -r fc63b1a3d2ef src/catedit/main.py --- 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, \ diff -r ea5f985156b1 -r fc63b1a3d2ef src/catedit/models.py --- 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() diff -r ea5f985156b1 -r fc63b1a3d2ef src/catedit/persistence.py --- 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 diff -r ea5f985156b1 -r fc63b1a3d2ef src/catedit/static/css/style.css --- 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; +} diff -r ea5f985156b1 -r fc63b1a3d2ef src/catedit/static/img/catedit_brand.png Binary file src/catedit/static/img/catedit_brand.png has changed diff -r ea5f985156b1 -r fc63b1a3d2ef src/catedit/static/js/property_functions.js --- 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"; } } diff -r ea5f985156b1 -r fc63b1a3d2ef src/catedit/templates/cateditor.html --- 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 @@