[odrs-web] Convert to a more flask-like structure



commit 11665e3339e51da97333c3dd45bab00d4979612d
Author: Richard Hughes <richard hughsie com>
Date:   Thu Jul 6 14:29:56 2017 +0100

    Convert to a more flask-like structure

 app/__init__.py                               |   48 ++
 database.py => app/db.py                      |  614 +++++++++++++------------
 app/models.py                                 |   61 +++
 {static => app/static}/Chart.js               |    0
 {static => app/static}/app-page.png           |  Bin 94032 -> 94032 bytes
 {static => app/static}/bar.png                |  Bin 154 -> 154 bytes
 {static => app/static}/favicon.ico            |  Bin 2550 -> 2550 bytes
 {static => app/static}/foot.png               |  Bin 699 -> 699 bytes
 {static => app/static}/general_bg.png         |  Bin 178 -> 178 bytes
 {static => app/static}/general_separator.png  |  Bin 212 -> 212 bytes
 {static => app/static}/gnome-16.png           |  Bin 650 -> 650 bytes
 {static => app/static}/gnome-odrs.png         |  Bin 14485 -> 14485 bytes
 {static => app/static}/layout.css             |    0
 {static => app/static}/review-submit.png      |  Bin 25010 -> 25010 bytes
 {static => app/static}/search-icon.png        |  Bin 395 -> 395 bytes
 {static => app/static}/style.css              |    0
 {static => app/static}/t.png                  |  Bin 317 -> 317 bytes
 {static => app/static}/top_bar-bg.png         |  Bin 185 -> 185 bytes
 {templates => app/templates}/default.html     |    1 -
 {templates => app/templates}/delete.html      |    0
 {templates => app/templates}/distros.html     |    0
 {templates => app/templates}/error.html       |    0
 {templates => app/templates}/graph-month.html |    0
 {templates => app/templates}/graph-year.html  |    0
 {templates => app/templates}/index.html       |    0
 {templates => app/templates}/login.html       |    0
 {templates => app/templates}/oars.html        |    0
 {templates => app/templates}/show-all.html    |    0
 {templates => app/templates}/show.html        |    0
 {templates => app/templates}/stats.html       |    0
 {templates => app/templates}/users.html       |    0
 api10.py => app/views.py                      |  217 ++++++----
 admin.py => app/views_admin.py                |  176 ++++----
 event.py                                      |   15 -
 flaskapp.py                                   |   74 +---
 odrs.wsgi                                     |    2 +-
 review.py                                     |   23 -
 user.py                                       |   31 --
 38 files changed, 644 insertions(+), 618 deletions(-)
---
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..861a48f
--- /dev/null
+++ b/app/__init__.py
@@ -0,0 +1,48 @@
+#!/usr/bin/python2
+# -*- coding: utf-8 -*-
+#
+# pylint: disable=invalid-name,missing-docstring
+#
+# Copyright (C) 2015-2017 Richard Hughes <richard hughsie com>
+# Licensed under the GNU General Public License Version 2
+
+import os
+
+from flask import Flask, flash, render_template, g
+from flask_login import LoginManager
+
+from .db import Database
+
+app = Flask(__name__)
+app.config.from_object(__name__)
+app.secret_key = os.environ['ODRS_REVIEWS_SECRET']
+
+lm = LoginManager()
+lm.init_app(app)
+
+def get_db():
+    db = getattr(g, '_database', None)
+    if db is None:
+        db = g.db = Database(app)
+    return db
+
+@app.teardown_appcontext
+def teardown_request(exception):
+    db = getattr(g, 'db', None)
+    if db is not None:
+        db.close()
+
+@lm.user_loader
+def load_user(user_id):
+    db = get_db()
+    user = db.users.get_by_id(user_id)
+    return user
+
+@app.errorhandler(404)
+def error_page_not_found(msg=None):
+    """ Error handler: File not found """
+    flash(msg)
+    return render_template('error.html'), 404
+
+from app import views
+from app import views_admin
diff --git a/database.py b/app/db.py
similarity index 91%
rename from database.py
rename to app/db.py
index 920cc05..a4a68ed 100644
--- a/database.py
+++ b/app/db.py
@@ -1,18 +1,19 @@
 #!/usr/bin/python
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2016 Richard Hughes <richard hughsie com>
+# pylint: disable=invalid-name,missing-docstring
+#
+# Copyright (C) 2016-2017 Richard Hughes <richard hughsie com>
 # Licensed under the GNU General Public License Version 3
 
-import pymysql as mdb
-import pymysql.cursors
+import os
 import cgi
 import datetime
 import hashlib
 
-from user import OdrsUser
-from event import OdrsEvent
-from review import OdrsReview
+import pymysql as mdb
+
+from .models import User, Event, Review
 
 class CursorError(Exception):
     def __init__(self, cur, e):
@@ -22,7 +23,7 @@ class CursorError(Exception):
 
 def _create_review(e):
     """ Parse a review """
-    review = OdrsReview()
+    review = Review()
     review.review_id = int(e[0])
     review.date_created = int(e[1].strftime("%s"))
     review.app_id = e[2]
@@ -43,7 +44,7 @@ def _create_review(e):
 
 def _create_event(e):
     """ Parse an event """
-    event = OdrsEvent()
+    event = Event()
     event.eventlog_id = int(e[0])
     event.date_created = int(e[1].strftime("%s"))
     event.user_addr = e[2]
@@ -55,7 +56,7 @@ def _create_event(e):
 
 def _create_user(e):
     """ Parse a user """
-    user = OdrsUser()
+    user = User()
     user.id = int(e[0])
     user.date_created = int(e[1].strftime("%s"))
     user.user_hash = e[2]
@@ -71,17 +72,16 @@ def _password_hash(value):
 def _get_datestr_from_dt(when):
     return int("%04i%02i%02i" % (when.year, when.month, when.day))
 
-class ReviewsDatabase(object):
+class Database(object):
 
-    def __init__(self, environ):
+    def __init__(self, app):
         """ Constructor for object """
-        assert environ
         self._db = None
         try:
-            if 'MYSQL_DB_HOST' in environ:
-                self._db = mdb.connect(environ['MYSQL_DB_HOST'],
-                                       environ['MYSQL_DB_USERNAME'],
-                                       environ['MYSQL_DB_PASSWORD'],
+            if 'MYSQL_DB_HOST' in os.environ:
+                self._db = mdb.connect(os.environ['MYSQL_DB_HOST'],
+                                       os.environ['MYSQL_DB_USERNAME'],
+                                       os.environ['MYSQL_DB_PASSWORD'],
                                        'odrs',
                                        use_unicode=True, charset='utf8')
             else:
@@ -91,13 +91,243 @@ class ReviewsDatabase(object):
         except mdb.Error as e:
             print("Error %d: %s" % (e.args[0], e.args[1]))
         assert self._db
+        self.users = DatabaseUsers(self._db)
+        self.reviews = DatabaseReviews(self._db)
+        self.eventlog = DatabaseEventlog(self._db)
 
-    def __del__(self):
+    def close(self):
         """ Clean up the database """
         if self._db:
             self._db.close()
 
-    def review_modify(self, review):
+    def analytics_inc_fetch(self, app_id, when=None):
+        """ Increments the fetch count on one specific application """
+        try:
+            cur = self._db.cursor()
+            if not when:
+                when = datetime.date.today()
+            datestr = _get_datestr_from_dt(when)
+            cur.execute("INSERT INTO analytics (datestr,app_id) VALUES (%s, %s) "
+                        "ON DUPLICATE KEY UPDATE fetch_cnt=fetch_cnt+1;",
+                        (datestr, app_id,))
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+
+    def get_stats_distro(self, limit=10):
+        """ Returns distro stats for reviews """
+        try:
+            cur = self._db.cursor()
+            cur.execute("SELECT DISTINCT(distro), COUNT(distro) AS total "
+                        "FROM reviews GROUP BY distro ORDER BY total DESC "
+                        "LIMIT %s;",
+                        (limit,))
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        data = []
+        for en in res:
+            data.append((en[0], en[1]))
+        return data
+
+    def get_stats_fetch(self, msg, limit=50):
+        """ Returns interesting statistics for the webapp """
+        try:
+            cur = self._db.cursor()
+            cur.execute("SELECT DISTINCT app_id, COUNT(app_id) as total "
+                        "FROM eventlog WHERE app_id IS NOT NULL "
+                        "AND message=%s GROUP BY app_id "
+                        "ORDER BY total DESC LIMIT %s;",
+                        (msg, limit,))
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        data = []
+        for en in res:
+            data.append((en[0], en[1]))
+        return data
+
+    def get_stats(self):
+        """ Returns interesting statistics for the webapp """
+        item = {}
+
+        # get the total number of reviews
+        try:
+            cur = self._db.cursor()
+            cur.execute("SELECT COUNT(*) FROM reviews;")
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        item['Active reviews'] = int(res[0][0])
+
+        # unique reviewers
+        try:
+            cur.execute("SELECT COUNT(DISTINCT(user_hash)) FROM reviews;")
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        item['Unique reviewers'] = int(res[0][0])
+
+        # total votes
+        try:
+            cur.execute("SELECT COUNT(*) FROM votes WHERE val = 1;")
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        item['User upvotes'] = int(res[0][0])
+        try:
+            cur.execute("SELECT COUNT(*) FROM votes WHERE val = -1;")
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        item['User downvotes'] = int(res[0][0])
+
+        # unique voters
+        try:
+            cur.execute("SELECT COUNT(DISTINCT(user_hash)) FROM votes;")
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        item['Unique voters'] = int(res[0][0])
+
+        # unique languages
+        try:
+            cur.execute("SELECT COUNT(DISTINCT(locale)) FROM reviews;")
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        item['Unique languages'] = int(res[0][0])
+
+        # unique distros
+        try:
+            cur.execute("SELECT COUNT(DISTINCT(distro)) FROM reviews;")
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        item['Unique distros'] = int(res[0][0])
+
+        # unique apps
+        try:
+            cur.execute("SELECT COUNT(DISTINCT(app_id)) FROM reviews;")
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        item['Unique apps reviewed'] = int(res[0][0])
+
+        # unique distros
+        try:
+            cur.execute("SELECT COUNT(*) FROM reviews WHERE reported > 0;")
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        item['Reported reviews'] = int(res[0][0])
+
+        # star reviews
+        for star in range(1, 6):
+            try:
+                cur.execute("SELECT COUNT(*) FROM reviews WHERE rating = %s;",
+                            (star * 20,))
+            except mdb.Error as e:
+                raise CursorError(cur, e)
+            res = cur.fetchall()
+            item['%i star reviews' % star] = int(res[0][0])
+
+        # done
+        return item
+
+    def get_stats_by_interval(self, size, interval, msg):
+        """ Gets stats data """
+        cnt = []
+        now = datetime.date.today()
+
+        # yes, there's probably a way to do this in one query
+        cur = self._db.cursor()
+        for i in range(size):
+            start = now - datetime.timedelta((i * interval) + interval - 1)
+            end = now - datetime.timedelta((i * interval) - 1)
+            try:
+                cur.execute("SELECT COUNT(*) FROM eventlog "
+                            "WHERE message = %s AND date_created BETWEEN %s "
+                            "AND %s", (msg, start, end,))
+            except mdb.Error as e:
+                raise CursorError(cur, e)
+            res = cur.fetchone()
+            cnt.append(int(res[0]))
+        return cnt
+
+    def get_analytics_by_interval(self, size, interval):
+        """ Gets analytics data """
+        array = []
+        now = datetime.date.today()
+
+        # yes, there's probably a way to do this in one query
+        cur = self._db.cursor()
+        for i in range(size):
+            start = _get_datestr_from_dt(now - datetime.timedelta((i * interval) + interval - 1))
+            end = _get_datestr_from_dt(now - datetime.timedelta((i * interval) - 1))
+            try:
+                cur.execute("SELECT fetch_cnt FROM analytics WHERE "
+                            "datestr BETWEEN %s "
+                            "AND %s", (start, end,))
+            except mdb.Error as e:
+                raise CursorError(cur, e)
+            res = cur.fetchall()
+
+            # add all these up
+            tmp = 0
+            for r in res:
+                tmp = tmp + int(r[0])
+            array.append(tmp)
+        return array
+
+    def get_analytics_fetch(self, limit=50):
+        """ Returns interesting statistics for the webapp """
+        try:
+            cur = self._db.cursor()
+            cur.execute("SELECT DISTINCT app_id, SUM(fetch_cnt) AS total "
+                        "FROM analytics WHERE app_id IS NOT NULL "
+                        "GROUP BY app_id ORDER BY total DESC LIMIT %s;",
+                        (limit,))
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchall()
+        data = []
+        for en in res:
+            data.append((en[0], en[1]))
+        return data
+
+class DatabaseEventlog(object):
+
+    def __init__(self, db):
+        """ Constructor for object """
+        self._db = db
+
+    def warn(self,
+             user_addr=None,
+             user_hash=None,
+             app_id=None,
+             message=None,
+             important=True):
+        """ Adds a warning to the event log """
+        try:
+            cur = self._db.cursor()
+            cur.execute("INSERT INTO eventlog (user_addr, user_hash, app_id, "
+                        "message, important) "
+                        "VALUES (%s, %s, %s, %s, %s);",
+                        (user_addr, user_hash, app_id, message, important,))
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+
+    def info(self, user_addr=None, user_hash=None, app_id=None, message=None):
+        """ Adds an info item to the event log """
+        self.warn(user_addr, user_hash, app_id, message, False)
+
+class DatabaseReviews(object):
+
+    def __init__(self, db):
+        """ Constructor for object """
+        self._db = db
+
+    def modify(self, review):
         """ Modifies a review """
         try:
             cur = self._db.cursor()
@@ -121,7 +351,7 @@ class ReviewsDatabase(object):
             raise CursorError(cur, e)
         return True
 
-    def review_add(self, review, user_addr):
+    def add(self, review, user_addr):
         """ Add a review to the database """
         try:
             cur = self._db.cursor()
@@ -142,7 +372,7 @@ class ReviewsDatabase(object):
         except mdb.Error as e:
             raise CursorError(cur, e)
 
-    def review_delete(self, review):
+    def delete(self, review):
         """ Deletes a review """
         try:
             cur = self._db.cursor()
@@ -152,7 +382,7 @@ class ReviewsDatabase(object):
             raise CursorError(cur, e)
         return True
 
-    def review_remove(self, review_id, user_hash):
+    def remove(self, review_id, user_hash):
         """ Marks a review as removed """
         try:
             cur = self._db.cursor()
@@ -163,7 +393,7 @@ class ReviewsDatabase(object):
             raise CursorError(cur, e)
         return True
 
-    def review_get_for_app_id(self, app_id):
+    def get_for_app_id(self, app_id):
         """ Returns all the reviews for an application (for client-side) """
         try:
             cur = self._db.cursor()
@@ -183,7 +413,7 @@ class ReviewsDatabase(object):
             reviews.append(_create_review(e))
         return reviews
 
-    def review_get_for_id(self, review_id):
+    def get_for_id(self, review_id):
         """ Returns a specific review """
         try:
             cur = self._db.cursor()
@@ -199,7 +429,7 @@ class ReviewsDatabase(object):
             return None
         return _create_review(res[0])
 
-    def review_exists(self, app_id, user_hash):
+    def exists(self, app_id, user_hash):
         """ Checks to see if a review exists for the application+user """
         try:
             cur = self._db.cursor()
@@ -213,19 +443,6 @@ class ReviewsDatabase(object):
             return True
         return False
 
-    def vote_exists(self, review_id, user_hash):
-        """ Checks to see if a vote exists for the review+user """
-        try:
-            cur = self._db.cursor()
-            cur.execute("SELECT date_created "
-                        "FROM votes WHERE review_id=%s AND user_hash=%s;",
-                        (review_id, user_hash,))
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchone()
-        if res is not None:
-            return True
-        return False
 
     def vote_add(self, review_id, val, user_hash):
         """ Votes on a specific review and add to the votes database """
@@ -246,7 +463,21 @@ class ReviewsDatabase(object):
         except mdb.Error as e:
             raise CursorError(cur, e)
 
-    def review_get_all(self):
+    def vote_exists(self, review_id, user_hash):
+        """ Checks to see if a vote exists for the review+user """
+        try:
+            cur = self._db.cursor()
+            cur.execute("SELECT date_created "
+                        "FROM votes WHERE review_id=%s AND user_hash=%s;",
+                        (review_id, user_hash,))
+        except mdb.Error as e:
+            raise CursorError(cur, e)
+        res = cur.fetchone()
+        if res is not None:
+            return True
+        return False
+
+    def get_all(self):
         """ Gets all non-removed reviews from the server for all applications """
         try:
             cur = self._db.cursor()
@@ -264,40 +495,55 @@ class ReviewsDatabase(object):
             reviews.append(_create_review(e))
         return reviews
 
-    def event_warn(self,
-                   user_addr=None,
-                   user_hash=None,
-                   app_id=None,
-                   message=None,
-                   important=True):
-        """ Adds a warning to the event log """
+    def get_rating_for_app_id(self, app_id, min_total=1):
+        """ Gets the ratings information for the application """
         try:
             cur = self._db.cursor()
-            cur.execute("INSERT INTO eventlog (user_addr, user_hash, app_id, "
-                        "message, important) "
-                        "VALUES (%s, %s, %s, %s, %s);",
-                        (user_addr, user_hash, app_id, message, important,))
+            cur.execute("SELECT COUNT(*) total,"
+                        "       SUM(rating = 0) star0,"
+                        "       SUM(rating = 20) star1,"
+                        "       SUM(rating = 40) star2,"
+                        "       SUM(rating = 60) star3,"
+                        "       SUM(rating = 80) star4,"
+                        "       SUM(rating = 100) star5 "
+                        "FROM reviews WHERE app_id = %s "
+                        "AND date_deleted=0;", (app_id,))
         except mdb.Error as e:
             raise CursorError(cur, e)
+        res = cur.fetchone()
+        if not res:
+            return []
+        item = {}
+        item['total'] = int(res[0])
+        if item['total'] < min_total:
+            return []
+        for i in range(6):
+            if res[i + 1]:
+                item['star%i' % i] = int(res[i + 1])
+            else:
+                item['star%i' % i] = 0
+        return item
 
-    def event_info(self, user_addr=None, user_hash=None, app_id=None, message=None):
-        """ Adds an info item to the event log """
-        self.event_warn(user_addr, user_hash, app_id, message, False)
-
-    def analytics_inc_fetch(self, app_id, when=None):
-        """ Increments the fetch count on one specific application """
+    def get_all_apps(self):
+        """ Returns interesting statistics for the webapp """
         try:
             cur = self._db.cursor()
-            if not when:
-                when = datetime.date.today()
-            datestr = _get_datestr_from_dt(when)
-            cur.execute("INSERT INTO analytics (datestr,app_id) VALUES (%s, %s) "
-                        "ON DUPLICATE KEY UPDATE fetch_cnt=fetch_cnt+1;",
-                        (datestr, app_id,))
+            cur.execute("SELECT DISTINCT(app_id) FROM reviews ORDER BY app_id;")
         except mdb.Error as e:
             raise CursorError(cur, e)
+        res = cur.fetchall()
+        data = []
+        for en in res:
+            data.append(en[0])
+        return data
 
-    def user_add(self, user_hash):
+class DatabaseUsers(object):
+
+    def __init__(self, db):
+        """ Constructor for object """
+        self._db = db
+
+    def add(self, user_hash):
         """ Add a user to the database """
         try:
             cur = self._db.cursor()
@@ -306,7 +552,7 @@ class ReviewsDatabase(object):
         except mdb.Error as e:
             raise CursorError(cur, e)
 
-    def user_get_all(self):
+    def get_all(self):
         """ Get all the users on the system """
         try:
             cur = self._db.cursor()
@@ -323,7 +569,7 @@ class ReviewsDatabase(object):
             users.append(_create_user(e))
         return users
 
-    def user_get_with_login(self, username, password):
+    def get_with_login(self, username, password):
         """ Get information about a specific login """
         try:
             cur = self._db.cursor()
@@ -339,7 +585,7 @@ class ReviewsDatabase(object):
             return None
         return _create_user(res)
 
-    def user_get_by_id(self, user_hash):
+    def get_by_id(self, user_hash):
         """ Get information about a specific user """
         try:
             cur = self._db.cursor()
@@ -354,7 +600,7 @@ class ReviewsDatabase(object):
             return None
         return _create_user(res)
 
-    def user_get_by_hash(self, user_hash):
+    def get_by_hash(self, user_hash):
         """ Get information about a specific user """
         try:
             cur = self._db.cursor()
@@ -369,7 +615,7 @@ class ReviewsDatabase(object):
             return None
         return _create_user(res)
 
-    def get_users_by_karma(self, best=True):
+    def get_by_karma(self, best=True):
         """ Returns interesting statistics for the webapp """
         try:
             cur = self._db.cursor()
@@ -389,13 +635,13 @@ class ReviewsDatabase(object):
             data.append(_create_user(res))
         return data
 
-    def user_update_karma(self, user_hash, val):
+    def update_karma(self, user_hash, val):
         """ Update the request time for a specific user ID """
 
         # if not existing, create it
-        user = self.user_get_by_hash(user_hash)
+        user = self.get_by_hash(user_hash)
         if not user:
-            self.user_add(user_hash)
+            self.add(user_hash)
             return
 
         # update the karma value
@@ -406,11 +652,11 @@ class ReviewsDatabase(object):
         except mdb.Error as e:
             raise CursorError(cur, e)
 
-    def user_ban(self, user_hash):
+    def ban(self, user_hash):
         """ Ban a user """
 
         # check it exists
-        user = self.user_get_by_hash(user_hash)
+        user = self.get_by_hash(user_hash)
         if not user:
             return
 
@@ -421,227 +667,3 @@ class ReviewsDatabase(object):
                         "WHERE user_hash = %s;", (user_hash,))
         except mdb.Error as e:
             raise CursorError(cur, e)
-
-    def reviews_get_rating_for_app_id(self, app_id, min_total=1):
-        """ Gets the ratings information for the application """
-        try:
-            cur = self._db.cursor()
-            cur.execute("SELECT COUNT(*) total,"
-                        "       SUM(rating = 0) star0,"
-                        "       SUM(rating = 20) star1,"
-                        "       SUM(rating = 40) star2,"
-                        "       SUM(rating = 60) star3,"
-                        "       SUM(rating = 80) star4,"
-                        "       SUM(rating = 100) star5 "
-                        "FROM reviews WHERE app_id = %s "
-                        "AND date_deleted=0;", (app_id,))
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchone()
-        if not res:
-            return []
-        item = {}
-        item['total'] = int(res[0])
-        if item['total'] < min_total:
-            return []
-        for i in range(6):
-            if res[i + 1]:
-                item['star%i' % i] = int(res[i + 1])
-            else:
-                item['star%i' % i] = 0
-        return item
-
-    def get_stats_distro(self, limit=10):
-        """ Returns distro stats for reviews """
-        try:
-            cur = self._db.cursor()
-            cur.execute("SELECT DISTINCT(distro), COUNT(distro) AS total "
-                        "FROM reviews GROUP BY distro ORDER BY total DESC "
-                        "LIMIT %s;",
-                        (limit,))
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        data = []
-        for en in res:
-            data.append((en[0], en[1]))
-        return data
-
-    def get_stats_fetch(self, msg, limit=50):
-        """ Returns interesting statistics for the webapp """
-        try:
-            cur = self._db.cursor()
-            cur.execute("SELECT DISTINCT app_id, COUNT(app_id) as total "
-                        "FROM eventlog WHERE app_id IS NOT NULL "
-                        "AND message=%s GROUP BY app_id "
-                        "ORDER BY total DESC LIMIT %s;",
-                        (msg, limit,))
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        data = []
-        for en in res:
-            data.append((en[0], en[1]))
-        return data
-
-    def get_all_apps(self):
-        """ Returns interesting statistics for the webapp """
-        try:
-            cur = self._db.cursor()
-            cur.execute("SELECT DISTINCT(app_id) FROM reviews ORDER BY app_id;")
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        data = []
-        for en in res:
-            data.append(en[0])
-        return data
-
-    def get_stats(self):
-        """ Returns interesting statistics for the webapp """
-        item = {}
-
-        # get the total number of reviews
-        try:
-            cur = self._db.cursor()
-            cur.execute("SELECT COUNT(*) FROM reviews;")
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        item['Active reviews'] = int(res[0][0])
-
-        # unique reviewers
-        try:
-            cur.execute("SELECT COUNT(DISTINCT(user_hash)) FROM reviews;")
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        item['Unique reviewers'] = int(res[0][0])
-
-        # total votes
-        try:
-            cur.execute("SELECT COUNT(*) FROM votes WHERE val = 1;")
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        item['User upvotes'] = int(res[0][0])
-        try:
-            cur.execute("SELECT COUNT(*) FROM votes WHERE val = -1;")
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        item['User downvotes'] = int(res[0][0])
-
-        # unique voters
-        try:
-            cur.execute("SELECT COUNT(DISTINCT(user_hash)) FROM votes;")
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        item['Unique voters'] = int(res[0][0])
-
-        # unique languages
-        try:
-            cur.execute("SELECT COUNT(DISTINCT(locale)) FROM reviews;")
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        item['Unique languages'] = int(res[0][0])
-
-        # unique distros
-        try:
-            cur.execute("SELECT COUNT(DISTINCT(distro)) FROM reviews;")
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        item['Unique distros'] = int(res[0][0])
-
-        # unique apps
-        try:
-            cur.execute("SELECT COUNT(DISTINCT(app_id)) FROM reviews;")
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        item['Unique apps reviewed'] = int(res[0][0])
-
-        # unique distros
-        try:
-            cur.execute("SELECT COUNT(*) FROM reviews WHERE reported > 0;")
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        item['Reported reviews'] = int(res[0][0])
-
-        # star reviews
-        for star in range(1, 6):
-            try:
-                cur.execute("SELECT COUNT(*) FROM reviews WHERE rating = %s;",
-                            (star * 20,))
-            except mdb.Error as e:
-                raise CursorError(cur, e)
-            res = cur.fetchall()
-            item['%i star reviews' % star] = int(res[0][0])
-
-        # done
-        return item
-
-    def get_stats_by_interval(self, size, interval, msg):
-        """ Gets stats data """
-        cnt = []
-        now = datetime.date.today()
-
-        # yes, there's probably a way to do this in one query
-        cur = self._db.cursor()
-        for i in range(size):
-            start = now - datetime.timedelta((i * interval) + interval - 1)
-            end = now - datetime.timedelta((i * interval) - 1)
-            try:
-                cur.execute("SELECT COUNT(*) FROM eventlog "
-                            "WHERE message = %s AND date_created BETWEEN %s "
-                            "AND %s", (msg, start, end,))
-            except mdb.Error as e:
-                raise CursorError(cur, e)
-            res = cur.fetchone()
-            cnt.append(int(res[0]))
-        return cnt
-
-    def get_analytics_by_interval(self, size, interval):
-        """ Gets analytics data """
-        array = []
-        now = datetime.date.today()
-
-        # yes, there's probably a way to do this in one query
-        cur = self._db.cursor()
-        for i in range(size):
-            start = _get_datestr_from_dt(now - datetime.timedelta((i * interval) + interval - 1))
-            end = _get_datestr_from_dt(now - datetime.timedelta((i * interval) - 1))
-            try:
-                cur.execute("SELECT fetch_cnt FROM analytics WHERE "
-                            "datestr BETWEEN %s "
-                            "AND %s", (start, end,))
-            except mdb.Error as e:
-                raise CursorError(cur, e)
-            res = cur.fetchall()
-
-            # add all these up
-            tmp = 0
-            for r in res:
-                tmp = tmp + int(r[0])
-            array.append(tmp)
-        return array
-
-    def get_analytics_fetch(self, limit=50):
-        """ Returns interesting statistics for the webapp """
-        try:
-            cur = self._db.cursor()
-            cur.execute("SELECT DISTINCT app_id, SUM(fetch_cnt) AS total "
-                        "FROM analytics WHERE app_id IS NOT NULL "
-                        "GROUP BY app_id ORDER BY total DESC LIMIT %s;",
-                        (limit,))
-        except mdb.Error as e:
-            raise CursorError(cur, e)
-        res = cur.fetchall()
-        data = []
-        for en in res:
-            data.append((en[0], en[1]))
-        return data
diff --git a/app/models.py b/app/models.py
new file mode 100644
index 0000000..67a6cb6
--- /dev/null
+++ b/app/models.py
@@ -0,0 +1,61 @@
+#!/usr/bin/python2
+# -*- coding: utf-8 -*-
+#
+# pylint: disable=invalid-name,missing-docstring,too-few-public-methods
+#
+# Copyright (C) 2015-2017 Richard Hughes <richard hughsie com>
+# Licensed under the GNU General Public License Version 2
+
+class User(object):
+    def __init__(self):
+        self.id = None
+        self.karma = 0
+        self.date_created = 0
+        self.user_hash = 0
+        self.is_banned = 0
+
+    @property
+    def is_authenticated(self):
+        return True
+
+    @property
+    def is_active(self):
+        return True
+
+    @property
+    def is_anonymous(self):
+        return False
+
+    def get_id(self):
+        return str(self.id)
+
+    def __repr__(self):
+        return '<User %r>' % (self.id)
+
+class Review(object):
+    def __init__(self):
+        self.review_id = 0
+        self.date_created = 0
+        self.app_id = None
+        self.locale = None
+        self.summary = None
+        self.description = None
+        self.version = None
+        self.distro = None
+        self.karma_up = 0
+        self.karma_down = 0
+        self.user_hash = None
+        self.user_display = None
+        self.rating = 0
+        self.date_deleted = None
+        self.reported = None
+
+class Event(object):
+    def __init__(self):
+        self.eventlog_id = 0
+        self.date_created = 0
+        self.user_addr = None
+        self.user_hash = None
+        self.message = None
+        self.app_id = None
+        self.important = False
diff --git a/static/Chart.js b/app/static/Chart.js
similarity index 100%
rename from static/Chart.js
rename to app/static/Chart.js
diff --git a/static/layout.css b/app/static/layout.css
similarity index 100%
rename from static/layout.css
rename to app/static/layout.css
diff --git a/static/style.css b/app/static/style.css
similarity index 100%
rename from static/style.css
rename to app/static/style.css
diff --git a/templates/default.html b/app/templates/default.html
similarity index 99%
rename from templates/default.html
rename to app/templates/default.html
index 964ac16..383725f 100644
--- a/templates/default.html
+++ b/app/templates/default.html
@@ -6,7 +6,6 @@
 <head>
   <title>{% block title %}{% endblock %}</title>
   <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
-  <base href="https://odrs.gnome.org/"; />
   <link href="layout.css" rel="stylesheet" type="text/css" media="screen" />
   <link href="style.css" rel="stylesheet" type="text/css" media="all" />
   <link rel="icon" type="image/png" href="https://www.gnome.org/img/logo/foot-16.png"; />
diff --git a/templates/delete.html b/app/templates/delete.html
similarity index 100%
rename from templates/delete.html
rename to app/templates/delete.html
diff --git a/templates/distros.html b/app/templates/distros.html
similarity index 100%
rename from templates/distros.html
rename to app/templates/distros.html
diff --git a/templates/error.html b/app/templates/error.html
similarity index 100%
rename from templates/error.html
rename to app/templates/error.html
diff --git a/templates/graph-month.html b/app/templates/graph-month.html
similarity index 100%
rename from templates/graph-month.html
rename to app/templates/graph-month.html
diff --git a/templates/graph-year.html b/app/templates/graph-year.html
similarity index 100%
rename from templates/graph-year.html
rename to app/templates/graph-year.html
diff --git a/templates/index.html b/app/templates/index.html
similarity index 100%
rename from templates/index.html
rename to app/templates/index.html
diff --git a/templates/login.html b/app/templates/login.html
similarity index 100%
rename from templates/login.html
rename to app/templates/login.html
diff --git a/templates/oars.html b/app/templates/oars.html
similarity index 100%
rename from templates/oars.html
rename to app/templates/oars.html
diff --git a/templates/show-all.html b/app/templates/show-all.html
similarity index 100%
rename from templates/show-all.html
rename to app/templates/show-all.html
diff --git a/templates/show.html b/app/templates/show.html
similarity index 100%
rename from templates/show.html
rename to app/templates/show.html
diff --git a/templates/stats.html b/app/templates/stats.html
similarity index 100%
rename from templates/stats.html
rename to app/templates/stats.html
diff --git a/templates/users.html b/app/templates/users.html
similarity index 100%
rename from templates/users.html
rename to app/templates/users.html
diff --git a/api10.py b/app/views.py
similarity index 72%
rename from api10.py
rename to app/views.py
index f81d047..003e9f7 100644
--- a/api10.py
+++ b/app/views.py
@@ -1,22 +1,23 @@
 #!/usr/bin/python2
 # -*- coding: utf-8 -*-
 #
-# pylint: disable=invalid-name
+# pylint: disable=invalid-name,missing-docstring
 #
-# Copyright (C) 2016 Richard Hughes <richard hughsie com>
-# Licensed under the GNU General Public License Version 3
+# Copyright (C) 2015-2017 Richard Hughes <richard hughsie com>
+# Licensed under the GNU General Public License Version 2
 
 import json
 import os
 import hashlib
 import math
 
-from flask import Blueprint, Response, request
+from flask import request, url_for, redirect, flash, render_template, send_from_directory, Response
+from flask_login import login_user, logout_user
 
-from database import ReviewsDatabase, CursorError
-from review import OdrsReview
+from app import app, get_db
 
-api = Blueprint('api10', __name__, url_prefix='/')
+from .db import CursorError
+from .models import Review
 
 def _get_user_key(user_hash, app_id):
     salt = os.environ['ODRS_REVIEWS_SECRET']
@@ -114,7 +115,54 @@ def _sanitised_version(val):
 
     return val
 
-@api.errorhandler(400)
+@app.route('/login', methods=['GET', 'POST'])
+def login():
+    if request.method != 'POST':
+        return render_template('login.html')
+    username = request.form['username']
+    password = request.form['password']
+    try:
+        db = get_db()
+        user = db.users.get_with_login(request.form['username'],
+                                       request.form['password'])
+    except CursorError as e:
+        flash(str(e))
+        return render_template('error.html'), 503
+    if not user:
+        flash('Credentials are not valid.')
+        return redirect(url_for('.login'))
+    login_user(user, remember=False)
+    flash('Logged in successfully.')
+    return redirect(url_for('.index'))
+
+@app.route("/logout")
+def logout():
+    logout_user()
+    flash('Logged out successfully.')
+    return redirect(url_for('.index'))
+
+@app.errorhandler(404)
+def error_page_not_found(msg=None):
+    """ Error handler: File not found """
+    flash(msg)
+    return render_template('error.html'), 404
+
+@app.route('/')
+def index():
+    """ start page """
+    return render_template('index.html')
+
+@app.route('/oars')
+def oars_index():
+    """ OARS page """
+    return render_template('oars.html')
+
+@app.route('/<path:resource>')
+def static_resource(resource):
+    """ Return a static image or resource """
+    return send_from_directory('static/', os.path.basename(resource))
+
+@app.errorhandler(400)
 def json_error(msg=None, errcode=400):
     """ Error handler: JSON output """
     item = {}
@@ -126,7 +174,7 @@ def json_error(msg=None, errcode=400):
                     status=errcode, \
                     mimetype="application/json")
 
-@api.errorhandler(401)
+@app.errorhandler(401)
 def error_permission_denied(msg=None):
     """ Error handler: Permission Denied """
     return json_error(msg, 401)
@@ -150,7 +198,7 @@ def _check_str(val):
         return False
     return True
 
-@api.route('/api/submit', methods=['POST'])
+@app.route('/1.0/reviews/api/submit', methods=['POST'])
 def submit():
     """
     Submits a new review.
@@ -183,23 +231,23 @@ def submit():
         if not _check_str(item[key]):
             return json_error('%s is not a valid string' % key)
     try:
-        db = ReviewsDatabase(os.environ)
 
         # user has already reviewed
-        if db.review_exists(item['app_id'], item['user_hash']):
-            db.event_warn(_get_client_address(),
-                          item['user_hash'],
-                          item['app_id'],
-                          "already reviewed")
+        db = get_db()
+        if db.reviews.exists(item['app_id'], item['user_hash']):
+            db.eventlog.warn(_get_client_address(),
+                             item['user_hash'],
+                             item['app_id'],
+                             "already reviewed")
             return json_error('already reviewed this app')
 
         # check user has not been banned
-        user = db.user_get_by_hash(item['user_hash'])
+        user = db.users.get_by_hash(item['user_hash'])
         if user and user.is_banned:
             return json_error('account has been disabled due to abuse')
 
         # create new
-        review = OdrsReview()
+        review = Review()
         review.app_id = item['app_id']
         review.locale = item['locale']
         review.summary = _sanitised_summary(item['summary'])
@@ -219,25 +267,25 @@ def submit():
             review.user_display = item['user_display']
 
         # log and add
-        db.event_info(_get_client_address(),
-                      review.user_hash,
-                      review.app_id,
-                      "reviewed")
-        db.review_add(review, _get_client_address())
+        db.eventlog.info(_get_client_address(),
+                         review.user_hash,
+                         review.app_id,
+                         "reviewed")
+        db.reviews.add(review, _get_client_address())
     except CursorError as e:
         return json_error(str(e))
     return json_success()
 
-@api.route('/api/app/<app_id>/<user_hash>')
-@api.route('/api/app/<app_id>')
-def app(app_id, user_hash=None):
+@app.route('/1.0/reviews/api/app/<app_id>/<user_hash>')
+@app.route('/1.0/reviews/api/app/<app_id>')
+def show_app(app_id, user_hash=None):
     """
     Return details about an application.
     """
     try:
-        db = ReviewsDatabase(os.environ)
-        db.event_info(_get_client_address(), user_hash, app_id, "getting")
-        reviews = db.review_get_for_app_id(app_id)
+        db = get_db()
+        db.eventlog.info(_get_client_address(), user_hash, app_id, "getting")
+        reviews = db.reviews.get_for_app_id(app_id)
     except CursorError as e:
         return json_error(str(e))
 
@@ -256,7 +304,7 @@ def app(app_id, user_hash=None):
                     status=200, \
                     mimetype="application/json")
 
-@api.route('/api/fetch', methods=['POST'])
+@app.route('/1.0/reviews/api/fetch', methods=['POST'])
 def fetch():
     """
     Return details about an application.
@@ -276,9 +324,9 @@ def fetch():
         return json_error('the user_hash is invalid')
 
     try:
-        db = ReviewsDatabase(os.environ)
+        db = get_db()
         db.analytics_inc_fetch(item['app_id'])
-        reviews = db.review_get_for_app_id(item['app_id'])
+        reviews = db.reviews.get_for_app_id(item['app_id'])
     except CursorError as e:
         return json_error(str(e))
 
@@ -286,15 +334,15 @@ def fetch():
     if 'compat_ids' in item:
         for app_id in item['compat_ids']:
             try:
-                reviews_tmp = db.review_get_for_app_id(app_id)
+                reviews_tmp = db.reviews.get_for_app_id(app_id)
             except CursorError as e:
                 return json_error(str(e))
             reviews.extend(reviews_tmp)
 
     # if user does not exist then create
-    user = db.user_get_by_hash(item['user_hash'])
+    user = db.users.get_by_hash(item['user_hash'])
     if not user:
-        db.user_add(item['user_hash'])
+        db.users.add(item['user_hash'])
 
     # add score for review using secret sauce
     items_new = []
@@ -312,7 +360,7 @@ def fetch():
         item_new['score'] = _get_review_score(review, item)
 
         # the UI can hide the vote buttons on reviews already voted on
-        if db.vote_exists(review.review_id, item['user_hash']):
+        if db.reviews.vote_exists(review.review_id, item['user_hash']):
             item_new['vote_id'] = 1
 
         items_new.append(item_new)
@@ -336,16 +384,16 @@ def fetch():
                     status=200, \
                     mimetype="application/json")
 
-@api.route('/api/all/<user_hash>')
-@api.route('/api/all')
+@app.route('/1.0/reviews/api/all/<user_hash>')
+@app.route('/1.0/reviews/api/all')
 def all(user_hash=None):
     """
     Return all the reviews on the server as a JSON object.
     """
     try:
-        db = ReviewsDatabase(os.environ)
-        db.event_info(_get_client_address(), user_hash, None, "getting all reviews")
-        reviews = db.review_get_all()
+        db = get_db()
+        db.eventlog.info(_get_client_address(), user_hash, None, "getting all reviews")
+        reviews = db.reviews.get_all()
     except CursorError as e:
         return json_error(str(e))
 
@@ -362,16 +410,16 @@ def all(user_hash=None):
                     status=200, \
                     mimetype="application/json")
 
-@api.route('/api/moderate/<user_hash>')
-@api.route('/api/moderate/<user_hash>/<locale>')
+@app.route('/1.0/reviews/api/moderate/<user_hash>')
+@app.route('/1.0/reviews/api/moderate/<user_hash>/<locale>')
 def moderate(user_hash, locale=None):
     """
     Return all the reviews on the server the user can moderate.
     """
     try:
-        db = ReviewsDatabase(os.environ)
-        db.event_info(_get_client_address(), user_hash, None, "getting moderatable reviews")
-        reviews = db.review_get_all()
+        db = get_db()
+        db.eventlog.info(_get_client_address(), user_hash, None, "getting moderatable reviews")
+        reviews = db.reviews.get_all()
     except CursorError as e:
         return json_error(str(e))
 
@@ -380,7 +428,7 @@ def moderate(user_hash, locale=None):
     for review in reviews:
         if locale and not _locale_is_compatible(review.locale, locale):
             continue
-        if not db.vote_exists(review.review_id, user_hash):
+        if not db.reviews.vote_exists(review.review_id, user_hash):
             item = review.__dict__
             item['user_skey'] = _get_user_key(user_hash, review.app_id)
             items_new.append(item)
@@ -412,29 +460,25 @@ def vote(val):
     if not len(item['user_skey']) == 40:
         return json_error('the user_skey is invalid')
 
-    # connect to database early
-    try:
-        db = ReviewsDatabase(os.environ)
-    except CursorError as e:
-        return json_error(str(e))
-
     if item['user_skey'] != _get_user_key(item['user_hash'], item['app_id']):
-        db.event_warn(_get_client_address(), item['user_hash'], None,
-                      "invalid user_skey of %s" % item['user_skey'])
+        db = get_db()
+        db.eventlog.warn(_get_client_address(), item['user_hash'], None,
+                         "invalid user_skey of %s" % item['user_skey'])
         #print("expected user_skey of %s" % _get_user_key(item['user_hash'], item['app_id']))
         return json_error('invalid user_skey')
     try:
 
         # the user already has a review
-        if db.vote_exists(item['review_id'], item['user_hash']):
-            db.event_warn(_get_client_address(), item['user_hash'], item['app_id'],
-                          "duplicate vote")
+        db = get_db()
+        if db.reviews.vote_exists(item['review_id'], item['user_hash']):
+            db.eventlog.warn(_get_client_address(), item['user_hash'], item['app_id'],
+                             "duplicate vote")
             return json_error('already voted on this app')
 
         # update the per-user karma
-        user = db.user_get_by_hash(item['user_hash'])
+        user = db.users.get_by_hash(item['user_hash'])
         if not user:
-            db.user_add(item['user_hash'])
+            db.users.add(item['user_hash'])
         else:
 
             # user is naughty
@@ -444,46 +488,46 @@ def vote(val):
             # the user is too harsh
             if val < 0 and user.karma < -50:
                 return json_error('all negative karma used up')
-        db.user_update_karma(item['user_hash'], val)
+        db.users.update_karma(item['user_hash'], val)
 
         # add the vote to the database
-        db.vote_add(item['review_id'], val, item['user_hash'])
-        db.event_info(_get_client_address(), item['user_hash'], item['app_id'],
-                      "voted %i on review" % val)
+        db.reviews.vote_add(item['review_id'], val, item['user_hash'])
+        db.eventlog.info(_get_client_address(), item['user_hash'], item['app_id'],
+                         "voted %i on review" % val)
 
     except CursorError as e:
         return json_error(str(e))
     return json_success('voted #%i %i' % (item['review_id'], val))
 
-@api.route('/api/upvote', methods=['POST'])
+@app.route('/1.0/reviews/api/upvote', methods=['POST'])
 def upvote():
     """
     Upvote an existing review by one karma point.
     """
     return vote(1)
 
-@api.route('/api/downvote', methods=['POST'])
+@app.route('/1.0/reviews/api/downvote', methods=['POST'])
 def downvote():
     """
     Downvote an existing review by one karma point.
     """
     return vote(-1)
 
-@api.route('/api/dismiss', methods=['POST'])
+@app.route('/1.0/reviews/api/dismiss', methods=['POST'])
 def dismiss():
     """
     Dismiss a review without rating it up or down.
     """
     return vote(0)
 
-@api.route('/api/report', methods=['POST'])
+@app.route('/1.0/reviews/api/report', methods=['POST'])
 def report():
     """
     Report a review for abuse.
     """
     return vote(-5)
 
-@api.route('/api/remove', methods=['POST'])
+@app.route('/1.0/reviews/api/remove', methods=['POST'])
 def remove():
     """
     Remove a review.
@@ -504,34 +548,31 @@ def remove():
     if not len(item['user_skey']) == 40:
         return json_error('the user_skey is invalid')
 
-    # connect to database early
-    try:
-        db = ReviewsDatabase(os.environ)
-    except CursorError as e:
-        return json_error(str(e))
     if item['user_skey'] != _get_user_key(item['user_hash'], item['app_id']):
-        db.event_warn(_get_client_address(), item['user_hash'], None,
-                      "invalid user_skey of %s" % item['user_skey'])
+        db = get_db()
+        db.eventlog.warn(_get_client_address(), item['user_hash'], None,
+                         "invalid user_skey of %s" % item['user_skey'])
         return json_error('invalid user_skey')
     try:
         # the user already has a review
-        db.review_remove(item['review_id'], item['user_hash'])
-        db.event_info(_get_client_address(),
-                      item['user_hash'],
-                      item['app_id'],
-                      "removed review")
+        db = get_db()
+        db.reviews.remove(item['review_id'], item['user_hash'])
+        db.eventlog.info(_get_client_address(),
+                         item['user_hash'],
+                         item['app_id'],
+                         "removed review")
     except CursorError as e:
         return json_error(str(e))
     return json_success('removed review #%i' % item['review_id'])
 
-@api.route('/api/ratings/<app_id>')
+@app.route('/1.0/reviews/api/ratings/<app_id>')
 def rating_for_id(app_id):
     """
     Get the star ratings for a specific application.
     """
     try:
-        db = ReviewsDatabase(os.environ)
-        ratings = db.reviews_get_rating_for_app_id(app_id)
+        db = get_db()
+        ratings = db.reviews.get_rating_for_app_id(app_id)
     except CursorError as e:
         return json_error(str(e))
 
@@ -540,17 +581,17 @@ def rating_for_id(app_id):
                     status=200, \
                     mimetype="application/json")
 
-@api.route('/api/ratings')
+@app.route('/1.0/reviews/api/ratings')
 def ratings():
     """
     Get the star ratings for a specific application.
     """
     item = {}
     try:
-        db = ReviewsDatabase(os.environ)
-        app_ids = db.get_all_apps()
+        db = get_db()
+        app_ids = db.reviews.get_all_apps()
         for app_id in app_ids:
-            ratings = db.reviews_get_rating_for_app_id(app_id, 2)
+            ratings = db.reviews.get_rating_for_app_id(app_id, 2)
             if len(ratings) == 0:
                 continue
             item[app_id] = ratings
diff --git a/admin.py b/app/views_admin.py
similarity index 75%
rename from admin.py
rename to app/views_admin.py
index fc58983..96aa784 100644
--- a/admin.py
+++ b/app/views_admin.py
@@ -1,20 +1,20 @@
 #!/usr/bin/python2
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2016 Richard Hughes <richard hughsie com>
-# Licensed under the GNU General Public License Version 3
+# pylint: disable=invalid-name,missing-docstring
+#
+# Copyright (C) 2015-2017 Richard Hughes <richard hughsie com>
+# Licensed under the GNU General Public License Version 2
 
-import os
 import datetime
 import calendar
 from math import ceil
 
-from flask import Blueprint, abort, request, flash, render_template, redirect, url_for
+from flask import abort, request, flash, render_template, redirect, url_for
 from flask_login import login_required
 
-from database import ReviewsDatabase, CursorError
-
-admin = Blueprint('admin', __name__, url_prefix='/admin')
+from app import app, get_db
+from .db import CursorError
 
 def _get_chart_labels_months():
     """ Gets the chart labels """
@@ -68,29 +68,26 @@ class Pagination(object):
                 yield num
                 last = num
 
-@admin.errorhandler(400)
+@app.errorhandler(400)
 def error_internal(msg=None, errcode=400):
     """ Error handler: Internal """
     flash("Internal error: %s" % msg)
     return render_template('error.html'), errcode
 
-@admin.errorhandler(401)
+@app.errorhandler(401)
 def error_permission_denied(msg=None):
     """ Error handler: Permission Denied """
     flash("Permission denied: %s" % msg)
     return render_template('error.html'), 401
 
 
-@admin.route('/graph_month')
+@app.route('/admin/graph_month')
 @login_required
 def graph_month():
     """
     Show nice graph graphs.
     """
-    try:
-        db = ReviewsDatabase(os.environ)
-    except CursorError as e:
-        return error_internal(str(e))
+    db = get_db()
     data_fetch = db.get_analytics_by_interval(30, 1)
     data_review = db.get_stats_by_interval(30, 1, 'reviewed')
     return render_template('graph-month.html',
@@ -98,16 +95,13 @@ def graph_month():
                            data_requests=data_fetch[::-1],
                            data_submitted=data_review[::-1])
 
-@admin.route('/graph_year')
+@app.route('/admin/graph_year')
 @login_required
 def graph_year():
     """
     Show nice graph graphs.
     """
-    try:
-        db = ReviewsDatabase(os.environ)
-    except CursorError as e:
-        return error_internal(str(e))
+    db = get_db()
     data_fetch = db.get_analytics_by_interval(12, 30)
     data_review = db.get_stats_by_interval(12, 30, 'reviewed')
     return render_template('graph-year.html',
@@ -115,14 +109,14 @@ def graph_year():
                            data_requests=data_fetch[::-1],
                            data_submitted=data_review[::-1])
 
-@admin.route('/stats')
+@app.route('/admin/stats')
 @login_required
-def stats():
+def show_stats():
     """
     Return the statistics page as HTML.
     """
     try:
-        db = ReviewsDatabase(os.environ)
+        db = get_db()
         stats = db.get_stats()
     except CursorError as e:
         return error_internal(str(e))
@@ -146,14 +140,14 @@ def stats():
                            results_viewed=results_viewed,
                            results_submitted=results_submitted)
 
-@admin.route('/distros')
+@app.route('/admin/distros')
 @login_required
 def distros():
     """
     Return the statistics page as HTML.
     """
     try:
-        db = ReviewsDatabase(os.environ)
+        db = get_db()
         stats = db.get_stats_distro(8)
     except CursorError as e:
         return error_internal(str(e))
@@ -168,7 +162,7 @@ def distros():
         data.append(s[1])
     return render_template('distros.html', labels=labels, data=data)
 
-@admin.context_processor
+@app.context_processor
 def utility_processor():
     def format_rating(rating):
         nr_stars = int(rating / 20)
@@ -199,27 +193,27 @@ def utility_processor():
                 format_timestamp=format_timestamp,
                 url_for_other_page=url_for_other_page)
 
-@admin.route('/review/<review_id>')
-def review(review_id):
+@app.route('/admin/review/<review_id>')
+def admin_show_review(review_id):
     """
     Show a specific review as HTML.
     """
     try:
-        db = ReviewsDatabase(os.environ)
-        review = db.review_get_for_id(review_id)
+        db = get_db()
+        review = db.reviews.get_for_id(review_id)
     except CursorError as e:
         return error_internal(str(e))
     if not review:
         return error_internal('no review with that ID')
     return render_template('show.html', r=review)
 
-@admin.route('/modify/<review_id>', methods=['POST'])
+@app.route('/admin/modify/<review_id>', methods=['POST'])
 @login_required
-def modify(review_id):
+def admin_modify(review_id):
     """ Change details about a review """
     try:
-        db = ReviewsDatabase(os.environ)
-        review = db.review_get_for_id(review_id)
+        db = get_db()
+        review = db.reviews.get_for_id(review_id)
     except CursorError as e:
         return error_internal(str(e))
     if not review:
@@ -234,57 +228,57 @@ def modify(review_id):
     review.description = request.form['description']
     review.summary = request.form['summary']
     review.version = request.form['version']
-    db.review_modify(review)
+    db.reviews.modify(review)
     return redirect(url_for('.review', review_id=review_id))
 
-@admin.route('/user_ban/<user_hash>')
+@app.route('/admin/users.ban/<user_hash>')
 @login_required
-def user_ban(user_hash):
+def admin_user_ban(user_hash):
     """ Change details about a review """
     try:
-        db = ReviewsDatabase(os.environ)
-        db.user_ban(user_hash)
+        db = get_db()
+        db.users.ban(user_hash)
     except CursorError as e:
         return error_internal(str(e))
     return redirect(url_for('.show_reported'))
 
-@admin.route('/unreport/<review_id>')
+@app.route('/admin/unreport/<review_id>')
 @login_required
-def unreport(review_id):
+def admin_unreport(review_id):
     """ Unreport a perfectly valid review """
     try:
-        db = ReviewsDatabase(os.environ)
-        review = db.review_get_for_id(review_id)
+        db = get_db()
+        review = db.reviews.get_for_id(review_id)
     except CursorError as e:
         return error_internal(str(e))
     if not review:
         return error_internal('no review with that ID')
     review.reported = 0
-    db.review_modify(review)
+    db.reviews.modify(review)
     return redirect(url_for('.review', review_id=review_id))
 
-@admin.route('/unremove/<review_id>')
+@app.route('/admin/unremove/<review_id>')
 @login_required
-def unremove(review_id):
+def admin_unremove(review_id):
     """ Unreport a perfectly valid review """
     try:
-        db = ReviewsDatabase(os.environ)
-        review = db.review_get_for_id(review_id)
+        db = get_db()
+        review = db.reviews.get_for_id(review_id)
     except CursorError as e:
         return error_internal(str(e))
     if not review:
         return error_internal('no review with that ID')
     review.date_deleted = 0
-    db.review_modify(review)
+    db.reviews.modify(review)
     return redirect(url_for('.review', review_id=review_id))
 
-@admin.route('/englishify/<review_id>')
+@app.route('/admin/englishify/<review_id>')
 @login_required
-def englishify(review_id):
+def admin_englishify(review_id):
     """ Marks a review as writen in English """
     try:
-        db = ReviewsDatabase(os.environ)
-        review = db.review_get_for_id(review_id)
+        db = get_db()
+        review = db.reviews.get_for_id(review_id)
     except CursorError as e:
         return error_internal(str(e))
     if not review:
@@ -294,53 +288,53 @@ def englishify(review_id):
         review.locale = 'en'
     else:
         review.locale = 'en_' + parts[1]
-    db.review_modify(review)
+    db.reviews.modify(review)
     return redirect(url_for('.review', review_id=review_id))
 
-@admin.route('/anonify/<review_id>')
+@app.route('/admin/anonify/<review_id>')
 @login_required
-def anonify(review_id):
+def admin_anonify(review_id):
     """ Removes the username from the review """
     try:
-        db = ReviewsDatabase(os.environ)
-        review = db.review_get_for_id(review_id)
+        db = get_db()
+        review = db.reviews.get_for_id(review_id)
     except CursorError as e:
         return error_internal(str(e))
     if not review:
         return error_internal('no review with that ID')
     review.user_display = None
-    db.review_modify(review)
+    db.reviews.modify(review)
     return redirect(url_for('.review', review_id=review_id))
 
-@admin.route('/delete/<review_id>/force')
+@app.route('/admin/delete/<review_id>/force')
 @login_required
-def delete_force(review_id):
+def admin_delete_force(review_id):
     """ Delete a review """
     try:
-        db = ReviewsDatabase(os.environ)
-        review = db.review_get_for_id(review_id)
+        db = get_db()
+        review = db.reviews.get_for_id(review_id)
     except CursorError as e:
         return error_internal(str(e))
     if not review:
         return error_internal('no review with that ID')
-    db.review_delete(review)
+    db.reviews.delete(review)
     return redirect(url_for('.show_all'))
 
-@admin.route('/delete/<review_id>')
+@app.route('/admin/delete/<review_id>')
 @login_required
-def delete(review_id):
+def admin_delete(review_id):
     """ Ask for confirmation to delete a review """
     return render_template('delete.html', review_id=review_id)
 
-@admin.route('/show/all', defaults={'page': 1})
-@admin.route('/show/all/page/<int:page>')
-def show_all(page):
+@app.route('/admin/show/all', defaults={'page': 1})
+@app.route('/admin/show/all/page/<int:page>')
+def admin_show_all(page):
     """
     Return all the reviews on the server as HTML.
     """
     try:
-        db = ReviewsDatabase(os.environ)
-        reviews = db.review_get_all()
+        db = get_db()
+        reviews = db.reviews.get_all()
     except CursorError as e:
         return error_internal(str(e))
     if not reviews and page != 1:
@@ -353,15 +347,15 @@ def show_all(page):
                            pagination=pagination,
                            reviews=reviews)
 
-@admin.route('/show/reported')
-def show_reported():
+@app.route('/admin/show/reported')
+def admin_show_reported():
     """
     Return all the reported reviews on the server as HTML.
     """
     reviews_filtered = []
     try:
-        db = ReviewsDatabase(os.environ)
-        reviews = db.review_get_all()
+        db = get_db()
+        reviews = db.reviews.get_all()
         for review in reviews:
             if review.reported > 0:
                 reviews_filtered.append(review)
@@ -369,15 +363,15 @@ def show_reported():
         return error_internal(str(e))
     return render_template('show-all.html', reviews=reviews_filtered)
 
-@admin.route('/show/user/<user_hash>')
-def show_user(user_hash):
+@app.route('/admin/show/user/<user_hash>')
+def admin_show_user(user_hash):
     """
     Return all the reviews from a user on the server as HTML.
     """
     reviews_filtered = []
     try:
-        db = ReviewsDatabase(os.environ)
-        reviews = db.review_get_all()
+        db = get_db()
+        reviews = db.reviews.get_all()
         for review in reviews:
             if review.user_hash == user_hash:
                 reviews_filtered.append(review)
@@ -385,15 +379,15 @@ def show_user(user_hash):
         return error_internal(str(e))
     return render_template('show-all.html', reviews=reviews_filtered)
 
-@admin.route('/show/app/<app_id>')
-def show_app(app_id):
+@app.route('/admin/show/app/<app_id>')
+def admin_show_app(app_id):
     """
     Return all the reviews from a user on the server as HTML.
     """
     reviews_filtered = []
     try:
-        db = ReviewsDatabase(os.environ)
-        reviews = db.review_get_all()
+        db = get_db()
+        reviews = db.reviews.get_all()
         for review in reviews:
             if review.app_id == app_id:
                 reviews_filtered.append(review)
@@ -401,15 +395,15 @@ def show_app(app_id):
         return error_internal(str(e))
     return render_template('show-all.html', reviews=reviews_filtered)
 
-@admin.route('/show/lang/<locale>')
-def show_lang(locale):
+@app.route('/admin/show/lang/<locale>')
+def admin_show_lang(locale):
     """
     Return all the reviews from a user on the server as HTML.
     """
     reviews_filtered = []
     try:
-        db = ReviewsDatabase(os.environ)
-        reviews = db.review_get_all()
+        db = get_db()
+        reviews = db.reviews.get_all()
         for review in reviews:
             if review.locale == locale:
                 reviews_filtered.append(review)
@@ -417,16 +411,16 @@ def show_lang(locale):
         return error_internal(str(e))
     return render_template('show-all.html', reviews=reviews_filtered)
 
-@admin.route('/users/all')
+@app.route('/admin/users/all')
 @login_required
-def users_all():
+def admin_users_all():
     """
     Return all the users as HTML.
     """
     try:
-        db = ReviewsDatabase(os.environ)
-        users_awesome = db.get_users_by_karma(best=True)
-        users_haters = db.get_users_by_karma(best=False)
+        db = get_db()
+        users_awesome = db.users.get_by_karma(best=True)
+        users_haters = db.users.get_by_karma(best=False)
     except CursorError as e:
         return error_internal(str(e))
     return render_template('users.html', users_awesome=users_awesome, users_haters=users_haters)
diff --git a/flaskapp.py b/flaskapp.py
index ab2edc3..f9c087b 100755
--- a/flaskapp.py
+++ b/flaskapp.py
@@ -1,80 +1,10 @@
 #!/usr/bin/python2
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2016 Richard Hughes <richard hughsie com>
+# Copyright (C) 2016-2017 Richard Hughes <richard hughsie com>
 # Licensed under the GNU General Public License Version 3
 
-import os
-from flask import Flask, request, url_for, redirect, flash, render_template, send_from_directory, abort
-from flask_login import LoginManager
-from flask_login import login_required, login_user, logout_user
-
-from api10 import api as api10
-from admin import admin
-from database import ReviewsDatabase, CursorError
-from user import OdrsUser
-
-app = Flask(__name__)
-app.config.from_object(__name__)
-app.secret_key = os.environ['ODRS_REVIEWS_SECRET']
-app.register_blueprint(api10, url_prefix='/1.0/reviews')
-app.register_blueprint(admin, url_prefix='/admin')
-
-login_manager = LoginManager()
-login_manager.init_app(app)
-
-@login_manager.user_loader
-def load_user(user_id):
-    db = ReviewsDatabase(os.environ)
-    user = db.user_get_by_id(user_id)
-    return user
-
-@app.route('/login', methods=['GET', 'POST'])
-def login():
-    if request.method != 'POST':
-        return render_template('login.html')
-    username = request.form['username']
-    password = request.form['password']
-    try:
-        db = ReviewsDatabase(os.environ)
-        user = db.user_get_with_login(request.form['username'],
-                                      request.form['password'])
-    except CursorError as e:
-        flash(str(e))
-        return render_template('error.html'), 503
-    if not user:
-        flash('Credentials are not valid.')
-        return redirect(url_for('.login'))
-    login_user(user, remember=False)
-    flash('Logged in successfully.')
-    return redirect(url_for('.index'))
-
-@app.route("/logout")
-def logout():
-    logout_user()
-    flash('Logged out successfully.')
-    return redirect(url_for('.index'))
-
-@app.errorhandler(404)
-def error_page_not_found(msg=None):
-    """ Error handler: File not found """
-    flash(msg)
-    return render_template('error.html'), 404
-
-@app.route('/')
-def index():
-    """ start page """
-    return render_template('index.html')
-
-@app.route('/oars')
-def oars_index():
-    """ OARS page """
-    return render_template('oars.html')
-
-@app.route('/<path:resource>')
-def static_resource(resource):
-    """ Return a static image or resource """
-    return send_from_directory('static/', resource)
+from app import app
 
 if __name__ == '__main__':
     app.debug = True
diff --git a/odrs.wsgi b/odrs.wsgi
index c9de12a..855747a 100644
--- a/odrs.wsgi
+++ b/odrs.wsgi
@@ -15,5 +15,5 @@ def application(environ, start_response):
                 'MYSQL_DB_PASSWORD',
                 'ODRS_REVIEWS_SECRET']:
         os.environ[key] = environ[key]
-    from flaskapp import app as _application
+    from app import app as _application
     return _application(environ, start_response)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]