From ad2cb1553f29a25ef54155cf49ac9beebd6ef11d Mon Sep 17 00:00:00 2001
From: YSelf Tool <yselftool@gmail.com>
Date: Fri, 4 Sep 2015 04:38:27 +0200
Subject: [PATCH] Implemented user control

---
 models/database.py              |   2 +-
 models/forms.py                 |   3 +-
 modules/admin.py                |  68 +++++
 server.py                       |  42 ++-
 static/css/style.css            |  83 ++++++
 static/js/sorttable.js          | 495 ++++++++++++++++++++++++++++++++
 templates/admin_index.html      |  26 ++
 templates/admin_user_edit.html  |   7 +
 templates/admin_user_index.html |  38 +++
 templates/admin_user_new.html   |   7 +
 templates/index.html            |   8 +
 templates/layout.html           |  27 +-
 templates/login.html            |   7 +
 templates/macros.html           |  35 ++-
 14 files changed, 832 insertions(+), 16 deletions(-)
 create mode 100644 modules/admin.py
 create mode 100644 static/js/sorttable.js
 create mode 100644 templates/admin_index.html
 create mode 100644 templates/admin_user_edit.html
 create mode 100644 templates/admin_user_index.html
 create mode 100644 templates/admin_user_new.html
 create mode 100644 templates/index.html
 create mode 100644 templates/login.html

diff --git a/models/database.py b/models/database.py
index 7f8a886..b32d9ef 100644
--- a/models/database.py
+++ b/models/database.py
@@ -1,4 +1,4 @@
-form flask.ext.login import UserMixin
+from flask.ext.login import UserMixin
 
 from shared import db
 
diff --git a/models/forms.py b/models/forms.py
index 9ae1b0a..58be60c 100644
--- a/models/forms.py
+++ b/models/forms.py
@@ -1,4 +1,4 @@
-from flask.ext.wtf import form
+from flask.ext.wtf import Form
 from wtforms import StringField, PasswordField, BooleanField, SelectMultipleField, SelectField, DateField, IntegerField, TextAreaField
 from wtforms.validators import InputRequired, Length, EqualTo, Email, Optional, Length, NumberRange, AnyOf
 from models.database import User
@@ -9,6 +9,7 @@ import shared
 class LoginForm(Form):
     username = StringField("Username", validators=[InputRequired("Entering your username is required.")])
     password = PasswordField("Password", validators=[InputRequired("Entering your password is required.")])
+    remember_me = BooleanField("Remember me?")
 
 class NewUserForm(Form):
     fullname = StringField("Full name", validators=[InputRequired("Entering your name is required.")])
diff --git a/modules/admin.py b/modules/admin.py
new file mode 100644
index 0000000..8775e4b
--- /dev/null
+++ b/modules/admin.py
@@ -0,0 +1,68 @@
+from flask import Blueprint, render_template, redirect, url_for, request, flash, abort, send_file, Response
+from flask.ext.login import login_required
+from passlib.hash import pbkdf2_sha256
+
+from models.database import User
+from models.forms import AdminUserForm, NewUserForm
+
+from shared import db, admin_permission
+
+admin = Blueprint("admin", __name__)
+
+
+@admin.route("/")
+@login_required
+@admin_permission.require()
+def index():
+    users = User.query.limit(10).all()
+    return render_template("admin_index.html", users=users)
+
+@admin.route("/user/")
+@login_required
+@admin_permission.require()
+def user():
+    users = User.query.all()
+    return render_template("admin_user_index.html", users=users)
+
+@admin.route("/user/edit", methods=["GET", "POST"])
+@login_required
+@admin_permission.require()
+def user_edit():
+    user_id = request.args.get("id", None)
+    if user_id is not None:
+        user = db.session.query(User).filter_by(id=user_id).first()
+        form = AdminUserForm(obj=user)
+        if form.validate_on_submit():
+            form.populate_obj(user)
+            db.session.commit()
+            return redirect(url_for(".index"))
+        else:
+            return render_template("admin_user_edit.html", form=form, id=user_id)
+    else:
+        return redirect(url_for(".index"))
+            
+
+@admin.route("/user/delete")
+@login_required
+@admin_permission.require()
+def user_delete():
+    user_id = request.args.get("id", None)
+    if user_id is not None:
+        user = User.query.filter_by(id=user_id).first()
+        db.session.delete(user)
+        db.session.commit()
+        flash("User deleted.", "alert-success")
+    return redirect(url_for(".user"))
+
+@admin.route("/user/new", methods=["GET", "POST"])
+@login_required
+@admin_permission.require()
+def user_new():
+    form = NewUserForm()
+    if form.validate_on_submit():
+        password_hash = pbkdf2_sha256.encrypt(form.password.data, rounds=200000, salt_size=16)
+        user = User(form.fullname.data, form.username.data, password_hash)
+        db.session.add(user)
+        db.session.commit()
+        return redirect(url_for(".user"))
+    return render_template("admin_user_new.html", form=form)
diff --git a/server.py b/server.py
index 88874a5..dd435f1 100755
--- a/server.py
+++ b/server.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 
-from flask import Flask, g, current_up, request, render_template, session, flash, redirect, url_for, abort
+from flask import Flask, g, current_app, request, render_template, session, flash, redirect, url_for, abort
 from flask.ext.login import login_user, logout_user, login_required, current_user
 from flask.ext.principal import Principal, Identity, AnonymousIdentity, identity_changed, identity_loaded, UserNeed, RoleNeed
 from passlib.hash import pbkdf2_sha256
@@ -35,6 +35,46 @@ def index():
         db.session.commit()
     return render_template("index.html")
 
+@app.route("/login", methods=["GET", "POST"])
+def login():
+    form = LoginForm()
+    if form.validate_on_submit():
+        user = db.session.query(User).filter_by(username=form.username.data).first()
+        if (user is not None) and (pbkdf2_sha256.verify(form.password.data, user.password)):
+            login_user(user, remember=form.remember_me.data)
+            identity_changed.send(current_app._get_current_object(), identity=Identity(user.id))
+            flash("Welcome back, {}!".format(user.fullname), "alert-success")
+            return redirect(request.args.get("next") or url_for(".index"))
+        else:
+            flash("Invalid username or wrong password", "alert-error")
+    return render_template("login.html", form=form)
+
+@app.route("/logout", methods=["GET", "POST"])
+@login_required
+def logout():
+    logout_user()
+    for key in ("identity.name", "identiy.auth_type"):
+        session.pop(key, None)
+    identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity())
+    flash("You have been logged out.", "alert-success")
+    return redirect(url_for(".index"))
+
+@app.route("/register", methods=["GET", "POST"])
+def register():
+    form = NewUserForm()
+    if form.validate_on_submit():
+        length = len(db.session.query(User).filter_by(username=form.username.data).all())
+        if length > 0:
+            flash("There already is a user with that name.")
+            return render_template("register.html", form=form)
+        password = pbkdf2_sha256.encrypt(form.password.data, rounds=200000, salt_size=16)
+        user = User(fullname, username, password, [])
+        db.session.add(user)
+        db.session.commit()
+        flash("Your account has been created, you may now log in with it.")
+        return redirect(url_for(".login"))
+    return render_template("register.html", form=form)
+
 
 @identity_loaded.connect_via(app)
 def on_identity_loaded(sender, identity):
diff --git a/static/css/style.css b/static/css/style.css
index 63e5682..4989711 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -14,3 +14,86 @@
 .flash-card.mdl-card.alert-error {
     background-color: red;
 }
+
+.rede-avatar {
+    width: 48px;
+    height: 48px;
+    border-radius: 24px;
+}
+
+.rede-layout .rede-header .mdl-textfield {
+    padding-top: 27px;
+}
+
+.rede-layout .mdl-layout__header .mdl-layout__drawer-button {
+    color: rgba(0, 0, 0, 0.54);
+}
+
+.mdl-layout__drawer .avatar {
+    margin-bottom: 16px;
+}
+
+.rede-drawer {
+    border: none;
+}
+
+.rede-drawer .mdl-menu__container {
+    z-index: -1;
+}
+
+.rede-drawer .rede-navigation {
+    z-index: -2;
+}
+
+.rede-drawer .mdl-menu .mdl-menu__item {
+    display: flex
+    align-items: center;
+}
+
+.rede-drawer-header {
+    box-sizing: border-box;
+    display: flex;
+    flex-direction: column;
+    justify-content: flex-end;
+    padding: 16px;
+    height: 151px;
+}
+
+.rede-avatar-dropdown {
+    display: flex;
+    position: relative;
+    flex-direction: row;
+    align-items: center;
+    width: 100%;
+}
+
+.rede-navigation {
+    flex-grow: 1;
+}
+
+.rede-layout .rede-navigation .mdl-navigation__link {
+    display: flex !important;
+    flex-direction: row;
+    align-items: center;
+    color: rgba(255, 255, 255, 0.56);
+    font-weight: 500;
+}
+
+.rede-layout .rede-navigation .mdl-navigation__link:hover {
+    background-color: #00BCD4;
+    color: #37474F;
+}
+
+.rede-navigation .mdl-navigation__link .material-icons {
+    font-size: 24px;
+    color: rgba(255, 255, 255, 0.56);
+    margin-right: 32px;
+}
+
+.rede-content {
+    max-width: 1080px;
+}
+
+.rede-separator {
+    height: 32px;
+}
diff --git a/static/js/sorttable.js b/static/js/sorttable.js
new file mode 100644
index 0000000..38b0fc6
--- /dev/null
+++ b/static/js/sorttable.js
@@ -0,0 +1,495 @@
+/*
+  SortTable
+  version 2
+  7th April 2007
+  Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
+
+  Instructions:
+  Download this file
+  Add <script src="sorttable.js"></script> to your HTML
+  Add class="sortable" to any table you'd like to make sortable
+  Click on the headers to sort
+
+  Thanks to many, many people for contributions and suggestions.
+  Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
+  This basically means: do what you want with it.
+*/
+
+
+var stIsIE = /*@cc_on!@*/false;
+
+sorttable = {
+  init: function() {
+    // quit if this function has already been called
+    if (arguments.callee.done) return;
+    // flag this function so we don't do the same thing twice
+    arguments.callee.done = true;
+    // kill the timer
+    if (_timer) clearInterval(_timer);
+
+    if (!document.createElement || !document.getElementsByTagName) return;
+
+    sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
+
+    forEach(document.getElementsByTagName('table'), function(table) {
+      if (table.className.search(/\bsortable\b/) != -1) {
+        sorttable.makeSortable(table);
+      }
+    });
+
+  },
+
+  makeSortable: function(table) {
+    if (table.getElementsByTagName('thead').length == 0) {
+      // table doesn't have a tHead. Since it should have, create one and
+      // put the first table row in it.
+      the = document.createElement('thead');
+      the.appendChild(table.rows[0]);
+      table.insertBefore(the,table.firstChild);
+    }
+    // Safari doesn't support table.tHead, sigh
+    if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];
+
+    if (table.tHead.rows.length != 1) return; // can't cope with two header rows
+
+    // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
+    // "total" rows, for example). This is B&R, since what you're supposed
+    // to do is put them in a tfoot. So, if there are sortbottom rows,
+    // for backwards compatibility, move them to tfoot (creating it if needed).
+    sortbottomrows = [];
+    for (var i=0; i<table.rows.length; i++) {
+      if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
+        sortbottomrows[sortbottomrows.length] = table.rows[i];
+      }
+    }
+    if (sortbottomrows) {
+      if (table.tFoot == null) {
+        // table doesn't have a tfoot. Create one.
+        tfo = document.createElement('tfoot');
+        table.appendChild(tfo);
+      }
+      for (var i=0; i<sortbottomrows.length; i++) {
+        tfo.appendChild(sortbottomrows[i]);
+      }
+      delete sortbottomrows;
+    }
+
+    // work through each column and calculate its type
+    headrow = table.tHead.rows[0].cells;
+    for (var i=0; i<headrow.length; i++) {
+      // manually override the type with a sorttable_type attribute
+      if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col
+        mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
+        if (mtch) { override = mtch[1]; }
+	      if (mtch && typeof sorttable["sort_"+override] == 'function') {
+	        headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
+	      } else {
+	        headrow[i].sorttable_sortfunction = sorttable.guessType(table,i);
+	      }
+	      // make it clickable to sort
+	      headrow[i].sorttable_columnindex = i;
+	      headrow[i].sorttable_tbody = table.tBodies[0];
+	      dean_addEvent(headrow[i],"click", sorttable.innerSortFunction = function(e) {
+
+          if (this.className.search(/\bsorttable_sorted\b/) != -1) {
+            // if we're already sorted by this column, just
+            // reverse the table, which is quicker
+            sorttable.reverse(this.sorttable_tbody);
+            this.className = this.className.replace('sorttable_sorted',
+                                                    'sorttable_sorted_reverse');
+            this.removeChild(document.getElementById('sorttable_sortfwdind'));
+            sortrevind = document.createElement('span');
+            sortrevind.id = "sorttable_sortrevind";
+            sortrevind.innerHTML = stIsIE ? '&nbsp<font face="webdings">5</font>' : '&nbsp;&#x25B4;';
+            this.appendChild(sortrevind);
+            return;
+          }
+          if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
+            // if we're already sorted by this column in reverse, just
+            // re-reverse the table, which is quicker
+            sorttable.reverse(this.sorttable_tbody);
+            this.className = this.className.replace('sorttable_sorted_reverse',
+                                                    'sorttable_sorted');
+            this.removeChild(document.getElementById('sorttable_sortrevind'));
+            sortfwdind = document.createElement('span');
+            sortfwdind.id = "sorttable_sortfwdind";
+            sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
+            this.appendChild(sortfwdind);
+            return;
+          }
+
+          // remove sorttable_sorted classes
+          theadrow = this.parentNode;
+          forEach(theadrow.childNodes, function(cell) {
+            if (cell.nodeType == 1) { // an element
+              cell.className = cell.className.replace('sorttable_sorted_reverse','');
+              cell.className = cell.className.replace('sorttable_sorted','');
+            }
+          });
+          sortfwdind = document.getElementById('sorttable_sortfwdind');
+          if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); }
+          sortrevind = document.getElementById('sorttable_sortrevind');
+          if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); }
+
+          this.className += ' sorttable_sorted';
+          sortfwdind = document.createElement('span');
+          sortfwdind.id = "sorttable_sortfwdind";
+          sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
+          this.appendChild(sortfwdind);
+
+	        // build an array to sort. This is a Schwartzian transform thing,
+	        // i.e., we "decorate" each row with the actual sort key,
+	        // sort based on the sort keys, and then put the rows back in order
+	        // which is a lot faster because you only do getInnerText once per row
+	        row_array = [];
+	        col = this.sorttable_columnindex;
+	        rows = this.sorttable_tbody.rows;
+	        for (var j=0; j<rows.length; j++) {
+	          row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];
+	        }
+	        /* If you want a stable sort, uncomment the following line */
+	        //sorttable.shaker_sort(row_array, this.sorttable_sortfunction);
+	        /* and comment out this one */
+	        row_array.sort(this.sorttable_sortfunction);
+
+	        tb = this.sorttable_tbody;
+	        for (var j=0; j<row_array.length; j++) {
+	          tb.appendChild(row_array[j][1]);
+	        }
+
+	        delete row_array;
+	      });
+	    }
+    }
+  },
+
+  guessType: function(table, column) {
+    // guess the type of a column based on its first non-blank row
+    sortfn = sorttable.sort_alpha;
+    for (var i=0; i<table.tBodies[0].rows.length; i++) {
+      text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]);
+      if (text != '') {
+        if (text.match(/^-?[�$�]?[\d,.]+%?$/)) {
+          return sorttable.sort_numeric;
+        }
+        // check for a date: dd/mm/yyyy or dd/mm/yy
+        // can have / or . or - as separator
+        // can be mm/dd as well
+        possdate = text.match(sorttable.DATE_RE)
+        if (possdate) {
+          // looks like a date
+          first = parseInt(possdate[1]);
+          second = parseInt(possdate[2]);
+          if (first > 12) {
+            // definitely dd/mm
+            return sorttable.sort_ddmm;
+          } else if (second > 12) {
+            return sorttable.sort_mmdd;
+          } else {
+            // looks like a date, but we can't tell which, so assume
+            // that it's dd/mm (English imperialism!) and keep looking
+            sortfn = sorttable.sort_ddmm;
+          }
+        }
+      }
+    }
+    return sortfn;
+  },
+
+  getInnerText: function(node) {
+    // gets the text we want to use for sorting for a cell.
+    // strips leading and trailing whitespace.
+    // this is *not* a generic getInnerText function; it's special to sorttable.
+    // for example, you can override the cell text with a customkey attribute.
+    // it also gets .value for <input> fields.
+
+    if (!node) return "";
+
+    hasInputs = (typeof node.getElementsByTagName == 'function') &&
+                 node.getElementsByTagName('input').length;
+
+    if (node.getAttribute("sorttable_customkey") != null) {
+      return node.getAttribute("sorttable_customkey");
+    }
+    else if (typeof node.textContent != 'undefined' && !hasInputs) {
+      return node.textContent.replace(/^\s+|\s+$/g, '');
+    }
+    else if (typeof node.innerText != 'undefined' && !hasInputs) {
+      return node.innerText.replace(/^\s+|\s+$/g, '');
+    }
+    else if (typeof node.text != 'undefined' && !hasInputs) {
+      return node.text.replace(/^\s+|\s+$/g, '');
+    }
+    else {
+      switch (node.nodeType) {
+        case 3:
+          if (node.nodeName.toLowerCase() == 'input') {
+            return node.value.replace(/^\s+|\s+$/g, '');
+          }
+        case 4:
+          return node.nodeValue.replace(/^\s+|\s+$/g, '');
+          break;
+        case 1:
+        case 11:
+          var innerText = '';
+          for (var i = 0; i < node.childNodes.length; i++) {
+            innerText += sorttable.getInnerText(node.childNodes[i]);
+          }
+          return innerText.replace(/^\s+|\s+$/g, '');
+          break;
+        default:
+          return '';
+      }
+    }
+  },
+
+  reverse: function(tbody) {
+    // reverse the rows in a tbody
+    newrows = [];
+    for (var i=0; i<tbody.rows.length; i++) {
+      newrows[newrows.length] = tbody.rows[i];
+    }
+    for (var i=newrows.length-1; i>=0; i--) {
+       tbody.appendChild(newrows[i]);
+    }
+    delete newrows;
+  },
+
+  /* sort functions
+     each sort function takes two parameters, a and b
+     you are comparing a[0] and b[0] */
+  sort_numeric: function(a,b) {
+    aa = parseFloat(a[0].replace(/[^0-9.-]/g,''));
+    if (isNaN(aa)) aa = 0;
+    bb = parseFloat(b[0].replace(/[^0-9.-]/g,''));
+    if (isNaN(bb)) bb = 0;
+    return aa-bb;
+  },
+  sort_alpha: function(a,b) {
+    if (a[0]==b[0]) return 0;
+    if (a[0]<b[0]) return -1;
+    return 1;
+  },
+  sort_ddmm: function(a,b) {
+    mtch = a[0].match(sorttable.DATE_RE);
+    y = mtch[3]; m = mtch[2]; d = mtch[1];
+    if (m.length == 1) m = '0'+m;
+    if (d.length == 1) d = '0'+d;
+    dt1 = y+m+d;
+    mtch = b[0].match(sorttable.DATE_RE);
+    y = mtch[3]; m = mtch[2]; d = mtch[1];
+    if (m.length == 1) m = '0'+m;
+    if (d.length == 1) d = '0'+d;
+    dt2 = y+m+d;
+    if (dt1==dt2) return 0;
+    if (dt1<dt2) return -1;
+    return 1;
+  },
+  sort_mmdd: function(a,b) {
+    mtch = a[0].match(sorttable.DATE_RE);
+    y = mtch[3]; d = mtch[2]; m = mtch[1];
+    if (m.length == 1) m = '0'+m;
+    if (d.length == 1) d = '0'+d;
+    dt1 = y+m+d;
+    mtch = b[0].match(sorttable.DATE_RE);
+    y = mtch[3]; d = mtch[2]; m = mtch[1];
+    if (m.length == 1) m = '0'+m;
+    if (d.length == 1) d = '0'+d;
+    dt2 = y+m+d;
+    if (dt1==dt2) return 0;
+    if (dt1<dt2) return -1;
+    return 1;
+  },
+
+  shaker_sort: function(list, comp_func) {
+    // A stable sort function to allow multi-level sorting of data
+    // see: http://en.wikipedia.org/wiki/Cocktail_sort
+    // thanks to Joseph Nahmias
+    var b = 0;
+    var t = list.length - 1;
+    var swap = true;
+
+    while(swap) {
+        swap = false;
+        for(var i = b; i < t; ++i) {
+            if ( comp_func(list[i], list[i+1]) > 0 ) {
+                var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
+                swap = true;
+            }
+        } // for
+        t--;
+
+        if (!swap) break;
+
+        for(var i = t; i > b; --i) {
+            if ( comp_func(list[i], list[i-1]) < 0 ) {
+                var q = list[i]; list[i] = list[i-1]; list[i-1] = q;
+                swap = true;
+            }
+        } // for
+        b++;
+
+    } // while(swap)
+  }
+}
+
+/* ******************************************************************
+   Supporting functions: bundled here to avoid depending on a library
+   ****************************************************************** */
+
+// Dean Edwards/Matthias Miller/John Resig
+
+/* for Mozilla/Opera9 */
+if (document.addEventListener) {
+    document.addEventListener("DOMContentLoaded", sorttable.init, false);
+}
+
+/* for Internet Explorer */
+/*@cc_on @*/
+/*@if (@_win32)
+    document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
+    var script = document.getElementById("__ie_onload");
+    script.onreadystatechange = function() {
+        if (this.readyState == "complete") {
+            sorttable.init(); // call the onload handler
+        }
+    };
+/*@end @*/
+
+/* for Safari */
+if (/WebKit/i.test(navigator.userAgent)) { // sniff
+    var _timer = setInterval(function() {
+        if (/loaded|complete/.test(document.readyState)) {
+            sorttable.init(); // call the onload handler
+        }
+    }, 10);
+}
+
+/* for other browsers */
+window.onload = sorttable.init;
+
+// written by Dean Edwards, 2005
+// with input from Tino Zijdel, Matthias Miller, Diego Perini
+
+// http://dean.edwards.name/weblog/2005/10/add-event/
+
+function dean_addEvent(element, type, handler) {
+	if (element.addEventListener) {
+		element.addEventListener(type, handler, false);
+	} else {
+		// assign each event handler a unique ID
+		if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
+		// create a hash table of event types for the element
+		if (!element.events) element.events = {};
+		// create a hash table of event handlers for each element/event pair
+		var handlers = element.events[type];
+		if (!handlers) {
+			handlers = element.events[type] = {};
+			// store the existing event handler (if there is one)
+			if (element["on" + type]) {
+				handlers[0] = element["on" + type];
+			}
+		}
+		// store the event handler in the hash table
+		handlers[handler.$$guid] = handler;
+		// assign a global event handler to do all the work
+		element["on" + type] = handleEvent;
+	}
+};
+// a counter used to create unique IDs
+dean_addEvent.guid = 1;
+
+function removeEvent(element, type, handler) {
+	if (element.removeEventListener) {
+		element.removeEventListener(type, handler, false);
+	} else {
+		// delete the event handler from the hash table
+		if (element.events && element.events[type]) {
+			delete element.events[type][handler.$$guid];
+		}
+	}
+};
+
+function handleEvent(event) {
+	var returnValue = true;
+	// grab the event object (IE uses a global event object)
+	event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
+	// get a reference to the hash table of event handlers
+	var handlers = this.events[event.type];
+	// execute each event handler
+	for (var i in handlers) {
+		this.$$handleEvent = handlers[i];
+		if (this.$$handleEvent(event) === false) {
+			returnValue = false;
+		}
+	}
+	return returnValue;
+};
+
+function fixEvent(event) {
+	// add W3C standard event methods
+	event.preventDefault = fixEvent.preventDefault;
+	event.stopPropagation = fixEvent.stopPropagation;
+	return event;
+};
+fixEvent.preventDefault = function() {
+	this.returnValue = false;
+};
+fixEvent.stopPropagation = function() {
+  this.cancelBubble = true;
+}
+
+// Dean's forEach: http://dean.edwards.name/base/forEach.js
+/*
+	forEach, version 1.0
+	Copyright 2006, Dean Edwards
+	License: http://www.opensource.org/licenses/mit-license.php
+*/
+
+// array-like enumeration
+if (!Array.forEach) { // mozilla already supports this
+	Array.forEach = function(array, block, context) {
+		for (var i = 0; i < array.length; i++) {
+			block.call(context, array[i], i, array);
+		}
+	};
+}
+
+// generic enumeration
+Function.prototype.forEach = function(object, block, context) {
+	for (var key in object) {
+		if (typeof this.prototype[key] == "undefined") {
+			block.call(context, object[key], key, object);
+		}
+	}
+};
+
+// character enumeration
+String.forEach = function(string, block, context) {
+	Array.forEach(string.split(""), function(chr, index) {
+		block.call(context, chr, index, string);
+	});
+};
+
+// globally resolve forEach enumeration
+var forEach = function(object, block, context) {
+	if (object) {
+		var resolve = Object; // default
+		if (object instanceof Function) {
+			// functions have a "length" property
+			resolve = Function;
+		} else if (object.forEach instanceof Function) {
+			// the object implements a custom forEach method so use that
+			object.forEach(block, context);
+			return;
+		} else if (typeof object == "string") {
+			// the object is a string
+			resolve = String;
+		} else if (typeof object.length == "number") {
+			// the object is array-like
+			resolve = Array;
+		}
+		resolve.forEach(object, block, context);
+	}
+};
+
diff --git a/templates/admin_index.html b/templates/admin_index.html
new file mode 100644
index 0000000..2f3d708
--- /dev/null
+++ b/templates/admin_index.html
@@ -0,0 +1,26 @@
+{% extends "layout.html" %}
+{% block title %}Index{% endblock %}
+
+{% block content %}
+    <table class="mdl-data-table mdl-js-table mdl-shadow--2dp sortable mdl-cell mdl-cell--3-col">
+        <thead>
+            <tr>
+                <th class="mdl-data-table_-cell--non-numeric">Full Name</th>
+                <th class="mdl-data-table_-cell--non-numeric">Username</th>
+                <th class="mdl-data-table_-cell--non-numeric">Roles</th>
+            </tr>
+        </thead>
+        <tbody>
+        {% for user in users %}
+            <tr>
+                <td class="mdl-data-table__cell--non-numeric"><a href="{{ url_for(".user_edit", id=user.id) }}">{{ user.fullname }}</a></td>
+                <td class="mdl-data-table__cell--non-numeric">{{ user.username }}</td>
+                <td class="mdl-data-table__cell--non-numeric">{% if user.roles is not none %}{{ ", ".join(user.roles) }}{% endif %}</td>
+            </tr>
+        {% endfor %}
+        </tbody>
+    </table>
+    <div class="mdl-card__actions">
+        <a class="mdl-button mdl-button--colored mdl-js-button" href="{{ url_for(".user") }}">All users</a>
+    </div>
+{% endblock %}
diff --git a/templates/admin_user_edit.html b/templates/admin_user_edit.html
new file mode 100644
index 0000000..80284e0
--- /dev/null
+++ b/templates/admin_user_edit.html
@@ -0,0 +1,7 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_form %}
+{% block title %}Edit User - Administration{% endblock %}
+
+{% block content %}
+        {{ render_form(form, action_url=url_for(".user_edit", id=id), action_text="Apply", title="Edit User") }}
+{% endblock %}
diff --git a/templates/admin_user_index.html b/templates/admin_user_index.html
new file mode 100644
index 0000000..4407a5e
--- /dev/null
+++ b/templates/admin_user_index.html
@@ -0,0 +1,38 @@
+{% extends "layout.html" %}
+{% block title %}Index{% endblock %}
+
+{% block content %}
+    <div class="mdl-cell mdl-cell--3-col mdl-grid mdl-grid--no-spacing">
+        <table class="mdl-data-table mdl-js-table mdl-shadow--2dp mdl-cell mdl-cell--3-col">
+            <thead>
+                <tr>
+                    <th class="mdl-data-table_-cell--non-numeric">Full Name</th>
+                    <th class="mdl-data-table_-cell--non-numeric">Username</th>
+                    <th class="mdl-data-table_-cell--non-numeric">Roles</th>
+                    <th class="mdl-data-table_-cell--non-numeric">Delete</th>
+                </tr>
+            </thead>
+            <tbody>
+            {% for user in users %}
+                <tr>
+                    <td class="mdl-data-table__cell--non-numeric"><a href="{{ url_for(".user_edit", id=user.id) }}">{{ user.fullname }}</a></td>
+                    <td class="mdl-data-table__cell--non-numeric">{{ user.username }}</td>
+                    <td class="mdl-data-table__cell--non-numeric">{% if user.roles is not none %}{{ ", ".join(user.roles) }}{% endif %}</td>
+                    <td class="mdl-data-table__cell--non-numeric">
+                        <a href="{{ url_for('.user_delete', id=user.id) }}">
+                            <i class="material-icons">delete</i>
+                        </a>
+                    </td>
+                </tr>
+            {% endfor %}
+            </tbody>
+        </table>
+        <div class="rede-separator">
+            <div class="mdl-cell mdl-cell--1-col">
+                <a class="mdl-button mdl-button--colored mdl-js-button mdl-button--fab mdl-js-ripple-effect" href="{{ url_for('.user_new') }}">
+                    <i class="material-icons">add</i>
+                </a>
+            </div>
+        </div>
+    </div>
+{% endblock %}
diff --git a/templates/admin_user_new.html b/templates/admin_user_new.html
new file mode 100644
index 0000000..359e7b6
--- /dev/null
+++ b/templates/admin_user_new.html
@@ -0,0 +1,7 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_form %}
+{% block title %}Add User - Administration{% endblock %}
+
+{% block content %}
+        {{ render_form(form, action_url=url_for(".user_new", id=id), action_text="Add", title="Create User") }}
+{% endblock %}
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..812a0d4
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,8 @@
+{% extends "layout.html" %}
+{% block title %}Index{% endblock %}
+
+{% block content %}
+    <div class="rede-nocontent mdl-color--white mdl-shadow--2dp mdl-cell mdl-cell-12-col mdl-grid">
+        No content here
+    </div>
+{% endblock %}
diff --git a/templates/layout.html b/templates/layout.html
index 00c1c54..ce07527 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -5,23 +5,26 @@
     <meta charset="utf-8" />
     <link rel="stylesheet" href="https://storage.googleapis.com/code.getmdl.io/1.0.4/material.red-blue.min.css" /> 
     <script src="https://storage.googleapis.com/code.getmdl.io/1.0.0/material.min.js"></script>
-    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Metrial+Icons">
+    <script src="{{ url_for('static', filename='js/sorttable.js') }}"></script>
+    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+    <meta name="description" content="moderation tool for handling speaking order">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="mobile-web-app-capable" content="yes">
     <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" />
     <title>{% block title %}Unknown Page{% endblock %} - Redeleitsystem</title>
     {% endblock %}
 </head>
 <body>
-<div class="rede-layout mdl-layout mdl-js-layout mdl-layout--fixed-draw mdl-layout--fixed-header">
-    <header class="rede-header hdml-layout__header mdl-color--white mdl-color--grey-100 mdl-color-text--grey-600">
+<div class="rede-layout mdl-layout mdl-js-layout mdl-layout--fixed-drawer mdl-layout--fixed-header">
+    <header class="rede-header mdl-layout__header mdl-color--white mdl-color--grey-100 mdl-color-text--grey-600">
         <div class="mdl-layout__header-row">
-            <span class="mdl-layout-title">Redeleitsystem</span>
+            <a href="{{ url_for('index') }}"><span class="mdl-layout-title">Redeleitsystem</span></a>
             <div class="mdl-layout-spacer"></div>
             <button class="mdl-button mdl-js-button mdl-js-ripple-effet mdl-button--icon" id="hdrbtn">
                 <i class="material-icons">more_vert</i>
             </button>
             <ul class="mdl-menu mdl-js-menu mdl-js-ripple-effect mdl-menu--bottom-right" for="hdrbtn">
-                {% block toplinks %}
+                {% block topnav %}
                     <li class="mdl-menu__item">Impressum</li>
                 {% endblock %}
             </ul>
@@ -42,16 +45,21 @@
                     <i class="material-icons" role="presentation">arrow_drop_down</i>
                     <span class="visuallyhidden">Account</span>
                 </button>
-                <ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effet" for="accbtn">
+                <ul class="mdl-menu mdl-menu--bottom-left mdl-js-menu mdl-js-ripple-effet" for="accbtn">
                     {% if current_user.is_authenticated() %}
-                    <li class="mdl-menu__item"><a class="mdl-navigation__link" href="{{ url_for(".logout") }}">Logout</a></li>
+                    <li class="mdl-menu__item"><a class="mdl-navigation__link" href="{{ url_for("logout") }}">Logout</a></li>
                     {% else %}
-                    <li class="mdl-menu__item"><a class="mdl-navigation__link" href="{{ url_for(".login") }}">Login</a></li>
-                    <li class="mdl-menu__item"><a class="mdl-navigation__link" href="{{ url_for(".register") }}">Register</a></li>
+                    <li class="mdl-menu__item"><a class="mdl-navigation__link" href="{{ url_for("login") }}">Login</a></li>
+                    <li class="mdl-menu__item"><a class="mdl-navigation__link" href="{{ url_for("register") }}">Register</a></li>
                     {% endif %}
                 </ul>
             </div>
         </header>
+        <nav class="rede-navigation mdl-navigation mdl-color--blue-grey-800">
+            {% if current_user.is_authenticated() and "admin" in current_user.roles %}
+            <a class="mdl-navigation__link" href="{{ url_for("admin.index") }}"><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">computer</i>Administration</a>
+            {% endif %}
+        </nav>
     </div>
     <main class="mdl-layout__content mdl-color--grey-100">
         <div class="mdl-grid rede-content">
@@ -60,5 +68,6 @@
         {% endblock %}
         </div>
     </main>
+    <script src="http://www.getmdl.io/material.min.js"></script>
 </body>
 </html>
diff --git a/templates/login.html b/templates/login.html
new file mode 100644
index 0000000..e4a9659
--- /dev/null
+++ b/templates/login.html
@@ -0,0 +1,7 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_form %}
+{% block title %}Index{% endblock %}
+
+{% block content %}
+    {{ render_form(form, action_url=url_for(".login"), action_text="Login", title="Login") }}
+{% endblock %}
diff --git a/templates/macros.html b/templates/macros.html
index e7a037e..c602fb6 100644
--- a/templates/macros.html
+++ b/templates/macros.html
@@ -9,8 +9,8 @@
 {%- endmacro %}
 
 {% macro render_stringfield(field) -%}
-    <div class="mdl-textfield mdl-js-textfield">
-        <input id="{{ field.id }}" name="{{ field.id }}" class="mdl-textfield__input" type="text" />
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+        <input id="{{ field.id }}" name="{{ field.id }}" class="mdl-textfield__input" type="text" value="{{ field.data }}" />
         <label class="mdl-textfield__label" for="{{ field.id }}">{{ field.label.text }}</label>
         {% if field.errors %}
             {% for e in errors %}
@@ -23,7 +23,7 @@
 {%- endmacro %}
 
 {% macro render_passwordfield(field) -%}
-    <div class="mdl-textfield mdl-js-textfield">
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
         <input id="{{ field.id }}" name="{{ field.id }}" class="mdl-textfield__input" type="password" />
         <label class="mdl-textfield__label" for="{{ field.id }}">{{ field.label.text }}</label>
         {% if field.errors %}
@@ -41,12 +41,38 @@
         <input type="checkbox" id="{{ field.id }}" name="{{ field.id }}" class="mdl-checkbox__input" />
         <span class="mdl-checkbox__label">{{ field.label.text }}</span>
     </label>
+    {% if field.errors %}
+        {% for e in errors %}
+            <div class="mdl-card__supporting-text">
+                {{ e }}
+            </div>
+        {% endfor %}
+    {% endif %}
 {%- endmacro %}
 
 {% macro render_csrftokenfield(field, kwargs) -%}
     {{ field(title=field.description, **kwargs) }}
 {%- endmacro %}
 
+{% macro render_selectmultiplefield(field) -%}
+        <button id="{{ field.id }}-button" class="mdl-button mdl-js-button" type="button">
+            {{ field.label.text }}
+        </button>
+        <select id="{{ field.id }}" name="{{ field.id }}" multiple class="mdl-menu mdl-js-menu mdl-menu--top-left" for="{{ field.id }}-button">
+            {% for name, text in field.choices %}
+                <option value="{{ name }}" {% if field.data is not none and name in field.data %}selected{% endif %} class="mdl-menu__item" >{{ text }}</option>
+            {% endfor %}
+        </select>
+        {% if field.errors %}
+            {% for e in field.errors %}
+                <p class="help-block">{{ e }}</p>
+            {% endfor %}
+        {% endif %}
+    </label>
+{%- endmacro %}
+
+
+
 {% macro render_form(form, action_url="", title=None, action_text="Submit", class_="mdl-card mdl-shadow--2dp", title_class="mdl-card__title", title_next_class="mdl-card__title-text", content_class="mdl-card__supporting-text", action_class="mdl-card__actions", btn_class="mdl-button mdl-js-button mdl-button--raised mdl-button-colored") -%}
     <div class="{{ class_ }}">
         <form method="POST" action="{{ action_url }}">
@@ -63,12 +89,13 @@
                         {{ render_booleanfield(f) }}
                     {% elif f.type == "CSRFTokenField" %}
                         {{ render_csrftokenfield(f, kwargs) }}
+                    {% elif f.type == "SelectMultipleField" %}
+                        {{ render_selectmultiplefield(f) }}
                     {% else %}
                         {{ f.type }}
                         {{ render_field(f) }}
                     {% endif %}
                 {% endfor %}
-                {% endfor %}
             </div>
             <div class="{{ action_class }}">
                 <button type="submit" class="{{ btn_class }}">{{ action_text }}</button>
-- 
GitLab