Merge branch '11_Redirect_to_edit_vehicle_page_if_no_consumable_is_configured' into 'development'

11 redirect to edit vehicle page if no consumable is configured



See merge request !28
This commit is contained in:
Joachim Lusiardi 2016-08-28 14:01:37 +02:00
commit bb7c2780e8
18 changed files with 264 additions and 50 deletions

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
.idea/ .idea/
tests/results/
**.DS_Store **.DS_Store
**.swp **.swp
**.pyc **.pyc
*.sqlite

View File

@ -1,6 +1,4 @@
from datetime import date from flask import Flask, flash
from flask import Flask
from flask import redirect, g from flask import redirect, g
from flask import render_template from flask import render_template
from flask import url_for from flask import url_for
@ -58,6 +56,10 @@ def user_registered_sighandler(app, user, confirm_token):
""" """
role = user_datastore.find_role('user') role = user_datastore.find_role('user')
user_datastore.add_role_to_user(user, role) user_datastore.add_role_to_user(user, role)
if user.email == app.config['ADMIN_MAIL']:
# if the user selected the preconfigured email for the admin account
role = user_datastore.find_role('admin')
user_datastore.add_role_to_user(user, role)
new_vehicle = Vehicle('default vehicle') new_vehicle = Vehicle('default vehicle')
db.session.add(new_vehicle) db.session.add(new_vehicle)
user.vehicles.append(new_vehicle) user.vehicles.append(new_vehicle)
@ -237,6 +239,10 @@ def select_consumable_for_new_pitstop(vid):
if vehicle is None or vehicle not in current_user.vehicles: if vehicle is None or vehicle not in current_user.vehicles:
return redirect(url_for('select_vehicle_for_new_pitstop')) return redirect(url_for('select_vehicle_for_new_pitstop'))
if len(vehicle.consumables) == 0:
flash('Please choose at least one consumable!', 'warning')
return redirect(url_for('edit_vehicle', vid=vid))
if len(vehicle.consumables) == 1: if len(vehicle.consumables) == 1:
return redirect(url_for('create_pit_stop_form', vid=vid, cid=vehicle.consumables[0].id)) return redirect(url_for('create_pit_stop_form', vid=vid, cid=vehicle.consumables[0].id))

View File

@ -3,7 +3,7 @@ body {
} }
.starter-template { .starter-template {
padding-top: 30px; padding-top: 0px;
padding-bottom: 60px; padding-bottom: 60px;
text-align: center; text-align: center;
} }
@ -59,6 +59,14 @@ h3:after{
text-align: left; text-align: left;
} }
.alert {
margin-bottom:0px;
}
.topspace {
margin-top: 5px;
}
// for small devices // for small devices
@media only screen @media only screen
and (min-device-width : 320px) and (min-device-width : 320px)

View File

@ -5,7 +5,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Password</div> <div class="panel-heading">Password</div>
<div class="panel-body"> <div class="panel-body">
<a href='{{ url_for('security.change_password') }}' class="btn btn-primary " role="button"> <a href="{{ url_for('security.change_password') }}" id="change_password" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Change <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Change
</a> </a>
</div> </div>
@ -13,7 +13,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Vehicles</div> <div class="panel-heading">Vehicles</div>
<div class="panel-body"> <div class="panel-body">
<a href="{{ url_for('create_vehicle') }}" class="btn btn-primary " role="button"> <a href="{{ url_for('create_vehicle') }}" id="create_vehicle" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> create <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> create
</a> </a>
</div> </div>
@ -40,11 +40,11 @@
{{ vehicle.consumables | length }} consumables {{ vehicle.consumables | length }} consumables
</td> </td>
<td> <td>
<a href="{{ url_for('edit_vehicle', vid=vehicle.id) }}" class="btn btn-primary " role="button"> <a href="{{ url_for('edit_vehicle', vid=vehicle.id) }}" id="edit_vehicle_{{loop.index}}" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
</a> </a>
{% if current_user.vehicles | length > 1 %} {% if current_user.vehicles | length > 1 %}
<a href="{{ url_for('delete_vehicle', vid=vehicle.id) }}" class="btn btn-primary btn-warning " role="button"> <a href="{{ url_for('delete_vehicle', vid=vehicle.id) }}" id="delete_vehicle_{{loop.index}}" class="btn btn-primary btn-warning " role="button">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a> </a>
{% else %} {% else %}
@ -59,7 +59,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Account</div> <div class="panel-heading">Account</div>
<div class="panel-body"> <div class="panel-body">
<a href='{{ url_for('delete_account') }}' class="btn btn-primary " role="button"> <a href="{{ url_for('delete_account') }}" id="delete_account" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete
</a> </a>
</div> </div>

View File

@ -16,7 +16,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Consumables</div> <div class="panel-heading">Consumables</div>
<div class="panel-body"> <div class="panel-body">
<a href="{{ url_for('create_consumable') }}" class="btn btn-primary " role="button"> <a href="{{ url_for('create_consumable') }}" id="create_consumable" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> create <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> create
</a> </a>
</div> </div>
@ -38,22 +38,16 @@
</tr> </tr>
{% for consumable in consumables %} {% for consumable in consumables %}
<tr> <tr>
<td> <td id="name_{{loop.index}}">{{ consumable.name }}</td>
{{ consumable.name }} <td id="unit_{{loop.index}}">{{ consumable.unit }}</td>
</td> <td id="count_{{loop.index}}">{{ consumable.vehicles | length }} vehicles</td>
<td>
{{ consumable.unit }}
</td>
<td>
{{ consumable.vehicles | length }} vehicles
</td>
<td> <td>
{% if not consumable.in_use %} {% if not consumable.in_use %}
<a href="{{ url_for('delete_consumable', cid=consumable.id) }}" class="btn btn-primary btn-warning " role="button"> <a href="{{ url_for('delete_consumable', cid=consumable.id) }}" id="delete_consumable{{loop.index}}" class="btn btn-primary btn-warning " role="button">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a> </a>
{% endif %} {% endif %}
<a href="{{ url_for('edit_consumable', cid=consumable.id) }}" class="btn btn-primary " role="button"> <a href="{{ url_for('edit_consumable', cid=consumable.id) }}" id="edit_consumable{{loop.index}}" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
</a> </a>
</td> </td>

View File

@ -5,7 +5,7 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-body"> <div class="panel-body">
<h3>Delete vehicle '{{consumable.name}}'?</h3> <h3>Delete consumable '{{consumable.name}}'?</h3>
<form class='form-horizontal' method="POST"> <form class='form-horizontal' method="POST">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ render_field_with_errors(form.submit) }} {{ render_field_with_errors(form.submit) }}

View File

@ -5,7 +5,7 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-body"> <div class="panel-body">
<h3>Edit vehicle</h3> <h3>Edit vehicle '{{vehicle.name}}'</h3>
<form class='form-horizontal' method="POST"> <form class='form-horizontal' method="POST">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ render_field_with_errors(form.name) }} {{ render_field_with_errors(form.name) }}

View File

@ -1,15 +1,15 @@
{% macro navigation() -%} {% macro navigation() -%}
{% if current_user.email %} {% if current_user.email %}
<li><a href='{{ url_for('select_vehicle_for_new_pitstop') }}'>Create Pitstop</a></li> <li><a id='new_pitstop_link' href='{{ url_for('select_vehicle_for_new_pitstop') }}'>Create Pitstop</a></li>
<li><a href='{{ url_for('get_statistics') }}'>Statistics</a></li> <li><a id='statistics_limk' href='{{ url_for('get_statistics') }}'>Statistics</a></li>
<li><a href='{{ url_for('get_account_page') }}'>Account</a></li> <li><a id='account_link' href='{{ url_for('get_account_page') }}'>Account</a></li>
{% if current_user.has_role('admin') %} {% if current_user.has_role('admin') %}
<li><a href='{{ url_for('get_admin_page') }}'>Admin</a></li> <li><a id='admin_link' href='{{ url_for('get_admin_page') }}'>Admin</a></li>
{% endif %} {% endif %}
<li><a href='{{ url_for('security.logout') }}'>Logout</a></li> <li><a id='logout_link' href='{{ url_for('security.logout') }}'>Logout</a></li>
{% else %} {% else %}
<li><a href='{{ url_for('security.login') }}'>Login</a></li> <li><a id='login_link' href='{{ url_for('security.login') }}'>Login</a></li>
<li><a href='{{ url_for('security.register') }}'>Register</a></li> <li><a id='register_link' href='{{ url_for('security.register') }}'>Register</a></li>
{% endif %} {% endif %}
{%- endmacro %} {%- endmacro %}
@ -151,8 +151,23 @@
</div><!--/.nav-collapse --> </div><!--/.nav-collapse -->
</div> </div>
</nav> </nav>
{% with messages = get_flashed_messages(with_categories=true) %}
<div class="container"> {% if messages %}
{% for category, message in messages %}
<div class="row topspace">
<div class="col-md-4" ></div>
<div class="col-md-4">
<div class="alert alert-{{category}} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{{ message }}
</div>
</div>
<div class="col-md-4" ></div>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="container topspace">
<div class="starter-template"> <div class="starter-template">
{% block body %} {% block body %}
{% endblock %} {% endblock %}

View File

@ -13,6 +13,7 @@
</ul> </ul>
<div id="my-tab-content" class="tab-content"> <div id="my-tab-content" class="tab-content">
{% for vehicle in current_user.vehicles %} {% for vehicle in current_user.vehicles %}
{% set vehicleloop = loop %}
<div class="tab-pane {% if loop.first %}active{% endif %}" id="v{{vehicle.id}}"> <div class="tab-pane {% if loop.first %}active{% endif %}" id="v{{vehicle.id}}">
<h3>{{vehicle.name}}</h3> <h3>{{vehicle.name}}</h3>
{% if vehicle.pitstops %} {% if vehicle.pitstops %}
@ -22,19 +23,19 @@
<table class="table table-striped table-bordered table-condensed"> <table class="table table-striped table-bordered table-condensed">
<tr> <tr>
<th>Date</th> <th>Date</th>
<td>{{pitstop.date}}</td> <td id="vehicle_{{vehicleloop.index}}_pitstop_{{loop.index}}_date">{{pitstop.date}}</td>
</tr> </tr>
<tr> <tr>
<th>Odometer</th> <th>Odometer</th>
<td>{{pitstop.odometer}} km</td> <td id="vehicle_{{vehicleloop.index}}_pitstop_{{loop.index}}_odo">{{pitstop.odometer}} km</td>
</tr> </tr>
<tr> <tr>
<th>{{ pitstop.consumable.name }}</th> <th>{{ pitstop.consumable.name }}</th>
<td>{{pitstop.amount}} {{ pitstop.consumable.unit }}</td> <td id="vehicle_{{vehicleloop.index}}_pitstop_{{loop.index}}_anmount">{{pitstop.amount}} {{ pitstop.consumable.unit }}</td>
</tr> </tr>
<tr> <tr>
<th>Costs</th> <th>Costs</th>
<td> <td id="vehicle_{{vehicleloop.index}}_pitstop_{{loop.index}}_cost">
{% if pitstop.costs %} {% if pitstop.costs %}
{{pitstop.costs}} € {{pitstop.costs}} €
{% else %} {% else %}
@ -44,10 +45,10 @@
</tr> </tr>
</table> </table>
{% if loop.first %} {% if loop.first %}
<a href="{{ url_for('edit_pit_stop_form', pid=pitstop.id) }}" class="btn btn-primary"> <a id="vehicle_{{vehicleloop.index}}_edit_pitstop_{{loop.index}}" href="{{ url_for('edit_pit_stop_form', pid=pitstop.id) }}" class="btn btn-primary">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
</a> </a>
<a href="{{ url_for('delete_pit_stop_form', pid=pitstop.id) }}" class="btn btn-primary btn-warning "> <a id="vehicle_{{vehicleloop.index}}_delete_pitstop_{{loop.index}}" href="{{ url_for('delete_pit_stop_form', pid=pitstop.id) }}" class="btn btn-primary btn-warning ">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a> </a>
{% endif %} {% endif %}

View File

@ -17,6 +17,7 @@ class Config:
MAIL_USE_SSL = False MAIL_USE_SSL = False
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMIN_MAIL = 'joachim@lusiardi.de'
@staticmethod @staticmethod
def init_app(app): def init_app(app):
@ -26,7 +27,7 @@ class Config:
class DevelopmentConfig(Config): class DevelopmentConfig(Config):
SECURITY_SEND_REGISTER_EMAIL = False SECURITY_SEND_REGISTER_EMAIL = False
DEBUG = True DEBUG = True
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:%s@database/pitstops' % (os.environ['DATABASE_ENV_MYSQL_ROOT_PASSWORD']) SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:%s@database/pitstops' % (os.environ.get('DATABASE_ENV_MYSQL_ROOT_PASSWORD'))
class TestingConfig(Config): class TestingConfig(Config):

View File

@ -2,19 +2,11 @@ version: '2'
services: services:
rollerverbrauch: rollerverbrauch:
build: . build: .
depends_on:
- database
volumes: volumes:
- ./compose_config/:/config - ./compose_config/:/config
environment: environment:
- config=/config/config.py - config=/config/config.py
- DATABASE_ENV_MYSQL_ROOT_PASSWORD=foobar123 - DATABASE_ENV_MYSQL_ROOT_PASSWORD=foobar123
- FLASK_CONFIG=testing
ports: ports:
- 5000 - 5000:5000
database:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=foobar123
- MYSQL_DATABASE=pitstops
ports:
- 3306

12
tests/Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM debian8_python3
COPY app/requirements.txt /requirements.txt
RUN pip3 install -r /requirements.txt; \
mkdir /data
ADD app /app
VOLUME ["/data"]
VOLUME ["/app/config]
EXPOSE 5000
ENTRYPOINT python3 /app/main.py

137
tests/app/main.py Normal file
View File

@ -0,0 +1,137 @@
import unittest
import inspect
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
def wait_net_service(server, port, timeout=None):
""" Wait for network service to appear
@param timeout: in seconds, if None or 0 wait forever
@return: True of False, if timeout is None may return only True or
throw unhandled network exception
"""
import socket
import errno
s = socket.socket()
if timeout:
from time import time as now
# time module is needed to calc timeout shared between two exceptions
end = now() + timeout
while True:
try:
if timeout:
next_timeout = end - now()
if next_timeout < 0:
return False
else:
s.settimeout(next_timeout)
s.connect((server, port))
except socket.timeout as err:
# this exception occurs only if timeout is set
if timeout:
return False
except socket.error as err:
# catch timeout exception from underlying network library
# this one is different from socket.timeout
if type(err.args) != tuple or err[0] != errno.ETIMEDOUT:
raise
else:
s.close()
return True
class BaseTestCase(unittest.TestCase):
def setUp(self):
self.screen_shot_counter = 0
self.driver = webdriver.Remote(
command_executor='http://selenium:4444/wd/hub',
desired_capabilities=DesiredCapabilities.FIREFOX)
self.driver.get("http://rollerverbrauch:5000")
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "email")))
def create_screenshot(self):
self.driver.get_screenshot_as_file(
'/results/%s_%s_%s.png' % (self.__class__.__name__, inspect.stack()[1][3], str(self.screen_shot_counter)))
self.screen_shot_counter += 1
def tearDown(self):
self.driver.close()
class RegisterCheck(BaseTestCase):
def setUp(self):
self.screen_shot_counter = 0
self.driver = webdriver.Remote(
command_executor='http://selenium:4444/wd/hub',
desired_capabilities=DesiredCapabilities.FIREFOX)
self.driver.get("http://rollerverbrauch:5000")
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "email")))
def test_page_loads(self):
self.driver.get("http://rollerverbrauch:5000")
self.create_screenshot()
self.assertIn("refuel", self.driver.title, "Title must contain reload")
def test_can_register(self):
self.driver.get("http://rollerverbrauch:5000")
self.driver.find_element_by_partial_link_text('Register').click()
self.create_screenshot()
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "submit")))
self.create_screenshot()
self.driver.find_element_by_id('email').send_keys('test@test.com')
self.driver.find_element_by_id('password').send_keys('test123')
self.driver.find_element_by_id('password_confirm').send_keys('test123')
self.create_screenshot()
self.driver.find_element_by_id('submit').click()
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "i1")))
self.create_screenshot()
def test_register_must_repeat_pwd(self):
self.driver.get("http://rollerverbrauch:5000")
self.driver.find_element_by_partial_link_text('Register').click()
self.create_screenshot()
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "submit")))
self.create_screenshot()
self.driver.find_element_by_id('email').send_keys('test1@test.com')
self.driver.find_element_by_id('password').send_keys('test123')
self.create_screenshot()
self.driver.find_element_by_id('submit').click()
self.create_screenshot()
error = self.driver.find_elements_by_class_name('error')
self.assertIsNotNone(error[0], 'we expect an error')
self.assertIn('Passwords do not match', error[0].text, 'wrong error message')
def test_register_must_be_equal_pwd(self):
self.driver.get("http://rollerverbrauch:5000")
self.driver.find_element_by_partial_link_text('Register').click()
self.create_screenshot()
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "submit")))
self.create_screenshot()
self.driver.find_element_by_id('email').send_keys('test1@test.com')
self.driver.find_element_by_id('password').send_keys('test123')
self.driver.find_element_by_id('password_confirm').send_keys('test1234')
self.create_screenshot()
self.driver.find_element_by_id('submit').click()
self.create_screenshot()
error = self.driver.find_elements_by_class_name('error')
self.assertIsNotNone(error[0], 'we expect an error')
self.assertIn('Passwords do not match', error[0].text, 'wrong error message')
class LoginCheck(BaseTestCase):
def can_login(self):
pass
if __name__ == "__main__":
wait_net_service('selenium', 4444)
unittest.main()

View File

@ -0,0 +1 @@
selenium

View File

@ -0,0 +1,16 @@
import os
#SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:%s@database/pitstops' % (os.environ['DATABASE_ENV_MYSQL_ROOT_PASSWORD'])
SQLALCHEMY_DATABASE_URI = 'sqlite:////data/rollerverbrauch.db'
MAIL_SERVER = ''
MAIL_PORT= 25
MAIL_USE_TLS = True
MAIL_USE_SSL = False
MAIL_USERNAME = ''
MAIL_PASSWORD = ''
SECURITY_EMAIL_SENDER = ''
SECURITY_PASSWORD_SALT = 'SecretSalt'
SECRET_KEY = 'SecretKey'
SECURITY_SEND_REGISTER_EMAIL = False

22
tests/docker-compose.yml Normal file
View File

@ -0,0 +1,22 @@
version: '2'
services:
tests:
build: .
depends_on:
- selenium
volumes:
- ./results:/results
selenium:
image: selenium/standalone-firefox
depends_on:
- rollerverbrauch
rollerverbrauch:
build: ..
volumes:
- ./compose_config/:/config
environment:
- config=/config/config.py
- DATABASE_ENV_MYSQL_ROOT_PASSWORD=foobar123
ports:
- 5000

7
tests/run_test.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
rm -f results/*
docker-compose build
docker-compose up --abort-on-container-exit
docker-compose down
docker-compose rm --all