Caching fix (now works on views.categories submodule) and config (dict CACHE_CONFIG, see Flask-cache doc for expected values) + Pagination for comments (changeset & issues) + Hooks to log API rate consumption and get pagination info
authorNicolas DURAND <nicolas.durand@iri.centrepompidou.fr>
Fri, 20 Feb 2015 10:55:54 +0100
changeset 45 1506da593f40
parent 44 5ab922a46f13
child 46 5bd3fb023396
Caching fix (now works on views.categories submodule) and config (dict CACHE_CONFIG, see Flask-cache doc for expected values) + Pagination for comments (changeset & issues) + Hooks to log API rate consumption and get pagination info
src/catedit/__init__.py
src/catedit/config.py.tmpl
src/catedit/persistence.py
src/catedit/resources.py
src/catedit/static/css/style.css
src/catedit/templates/categories/editor.html
src/catedit/templates/macros.html
src/catedit/templates/social/comment_thread_layout.html
src/catedit/templates/social/discussion.html
src/catedit/views/categories.py
src/catedit/views/home.py
src/catedit/views/social.py
src/catedit/views/utils.py
--- a/src/catedit/__init__.py	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/__init__.py	Fri Feb 20 10:55:54 2015 +0100
@@ -5,9 +5,11 @@
 
 from logging import FileHandler, Formatter
 import os
+import re
 from catedit.version import CURRENT_VERSION
+from requests import request
 
-from flask import Flask, session
+from flask import Flask, session, request, url_for
 from flask_wtf.csrf import CsrfProtect
 from flask.ext.github import GitHub
 from flask.ext.cache import Cache
@@ -18,7 +20,6 @@
 # Set up app
 app = Flask(__name__)
 app.config.from_object(AppSettings)
-cache = Cache(app, config={"CACHE_TYPE": "simple"})
 app_configured = False
 try:
     from catedit.config import AppConfig
@@ -34,6 +35,8 @@
 if not app_configured:
     raise Exception("Catedit not configured")
 
+cache = Cache(app, config=app.config["CACHE_CONFIG"])
+
 # CSRF protection
 CsrfProtect(app)
 
@@ -58,11 +61,76 @@
         # logger.debug("I made an authentified request")
         return session["user_code"]
 
+def log_api_rate(r, *args, **kwargs):
+    """
+        Utility hook function to link to every github call as a kwarg so the
+        app logs how many requests can still be made, after the current request
+    """
+    app.logger.debug(
+        str(r.request.method) + " "
+        + str(r.url) + " - "
+        + "Remaining requests count: "
+        + str(r.headers["X-RateLimit-Remaining"]) + "/"
+        + str(r.headers["X-RateLimit-Limit"])
+    )
+
+def save_links(r, *args, **kwargs):
+    """
+        Utility hook function that stores the links in the header of
+        the response in order to use them for further API requests.
+
+        After that hook, the entry "pagination_links" in session is updated
+        with last page and current_page, as well as the resource to request
+        from for each header link
+    """
+    session["pagination_links"] = {}
+    log_api_rate(r, *args, **kwargs)
+    if r.headers.get("link", None) is not None:
+        session["pagination_links"] = r.links
+        for (key, item) in session["pagination_links"].items():
+            resource = item["url"][len(github.BASE_URL):]
+            item["url"] = resource
+        if session["pagination_links"].get("next", None) is not None:
+            page_arg = re.search(
+                "(\?|&)page=\d",
+                string=session["pagination_links"]["next"]["url"]
+            )
+            session["pagination_links"]["current_page"] = int(
+                page_arg.group(0)[-1]
+            )-1
+            if session["pagination_links"].get("last", None) is not None:
+                last_page_arg = re.search(
+                    "(\?|&)page=\d",
+                    string=session["pagination_links"]["last"]["url"]
+                )
+                session["pagination_links"]["last_page"] = int(
+                    page_arg.group(0)[-1]
+                )
+            else:
+                # We don't know what is the last page (case: github commits
+                # API)
+                session["pagination_links"]["last_page"] = -1
+        elif session["pagination_links"].get("prev", None) is not None:
+            # This means we're at the last page
+            page_arg = re.search(
+                "(\?|&)page=\d",
+                string=session["pagination_links"]["prev"]["url"]
+            )
+            session["pagination_links"]["current_page"] = int(
+                page_arg.group(0)[-1]
+            )+1
+            session["pagination_links"] \
+                   ["last_page"] = session["pagination_links"]["current_page"]
+        else:
+            session["pagination_links"]["current_page"] = 1
+            session["pagination_links"]["last_page"] = 1
+    else:
+        session.pop("pagination_links", None)
+
 # Api
 api = Api(app)
 
 # Version
-
 app.config["CURRENT_VERSION"] = CURRENT_VERSION
 
 # Views and APIs
@@ -82,6 +150,18 @@
                  '/category-changes',
                  endpoint='category_changes')
 
+# Pagination utility functions for templates
+def url_for_other_page(page):
+    args = request.view_args.copy()
+    args['page'] = page
+    return url_for(request.endpoint, **args)
+app.jinja_env.globals['url_for_other_page'] = url_for_other_page
+
+def url_for_other_per_page(per_page):
+    args = request.view_args.copy()
+    args['per_page'] = per_page
+    return url_for(request.endpoint, **args)
+app.jinja_env.globals['url_for_other_per_page'] = url_for_other_per_page
 
 # Set up logging
 if app.config["LOGGING_CONFIG"]["IS_LOGGING"]:
--- a/src/catedit/config.py.tmpl	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/config.py.tmpl	Fri Feb 20 10:55:54 2015 +0100
@@ -30,6 +30,15 @@
     }
 
     """
+        Cache parameters:
+        CACHE_TYPE is the type of cache to be used (see Flask-cache doc to
+        configure caches)
+    """
+    CACHE_CONFIG = {
+        "CACHE_TYPE": "simple"
+    }
+
+    """
         Category persistence parameters
         METHOD can be:
         * "PersistenceToFile" : will save categories to files on system
--- a/src/catedit/persistence.py	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/persistence.py	Fri Feb 20 10:55:54 2015 +0100
@@ -9,7 +9,7 @@
 for categories)
 """
 from abc import ABCMeta, abstractmethod
-from catedit import github, app
+from catedit import github, app, log_api_rate
 from base64 import b64decode
 from flask.ext.github import GitHubError
 import os
@@ -220,7 +220,8 @@
                 "repos/"
                 + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
                 + self.repository
-                + "/git/refs/heads/master"
+                + "/git/refs/heads/master",
+                hooks=dict(response=log_api_rate)
             )
             logger.debug(str(ref_master))
         except GitHubError as ghe:
@@ -234,7 +235,6 @@
                 + "/git/refs/heads/master"
             )
             logger.error(ghe.response.text)
-        logger.debug(str(github.get("rate_limit")["resources"]))
 
         # point 2
         try:
@@ -243,14 +243,14 @@
                 + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
                 + self.repository
                 + "/git/commits/"
-                + ref_master["object"]["sha"]
+                + ref_master["object"]["sha"],
+                hooks=dict(response=log_api_rate)
             )
             logger.debug(str(last_commit_master))
         except GitHubError as ghe:
             logger.error("GitHubError trying to get the commit associated "
                          + "to the latest reference to the master branch")
             logger.error(ghe.response.text)
-        logger.debug(str(github.get("rate_limit")["resources"]))
 
         # Point 3
         try:
@@ -260,7 +260,8 @@
                 + self.repository
                 + "/git/trees/"
                 + last_commit_master["tree"]["sha"]
-                + "?recursive=1"
+                + "?recursive=1",
+                hooks=dict(response=log_api_rate)
             )
             logger.debug(str(last_commit_tree))
         except GitHubError as ghe:
@@ -268,7 +269,6 @@
                          + "associated to the latest reference to the master "
                          + "branch")
             logger.error(ghe.response.text)
-        logger.debug(str(github.get("rate_limit")["resources"]))
 
         # Point 4
         new_tree_data = {"tree": []}
@@ -319,7 +319,8 @@
                                                     ["REPOSITORY_OWNER"] + "/"
                                         + self.repository
                                         + "/git/blobs",
-                                        data=new_blob_data
+                                        data=new_blob_data,
+                                        hooks=dict(response=log_api_rate)
                                     )
                                     blob_sha = new_blob["sha"]
                                     break
@@ -347,7 +348,8 @@
         # exist yet in the last commit tree in order to create blobs for them
         for (cat_name, cat_content) in modification_dict.items():
             logger.debug(app.config["PERSISTENCE_CONFIG"]
-                                   ["CATEGORIES_PATH"] + cat_content
+                                   ["CATEGORIES_PATH"]
+                         + cat_content
                          + " should not be in "
                          + str([elt["path"] for
                                 elt in last_commit_tree["tree"]]))
@@ -365,7 +367,8 @@
                                     ["REPOSITORY_OWNER"] + "/"
                         + self.repository
                         + "/git/blobs",
-                        data=new_blob_data
+                        data=new_blob_data,
+                        hooks=dict(response=log_api_rate)
                     )
                 except GitHubError as ghe:
                     logger.error(
@@ -374,7 +377,7 @@
                         + str(new_blob_data)
                     )
                     logger.error(ghe.response.text)
-                logger.debug(str(github.get("rate_limit")["resources"]))
+
                 new_tree_data["tree"].append({
                     "path": app.config["PERSISTENCE_CONFIG"]
                                       ["CATEGORIES_PATH"] + cat_name,
@@ -392,7 +395,8 @@
                 + self.repository
                 + "/git/trees",
                 data=new_tree_data
-            )
+            ),
+            hooks=dict(response=log_api_rate)
         except GitHubError as ghe:
             logger.error(
                 "GitHubError trying to post a new tree with following data: "
@@ -412,7 +416,8 @@
                 + self.repository
                 + "/git/commits",
                 data=new_commit_data
-            )
+            ),
+            hooks=dict(response=log_api_rate)
             logger.debug(str(new_commit))
         except GitHubError as ghe:
             logger.error(
@@ -420,7 +425,6 @@
                 + str(new_commit_data)
             )
             logger.error(ghe.response.text)
-        logger.debug(str(github.get("rate_limit")["resources"]))
 
         # Point 6
         new_head_data = {"sha": new_commit["sha"], "force": "true"}
@@ -431,7 +435,8 @@
                 + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
                 + self.repository
                 + "/git/refs/heads/master",
-                data=json.dumps(new_head_data)
+                data=json.dumps(new_head_data),
+                hooks=dict(response=log_api_rate)
             )
             logger.debug(str(new_head))
         except GitHubError as ghe:
@@ -441,7 +446,7 @@
                 + str(new_head_data)
             )
             logger.error(ghe.response.text)
-        logger.debug(str(github.get("rate_limit")["resources"]))
+
 
 
     def load(self, **kwargs):
@@ -456,7 +461,8 @@
                 + self.repository
                 + "/contents/"
                 + app.config["PERSISTENCE_CONFIG"]["CATEGORIES_PATH"]
-                + kwargs["name"]
+                + kwargs["name"],
+                hooks=dict(response=log_api_rate)
             )
             file_content = str(b64decode(filedict["content"]), "utf-8")
         except GitHubError as ghe:
@@ -465,7 +471,7 @@
                          + "have access to the repository or the file doesn't "
                          + "exist ")
             logger.error(ghe.response.text)
-        logger.debug(str(github.get("rate_limit")["resources"]))
+
         return file_content
 
     def delete(self, **kwargs):
@@ -481,43 +487,95 @@
     def list(self, **kwargs):
         """
             Lists all files in the github repository (as set in config.py)
+
+            Process is as follows:
+            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) From the tree extract all the filenames
+            5) For each file name, get the content
         """
+
         filenames_list = []
+        # point 1
         try:
-            files_in_repo = github.get(
+            ref_master = github.get(
+                "repos/"
+                + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
+                + self.repository
+                + "/git/refs/heads/master",
+                hooks=dict(response=log_api_rate)
+            )
+        except GitHubError as ghe:
+            logger.error("GitHubError trying to get the reference "
+                         + "to the master branch")
+            logger.error(
+                "Endpoint: "
+                + "repos/"
+                + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
+                + self.repository
+                + "/git/refs/heads/master"
+            )
+            logger.error(ghe.response.text)
+
+        # point 2
+        try:
+            last_commit_master = github.get(
                 "repos/"
                 + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
                 + self.repository
-                + "/contents/"
-                + app.config["PERSISTENCE_CONFIG"]["CATEGORIES_PATH"] + "?per_page=100"
+                + "/git/commits/"
+                + ref_master["object"]["sha"],
+                hooks=dict(response=log_api_rate)
             )
-            filenames_list = [github_file["name"]
-                              for github_file in files_in_repo]
-            # logger.debug(filenames_list)
         except GitHubError as ghe:
-            logger.error("Github Error trying to get the file list in the "
-                         + "category repository")
-            logger.error("IMPORTANT: This message can mean there is no "
-                         + "category in the repository " + self.repository)
+            logger.error("GitHubError trying to get the commit associated "
+                         + "to the latest reference to the master branch")
             logger.error(ghe.response.text)
-        logger.debug(str(github.get("rate_limit")["resources"]))
 
+        # Point 3
+        try:
+            last_commit_tree = github.get(
+                "repos/"
+                + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
+                + self.repository
+                + "/git/trees/"
+                + last_commit_master["tree"]["sha"]
+                + "?recursive=1",
+                hooks=dict(response=log_api_rate)
+            )
+        except GitHubError as ghe:
+            logger.error("GitHubError trying to get the tree from the commit "
+                         + "associated to the latest reference to the master "
+                         + "branch")
+            logger.error(ghe.response.text)
+
+        # Point 4
+        filenames_list = [
+            elt["path"]
+            for elt in last_commit_tree["tree"]
+            if elt["type"] == "blob" and elt["path"].startswith(
+                app.config["PERSISTENCE_CONFIG"]["CATEGORIES_PATH"]
+            )
+        ]
+
+        # Point 5
         file_content_list = []
         for filename in filenames_list:
             try:
                 filedict = github.get(
                     "repos/"
-                    + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
-                    + self.repository
+                    + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"]
+                    + "/" + self.repository
                     + "/contents/"
-                    + app.config["PERSISTENCE_CONFIG"]["CATEGORIES_PATH"]
-                    + filename
+                    + filename,
+                    hooks=dict(response=log_api_rate)
                 )
                 file_content_list.append(str(b64decode(filedict["content"]),
                                          "utf-8"))
             except GitHubError as ghe:
                 logger.error("Github Error trying to get file: "+filename)
                 logger.error(ghe.response.text)
-        logger.debug(str(github.get("rate_limit")["resources"]))
+
         # logger.debug(file_content_list)
         return file_content_list
--- a/src/catedit/resources.py	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/resources.py	Fri Feb 20 10:55:54 2015 +0100
@@ -9,7 +9,7 @@
 from rdflib import Graph
 from flask.ext.restful import Resource, reqparse
 from flask import session
-from catedit import app, cache
+from catedit import app, cache, github
 from catedit.models import Category, CategoryManager
 import catedit.persistence
 from io import StringIO
@@ -30,33 +30,41 @@
         The API to create and edit categories, returns rdf graph serializations
         when successful
     """
-    @cache.memoize(timeout=3600)
-    def get(self, repository, cat_id=None):
+    def get(self, repository, cat_id=""):
         """
             API to get the category of id cat_id, or if cat_id is None,
             get the list of category
+
+            The result of this API function goes into the Flask Cache.
         """
-        cat_manager_instance = CategoryManager(
-            getattr(
-                catedit.persistence,
-                app.config["PERSISTENCE_CONFIG"]["METHOD"]
-            )(repository=repository),
-        )
-        if cat_id is not None:
-            cat = cat_manager_instance.load_category(cat_id)
-            if cat is not None:
-                return cat.cat_graph.serialize(
-                    format='turtle'
-                ).decode("utf-8"), 200
+        cache_key = "categoryapi_get_" + repository + "_" + cat_id
+        if cache.get(cache_key) is None:
+            rv = None
+            cat_manager_instance = CategoryManager(
+                getattr(
+                    catedit.persistence,
+                    app.config["PERSISTENCE_CONFIG"]["METHOD"]
+                )(repository=repository),
+            )
+            if cat_id != "":
+                cat = cat_manager_instance.load_category(cat_id)
+                if cat is not None:
+                    rv = cat.cat_graph.serialize(
+                        format='turtle'
+                    ).decode("utf-8"), 200
+                else:
+                    rv = 404
             else:
-                return 404
+                response = []
+                for cat in cat_manager_instance.list_categories():
+                    response.append(
+                        cat.cat_graph.serialize(format='turtle').decode("utf-8")
+                    )
+                rv = response, 200
+            cache.set(cache_key, rv, timeout=3600)
+            return rv
         else:
-            response = []
-            for cat in cat_manager_instance.list_categories():
-                response.append(
-                    cat.cat_graph.serialize(format='turtle').decode("utf-8")
-                )
-            return response, 200
+            return cache.get(cache_key)
 
     # update category cat_id
     def put(self, repository, cat_id=None, cat_data=None):
--- a/src/catedit/static/css/style.css	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/static/css/style.css	Fri Feb 20 10:55:54 2015 +0100
@@ -29,6 +29,22 @@
   float:right;
 }
 
+
+ul.pagination{
+  display: inline-block;
+  vertical-align: middle;
+  margin-top: 0px;
+  margin-bottom: 0px;
+}
+
+form.form-pagination{
+  display: inline-block;
+  vertical-align: middle;
+  margin-left: 10px;
+  margin-top: 0px;
+  margin-bottom: 0px;
+}
+
 ul.nav a.navbar-decorative
 {
   color: white !important;
--- a/src/catedit/templates/categories/editor.html	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/templates/categories/editor.html	Fri Feb 20 10:55:54 2015 +0100
@@ -60,11 +60,11 @@
     <label>Propriétés </label>
     <div class="form-inline">
       <select id="property_selector" class="form-control" onChange="CatEditScripts.displayCorrespondingField();" {{readonly}}>
-        <option label="property_type_default" selected="selected">
+        <option label="Liste des propriétés ..." selected="selected">
           Liste des propriétés ...
         </option>
         {% for predicate in config["PROPERTY_LIST"] %}
-        <option value='{{ predicate }}' label={{ config["PROPERTY_LIST"][predicate]["object_type"] }} >{{ config["PROPERTY_LIST"][predicate]["descriptive_label_fr"] }}</option>
+        <option value='{{ predicate }}' label="{{ config['PROPERTY_LIST'][predicate]['descriptive_label_fr'] }}" >{{ config["PROPERTY_LIST"][predicate]["descriptive_label_fr"] }}</option>
         {% endfor %}
       </select>
       <input type="text" id="literal-field" class="hidden form-control">
--- a/src/catedit/templates/macros.html	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/templates/macros.html	Fri Feb 20 10:55:54 2015 +0100
@@ -102,3 +102,35 @@
     {% endif %}
   {% endfor %}
 {%- endmacro %}
+
+{% macro render_pagination(pagination) -%}
+  {% if pagination %}
+    <ul class="pagination">
+    {% if pagination.has_prev %}
+      <li><a href="{{ url_for_other_page(pagination.page - 1)}}" aria-label="Previous">&laquo;</a></li>
+    {% endif %}
+    {% for page in pagination.iter_pages() %}
+      {% if page %}
+        {% if page != pagination.page %}
+          <li><a href="{{ url_for_other_page(page) }}">{{ page }}</a></li>
+        {% else %}
+          <li class="active"><a>{{ page }}</a></li>
+        {% endif %}
+      {% else %}
+        <li><span class=ellipsis>…</span></li>
+      {% endif %}
+    {% endfor %}
+    {% if pagination.has_next %}
+      <li><a href="{{ url_for_other_page(pagination.page + 1)}}" aria-label="Next">&raquo;</a></li>
+    {% endif %}
+    </ul>
+    <form class="form form-inline form-pagination">
+      <label class="label-pagination">Commentaires par page:</label>
+      <select class="form-control" name="comment_count" onchange="window.location.href=this.form.comment_count.options[this.form.comment_count.selectedIndex].value">
+        {% for count in [10, 30, 50, 100] %}
+          <option value="{{url_for_other_per_page(count)}}" {% if pagination.per_page==count %}selected="selected"{% endif %}>{{count}}</option>
+        {% endfor %}
+      </select>
+    </form>
+  {% endif %}
+{%- endmacro %}
--- a/src/catedit/templates/social/comment_thread_layout.html	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/templates/social/comment_thread_layout.html	Fri Feb 20 10:55:54 2015 +0100
@@ -17,7 +17,6 @@
 {% block additional_content %}
 {% endblock additional_content %}
 <h3><strong>Discussion</strong></h3>
-  <small></small>
 </h3>
 {% if comment_form.comment_field.errors %}
 <div class="alert alert-danger">
@@ -43,7 +42,7 @@
   <tbody>
     {% if comments["comment_list"]|length == 0 %}
       <tr>
-        <td colspan="3"> Aucun commentaire n'a été posté pour le moment </td>
+        <td colspan="3"> Aucun commentaire à afficher </td>
       </tr>
     {% else %}
       {% for comment in comments["comment_list"] %}
@@ -54,7 +53,13 @@
         </tr>
       {% endfor %}
     {% endif %}
-    <tr>
+    <tr class="info tr-pagination">
+      <td colspan="3" class="text-right">
+        {% import "macros.html" as macros %}
+        {{ macros.render_pagination(pagination) }}
+      </td>
+    </tr>
+    <tr class="active">
       <td colspan="2" class="text-right">
         {{ comment_form.comment_field.label }}
       </td>
@@ -68,7 +73,7 @@
             <button type="submit" class="btn btn-default">Envoyer commentaire</button>
             <a href="{{ url_for('social.index', repository=current_repository)}}"class="btn btn-default">Retour</a>
           </fieldset>
-        </form><br>
+        </form>
       </td>
     </tr>
   </tbody>
--- a/src/catedit/templates/social/discussion.html	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/templates/social/discussion.html	Fri Feb 20 10:55:54 2015 +0100
@@ -14,6 +14,12 @@
   <li class="active"><a>Discussion</a></li>
 {% endblock navbar_items %}
 {% block comment_posting_target %}{{url_for("social.discussion", discussion_id=discussion_id, repository=current_repository)}}{% endblock %}
+{% block comment_thread_options %}
+  {% for count in [10, 30, 50, 100] %}
+    <option value="{{url_for('social.discussion', discussion_id=discussion_id, repository=current_repository, per_page=count, page=1)}}" {% if comments["per_page"]==count %}selected="selected"{% endif %}>{{count}}</option>
+  {% endfor %}
+{% endblock comment_thread_options %}
+
 {% block page_content %}
   {% if discussion_id == "new" %}
   <h2><b>CatEdit</b> - <small>{{current_repository}}</small></h2>
--- a/src/catedit/views/categories.py	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/views/categories.py	Fri Feb 20 10:55:54 2015 +0100
@@ -3,9 +3,10 @@
 The views functions that handle category management and editing
 """
 
-from catedit import app
+from catedit import app, cache
 from catedit.models import Category
-from views.utils import check_user_status
+from catedit.views.utils import check_user_status_and_repo_access, \
+                                get_current_category_list
 from flask import render_template, request, redirect, url_for, session, \
                   abort, Blueprint
 from flask_wtf import Form
@@ -19,101 +20,6 @@
 module = Blueprint('categories', __name__)
 logger = app.logger
 
-def make_category_list(repository, without_changes=False):
-    """
-        Shortcut function that generates the list of category to use for
-        view templates.
-        Each category is a dict with the following format:
-        {
-            "cat_label": category label,
-             "cat_description": category description,
-             "cat_id": category id,
-             "cat_properties": category properties,
-             "state": category state (one of {"untouched", "created",
-             "edited", "deleted"})
-        }
-    """
-    cat_api_instance = CategoryAPI()
-    cat_changes_api_instance = CategoryChangesAPI()
-
-    deleted_cat_dict = {}
-    modified_cat_dict = {}
-    serialized_cat_list = []
-    if session.get("user_logged", None) is not None:
-        serialized_cat_list = cat_api_instance.get(repository=repository) \
-                                                  [0]
-        cat_changes = cat_changes_api_instance.get(repository=repository) \
-                                                  [0]
-        modified_cat_dict = cat_changes["modified_categories"]
-        deleted_cat_dict = cat_changes["deleted_categories"]
-    # logger.debug(serialized_cat_list)
-    cat_list = []
-    original_cat_list = []
-    for serialized_cat in serialized_cat_list:
-        cat_rdf_graph = Graph()
-        cat_rdf_graph.parse(source=StringIO(serialized_cat),
-                            format='turtle')
-        original_cat_list.append(Category(graph=cat_rdf_graph))
-
-    if without_changes:
-        for category in original_cat_list:
-            cat_list.append(
-                {
-                    "cat_label": category.label,
-                    "cat_description": category.description,
-                    "cat_id": category.cat_id,
-                    "cat_properties": category.properties,
-                    "state": "original"
-                }
-            )
-    else:
-        edited_cat_list = []
-        for modified_cat_name in modified_cat_dict.keys():
-            new_cat_rdf_graph = Graph()
-            new_cat_rdf_graph.parse(
-                source=StringIO(
-                    modified_cat_dict[modified_cat_name]
-                ),
-                format='turtle'
-            )
-            edited_cat_list.append(Category(graph=new_cat_rdf_graph))
-        # first we find the untouched, edited and deleted categories
-        cat_state = ""
-        for category in original_cat_list:
-            if category.cat_id not in modified_cat_dict.keys():
-                if category.cat_id in deleted_cat_dict.keys():
-                    cat_state = "deleted"
-                else:
-                    cat_state = "untouched"
-
-                cat_list.append(
-                    {
-                        "cat_label": category.label,
-                        "cat_description": category.description,
-                        "cat_id": category.cat_id,
-                        "cat_properties": category.properties,
-                        "state": cat_state
-                    }
-                )
-
-        # now we must find the not yet submitted categories that were created
-        cat_state = ""
-        logger.debug("Edited cat list: "
-                     + str([cat.label for cat in edited_cat_list])
-                     + " - Original cat list: "
-                     + str([cat.label for cat in original_cat_list]))
-        for category in edited_cat_list:
-            if category.cat_id not in [cat.cat_id for
-                                       cat in original_cat_list]:
-                cat_state = "created"
-            else:
-                cat_state = "modified"
-            cat_list.append({"cat_label": category.label,
-                             "cat_description": category.description,
-                             "cat_id": category.cat_id,
-                             "cat_properties": category.properties,
-                             "state": cat_state})
-    return cat_list
 
 @module.route(
     '/<string:repository>/workshop',
@@ -140,7 +46,7 @@
     if repository not in app.config["PERSISTENCE_CONFIG"]["REPOSITORY_LIST"]:
         abort(404)
 
-    check_user_status(repository)
+    check_user_status_and_repo_access(repository)
 
     cat_api_instance = CategoryAPI()
     cat_changes_api_instance = CategoryChangesAPI()
@@ -168,7 +74,7 @@
         return redirect(url_for('categories.workshop', repository=repository))
     elif request.method == "GET":
 
-        cat_list = make_category_list(repository=repository)
+        cat_list = get_current_category_list(repository=repository)
             # logger.debug(c.properties)
         cat_list = sorted(cat_list, key=lambda cat: cat['cat_id'])
         return render_template('categories/workshop.html',
@@ -212,7 +118,7 @@
     if repository not in app.config["PERSISTENCE_CONFIG"]["REPOSITORY_LIST"]:
         abort(404)
 
-    check_user_status(repository)
+    check_user_status_and_repo_access(repository)
 
     cat_api_instance = CategoryAPI()
     cat_changes_api_instance = CategoryChangesAPI()
@@ -236,10 +142,10 @@
         # if it's a GET with no delete_cat_id or deleted_changes_id, then we'll
         # display the changes
         elif request.method == "GET":
-            cat_list = make_category_list(repository=repository)
-            original_cat_list = make_category_list(
+            cat_list = get_current_category_list(repository=repository)
+            original_cat_list = get_current_category_list(
                 repository=repository,
-                without_changes=True
+                with_local_changes=False
             )
 
             created_cat_count = len([
@@ -333,7 +239,7 @@
     if repository not in app.config["PERSISTENCE_CONFIG"]["REPOSITORY_LIST"]:
         abort(404)
 
-    check_user_status(repository)
+    check_user_status_and_repo_access(repository)
 
     cat_api_instance = CategoryAPI()
     cat_changes_api_instance = CategoryChangesAPI()
--- a/src/catedit/views/home.py	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/views/home.py	Fri Feb 20 10:55:54 2015 +0100
@@ -3,8 +3,7 @@
 The views functions that handle authentication and index pages
 """
 
-from catedit import app, github
-from views.utils import check_user_status
+from catedit import app, github, log_api_rate
 from requests import get
 from requests.auth import HTTPBasicAuth
 from flask import render_template, request, redirect, url_for, \
@@ -56,11 +55,10 @@
     session["user_logged"] = True
     session["user_login"] = "auth-error"
     try:
-        logger.debug(
-            "after login: "
-            + str(github.get("rate_limit")["resources"])
-        )
-        session["user_login"] = github.get("user")["login"]
+        session["user_login"] = github.get(
+            "user",
+            hooks=dict(response=log_api_rate)
+        )["login"]
     except GitHubError as ghe:
         logger.error(
             "GitHubError trying to get the user login"
@@ -69,7 +67,7 @@
     try:
         repo_list = []
         repo_list = github.get("user/repos")
-        logger.debug(str(github.get("rate_limit")["resources"]))
+
         for repo in repo_list:
             logger.debug(repo["name"])
         user_repos_name = [repo["name"] for repo in repo_list]
--- a/src/catedit/views/social.py	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/views/social.py	Fri Feb 20 10:55:54 2015 +0100
@@ -4,11 +4,12 @@
 """
 
 from catedit import app
-from views.utils import check_user_status, get_comments, \
-                        post_comment, get_commits, get_issues, \
-                        get_category_list
+from catedit.views.utils import check_user_status_and_repo_access, \
+                                get_comments, post_comment, get_commits, \
+                                get_issues, get_category_list_for_commit, \
+                                Pagination
 from flask import render_template, request, redirect, url_for, \
-                  abort, Blueprint
+                  abort, Blueprint, session
 from flask_wtf import Form
 from wtforms import TextAreaField, StringField
 from wtforms.validators import DataRequired
@@ -27,7 +28,7 @@
     if repository not in app.config["PERSISTENCE_CONFIG"]["REPOSITORY_LIST"]:
         abort(404)
 
-    check_user_status(repository)
+    check_user_status_and_repo_access(repository)
 
     changeset_list = get_commits(repository)
     discussion_list = get_issues(repository)
@@ -51,8 +52,14 @@
 
 
 @module.route("/<string:repository>/changesets/<string:changeset_id>",
-              methods=["GET", "POST"])
-def changeset(repository, changeset_id):
+              methods=["GET", "POST"],
+              defaults={"per_page": 10, "page": 1})
+@module.route(
+    "/<string:repository>/changesets/<string:changeset_id>"
+    + "/page/<int:page>-per_page-<int:per_page>",
+    methods=["GET", "POST"]
+)
+def changeset(repository, changeset_id, per_page, page):
     """
         View that displays a snapshot of the repository as it was for a given
         changeset, and the related discussion to this changeset. Allows
@@ -69,15 +76,25 @@
     if repository not in app.config["PERSISTENCE_CONFIG"]["REPOSITORY_LIST"]:
         abort(404)
 
-    check_user_status(repository)
-
+    check_user_status_and_repo_access(repository)
     comment_form = CommentForm()
-    comments_list = get_comments({
-        "repository": repository,
-        "type": "commits",
-        "id": changeset_id
-    })
-    cat_list = get_category_list(repository, changeset_id)
+    comments_list = get_comments(
+        repository=repository,
+        thread_type="commits",
+        thread_id=changeset_id,
+        page=page,
+        per_page=per_page
+    )
+    pagination=None
+    if session.get("pagination_links", None) is not None:
+        # If there are multiple pages we create a pagination class that
+        # will be sent to the template
+        pagination = Pagination(
+            page=session["pagination_links"]["current_page"],
+            per_page=per_page,
+            last_page=session["pagination_links"]["last_page"]
+        )
+    cat_list = get_category_list_for_commit(repository, changeset_id)
 
     if request.method == "GET":
         return render_template(
@@ -86,31 +103,33 @@
             comments=comments_list,
             changeset_id=changeset_id,
             comment_form=comment_form,
-            current_repository=repository
+            current_repository=repository,
+            pagination=pagination
+        )
+    elif request.method == "POST" and comment_form.validate_on_submit():
+        return_id = post_comment(
+            repository=repository,
+            thread_type="commits",
+            thread_id=changeset_id,
+            comment_body=comment_form.comment_field.data
         )
-    elif request.method == "POST":
-        if comment_form.validate_on_submit():
-            comment_data = {
-                "repository": repository,
-                "type": "commits",
-                "thread_id": changeset_id,
-                "comment_body": comment_form.comment_field.data
-            }
-            return_id = post_comment(comment_data)
-            return redirect(url_for(
-                "social.changeset",
-                repository=repository,
-                changeset_id=return_id
-            ))
-        else:
-            return render_template(
-                "social/changeset.html",
-                cat_list=cat_list,
-                comments=comments_list,
-                changeset_id=changeset_id,
-                comment_form=comment_form,
-                current_repository=repository
-            )
+        return redirect(url_for(
+            "social.changeset",
+            repository=repository,
+            changeset_id=return_id,
+            per_page=per_page
+        ))
+    else:
+        # Form didn't validate
+        return render_template(
+            "social/changeset.html",
+            cat_list=cat_list,
+            comments=comments_list,
+            changeset_id=changeset_id,
+            comment_form=comment_form,
+            current_repository=repository,
+            pagination=pagination
+        )
 
 
 class NewDiscussionForm(Form):
@@ -126,29 +145,45 @@
     )
 
 
-@module.route("/<string:repository>/discussions/<string:discussion_id>",
-              methods=["GET", "POST"])
-def discussion(repository, discussion_id):
+@module.route(
+    "/<string:repository>/discussions/<string:discussion_id>",
+    methods=["GET", "POST"],
+    defaults={"per_page": 10, "page": 1}
+)
+@module.route(
+    "/<string:repository>/discussions/<string:discussion_id>"
+    + "/page/<int:page>-per_page-<int:per_page>",
+    methods=["GET", "POST"]
+)
+def discussion(repository, discussion_id, per_page, page):
     """
         View that displays the discussion of a given id and allows users
         to post comments on it.
     """
-    if repository not in app.config["PERSISTENCE_CONFIG"]["REPOSITORY_LIST"]:
-        abort(404)
-
-    check_user_status(repository)
+    check_user_status_and_repo_access(repository)
 
     comment_form = None
+    pagination = None
     if discussion_id == "new":
         comment_form = NewDiscussionForm()
         comments_list = []
     else:
         comment_form = CommentForm()
-        comments_list = get_comments({
-            "repository": repository,
-            "type": "issues",
-            "id": discussion_id
-        })
+        comments_list = get_comments(
+            repository=repository,
+            thread_type="issues",
+            thread_id=discussion_id,
+            page=page,
+            per_page=per_page
+        )
+        if session.get("pagination_links", None) is not None:
+            # If there are multiple pages we create a pagination class that
+            # will be sent to the template
+            pagination = Pagination(
+                page=session["pagination_links"]["current_page"],
+                per_page=per_page,
+                last_page=session["pagination_links"]["last_page"]
+            )
 
     if request.method == "GET":
         return render_template(
@@ -156,29 +191,39 @@
             comments=comments_list,
             comment_form=comment_form,
             current_repository=repository,
-            discussion_id=discussion_id
+            discussion_id=discussion_id,
+            pagination=pagination
         )
-    elif request.method == "POST":
-        if comment_form.validate_on_submit():
-            comment_data = {
-                "repository": repository,
-                "type": "issues",
-                "thread_id": discussion_id,
-                "comment_body": comment_form.comment_field.data
-            }
+    elif request.method == "POST" and comment_form.validate_on_submit():
             if discussion_id == "new":
-                comment_data["thread_title"] = comment_form.discussion_title.data
-            return_id = post_comment(comment_data)
+                return_id = post_comment(
+                    repository=repository,
+                    thread_type="issues",
+                    thread_id=discussion_id,
+                    comment_body=comment_form.comment_field.data,
+                    thread_title=comment_form.discussion_title.data
+                )
+            else:
+                return_id = post_comment(
+                    repository=repository,
+                    thread_type="issues",
+                    thread_id=discussion_id,
+                    comment_body=comment_form.comment_field.data
+                )
             return redirect(url_for(
                 "social.discussion",
                 repository=repository,
-                discussion_id=return_id
+                discussion_id=return_id,
+                page=session["pagination_links"]["last_page"],
+                per_page=per_page
             ))
-        else:
-            return render_template(
-                "social/discussion.html",
-                comments=comments_list,
-                comment_form=comment_form,
-                current_repository=repository,
-                discussion_id=discussion_id
-            )
+    else:
+        # Form didn't validate
+        return render_template(
+            "social/discussion.html",
+            comments=comments_list,
+            comment_form=comment_form,
+            current_repository=repository,
+            discussion_id=discussion_id,
+            pagination=pagination
+        )
--- a/src/catedit/views/utils.py	Tue Feb 17 12:07:08 2015 +0100
+++ b/src/catedit/views/utils.py	Fri Feb 20 10:55:54 2015 +0100
@@ -1,11 +1,13 @@
 """
 utils.py:
-Module that groups utility functions that are used by views, partly because
-most of them do requests to the Github API and as such must be cached
+Module that groups utility functions and classes that are used by views,
+partly because most of them do requests to the Github API and as such must
+be cached
 """
 
-from catedit import app, github, cache
+from catedit import app, github, cache, log_api_rate, save_links
 from catedit.models import Category
+from catedit.resources import CategoryAPI, CategoryChangesAPI
 from flask import redirect, url_for, session, abort
 from flask.ext.github import GitHubError
 from datetime import datetime
@@ -15,25 +17,73 @@
 
 logger = app.logger
 
+class Pagination(object):
 
-def check_user_status(repository):
+    def __init__(self, page, per_page, last_page):
+        self.page = page
+        self.last_page = last_page
+        self.per_page = per_page
+
+    @property
+    def pages(self):
+        return self.last_page
+
+    @property
+    def has_prev(self):
+        return self.page > 1
+
+    @property
+    def has_next(self):
+        if self.last_page != -1:
+            return self.page < self.pages
+        else:
+            return True
+
+    def iter_pages(self, left_edge=2, left_current=2,
+                   right_current=5, right_edge=2):
+        last = 0
+        if self.last_page != -1:
+            for num in range(1, self.pages+1):
+                if num <= left_edge or \
+                   (num > self.page - left_current - 1 and \
+                    num < self.page + right_current) or \
+                   num > self.pages+1 - right_edge:
+                    if last + 1 != num:
+                        yield None
+                    yield num
+                    last = num
+        else:
+            for num in range(1, self.page+2):
+                if num <= left_edge or \
+                   num > self.page - left_current - 1:
+                    if last + 1 != num:
+                        yield None
+                    yield num
+                    last = num
+
+
+def check_user_status_and_repo_access(repository):
     """
         Function to call at the beginning of every view that would require
         authentication and editing privilege to work properly (basically
         everything save login and index page)
     """
+    if repository not in app.config["PERSISTENCE_CONFIG"]["REPOSITORY_LIST"]:
+        return redirect(url_for("home.index"))
     if not session.get("user_logged", None):
         return redirect(url_for("home.index"))
     if not session.get("user_can_edit", {}).get(repository, False):
         return redirect(url_for("home.index"))
 
 
-@cache.memoize(timeout=3600)
-def get_comments(request_data):
+def get_comments(repository, thread_type, thread_id, page=1, per_page=30):
     """
-        Function that takes a dict with the following two values:
+        Function that takes the following args:
+            * repository: the repository from which to get comments from
             * type: either "issues" or "commits"
             * id: the id of the issue of commit to get comments from
+            * page: the page of comments to get
+            * per_page: the number of comments per page
         then builds a comment list with each comment being a dict with the
         following format:
         {
@@ -44,28 +94,35 @@
     """
     github_comments_data = []
 
-    if request_data["type"] == "commits":
-        commits_list = get_commits(request_data["repository"])
-        if request_data["id"] not in [commit["id"] for commit in commits_list]:
+    if thread_type == "commits":
+        commits_list = get_commits(repository)
+        if thread_id not in [commit["id"] for commit in commits_list]:
             abort(404)
-    elif request_data["type"] == "issues":
-        issues_list = get_issues(request_data["repository"])
-        if request_data["id"] not in [issue["id"] for issue in issues_list]:
+    elif thread_type == "issues":
+        issues_list = get_issues(repository)
+        if thread_id not in [issue["id"] for issue in issues_list]:
             abort(404)
 
     try:
         github_comments_data = github.get(
             "repos/"
             + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
-            + request_data["repository"] + "/"
-            + request_data["type"] + "/"
-            + request_data["id"] + "/comments?per_page=100"
+            + repository + "/"
+            + thread_type + "/"
+            + thread_id
+            + "/comments?per_page=" + str(per_page)
+            + "&page=" + str(page),
+            hooks=dict(response=save_links)
         )
 
     except GitHubError as ghe:
         logger.error(
-            "Error trying to get comments with following data: "
-            + str(request_data)
+            "Error trying to get comments with following data:"
+            + " - repository : " + repository
+            + " - thread_type : " + thread_type
+            + " - thread_id : " + thread_id
+            + " - page : " + page
+            + " - per_page : " + per_page
         )
         logger.error(ghe.response.text)
 
@@ -85,20 +142,21 @@
         discussion_data = github.get(
             "repos/"
             + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
-            + request_data["repository"] + "/"
-            + request_data["type"] + "/"
-            + request_data["id"]
+            + repository + "/"
+            + thread_type + "/"
+            + thread_id,
+            hooks=dict(response=log_api_rate)
         )
     except GitHubError as ghe:
         logger.error(
-            "Error trying to get the or issue of id " + request_data["id"]
+            "Error trying to get the or issue of id " + thread_id
         )
         logger.error(
             "endpoint: " + "repos/"
             + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
-            + request_data["repository"] + "/"
-            + request_data["type"] + "/"
-            + request_data["id"]
+            + repository + "/"
+            + thread_type + "/"
+            + thread_id
         )
         logger.error(ghe.response.text)
 
@@ -107,7 +165,9 @@
     thread_title = ""
     thread_opening_post = ""
 
-    if request_data["type"] == "commits":
+    route_target = ""
+
+    if thread_type == "commits":
         thread_author = discussion_data.get("author", {}).get("login", "")
         thread_opening_date = convert_github_date(
             discussion_data.get(
@@ -119,7 +179,8 @@
             ).get("date", "")
         )
         thread_title = discussion_data.get("commit", {}).get("message", "")
-    elif request_data["type"] == "issues":
+    elif thread_type == "issues":
+        route_target = "social.discussion"
         thread_author = discussion_data.get("user", {}).get("login", "")
         thread_opening_date = convert_github_date(
             discussion_data.get("created_at", "0001-01-01T00:00:00Z")
@@ -127,18 +188,21 @@
         thread_title = discussion_data.get("title", "")
         thread_opening_post = discussion_data.get("body", "")
 
-    logger.debug(str(github.get("rate_limit")["resources"]))
+
     thread_dict = {
         "author": thread_author,
         "title": thread_title,
         "opening_date": thread_opening_date,
         "comment_list": comment_list,
-        "opening_post": thread_opening_post
+        "opening_post": thread_opening_post,
+        "per_page": per_page
     }
+
     return thread_dict
 
 
-def post_comment(request_data):
+def post_comment(repository, thread_type, thread_id,
+                 comment_body, thread_title=""):
     """
         Function that posts a given comment to github.
 
@@ -150,43 +214,52 @@
         * comment_body is the content of the comment
     """
     comment_data = {
-        "body": request_data["comment_body"]
+        "body": comment_body
     }
     return_id = ""
-    if request_data["thread_id"] != "new":
+    if thread_id != "new":
         try:
             github_response = github.post(
                 "repos/"
                 + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
-                + request_data["repository"] + "/"
-                + request_data["type"] + "/"
-                + request_data["thread_id"]
+                + repository + "/"
+                + thread_type + "/"
+                + thread_id
                 + "/comments",
-                data=comment_data
+                data=comment_data,
+                hooks=dict(response=log_api_rate)
             )
-            return_id = request_data["thread_id"]
+            return_id = thread_id
         except GitHubError as ghe:
             logger.error(
                 "Error posting comment with following data: "
-                + str(request_data)
+                + " - repository : " + repository
+                + " - thread_id : " + thread_id
+                + " - thread_type : " + thread_type
+                + " - comment_body : " + comment_body
             )
             logger.error(ghe.response.text)
     else:
         # We're posting a new issue
-        comment_data["title"] = request_data["thread_title"]
+        comment_data["title"] = thread_title
         try:
             github_response = github.post(
                 "repos/"
                 + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
-                + request_data["repository"] + "/"
-                + request_data["type"],
-                data=comment_data
+                + repository + "/"
+                + thread_type,
+                data=comment_data,
+                hooks=dict(response=log_api_rate)
             )
             return_id = str(github_response["number"])
         except GitHubError as ghe:
             logger.error(
                 "Error posting new issue with following data: "
-                + str(request_data)
+                + " - repository : " + repository
+                + " - thread_id : " + thread_id
+                + " - thread_type : " + thread_type
+                + " - thread_title : " + thread_title
+                + " - comment_body : " + comment_body
             )
             logger.error(ghe.response.text)
     cache.clear()
@@ -211,7 +284,8 @@
             "repos/"
             + app.config["PERSISTENCE_CONFIG"]["REPOSITORY_OWNER"] + "/"
             + repository
-            + "/commits?per_page=100"
+            + "/commits?per_page=5",
+            hooks=dict(response=save_links)
         )
     except GitHubError as ghe:
         logger.error("Error getting commits for repo " + repository)
@@ -221,7 +295,7 @@
             "id": commit["sha"],
             "title": commit["commit"]["message"],
             "date": convert_github_date(
-                commit["commit"]["committer"]["date"],
+                commit["commit"]["committer"]["date"]
             ),
             "author": commit["commit"]["committer"]["name"],
             "comment_count": commit["commit"]["comment_count"],
@@ -229,7 +303,6 @@
         for commit in commits_data
     ]
 
-    logger.debug(str(github.get("rate_limit")["resources"]))
     return changeset_list
 
 
@@ -271,12 +344,12 @@
         for issue in issues_data
     ]
 
-    logger.debug(str(github.get("rate_limit")["resources"]))
+
     return discussion_list
 
 
 @cache.memoize(timeout=3600)
-def get_category_list(repository, changeset_id):
+def get_category_list_for_commit(repository, changeset_id):
     """
         Get the category list as it was following the changeset of
         id changeset_id
@@ -298,7 +371,7 @@
         logger.error("Error trying to get the commit of id " + changeset_id)
         logger.error(ghe.response.text)
 
-    logger.debug(str(github.get("rate_limit")["resources"]))
+
 
     tree_sha = commit_data.get("commit", {}).get("tree", {}).get("sha", "")
 
@@ -316,7 +389,7 @@
         logger.error("Error trying to get the tree of sha " + tree_sha)
         logger.error(ghe.response.text)
 
-    logger.debug(str(github.get("rate_limit")["resources"]))
+
     logger.debug(tree_data)
 
     # Third step and fourth step
@@ -355,7 +428,6 @@
                 }
             )
 
-    logger.debug(str(github.get("rate_limit")["resources"]))
     return cat_list
 
 
@@ -370,3 +442,102 @@
     ).strftime(
         "%d/%m/%Y à %H:%M"
     )
+
+
+def get_current_category_list(repository, with_local_changes=True):
+    """
+        Shortcut function that generates the list of category to use for
+        view templates.
+        Each category is a dict with the following format:
+        {
+            "cat_label": category label,
+             "cat_description": category description,
+             "cat_id": category id,
+             "cat_properties": category properties,
+             "state": category state (one of {"untouched", "created",
+             "edited", "deleted"})
+        }
+    """
+    cat_api_instance = CategoryAPI()
+    cat_changes_api_instance = CategoryChangesAPI()
+
+    deleted_cat_dict = {}
+    modified_cat_dict = {}
+    serialized_cat_list = []
+    if session.get("user_logged", None) is not None:
+        serialized_cat_list = cat_api_instance.get(repository=repository) \
+                                                  [0]
+        cat_changes = cat_changes_api_instance.get(repository=repository) \
+                                                  [0]
+        modified_cat_dict = cat_changes["modified_categories"]
+        deleted_cat_dict = cat_changes["deleted_categories"]
+    # logger.debug(serialized_cat_list)
+    cat_list = []
+    original_cat_list = []
+    for serialized_cat in serialized_cat_list:
+        cat_rdf_graph = Graph()
+        cat_rdf_graph.parse(source=StringIO(serialized_cat),
+                            format='turtle')
+        original_cat_list.append(Category(graph=cat_rdf_graph))
+
+    if with_local_changes:
+        # We want the categories updated with the changes current user made
+        edited_cat_list = []
+        for modified_cat_name in modified_cat_dict.keys():
+            new_cat_rdf_graph = Graph()
+            new_cat_rdf_graph.parse(
+                source=StringIO(
+                    modified_cat_dict[modified_cat_name]
+                ),
+                format='turtle'
+            )
+            edited_cat_list.append(Category(graph=new_cat_rdf_graph))
+        # first we find the untouched, edited and deleted categories
+        cat_state = ""
+        for category in original_cat_list:
+            if category.cat_id not in modified_cat_dict.keys():
+                if category.cat_id in deleted_cat_dict.keys():
+                    cat_state = "deleted"
+                else:
+                    cat_state = "untouched"
+
+                cat_list.append(
+                    {
+                        "cat_label": category.label,
+                        "cat_description": category.description,
+                        "cat_id": category.cat_id,
+                        "cat_properties": category.properties,
+                        "state": cat_state
+                    }
+                )
+
+        # now we must find the not yet submitted categories that were created
+        cat_state = ""
+        logger.debug("Edited cat list: "
+                     + str([cat.label for cat in edited_cat_list])
+                     + " - Original cat list: "
+                     + str([cat.label for cat in original_cat_list]))
+        for category in edited_cat_list:
+            if category.cat_id not in [cat.cat_id for
+                                       cat in original_cat_list]:
+                cat_state = "created"
+            else:
+                cat_state = "modified"
+            cat_list.append({"cat_label": category.label,
+                             "cat_description": category.description,
+                             "cat_id": category.cat_id,
+                             "cat_properties": category.properties,
+                             "state": cat_state})
+    else:
+        # We only want the categories
+        for category in original_cat_list:
+            cat_list.append(
+                {
+                    "cat_label": category.label,
+                    "cat_description": category.description,
+                    "cat_id": category.cat_id,
+                    "cat_properties": category.properties,
+                    "state": "original"
+                }
+            )
+    return cat_list