Pylint + pep8 + adapted code to Python 3 + added support for authentication when persistence is set to PersistenceToFile + cleaning up settings.py/config.py.tmpl and updated readme
--- a/.hgignore Tue Dec 16 11:14:55 2014 +0100
+++ b/.hgignore Wed Dec 17 17:21:06 2014 +0100
@@ -4,3 +4,5 @@
*.pyc
src/catedit/config.py
src/catedit/log/
+run/log/
+run/files/
\ No newline at end of file
--- a/Readme.md Tue Dec 16 11:14:55 2014 +0100
+++ b/Readme.md Wed Dec 17 17:21:06 2014 +0100
@@ -28,7 +28,11 @@
* HOST : The host on which the app will run, default is "0.0.0.0" for development purpose as "localhost" won't work with vagrant and windows.
* LOGGING : Wether or not the app write log files
+* LOGGING_LEVEL : if LOGGING is True, indicates what logging level will be used
* DEBUG : Wether or not the app is on debug mode. If True, then each modification of a given file will restart the server allowing for easy development.
+* SECRET_KEY : Secret key to secure WTForms
+* PERSISTENCE_METHOD : What Persistence method will be used. Currently, either "PersistenceToFile" or "PersistenceToGithub"
+* FILE_SAVE_DIRECTORY : If using PersistenceToFile, directory where the turtle files will be saved locally
* REPOSITORY_NAME : The name of the repository your app will use to store categories
* REPOSITORY_OWNER : The name of the owner of the repository (typically a "admin" user)
* CATEGORIES_PATH : Where on the repository categories will be stored. Default to /categories
@@ -44,7 +48,7 @@
## Additional/Advanced informations ##
-** Changing the property list : ** If you want to change the property list available to users editing/creating categories, you have to put the following entry in config.py
+** Changing the property list : ** If you want to change the property list available to users editing/creating categories, you have to edit the following entry in config.py
* PROPERTY_LIST : My list of properties ...
--- a/dev/modules/sysconfig/manifests/packages.pp Tue Dec 16 11:14:55 2014 +0100
+++ b/dev/modules/sysconfig/manifests/packages.pp Wed Dec 17 17:21:06 2014 +0100
@@ -7,13 +7,14 @@
'libjpeg8-dev',
'libxslt-dev',
'libxml2',
- 'mercurial'
+ 'mercurial',
+ 'pylint'
]
-
+
package { $catedit_pkgs: ensure => "installed" }
#upgrade setuptools
exec { '/usr/bin/easy_install --upgrade setuptools': require => Package[$catedit_pkgs]}
-
+
}
--- a/dev/modules/sysconfig/manifests/virtualenv.pp Tue Dec 16 11:14:55 2014 +0100
+++ b/dev/modules/sysconfig/manifests/virtualenv.pp Wed Dec 17 17:21:06 2014 +0100
@@ -15,7 +15,7 @@
provider => 'shell',
require => Exec['easy_install_pip'];
'create_virtualenv':
- command => "virtualenv ${path_to_virtualenv}",
+ command => "virtualenv -p `which python3.4` ${path_to_virtualenv}",
timeout => 2400,
returns => [ 0, 100 ],
provider => 'shell',
--- a/src/catedit/__init__.py Tue Dec 16 11:14:55 2014 +0100
+++ b/src/catedit/__init__.py Wed Dec 17 17:21:06 2014 +0100
@@ -1,15 +1,19 @@
+"""
+__init__.py:
+module main file used to configure the Flask app
+"""
from flask import Flask, session
from flask.ext.github import GitHub
from flask.ext.cache import Cache
-from settings import *
-from config import *
-from logging import *
+from settings import AppSettings
+from config import AppConfig
+from logging import FileHandler, Formatter
# set up app and database
app = Flask(__name__)
-app.config.from_object(appSettings)
+app.config.from_object(AppSettings)
cache = Cache(app, config={"CACHE_TYPE": "simple"})
-app.config.from_object(appConfig)
+app.config.from_object(AppConfig)
github = GitHub(app)
--- a/src/catedit/api.py Tue Dec 16 11:14:55 2014 +0100
+++ b/src/catedit/api.py Wed Dec 17 17:21:06 2014 +0100
@@ -1,82 +1,101 @@
-from flask.ext.restful import Resource, fields, Api, reqparse
+"""
+api.py:
+contains the api that links the views (views.py) to the model (models.py) and
+its persistence method (persistence.py). As it only trades rdf graphs
+serializations strings, it isn't bound to one specific view
+"""
+
+from flask.ext.restful import Resource, Api, reqparse
from flask import request
from catedit import app, cache
-from models import Category, CategoryManager
-from rdflib import Graph, RDF
-from utils import *
-from settings import *
-from config import *
-import os
+from catedit.models import Category, CategoryManager
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):
- # returns category cat_id or all if cat_id is None
+ """
+ The API to create and edit categories, returns rdf graph serializations
+ when successful
+ """
@classmethod
@cache.memoize(timeout=3600)
- def get(self, cat_id=None):
+ def get(cls, cat_id=None):
+ """
+ API to get the category of id cat_id, or if cat_id is None,
+ get the list of category
+ """
cat_manager_instance = CategoryManager()
if cat_id is not None:
- c = cat_manager_instance.load_cat(cat_id)
- return c.cat_graph.serialize(format='turtle')
+ cat = cat_manager_instance.load_cat(cat_id)
+ return cat.cat_graph.serialize(format='turtle').decode("utf-8")
else:
response = []
- for c in cat_manager_instance.list_cat():
- response.append(c.cat_graph.serialize(format='turtle'))
+ for cat in cat_manager_instance.list_cat():
+ response.append(cat.cat_graph.serialize(format='turtle')
+ .decode("utf-8"))
return response
# update category cat_id
@classmethod
- def put(self, cat_id):
- args = cat_parser.parse_args()
+ def put(cls, cat_id):
+ """
+ API to edit the category of id cat_id
+ """
+ args = CAT_PARSER.parse_args()
cat_manager_instance = CategoryManager()
new_property_list = []
- logger.debug(args["property_predicate"])
- logger.debug(args["property_object"])
- 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["ONTOLOGY_NAMESPACE"] + property_object
- logger.debug(property_object_to_append)
- new_property_list.append((property_predicate,
- property_object_to_append))
- logger.debug(new_property_list)
- c = cat_manager_instance.load_cat(cat_id)
- c.edit_category(new_description=args["description"],
- new_label=args["label"],
- new_other_properties=new_property_list)
- cat_manager_instance.save_cat(c, message=args["commit_message"])
- logger.debug("put id: "+c.cat_id)
+ 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 c.cat_graph.serialize(format='turtle'), 200
+ return cat.cat_graph.serialize(format='turtle').decode("utf-8"), 200
# Maybe not send the whole cat back, see if it's worth it
@classmethod
- def post(self):
- args = cat_parser.parse_args()
+ def post(cls):
+ """
+ API to create a new category
+ """
+ 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')):
@@ -85,30 +104,34 @@
[property_predicate]
["object_type"]) == "uriref-category":
property_list.append((property_predicate,
- app.config["ONTOLOGY_NAMESPACE"]
+ app.config["CATEGORY_NAMESPACE"]
+ property_object))
else:
property_list.append((property_predicate,
property_object))
- logger.debug(property_list)
- c = Category(label=args["label"],
- description=args["description"],
- other_properties=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.save_cat(c, message=args["commit_message"])
- logger.debug("post id: "+c.cat_id)
+ cat_manager_instance.save_cat(cat, message=args["commit_message"])
+ LOGGER.debug("post id: "+cat.cat_id)
cache.clear()
- return c.cat_graph.serialize(format='turtle'), 201
+ return cat.cat_graph.serialize(format='turtle').decode("utf-8"), 201
@classmethod
- def delete(self, cat_id):
+ def delete(cls, cat_id):
+ """
+ API to delete the category of id cat_id
+ """
+ args = CAT_PARSER.parse_args()
cat_manager_instance = CategoryManager()
- if not request.form["delete_message"]:
+ if args["delete_message"] is None:
message = "Deleting category "+cat_id
else:
- message = request.form["delete_message"]
+ 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/config.py.tmpl Tue Dec 16 11:14:55 2014 +0100
+++ b/src/catedit/config.py.tmpl Wed Dec 17 17:21:06 2014 +0100
@@ -1,17 +1,83 @@
-# Overwriting settings
-class appConfig(object):
+"""
+config.py:
+Contains all settings that can change depending on the deployment environment
+"""
+
+from rdflib import URIRef, RDF, RDFS, Literal
+from rdflib.namespace import SKOS
+
+class AppConfig(object):
+
+ # Debug and running settings
- DEBUG = False
+ HOST = "0.0.0.0"
+ DEBUG = True
+ LOGGING_LEVEL = "DEBUG"
+
+ # WTForms settings
+
+ SECRET_KEY = 'totally-secret-key'
+
+ # Saving file for local persistence
+ FILE_SAVE_DIRECTORY = "../../run/files/"
+
+ # Logging config
+ LOG_FILE_PATH = "../../run/log/log.txt"
LOGGING = False
- SERVER_NAME = "0.0.0.0"
- # Github repository settings
+ # Github repository config
- REPOSITORY_NAME = "habitabilite-prototype"
+ REPOSITORY_NAME = "catedit-dev-testing"
REPOSITORY_OWNER = "catedit-system"
CATEGORIES_PATH = "categories/"
# Github parameters
- GITHUB_CLIENT_ID = "3e31f3b000a4914f75ef"
- GITHUB_CLIENT_SECRET = "bc17eb0ec11385628c2e75aacb5ff8ef5f29e490"
+ GITHUB_CLIENT_ID = "github-id-placeholder"
+ GITHUB_CLIENT_SECRET = "github-secret-placeholder"
+
+ # Property List
+
+ PROPERTY_LIST = {
+ "subClassOf": {
+ "descriptive_label_fr": "Sous-classe de",
+ "descriptive_label_en": "Subclass of",
+ "object_type": "uriref-category",
+ "rdflib_class": RDFS.subClassOf,
+ "object_rdflib_class": URIRef,
+ },
+ "value": {
+ "descriptive_label_fr": "Valeur",
+ "descriptive_label_en": "Value",
+ "object_type": "literal",
+ "rdflib_class": RDF.value,
+ "object_rdflib_class": Literal,
+ },
+ "type": {
+ "descriptive_label_fr": "Type",
+ "descriptive_label_en": "Type",
+ "object_type": "uriref-category",
+ "rdflib_class": RDF.type,
+ "object_rdflib_class": URIRef,
+ },
+ "resource": {
+ "descriptive_label_fr": "Ressource",
+ "descriptive_label_en": "Resource",
+ "object_type": "uriref-link",
+ "rdflib_class": RDFS.Resource,
+ "object_rdflib_class": URIRef,
+ },
+ "related": {
+ "descriptive_label_fr": "En relation avec",
+ "descriptive_label_en": "Related to",
+ "object_type": "uriref-category",
+ "rdflib_class": SKOS.related,
+ "object_rdflib_class": URIRef,
+ }
+ }
+
+ # Category persistence parameters
+ # "PersistenceToFile" : will save categories to files on system
+ # "PersistenceToGithub" : will save categories to files on Github
+
+ PERSISTENCE_METHOD = "PersistenceToGithub"
--- a/src/catedit/main.py Tue Dec 16 11:14:55 2014 +0100
+++ b/src/catedit/main.py Wed Dec 17 17:21:06 2014 +0100
@@ -1,8 +1,12 @@
-from catedit import app
+"""
+main.py:
+script that is used to boot up the application
+"""
-from api import api
-from models import *
-from views import *
+from catedit import app
+from catedit.api import api
+from catedit.views import cat_editor, cat_recap, github_login, \
+ github_callback, logout
if __name__ == '__main__':
app.run(host=app.config["HOST"])
--- a/src/catedit/models.py Tue Dec 16 11:14:55 2014 +0100
+++ b/src/catedit/models.py Wed Dec 17 17:21:06 2014 +0100
@@ -1,34 +1,44 @@
-from flask import Flask, request
-from flask.ext.restful import fields
-from rdflib import Graph, RDF, RDFS, BNode, Literal, URIRef
-from uuid import uuid4
+"""
+models.py:
+contains the "principal" objects that will be manipulated by the application:
+* categories
+* helper classes to manage category life cycle
+"""
+
+from rdflib import Graph, RDF, RDFS, Literal, URIRef
+# from uuid import uuid4
from io import StringIO
from slugify import slugify
-import persistence
from catedit import app
-
-logger = app.logger
+import catedit.persistence
-"""
-Namespace: ld.iri-research.org/ontology/categorisation/#
-Category URI: ld.iri-research.org/ontology/categorisation/#cat_id
-Category Class:
-label is the rdf label of the category
-description is the description of the category
-other_properties is a dictionnary containing every other supported property
-as defined in the PROPERTY_LIST dict in settings.py
-"""
+LOGGER = app.logger
class Category(object):
+ """
+ Category Class:
+
+ Init:
+ * label is the rdf label of the category, unique and non-empty
+ * description is the description of the category, unique and non-empty
+ * other_properties is a dictionnary containing every other supported
+ property as defined in the PROPERTY_LIST dict in settings.py
+ * graph is used if we want to create the Category from a turtle
+ rdf graph
+
+ Additional info:
+ * cat_id is a generated, hidden id that uniquely identify
+ a given category
+ """
def __init__(self, label=None, description=None,
other_properties=None, graph=None):
- if not(graph):
+ if not graph:
# cat_id = uuid4().hex - Alternate method of generating ids
- cat_id = "category_id_"+slugify(bytes(label))
+ cat_id = "category_id_"+slugify(label)
self.cat_graph = Graph()
- self.this_category = URIRef(app.config["ONTOLOGY_NAMESPACE"] +
+ self.this_category = URIRef(app.config["CATEGORY_NAMESPACE"] +
cat_id)
self.cat_graph.add((self.this_category, RDF.ID, Literal(cat_id)))
@@ -53,12 +63,15 @@
else:
self.cat_graph = graph
- self.this_category = self.cat_graph \
- .subjects(predicate=RDF.ID) \
- .next() # Warning: not foolproof
+ # Warning: not foolproof
+ self.this_category = next(self.cat_graph
+ .subjects(predicate=RDF.ID))
@property
def label(self):
+ """
+ Returns category label
+ """
return_value = self.cat_graph.value(self.this_category, RDFS.label)
if return_value is None:
return None
@@ -67,6 +80,9 @@
@property
def description(self):
+ """
+ Returns category description
+ """
return_value = \
self.cat_graph.value(self.this_category, RDF.Description)
if return_value is None:
@@ -76,10 +92,16 @@
@property
def cat_id(self):
+ """
+ Returns category id
+ """
return self.cat_graph.value(self.this_category, RDF.ID).toPython()
@property
def properties(self):
+ """
+ Returns category property list
+ """
property_list = []
for key in app.config["PROPERTY_LIST"]:
for obj in self.cat_graph \
@@ -94,10 +116,19 @@
new_description=False,
new_other_properties=False):
"""
- Checks if there is a new label and if so apply changes
+ 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
"""
- if new_label is not (False or None) and \
- new_label != self.label:
+
+ if (new_label is not False) and \
+ (new_label is not None) and \
+ (new_label != self.label):
self.cat_graph.remove((self.this_category,
RDFS.label,
self.cat_graph.label(self.this_category)))
@@ -105,11 +136,9 @@
RDFS.label,
Literal(new_label)))
- """
- Checks if there is a new description and if so apply changes
- """
- if new_description is not (False or None) and \
- new_description != self.description:
+ if (new_description is not False) and \
+ (new_description is not None) and \
+ (new_description != self.description):
self.cat_graph.remove((self.this_category,
RDF.Description,
self.cat_graph.value(self.this_category,
@@ -118,27 +147,23 @@
RDF.Description,
Literal(new_description)))
- """
- Checks if there is a new property list and if so apply changes
- (can be [] so it deletes everything)
- """
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("before suppressing properties: ")
+ LOGGER.debug(self.properties)
+ LOGGER.debug("will replace with: ")
+ LOGGER.debug(new_other_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)
+ 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"]
@@ -149,40 +174,50 @@
["object_rdflib_class"](obj)))
-"""
-CategoryManager class
-
-This class deals with creation and loading of Category objects
-
-Persistence method is set in config files
-"""
-
-
class CategoryManager(object):
+ """
+ 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 load_cat(self, cat_id):
- p = getattr(persistence, app.config["PERSISTENCE_METHOD"])()
- cat_serial = p.load(name=cat_id)
+ """
+ Loads a category from its id
+ """
+ persistence = getattr(catedit.persistence,
+ app.config["PERSISTENCE_METHOD"])()
+ cat_serial = 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):
- p = getattr(persistence, app.config["PERSISTENCE_METHOD"])()
- p.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
+ """
+ persistence = getattr(catedit.persistence,
+ app.config["PERSISTENCE_METHOD"])()
+ 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
+ """
cat_list = self.list_cat()
for cat in cat_list:
- if cat.cat_id != app.config["ONTOLOGY_NAMESPACE"]+deleted_cat_id:
+ 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["ONTOLOGY_NAMESPACE"] +
+ (obj == (app.config["CATEGORY_NAMESPACE"] +
deleted_cat_id))
):
new_property_list_for_cat.append((predicate, obj))
@@ -193,13 +228,18 @@
cat,
message=message+", cleaning up other properties"
)
- p = getattr(persistence, app.config["PERSISTENCE_METHOD"])()
- p.delete(name=deleted_cat_id, message=message)
+ persistence = getattr(catedit.persistence,
+ app.config["PERSISTENCE_METHOD"])()
+ persistence.delete(name=deleted_cat_id, message=message)
def list_cat(self):
- p = getattr(persistence, app.config["PERSISTENCE_METHOD"])()
- cat_serial_list = p.list()
- logger.debug(cat_serial_list)
+ """
+ Lists all categories available
+ """
+ persistence = getattr(catedit.persistence,
+ app.config["PERSISTENCE_METHOD"])()
+ cat_serial_list = persistence.list()
+ # LOGGER.debug(cat_serial_list)
cat_list = []
for cat_serial in cat_serial_list:
loaded_cat_graph = Graph()
@@ -207,6 +247,6 @@
source=StringIO(cat_serial),
format='turtle'
)
- c = Category(graph=loaded_cat_graph)
- cat_list.append(c)
+ cat = Category(graph=loaded_cat_graph)
+ cat_list.append(cat)
return cat_list
--- a/src/catedit/persistence.py Tue Dec 16 11:14:55 2014 +0100
+++ b/src/catedit/persistence.py Wed Dec 17 17:21:06 2014 +0100
@@ -1,76 +1,132 @@
+"""
+persistence.py:
+contains what we need to make a (category) objects (see models.py) persist
+beyond application scope and load them from outside sources
+
+For now we can save, load and list
+* to/from a file (using a turtle rdf graph serialization for categories)
+* to/from a github repository (using a turtle rdf graph serialization
+for categories)
+"""
from abc import ABCMeta, abstractmethod
-from catedit import app
+from catedit import app, github
from base64 import b64encode, b64decode
+from flask.ext.github import GitHubError
import os
import json
+LOGGER = app.logger
+
class Persistence:
+ """
+ Meta-class for all persistence classes
+ """
__metaclass__ = ABCMeta
@abstractmethod
def save(self, **kwargs):
+ """
+ Abstract - Saves object
+ """
return
@abstractmethod
def delete(self, **kwargs):
+ """
+ Abstract - Deletes object
+ """
return
@abstractmethod
def load(self, **kwargs):
+ """
+ Abstract - Loads object
+ """
return
@abstractmethod
def list(self, **kwargs):
+ """
+ Abstract - Lists objects
+ """
return
-"""
-kwargs for saving to a file should be
-
-pathDir = directory to save the file in
-fileName = name of the file to write in
-content = desired content of the file
-
-There is still adjustments to do on this
-"""
-
class PersistenceToFile(Persistence):
+ """
+ Persistence Class for saving to a local file (see config.py
+ for tweaking)
+
+ Expected kwargs for saving to/loading from a file are:
+ * name : name of the file to write in/read from
+ * content : desired content of the file when writing
+ """
def save(self, **kwargs):
+ """
+ Saves to a file
+ """
path_to_save = app.config["FILE_SAVE_DIRECTORY"]+kwargs["name"]
- file = open(path_to_save, 'wb')
- file.write(kwargs["content"])
- file.close()
+ file_to_save = open(path_to_save, 'wb')
+ file_to_save.write(kwargs["content"])
+ file_to_save.close()
def load(self, **kwargs):
+ """
+ Loads from a file
+ """
path_to_load = app.config["FILE_SAVE_DIRECTORY"]+kwargs["name"]
- file = open(path_to_load, 'rb')
- file_content = file.read()
- file.close()
+ file_to_load = open(path_to_load, 'rb')
+ file_content = file_to_load.read()
+ file_to_load.close()
return file_content
def delete(self, **kwargs):
+ """
+ Deletes a file
+ """
path_to_delete = app.config["FILE_SAVE_DIRECTORY"]+kwargs["name"]
os.remove(path_to_delete)
# IDEA: return { file_name: file_content } type dict
def list(self, **kwargs):
+ """
+ Lists all files in file directory (as set in config.py)
+ """
file_content_list = []
for file_name in os.listdir(app.config["FILE_SAVE_DIRECTORY"]):
if not file_name or file_name[0] == ".":
continue
- file = open(app.config["FILE_SAVE_DIRECTORY"]+file_name)
- file_content = file.read()
- file.close()
+ path_to_load = open(app.config["FILE_SAVE_DIRECTORY"]+file_name)
+ 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
class PersistenceToGithub(Persistence):
+ """
+ Persistence Class for saving to/loading from a Github repository (see
+ config.py for tweaks)
+
+ 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 or deleting, commit message to be saved
+ to Github
+ """
def save(self, **kwargs):
- # logger.debug(kwargs["content"])
- request_data = {"content": b64encode(kwargs["content"]),
+ """
+ Saves to a Github repository
+
+ 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
+ """
+ # LOGGER.debug(kwargs["content"])
+ request_data = {"content": str(b64encode(kwargs["content"]), "ascii"),
"message": kwargs["message"]}
try:
filedict = github.get("repos/"
@@ -81,8 +137,14 @@
+ kwargs["name"])
request_data["sha"] = filedict["sha"]
except GitHubError:
- pass
- # logger.debug(json.dumps(request_data))
+ 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(json.dumps(request_data))
try:
github.request('PUT',
"repos/"
@@ -93,9 +155,15 @@
+ kwargs["name"],
data=json.dumps(request_data))
except GitHubError:
- pass
+ 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(GitHubError.response.text)
def load(self, **kwargs):
+ """
+ Loads from a Github repository
+ """
try:
filedict = github.get("repos/"
+ app.config["REPOSITORY_OWNER"]+"/"
@@ -103,12 +171,18 @@
+ "/contents/"
+ app.config["CATEGORIES_PATH"]
+ kwargs["name"])
- file_content = b64decode(filedict["content"])
+ file_content = str(b64decode(filedict["content"]), "utf-8")
except GitHubError:
- pass
+ 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(GitHubError.response.text)
return file_content
def delete(self, **kwargs):
+ """
+ Deletes from a Github repository
+ """
request_data = {"message": kwargs["message"]}
try:
filedict = github.get("repos/"
@@ -119,7 +193,9 @@
+ kwargs["name"])
request_data["sha"] = filedict["sha"]
except GitHubError:
- pass
+ LOGGER.debug("Github Error trying to get sha for \
+ file: "+kwargs["name"])
+ LOGGER.debug(GitHubError.response.text)
try:
github.request('DELETE',
@@ -129,9 +205,13 @@
+ kwargs["name"],
data=json.dumps(request_data))
except GitHubError:
- pass
+ LOGGER.debug("Github Error trying to delete file: "+kwargs["name"])
+ LOGGER.debug(GitHubError.response.text)
def list(self, **kwargs):
+ """
+ Lists all files in the github repository (as set in config.py)
+ """
filenames_list = []
try:
files_in_repo = github.get("repos/"
@@ -139,10 +219,17 @@
+ app.config["REPOSITORY_NAME"]
+ "/contents/"
+ app.config["CATEGORIES_PATH"])
- filenames_list = [file["name"] for file in files_in_repo]
- # logger.debug(filenames_list)
+ filenames_list = [github_file["name"]
+ for github_file in files_in_repo]
+ # LOGGER.debug(filenames_list)
except GitHubError:
- pass
+ 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 \
+ may not have access to the repository or it doesn't \
+ exist")
+ LOGGER.debug(GitHubError.response.text)
+
file_content_list = []
for filename in filenames_list:
try:
@@ -152,8 +239,10 @@
+ "/contents/"
+ app.config["CATEGORIES_PATH"]
+ filename)
- file_content_list.append(b64decode(filedict["content"]))
+ file_content_list.append(str(b64decode(filedict["content"]),
+ "utf-8"))
except GitHubError:
- pass
- # logger.debug(file_content_list)
+ LOGGER.debug("Github Error trying to get file: "+filename)
+ LOGGER.debug(GitHubError.response.text)
+ LOGGER.debug(file_content_list)
return file_content_list
--- a/src/catedit/settings.py Tue Dec 16 11:14:55 2014 +0100
+++ b/src/catedit/settings.py Wed Dec 17 17:21:06 2014 +0100
@@ -1,83 +1,15 @@
-from rdflib import URIRef, RDF, RDFS, Literal
-from rdflib.namespace import SKOS
-
-
-class appSettings(object):
+"""
+settings.py:
+Contains all settings that are needed but are not to be changed
+"""
- # Debug and running settings
-
- HOST = "0.0.0.0"
- DEBUG = True
- LOGGING_LEVEL = "DEBUG"
+class AppSettings(object):
# WTForms settings
WTF_CSRF_ENABLED = True
- SECRET_KEY = 'totally-secret-key'
# RDF Namespace and prefixes
ONTOLOGY_NAMESPACE = "http://ld.iri-research.org/ontology/categorisation#"
- CATEGORY_NAMESPACE = "http://ld.iri-research.org/categorisation/category/"
-
- # Saving file for local persistence
- FILE_SAVE_DIRECTORY = "../../run/files/"
-
- # Logging config
- LOG_FILE_PATH = "../../run/log/log.txt"
- LOGGING = False
-
- # Github repository config
-
- REPOSITORY_NAME = "catedit-dev-testing"
- REPOSITORY_OWNER = "catedit-system"
- CATEGORIES_PATH = "categories/"
-
- # Github parameters
-
- GITHUB_CLIENT_ID = "3e31f3b000a4914f75ef"
- GITHUB_CLIENT_SECRET = "bc17eb0ec11385628c2e75aacb5ff8ef5f29e490"
-
- # Property List
-
- PROPERTY_LIST = {
- "subClassOf": {
- "descriptive_label_fr": "Sous-classe de",
- "descriptive_label_en": "Subclass of",
- "object_type": "uriref-category",
- "rdflib_class": RDFS.subClassOf,
- "object_rdflib_class": URIRef,
- },
- "value": {
- "descriptive_label_fr": "Valeur",
- "descriptive_label_en": "Value",
- "object_type": "literal",
- "rdflib_class": RDF.value,
- "object_rdflib_class": Literal,
- },
- "type": {
- "descriptive_label_fr": "Type",
- "descriptive_label_en": "Type",
- "object_type": "uriref-category",
- "rdflib_class": RDF.type,
- "object_rdflib_class": URIRef,
- },
- "resource": {
- "descriptive_label_fr": "Ressource",
- "descriptive_label_en": "Resource",
- "object_type": "uriref-link",
- "rdflib_class": RDFS.Resource,
- "object_rdflib_class": URIRef,
- },
- "related": {
- "descriptive_label_fr": "En relation avec",
- "descriptive_label_en": "Related to",
- "object_type": "uriref-category",
- "rdflib_class": SKOS.related,
- "object_rdflib_class": URIRef,
- }
- }
-
- # Category persistence parameters
-
- PERSISTENCE_METHOD = "PersistenceToFile"
+ CATEGORY_NAMESPACE = "http://ld.iri-research.org/ontology/categorisation/category#"
--- a/src/catedit/templates/cateditor.html Tue Dec 16 11:14:55 2014 +0100
+++ b/src/catedit/templates/cateditor.html Wed Dec 17 17:21:06 2014 +0100
@@ -31,7 +31,7 @@
<li class="active"><a>Editeur de catégorie: {% if cat_id: %} Edition {% else %} Création {% endif %}</a></li>
</ul>
<div class="navbar-text navbar-right">
- {% if not session.get("user_code", None)%} Non authentifié - <a href="{{ url_for('github_login') }}" class="navbar-link">S'authentifier</a>
+ {% 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>
@@ -106,7 +106,7 @@
<tr id="property_tr{{property_count}}">
{% if config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-category" %}
{% for cat in cat_list %}
- {% if object == config["ONTOLOGY_NAMESPACE"]+cat.cat_id %}
+ {% 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 }}">
--- a/src/catedit/templates/catrecap.html Tue Dec 16 11:14:55 2014 +0100
+++ b/src/catedit/templates/catrecap.html Wed Dec 17 17:21:06 2014 +0100
@@ -48,7 +48,7 @@
<li><a href="{{ url_for('cat_editor') }}">Editeur de catégorie: Création</a></li>
</ul>
<div class="navbar-text navbar-right">
- {% if not session.get("user_code", None)%}Non authentifié - <a href="{{ url_for('github_login') }}" class="navbar-link">S'authentifier</a>
+ {% 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>
@@ -97,7 +97,7 @@
<dd>
{% if config["PROPERTY_LIST"][predicate]["object_type"]=="uriref-category" %}
{% for cat in cat_list %}
- {% if object == config["ONTOLOGY_NAMESPACE"]+cat.cat_id %}
+ {% if object == config["CATEGORY_NAMESPACE"]+cat.cat_id %}
{{ cat.cat_label }}
{% endif %}
{% endfor %}
--- a/src/catedit/views.py Tue Dec 16 11:14:55 2014 +0100
+++ b/src/catedit/views.py Wed Dec 17 17:21:06 2014 +0100
@@ -1,17 +1,27 @@
+"""
+views.py:
+The views functions that handle the front-end of the application
+"""
+
from catedit import app, github
-from models import Category, CategoryManager
+from catedit.models import Category
from flask import render_template, request, redirect, url_for, session
from flask.ext.github import GitHubError
from flask_wtf import Form
-from api import CategoryAPI
+from catedit.api import CategoryAPI
from wtforms import StringField, TextAreaField
from wtforms.validators import DataRequired
from rdflib import Graph
from io import StringIO
-logger = app.logger
+LOGGER = app.logger
+
class NewCategoryMinimalForm(Form):
+ """
+ Custom form class for creating a category with the absolute minimal
+ attributes (label and description)
+ """
label = StringField(
"Nom de la categorie (obligatoire)",
validators=[DataRequired()]
@@ -26,31 +36,37 @@
)
-@app.route('/', methods=['GET'])
-@app.route('/catrecap', methods=['GET'])
-@app.route('/catrecap/delete/<string:delete_cat_id>', methods=['POST'])
-def cat_recap(delete_cat_id=None):
+@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):
+ """
+ View that has a list of all categories available. Template is
+ catrecap.html, located in src/templates/
+
+ Note: it also handles category deletion from the same page.
+ """
cat_api_instance = CategoryAPI()
- # list categories
if delete_cat_id is None:
+ LOGGER.debug("Category to delete is None")
serialized_cat_list = cat_api_instance.get()
- logger.debug(serialized_cat_list)
+ # LOGGER.debug(serialized_cat_list)
cat_list = []
- logger.debug(cat_list)
for serialized_cat in serialized_cat_list:
cat_rdf_graph = Graph()
cat_rdf_graph.parse(source=StringIO(serialized_cat),
format='turtle')
- c = Category(graph=cat_rdf_graph)
+ cat = Category(graph=cat_rdf_graph)
- cat_list.append({"cat_label": c.label,
- "cat_description": c.description,
- "cat_id": c.cat_id,
- "cat_properties": c.properties})
- logger.debug(c.properties)
+ 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'))
@@ -58,6 +74,10 @@
@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()
@@ -66,40 +86,42 @@
cat_rdf_graph = Graph()
cat_rdf_graph.parse(source=StringIO(serialized_cat),
format='turtle')
- c = Category(graph=cat_rdf_graph)
+ cat = Category(graph=cat_rdf_graph)
- cat_list.append({"cat_label": c.label,
- "cat_description": c.description,
- "cat_id": c.cat_id,
- "cat_properties": c.properties})
+ 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:
- catSerial = cat_api_instance.get(cat_id)
+ specific_serialized_cat = cat_api_instance.get(cat_id)
cat_rdf_graph = Graph()
- cat_rdf_graph.parse(source=StringIO(catSerial),
+ cat_rdf_graph.parse(source=StringIO(specific_serialized_cat),
format='turtle')
- c = Category(graph=cat_rdf_graph)
+ cat = Category(graph=cat_rdf_graph)
setattr(NewCategoryMinimalForm,
'label',
StringField("Nom de la categorie",
validators=[DataRequired()],
- default=c.label))
+ default=cat.label))
setattr(NewCategoryMinimalForm,
'description',
TextAreaField("Description de la categorie",
validators=[DataRequired()],
- default=c.description))
- logger.debug("CatForm fields preset to "+c.label+" and "+c.description)
+ default=cat.description))
+ LOGGER.debug("CatForm fields preset to "
+ + cat.label + " and "
+ + cat.description)
cat_form = NewCategoryMinimalForm(request.form)
# GET + cat_id = Edit cat form
if request.method == 'GET':
return render_template('cateditor.html',
- cat_id=c.cat_id,
- cat_properties=c.properties,
+ cat_id=cat.cat_id,
+ cat_properties=cat.properties,
form=cat_form,
cat_list=cat_list)
@@ -110,7 +132,7 @@
else:
return render_template('cateditor.html',
cat_id=cat_id,
- cat_properties=c.properties,
+ cat_properties=cat.properties,
form=cat_form,
cat_list=cat_list)
@@ -144,43 +166,69 @@
@app.route('/catedit-github-login')
def github_login():
- return github.authorize(scope="repo")
+ """
+ Function that manages authentication (Github), login
+
+ Note: If Persistence is set to PersistenceToFile (categories stored
+ in local files, used for debugging), creates a mock user named
+ "FileEditUser"
+ """
+ if app.config["PERSISTENCE_METHOD"] == "PersistenceToGithub":
+ return github.authorize(scope="repo")
+ elif app.config["PERSISTENCE_METHOD"] == "PersistenceToFile":
+ session["user_logged"] = True
+ session["user_can_edit"] = True
+ session["user_login"] = "FileEditUser"
+ return redirect(url_for('cat_recap'))
@app.route('/catedit-github-callback')
@github.authorized_handler
def github_callback(oauth_code):
+ """
+ Function that handles callback from Github after succesful login
+ """
session.permanent = False
session["user_code"] = oauth_code
session["user_logged"] = True
session["user_login"] = github.get("user")["login"]
try:
- repoList = []
- repoList = github.get("user/repos")
- for repo in repoList:
- logger.debug(repo["name"])
+ repo_list = []
+ repo_list = github.get("user/repos")
+ for repo in repo_list:
+ LOGGER.debug(repo["name"])
session["user_can_edit"] = True
if not any(repo["name"] == app.config["REPOSITORY_NAME"]
- for repo in repoList):
+ 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!")
- pass
+ LOGGER.debug("error getting repos!")
- logger.debug(session["user_login"])
+ LOGGER.debug(session["user_login"])
return redirect(url_for('cat_recap'))
@github.access_token_getter
def token_getter():
+ """
+ Utility function for github-flask module to get user token when
+ 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"]
@app.route('/catedit-logout')
def logout():
+ """
+ Function that manages authentication (Github), logout
+
+ Note: if you want to switch github users, you will have to logout of
+ Github, else when logging back in, github will send the app the
+ same oauth code
+ """
session["user_code"] = None
session["user_logged"] = None
session["user_login"] = None
--- a/virtualenv/requirements.txt Tue Dec 16 11:14:55 2014 +0100
+++ b/virtualenv/requirements.txt Wed Dec 17 17:21:06 2014 +0100
@@ -1,8 +1,20 @@
-rdflib
-flask
-flask-restful
-flask-wtf
-Github-Flask
-flask-cache
-python-slugify
-pep8
+Flask==0.10.1
+Flask-Cache==0.13.1
+Flask-RESTful==0.3.1
+Flask-WTF==0.10.3
+GitHub-Flask==2.0.0
+Jinja2==2.7.3
+MarkupSafe==0.23
+Unidecode==0.04.16
+WTForms==2.0.1
+Werkzeug==0.9.6
+aniso8601==0.90
+isodate==0.5.1
+itsdangerous==0.24
+pep8==1.5.7
+pyparsing==2.0.3
+python-slugify==0.1.0
+pytz==2014.10
+rdflib==4.1.2
+requests==2.5.0
+six==1.8.0