[chronojump-server] Authentication implemented for Chronojump Server. Now the results can be selected and removed



commit 27e1fce24f70a2afbf4dd775b80cbe32f82fa64a
Author: Marcos Venteo <mventeo gmail com>
Date:   Tue Aug 29 21:07:52 2017 +0200

    Authentication implemented for Chronojump Server. Now the results can be selected and removed

 chronojumpserver/__init__.py                 |   14 ++++
 chronojumpserver/api.py                      |   37 +++++++++-
 chronojumpserver/forms.py                    |    6 ++
 chronojumpserver/js/players.js               |    3 +-
 chronojumpserver/js/results.js               |   90 +++++++++++++++++++++++---
 chronojumpserver/js/sprints.js               |    2 +-
 chronojumpserver/models.py                   |   42 +++++++++++-
 chronojumpserver/static/style.css            |   22 ++++++
 chronojumpserver/templates/_formhelpers.html |    3 +
 chronojumpserver/templates/index.html        |   13 +++-
 chronojumpserver/templates/layout.html       |    4 +-
 chronojumpserver/templates/login.html        |   32 +++++++++
 chronojumpserver/templates/results.html      |    2 +-
 chronojumpserver/templates/sprints.html      |    2 +-
 chronojumpserver/views.py                    |   75 ++++++++++++++++++++-
 requirements.txt                             |    1 +
 16 files changed, 320 insertions(+), 28 deletions(-)
---
diff --git a/chronojumpserver/__init__.py b/chronojumpserver/__init__.py
index 83f4d5f..b8e8e9e 100755
--- a/chronojumpserver/__init__.py
+++ b/chronojumpserver/__init__.py
@@ -5,6 +5,8 @@ Chronojump Server Main application...
 """
 import click
 from flask import Flask, send_from_directory
+from flask_login import LoginManager
+
 import ConfigParser
 import os
 
@@ -15,6 +17,10 @@ config.read('/etc/chronojump.conf')
 
 app = Flask(__name__)
 
+login_manager = LoginManager()
+login_manager.init_app(app)
+login_manager.login_view = "login"
+
 # set the secret key.  keep this really secret:
 app.config['MYSQL_DATABASE_USER'] = config.get("db", "user")
 app.config['MYSQL_DATABASE_PASS'] = config.get("db", "password")
@@ -68,6 +74,8 @@ def populate_test_data():
     TestData().create_exercises()
 
 
+
+
 # IMPORTANT: imports always at the end of the module, after app is created
 # Following design patterns from ....
 import chronojumpserver.views
@@ -83,3 +91,9 @@ def shutdown_session(exception=None):
 
 from chronojumpserver.database import init_db
 init_db()
+
+from chronojumpserver.models import User
+# To retrieve the user from session
+@login_manager.user_loader
+def load_user(user_id):
+    return User.get(user_id)
diff --git a/chronojumpserver/api.py b/chronojumpserver/api.py
index b1c7b92..b2e2278 100755
--- a/chronojumpserver/api.py
+++ b/chronojumpserver/api.py
@@ -8,6 +8,7 @@ from chronojumpserver.database import db_session
 from chronojumpserver.models import Person, ResultEncoder, Station, Task
 from chronojumpserver.models import ResultSprint, Exercise, RFIDHistory
 from flask import jsonify, request
+from flask_login import login_required
 from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
 from time import sleep
 import os
@@ -80,7 +81,21 @@ def register_rfid():
     msg = ""
     # From original crhonojump-flask source read RFID
     rfidFile = '/tmp/chronojump_rfid.txt'
-    if os.environ.get('NO_MONO', False):
+    # Check if we are in development, to read directly the value pass manually
+    # instead of the mono executable
+
+    try:
+        no_mono = int(os.environ['NO_MONO'])
+    except KeyError:
+        # No development environment
+        no_mono = 0
+        pass
+    except TypeError:
+        # Invalid value passed to no_mono
+        no_mono = 0
+        pass
+    #print "DEBUG: NO_MONO = %d" % int(no_mono)
+    if no_mono == 1:
         sleep(4)
     else:
         if os.access(rfidFile, os.W_OK):
@@ -89,14 +104,15 @@ def register_rfid():
         mono_path = os.path.join(app.config['MONO_PATH'], 'RFID.exe')
         rfidReadedStatus = subprocess.call(
             "mono %s" % mono_path, shell=True)
-
-        print rfidReadedStatus
+        # Debug: Print output status
+        #print rfidReadedStatus
 
     # Read the RFID read
     try:
         with open(rfidFile) as f:
             rfid = f.read()
-    except:
+    except Exception, e:
+        print "ERROR: %s" % str(e)
         rfid = ""
 
     if rfid:
@@ -284,3 +300,16 @@ def active_tasks_in_station():
         players_stations.append({ 'id' : player.id, 'stations': stations})
 
     return jsonify(players_stations=players_stations)
+
+
+@app.route('/api/v1/results/delete', methods=['GET', 'POST'])
+def remove_results():
+    if request.method == "POST":
+        results = request.json
+        for _resultId in results:
+            resultId = int(_resultId)
+            r = ResultEncoder.getById(resultId)
+            db_session.delete(r)
+            db_session.commit()
+
+    return jsonify(msg="Done")
diff --git a/chronojumpserver/forms.py b/chronojumpserver/forms.py
index 17be33d..e116e94 100755
--- a/chronojumpserver/forms.py
+++ b/chronojumpserver/forms.py
@@ -12,3 +12,9 @@ class PersonForm(FlaskForm):
     weight=FloatField('Pes', validators=[DataRequired('El camp pes és obligatori.'.decode('utf-8'))])
     photo=FileField('Foto del Jugador')
     rfid = StringField('RFID', [DataRequired('S\'ha de registrar un RFID per jugador.'.decode('utf-8')), 
validators.Length(max=23)])
+
+
+class LoginForm(FlaskForm):
+    username = StringField('Usuari', validators=[DataRequired('El usuari és obligatori!'.decode('utf-8'))])
+    password = PasswordField('Contrasenya', validators=[DataRequired('La Contrasenya és 
obligatoria!'.decode('utf-8'))])
+    pass
diff --git a/chronojumpserver/js/players.js b/chronojumpserver/js/players.js
index 7418b67..0db4bb9 100755
--- a/chronojumpserver/js/players.js
+++ b/chronojumpserver/js/players.js
@@ -198,7 +198,8 @@ $(document).ready(function() {
                 data: null,
                 title: "",
                 orderable: false,
-                render: function(val) {
+                className: 'colAlignRight',
+                  render: function(val) {
                     return "<button class='btn btn-primary'>Afegeix</button>"
                 }
             }
diff --git a/chronojumpserver/js/results.js b/chronojumpserver/js/results.js
index b42dc96..984eac0 100755
--- a/chronojumpserver/js/results.js
+++ b/chronojumpserver/js/results.js
@@ -30,6 +30,12 @@
 
 $(document).ready(function() {
 
+  var refreshIntervalId;
+
+  var COLUMN_PLAYER = 4;
+  var COLUMN_STATION = 5;
+  var COLUMN_EXERCISE = 6;
+
   // Initialize datatable with results
   $("input[name='filterByDayOptions']").on('click', function() {
     var table = $('#results').DataTable();
@@ -68,6 +74,13 @@ $(document).ready(function() {
         visible: false
       },
       {
+        type: "html",
+        orderable:false,
+        render: function(value, type, row) {
+          return '<input class="deleteCheckbox" type="checkbox" data-result-id="' + row.id + '"/>';
+        }
+      },
+      {
         type: "customdate",
         title: "Data",
         data: "dt",
@@ -178,9 +191,44 @@ $(document).ready(function() {
         render: $.fn.dataTable.render.number('', ',', 2)
       }
     ],
-    "dom": '<"resultsFilter">fBrtip',
+    "dom": "<'row'<'resultsFilter'>><'row'<'col-sm-6'B><'col-sm-6'f>>rtip",
     buttons: [
-            { extend: 'csv', text: 'Exportar Resultats', className: "btn btn-primary", fieldSeparator: ";"}
+            { extend: 'csv', text: 'Exportar Resultats', className: "btn btn-primary", fieldSeparator: 
"&nbsp;"},
+            { text: 'Eliminar registres',
+              className: "btn btn-danger btnDeleteResults",
+              enabled: false,
+              action: function(e, dt, node, config) {
+                // Ask for confirmation
+                var total =  $('.deleteCheckbox:checked').length;
+                if (total == 1) {
+                  var r = confirm("Estàs segur de que vols esborrar aquest resultat?");
+                } else {
+                  var r = confirm("Estàs segur de que vols esborrar aquests resultats?");
+                }
+                if (r == true) {
+                  var results = [];
+                  $.each($('.deleteCheckbox:checked'), function(index, value) {
+                    var v = $(value);
+                    console.log(index + ":" + v.attr('data-result-id'));
+                    results.push(v.attr('data-result-id'));
+
+                  });
+                  var url = '/api/v1/results/delete';
+                  $.ajax({
+                    url: url,
+                    type: "POST",
+                    contentType: 'application/json;charset=UTF-8',
+                    data: JSON.stringify(results)
+                  }).done(function(msg){
+                    // Results have been deleted. Refresh table and enable again the interval
+                    console.log(msg);
+                    $('.btnDeleteResults').addClass('disabled');
+                    table.ajax.reload(null, false);
+                    refreshIntervalId = refreshIntervalTrigger();
+                    console.log("Interval refresh is enabled with id " + refreshIntervalId);
+                  })
+                }
+              }}
         ],
     "pageLength": 15,
     "order": [ 
@@ -214,7 +262,7 @@ $(document).ready(function() {
 
         var column = this;
         var idx = column.index();
-        if (idx == 3) {
+        if (idx == COLUMN_PLAYER) {
           var select = $('<select class="form-control"><option value="">Tots els jugadors</option></select>')
             .appendTo($('#filterByPlayer'))
             .on('change', function() {
@@ -234,7 +282,7 @@ $(document).ready(function() {
           column.data().unique().sort().each(function(d, j) {
             select.append('<option value="' + d + '">' + d + '</option>')
           });
-        } else if (idx == 4) {
+        } else if (idx == COLUMN_STATION) {
           var select = $('<select class="form-control"><option value="">Totes les 
estacions</option></select>')
             .appendTo($('#filterByStation'))
             .on('change', function() {
@@ -251,7 +299,7 @@ $(document).ready(function() {
           column.data().unique().sort().each(function(d, j) {
             select.append('<option value="' + d + '">' + d + '</option>')
           });
-        } else if (idx == 5) {
+        } else if (idx == COLUMN_EXERCISE) {
           var select = $('<select class="form-control"><option value="">Tots els 
exercicis</option></select>')
             .appendTo($('#filterByExercice'))
             .on('change', function() {
@@ -270,13 +318,37 @@ $(document).ready(function() {
           });
         }
       });
+
+      // Called every time a delete checkbox is changed
+      $('.deleteCheckbox').on('change', function() {
+          var totalChecked = $('.deleteCheckbox:checked').length;
+          if (totalChecked > 0) {
+            // Disable the refresh interval
+            if (refreshIntervalId != -1) {
+              clearInterval(refreshIntervalId);
+              console.log("Interval refresh is disabled");
+              refreshIntervalId = -1;
+              $('.btnDeleteResults').removeClass('disabled');
+            }
+          } else {
+            // Enable again the refresh interval
+            $('.btnDeleteResults').addClass('disabled');
+            refreshIntervalId = refreshIntervalTrigger();
+            console.log("Interval refresh is enabled with id " + refreshIntervalId);
+          }
+      });
+
     }
   });
 
 
-  setInterval(function() {
-     /* Set the interval for refresh */
-     table.ajax.reload(null, false);
-   }, 15000);
+  function refreshIntervalTrigger() {
+    return setInterval(function() {
+       /* Set the interval for refresh */
+       table.ajax.reload(null, false);
+     }, 15000);
+   };
 
+   refreshIntervalId = refreshIntervalTrigger();
+   console.log("Interval refresh is enabled with id " + refreshIntervalId);
 });
diff --git a/chronojumpserver/js/sprints.js b/chronojumpserver/js/sprints.js
old mode 100644
new mode 100755
index 79e3e4e..8fc0749
--- a/chronojumpserver/js/sprints.js
+++ b/chronojumpserver/js/sprints.js
@@ -171,7 +171,7 @@ $(document).ready(function() {
         render: $.fn.dataTable.render.number('', ',', 2)
       }
     ],
-    "dom": '<"resultsFilter">fBrtip',
+    "dom": "<'row'<'resultsFilter'>><'row'<'col-sm-6'B><'col-sm-6'f>>rtip",
     buttons: [
             { extend: 'csv', text: 'Exportar Sprints', className: "btn btn-primary", fieldSeparator: ";"}
         ],
diff --git a/chronojumpserver/models.py b/chronojumpserver/models.py
index 09c776e..a2744dd 100755
--- a/chronojumpserver/models.py
+++ b/chronojumpserver/models.py
@@ -4,7 +4,7 @@ from datetime import datetime
 from sqlalchemy import Column, Integer, String, Float, DateTime,  ForeignKey, UniqueConstraint
 from sqlalchemy.types import Boolean
 from sqlalchemy.orm import relationship
-
+from flask_login import UserMixin
 
 class HelperMixin(object):
 
@@ -387,6 +387,11 @@ class ResultEncoder(Base):
         """Representation od the object."""
         return '<ResultEncoder %d>' % (self.id)
 
+    @classmethod
+    def getById(cls, object_id):
+        o = cls.query.filter(cls.id == object_id).first()
+        return o
+
 
 class ResultSprint(Base):
     __tablename__ = 'resultSprint'
@@ -444,5 +449,38 @@ class ResultSprint(Base):
         }
 
     def __repr__(self):
-        """Representation od the object."""
+        """Representation of the object."""
         return '<ResultSprint %d>' % (self.id)
+
+
+
+class User(UserMixin, Base):
+    __tablename__ = 'user'
+
+    id = Column('id', Integer, primary_key=True)
+    username = Column('username', String(50))
+    password = Column('password', String(50))
+    user_type = Column('user_type', String(50))
+
+    def __init__(self, username, password, user_type='default'):
+        self.username = username
+        self.password = password
+        self.user_type = user_type
+
+    @property
+    def serialize(self):
+        """Object serialization (Json)."""
+        return {
+            'id': self.id,
+            'username': self.username,
+            'password': self.password,
+            'user_type': self.user_type
+        }
+
+    @classmethod
+    def get(cls, user_id):
+        return User.query.filter(User.id == user_id).first()
+
+    def __repr__(self):
+        """Representation of the object."""
+        return '<User %d>' % (self.id)
diff --git a/chronojumpserver/static/style.css b/chronojumpserver/static/style.css
index 44289d5..d163fb7 100755
--- a/chronojumpserver/static/style.css
+++ b/chronojumpserver/static/style.css
@@ -19,6 +19,18 @@ body.home {
   margin-top: 0px;
 }
 
+body.login {
+  background-color: #e3e3e3;
+}
+
+.btn-primary {
+    background-color: #0f2351;
+}
+
+.pagination>.active {
+    background-color: #0f2351;
+}
+
 .navbar-fixed-top {
   background-color: #0f2351;
   height: 80px;
@@ -64,6 +76,16 @@ body.home {
   color: blue;
 }
 
+.dt-buttons > a.btn {
+    margin-right: 4px;
+    border-radius: 4px;
+
+}
+
+.colAlignRight {
+    text-align: right;
+}
+
 #players td {
     vertical-align: middle;
 }
diff --git a/chronojumpserver/templates/_formhelpers.html b/chronojumpserver/templates/_formhelpers.html
index f701814..b2d8d7e 100755
--- a/chronojumpserver/templates/_formhelpers.html
+++ b/chronojumpserver/templates/_formhelpers.html
@@ -7,6 +7,9 @@
       {% elif field.type == "StringField" %}
         <label for="{{field.name}}">{{field.label}}</label>
         <input class="form-control" type="text" value="{{field.data}}" name="{{field.name}}" 
id="{{field.name}}"/>
+      {% elif field.type == "PasswordField" %}
+          <label for="{{field.name}}">{{field.label}}</label>
+          <input class="form-control" type="password" value="{{field.data}}" name="{{field.name}}" 
id="{{field.name}}"/>
       {% elif field.type == "FileField" %}
         <div class="text-center">
           {% if field.data %}
diff --git a/chronojumpserver/templates/index.html b/chronojumpserver/templates/index.html
index 7a06130..5aff6c0 100755
--- a/chronojumpserver/templates/index.html
+++ b/chronojumpserver/templates/index.html
@@ -17,11 +17,13 @@
 {% endblock %}
 
 {% block content %}
-<div class="row" style="margin-top:100px">
-    <div class="col-md-6">
-
+<div class="row" style="margin-top:20px">
+    <div class="col-md-12">
+        {% if current_user.is_authenticated %}
+        <!--<div class="alert alert-success">Hola {{ current_user.username }}!</div>-->
+        {% endif %}
     </div>
-    <div class="col-md-6">
+    <div class="col-md-offset-6 col-md-6">
         <h2 class="text-center text-uppercase">Opcions</h2>
     </div>
     <div class="col-md-6" >
@@ -32,6 +34,9 @@
         <a class="btn btn-primary btn-lg btn-block" href="{{ url_for('show_sprints')}}">Sprints</a>
         <a class="btn btn-primary btn-lg btn-block" href="{{ url_for('show_players')}}">Llistat jugadors</a>
         <a class="btn btn-primary btn-lg btn-block" href="{{ 
url_for('show_stations')}}">Estacions/Exercisis</a>
+        {% if current_user.is_authenticated %}
+            <a class="btn btn-primary btn-lg btn-block" href="{{ url_for('logout')}}">Sortir i tancar 
sessió</a>
+        {% endif %}
     </div>
 </div>
 {% endblock %}
diff --git a/chronojumpserver/templates/layout.html b/chronojumpserver/templates/layout.html
index 437b9b7..eeaa820 100755
--- a/chronojumpserver/templates/layout.html
+++ b/chronojumpserver/templates/layout.html
@@ -34,7 +34,9 @@
                   <li><a href="{{ url_for('show_sprints')}}">Sprints</a></li>
                   <li><a href="{{ url_for('show_players')}}">Llistat Jugadors</a></li>
                   <li><a href="{{ url_for('show_stations')}}">Estacions/Exercisis</a></li>
-                  </li>
+                  {% if current_user.is_authenticated %}
+                      <li><a href="{{ url_for('logout')}}">Tancar sessió</a></li>
+                  {% endif %}
               </ul>
           </div>
         </div>
diff --git a/chronojumpserver/templates/login.html b/chronojumpserver/templates/login.html
new file mode 100755
index 0000000..07c7c97
--- /dev/null
+++ b/chronojumpserver/templates/login.html
@@ -0,0 +1,32 @@
+{% from "_formhelpers.html" import render_field %}
+{% extends 'layout.html' %}
+
+
+{% block nav %}{% endblock %}
+
+{% block body_class %}login{% endblock %}
+
+{% block content %}
+    <div class="container">
+        <div class="row">
+            <div class="col-sm-offset-3 col-sm-6" style="margin-top:50px;">
+                <div class="" style="background-color:#fff;;border-radius:15px;padding:10px">
+                    <div style=" margin-bottom: 20px;background-color: #0f2351;padding:-10px">
+                        <img class="img-responsive" src="{{ url_for('static', 
filename='images/chronojump-logo.png')}}" height="180px">
+                    </div>
+                    {% if error_msg %}
+                        <div class="alert alert-danger text-center">Error: {{error_msg}}</div>
+                    {% endif %}
+                    <form method="post" enctype="multipart/form-data">
+                      {{ form.csrf_token }}
+
+                      <p class="text-center">Introdueix el teu usuari i contrasenya</p>
+                      {{ render_field(form.username)}}
+                      {{ render_field(form.password)}}
+                      <button class="btn btn-primary btn-block" type="submit">Accedir</button>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+{% endblock %}
diff --git a/chronojumpserver/templates/results.html b/chronojumpserver/templates/results.html
index ce5fb85..edeb9d3 100755
--- a/chronojumpserver/templates/results.html
+++ b/chronojumpserver/templates/results.html
@@ -12,7 +12,7 @@
        <h1>Resultats</h1>
 </div>
 
-<div id="resultsFilter" class="row pull-left">
+<div id="resultsFilter" class="row">
        <div class="col-sm-6">
                <label class="radio-inline dayfilter" style="margin-top:5px">
                  <input type="radio" name="filterByDayOptions" id="filterByDay1" value="1" >1d
diff --git a/chronojumpserver/templates/sprints.html b/chronojumpserver/templates/sprints.html
old mode 100644
new mode 100755
index d8cbf1b..89ec5e5
--- a/chronojumpserver/templates/sprints.html
+++ b/chronojumpserver/templates/sprints.html
@@ -11,7 +11,7 @@
 <div class="page-header">
        <h1>Sprints</h1>
 </div>
-<div id="resultsFilter" class="row pull-left">
+<div id="resultsFilter" class="row">
        <div class="col-sm-8">
                <label class="radio-inline dayfilter" style="margin-top:5px">
                  <input type="radio" name="filterByDayOptions" id="filterByDay1" value="1" >1d
diff --git a/chronojumpserver/views.py b/chronojumpserver/views.py
index ca108f9..792b623 100755
--- a/chronojumpserver/views.py
+++ b/chronojumpserver/views.py
@@ -1,17 +1,29 @@
 # -*- coding: utf-8 -*-
 """Chronojump Server views controller."""
 from chronojumpserver import app
-from flask import render_template, request, redirect, url_for
+from flask import render_template, request, redirect, url_for, abort, flash
+from urlparse import urlparse, urljoin
 from flask_wtf.file import FileField
-from chronojumpserver.models import Person, Station, RFIDHistory
-from chronojumpserver.forms import PersonForm
-
+from chronojumpserver.models import Person, Station, RFIDHistory, User
+from chronojumpserver.forms import PersonForm, LoginForm
+from flask_login import login_required, login_user, logout_user
 from chronojumpserver.database import db_session
 import os
 from time import time
 
 
+def is_safe_url(target):
+    """
+        Snippet to check if the url is safe, specially when coming
+        from login action.
+    """
+    ref_url = urlparse(request.host_url)
+    test_url = urlparse(urljoin(request.host_url, target))
+    return test_url.scheme in ('http', 'https') and \
+           ref_url.netloc == test_url.netloc
+
 @app.route('/')
+@login_required
 def index():
     """Chronojump Server Home page."""
     return render_template('index.html')
@@ -26,11 +38,13 @@ def airport():
     return render_template('airport.html', stations=stations, players=players)
 
 @app.route('/results')
+@login_required
 def show_results():
     """Show results view."""
     return render_template('results.html')
 
 @app.route('/sprints')
+@login_required
 def show_sprints():
     """Show sprints view."""
     return render_template('sprints.html')
@@ -50,6 +64,7 @@ def show_players():
 
 
 @app.route('/stations')
+@login_required
 def show_stations():
     """Show Stations and Exercises."""
     stations = []
@@ -89,6 +104,7 @@ def _update_player_photo(player_id, photo, previous_imageName):
 
 
 @app.route('/player/<player_id>', methods=['GET', 'POST'])
+@login_required
 def player_detail(player_id):
     """Show players detail."""
     has_errors = False
@@ -145,6 +161,7 @@ def player_detail(player_id):
 
 
 @app.route('/player/add', methods=['GET', 'POST'])
+@login_required
 def add_player():
     """Show form to add a new player."""
     has_errors = False
@@ -187,3 +204,53 @@ def add_player():
 
     return render_template('player_detail.html', form=form, msg=msg,
         has_errors=has_errors)
+
+@app.route('/login', methods=['GET', 'POST'])
+def login():
+    # Here we use a class of some kind to represent and validate our
+    # client-side form data. For example, WTForms is a library that will
+    # handle this for us, and we use a custom LoginForm to validate.
+    form = LoginForm()
+    if request.method == "GET":
+        form.username.data = ""
+        form.password.data = ""
+    if form.validate_on_submit():
+        import md5
+        username = form.username.data
+        password = md5.md5(form.password.data).hexdigest()
+        print password
+        user = User.query.filter(User.username == username).first()
+        if user:
+            print "DEBUG: User %s found" % user.username
+            print user.password
+            if password == user.password:
+                print "DEBUG: Passwords match. Allow login"
+                # Login and validate the user.
+                # user should be an instance of your `User` class
+                login_user(user)
+
+                flash('Logged in successfully.')
+                next = request.args.get('next')
+                # is_safe_url should check if the url is safe for redirects.
+                # See http://flask.pocoo.org/snippets/62/ for an example.
+                if not is_safe_url(next):
+                    return abort(400)
+
+                return redirect(next or url_for('index'))
+            else:
+                # Invalid password
+                error_msg = u"Contrasenya invàlida"
+                return render_template('login.html', form=form, error_msg=error_msg)
+        else:
+            # Invalid user
+            error_msg = u"El usuari %s no existeix!" % username
+            return render_template('login.html', form=form, error_msg=error_msg)
+
+
+    return render_template('login.html', form=form)
+
+@app.route("/logout")
+@login_required
+def logout():
+    logout_user()
+    return redirect(url_for('login'))
diff --git a/requirements.txt b/requirements.txt
index 69ebcd3..4ae1261 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -20,3 +20,4 @@ SQLAlchemy==1.1.10
 virtualenv==15.1.0
 Werkzeug==0.9.6
 Flask-WTF==0.14.2
+flask-login


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