Merge branch 'master' of github.com:alphagov/notifications-admin

This commit is contained in:
Martyn Inglis
2015-12-16 14:01:09 +00:00
31 changed files with 408 additions and 110 deletions

2
.gitignore vendored
View File

@@ -63,3 +63,5 @@ app/assets/stylesheets/govuk_template/.sass-cache/
.sass-cache/
cache/
app/static/*
app/static/stylesheets/*.css

View File

@@ -44,4 +44,16 @@
#global-header #logo {
white-space: nowrap;
}
.column-three-quarters {
@include grid-column(3/4);
}
a:visited {
color: #005ea5;
}

View File

@@ -0,0 +1,23 @@
#navigation {
padding: 50px 0 0 0;
}
#navigation li {
@include core-16();
margin: 3px 0;
}
#navigation ul {
border-bottom: 1px solid #777;
margin: 0 20px 0 0;
list-style-type: none;
padding: 0;
}
#navigation a:link,a:visited {
text-decoration: none;
}
#navigation a:hover {
color: #2e8aca;
}

View File

@@ -33,6 +33,7 @@
@import "components/sms-message";
@import "components/submit-form";
@import "components/table";
@import "components/navigation";
// TODO: break this up
@import "app";

View File

@@ -3,4 +3,4 @@ from flask import Blueprint
main = Blueprint('main', __name__)
from app.main.views import index, sign_in, register, two_factor, verify, sms
from app.main.views import index, sign_in, register, two_factor, verify, sms, add_service

View File

@@ -0,0 +1,41 @@
from datetime import datetime
from app import db
from app.models import Service
def insert_new_service(service_name, user):
service = Service(name=service_name,
created_at=datetime.now(),
limit=1000,
active=False,
restricted=True)
add_service(service)
service.users.append(user)
db.session.commit()
return service.id
def get_service_by_id(id):
return Service.query.get(id)
def unrestrict_service(service_id):
service = get_service_by_id(service_id)
service.restricted = False
add_service(service)
def activate_service(service_id):
service = get_service_by_id(service_id)
service.active = True
add_service(service)
def add_service(service):
db.session.add(service)
db.session.commit()
def find_service_by_service_name(service_name):
return Service.query.filter_by(name=service_name).first()

View File

@@ -5,7 +5,7 @@ from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Email, Length, Regexp
from app.main.dao import verify_codes_dao
from app.main.dao import verify_codes_dao, services_dao
from app.main.encryption import check_hash
from app.main.validators import Blacklist
@@ -82,3 +82,14 @@ def validate_code(field, code):
return True
else:
return False
class AddServiceForm(Form):
service_name = StringField(validators=[DataRequired(message='Please enter your service name')])
def validate_service_name(self, a):
if services_dao.find_service_by_service_name(self.service_name.data) is not None:
self.service_name.errors.append('Duplicate service name')
return False
else:
return True

View File

@@ -0,0 +1,25 @@
from flask import render_template, jsonify, redirect, session
from flask_login import login_required
from app.main import main
from app.main.dao import services_dao, users_dao
from app.main.forms import AddServiceForm
@main.route("/add-service", methods=['GET'])
@login_required
def add_service():
return render_template('views/add-service.html', form=AddServiceForm())
@main.route("/add-service", methods=['POST'])
@login_required
def process_add_service():
form = AddServiceForm()
if form.validate_on_submit():
user = users_dao.get_user_by_id(session['user_id'])
services_dao.insert_new_service(form.service_name.data, user)
return redirect('/dashboard')
else:
return jsonify(form.errors), 400

View File

@@ -35,12 +35,6 @@ def dashboard():
return render_template('views/dashboard.html')
@main.route("/add-service")
@login_required
def addservice():
return render_template('views/add-service.html')
@main.route("/email-not-received")
def emailnotreceived():
return render_template('views/email-not-received.html')

View File

@@ -78,6 +78,39 @@ class User(db.Model):
return True
user_to_service = db.Table(
'user_to_service',
db.Model.metadata,
db.Column('user_id', db.Integer, db.ForeignKey('users.id')),
db.Column('service_id', db.Integer, db.ForeignKey('services.id'))
)
class Service(db.Model):
__tablename__ = 'services'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False, unique=True)
created_at = db.Column(db.DateTime, index=False, unique=False, nullable=False)
active = db.Column(db.Boolean, index=False, unique=False, nullable=False)
limit = db.Column(db.BigInteger, index=False, unique=False, nullable=False)
users = db.relationship('User', secondary=user_to_service, backref=db.backref('user_to_service', lazy='dynamic'))
restricted = db.Column(db.Boolean, index=False, unique=False, nullable=False)
def serialize(self):
serialized = {
'id': self.id,
'name': self.name,
'createdAt': self.created_at.strftime(DATETIME_FORMAT),
'active': self.active,
'restricted': self.restricted,
'limit': self.limit,
'user': self.users.serialize()
}
return filter_null_value_fields(serialized)
def filter_null_value_fields(obj):
return dict(
filter(lambda x: x[1] is not None, obj.items())

View File

@@ -0,0 +1,21 @@
{% macro main_nav() %}
<div id="navigation">
<h3><a href="{{ url_for('.dashboard') }}">Service name</a></h3>
<ul>
<li><a href="{{ url_for('.sendsms') }}">Send text messages</a></li>
<li><a href="{{ url_for('.sendemail') }}">Send emails</a></li>
<li><a href="{{ url_for('.showjobs') }}">Activity</a></li>
</ul>
<ul>
<li><a href="{{ url_for('.apikeys') }}">API keys and documentation</a></li>
</ul>
<ul>
<li><a href="{{ url_for('.manageusers') }}">Manage users</a></li>
<li><a href="{{ url_for('.servicesettings') }}">Service settings</a></li>
</ul>
<ul>
<li><a href="/user-profile">Your details</a></li>
</ul>
</div>
{% endmacro %}

View File

@@ -16,15 +16,17 @@ GOV.UK Notify | Set up service
<li>as your email sender name</li>
</ul>
<p>
<label class="form-label" for="emailverify">Service name</label>
<input class="form-control" id="emailverify" type="text"><br>
<span class="font-xsmall">For example, 'Vehicle tax' or 'Carer's allowance'</span>
</p>
<form autocomplete="off" action="" method="post">
{{ form.hidden_tag() }}
<label class="form-label">Service name</label>
{{ form.service_name(class="form-control-2-3", autocomplete="off") }} <br>
<span class="font-xsmall">For example, 'Vehicle tax' or 'Carer's allowance'</span>
<p>
<a class="button" href="dashboard" role="button">Continue</a>
</p>
<p>
<button class="button" href="dashboard" role="button">Continue</button>
</p>
</form>
</div>
</div>
</div>

View File

@@ -1,20 +1,17 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | API keys and documentation
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">API keys and documentation</h1>
<p>Here's where developers can access information about the API and access keys</p>
<p><a href="dashboard">Back to dashboard</a></p>
</div>
</div>
{% endblock %}

View File

@@ -1,13 +1,11 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | Send email
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Send email</h1>
<p>This page will be where we check the email messages we're about to send</p>
@@ -15,7 +13,6 @@ GOV.UK Notify | Send email
<p>
<a class="button" href="dashboard" role="button">Send email messages</a>
</p>
</div>
</div>
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% from "components/sms-message.html" import sms_message %}
{% from "components/table.html" import table, field %}
{% from "components/placeholder.html" import placeholder %}
@@ -8,10 +8,9 @@
GOV.UK Notify | Send text messages
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Send text messages</h1>
<h2 class="heading-medium">Check and confirm</h2>
@@ -61,7 +60,6 @@
url_for(".sendsms")
) }}
</div>
</div>
{% endblock %}

View File

@@ -1,26 +1,15 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | Dashboard
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Dashboard</h1>
<ul>
<li><a href="{{ url_for('.sendsms') }}">Send text messages</a></li>
<li><a href="{{ url_for('.sendemail') }}">Send email messages</a></li>
<li><a href="{{ url_for('.showjobs') }}">View notifications activity</a></li>
<li><a href="{{ url_for('.userprofile') }}">User profile</a></li>
<li><a href="{{ url_for('.manageusers') }}">Manage users</a></li>
<li><a href="{{ url_for('.managetemplates')}}">Manage templates</a></li>
<li><a href="{{ url_for('.servicesettings') }}">Service settings</a></li>
<li><a href="{{ url_for('.apikeys') }}">API keys and documentation</a></li>
</ul>
</div>
</div>
<p>Dashboard goes here.</p>
{% endblock %}

View File

@@ -1,20 +1,18 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | Edit template
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Edit template</h1>
<p>Here's where you can edit an exiting template (including delete) or add a new one</p>
<p><a href="manage-templates">Back to manage templates</a></p>
</div>
</div>
{% endblock %}

View File

@@ -1,13 +1,11 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | Notifications activity
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Notifications for a specific job</h1>
<p>This page will be where we list the notifications for a specific job.</p>
@@ -16,7 +14,6 @@ GOV.UK Notify | Notifications activity
<li><a href="/jobs/job/notification">view a specific notification</a></li>
<li><a href="/jobs">view all the activity for this service</a></li>
</ul>
</div>
</div>
{% endblock %}

View File

@@ -1,13 +1,11 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | Notifications activity
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Notifications activity</h1>
<p>This page will be where we show the list of jobs that this service has processed</p>
@@ -15,7 +13,6 @@ GOV.UK Notify | Notifications activity
<p>
<a href="jobs/job">view a particular notification job</a>
</p>
</div>
</div>
{% endblock %}

View File

@@ -1,13 +1,12 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | Manage templates
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Manage templates</h1>
<p>Here's where you can view templates, choose to add one, or edit/delete one.</p>
@@ -17,7 +16,6 @@ GOV.UK Notify | Manage templates
<p>
<a class="button" href="edit-template" role="button">Add a new message template</a>
</p>
</div>
</div>
{% endblock %}

View File

@@ -1,20 +1,17 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | Manage users
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Manage users</h1>
<p>Here's where you can add or remove users of a service.</p>
<p><a href="dashboard">Back to dashboard</a></p>
</div>
</div>
{% endblock %}

View File

@@ -1,13 +1,12 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | Notifications activity
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">A specific notification</h1>
<p>This page will be where we show what happened for a specific notification.</p>
@@ -15,7 +14,6 @@ GOV.UK Notify | Notifications activity
<p>
<a href="/jobs/job">View other notifications in this job</a>
</p>
</div>
</div>
{% endblock %}

View File

@@ -1,13 +1,12 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | Send email
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Send email</h1>
<p>This page will be where we construct email messages</p>
@@ -15,7 +14,6 @@ GOV.UK Notify | Send email
<p>
<a class="button" href="check-email" role="button">Continue</a>
</p>
</div>
</div>
{% endblock %}

View File

@@ -1,14 +1,12 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% from "components/sms-message.html" import sms_message %}
{% block page_title %}
GOV.UK Notify | Send text messages
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<form method="POST" enctype="multipart/form-data">
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Send text messages</h1>
@@ -46,8 +44,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="button" value="Continue" />
</p>
</div>
</div>
</form>
{% endblock %}

View File

@@ -1,20 +1,17 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | Service settings
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Service settings</h1>
<p>Here's where users can update their service profile.</p>
<p><a href="dashboard">Back to dashboard</a></p>
</div>
</div>
{% endblock %}

View File

@@ -1,20 +1,16 @@
{% extends "admin_template.html" %}
{% extends "withnav_template.html" %}
{% block page_title %}
GOV.UK Notify | User settings
{% endblock %}
{% block content %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">User profile</h1>
<p>Here's where users can update their profile, password etc.</p>
<p><a href="dashboard">Back to dashboard</a></p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends "admin_template.html" %}
{% from "main_nav.html" import main_nav with context %}
{% block content %}
<div class="grid-row">
<div class="column-third">
{{ main_nav() }}
</div>
<div class="column-two-thirds">
{% block maincolumn_content %}{% endblock %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,42 @@
"""empty message
Revision ID: 60_add_service
Revises: 50_alter_verify_code_type
Create Date: 2015-12-15 09:25:09.000431
"""
# revision identifiers, used by Alembic.
revision = '60_add_service'
down_revision = '50_alter_verify_code_type'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('services',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('active', sa.Boolean(), nullable=False),
sa.Column('limit', sa.BigInteger(), nullable=False),
sa.Column('restricted', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('user_to_service',
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('service_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], )
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('user_to_service')
op.drop_table('services')
### end Alembic commands ###

View File

@@ -0,0 +1,62 @@
import pytest
import sqlalchemy
from app.main.dao import services_dao
from tests.app.main import create_test_user
def test_can_insert_and_retrieve_new_service(notifications_admin, notifications_admin_db, notify_db_session):
user = create_test_user()
id = services_dao.insert_new_service('testing service', user)
saved_service = services_dao.get_service_by_id(id)
assert id == saved_service.id
assert saved_service.users == [user]
assert saved_service.name == 'testing service'
def test_unrestrict_service_updates_the_service(notifications_admin, notifications_admin_db, notify_db_session):
user = create_test_user()
id = services_dao.insert_new_service('unrestricted service', user)
saved_service = services_dao.get_service_by_id(id)
assert saved_service.restricted is True
services_dao.unrestrict_service(id)
unrestricted_service = services_dao.get_service_by_id(id)
assert unrestricted_service.restricted is False
def test_activate_service_update_service(notifications_admin, notifications_admin_db, notify_db_session):
user = create_test_user()
id = services_dao.insert_new_service('activated service', user)
service = services_dao.get_service_by_id(id)
assert service.active is False
services_dao.activate_service(id)
activated_service = services_dao.get_service_by_id(id)
assert activated_service.active is True
def test_get_service_returns_none_if_service_does_not_exist(notifications_admin,
notifications_admin_db,
notify_db_session):
service = services_dao.get_service_by_id(1)
assert service is None
def test_find_by_service_name_returns_right_service(notifications_admin,
notifications_admin_db,
notify_db_session):
user = create_test_user()
id = services_dao.insert_new_service('testing service', user)
another = services_dao.insert_new_service('Testing the Service', user)
found = services_dao.find_service_by_service_name('testing service')
assert found.id == id
assert found.name == 'testing service'
found_another = services_dao.find_service_by_service_name('Testing the Service')
assert found_another == services_dao.get_service_by_id(another)
def test_should_not_allow_two_services_of_the_same_name(notifications_admin, notifications_admin_db, notify_db_session):
user = create_test_user()
services_dao.insert_new_service('duplicate service', user)
with pytest.raises(sqlalchemy.exc.IntegrityError) as error:
services_dao.insert_new_service('duplicate service', user)
assert 'duplicate key value violates unique constraint "services_name_key' in error.value

View File

@@ -0,0 +1,18 @@
from app.main.dao import services_dao
from app.main.forms import AddServiceForm
from tests.app.main import create_test_user
def test_form_should_have_errors_when_duplicate_service_is_added(notifications_admin,
notifications_admin_db,
notify_db_session):
with notifications_admin.test_request_context(method='POST',
data={'service_name': 'some service'}) as req:
user = create_test_user()
services_dao.insert_new_service('some service', user)
req.session['user_id'] = user.id
form = AddServiceForm(req.request.form)
assert form.validate() is False
assert len(form.errors) == 1
expected = {'service_name': ['Duplicate service name']}
assert form.errors == expected

View File

@@ -0,0 +1,42 @@
from app.main.dao import verify_codes_dao, services_dao
from tests.app.main import create_test_user
def test_get_should_render_add_service_template(notifications_admin, notifications_admin_db, notify_db_session):
with notifications_admin.test_client() as client:
with client.session_transaction() as session:
user = create_test_user()
session['user_id'] = user.id
verify_codes_dao.add_code(user_id=user.id, code='12345', code_type='sms')
client.post('/two-factor', data={'sms_code': '12345'})
response = client.get('/add-service')
assert response.status_code == 200
assert 'Set up notifications for your service' in response.get_data(as_text=True)
def test_should_add_service_and_redirect_to_next_page(notifications_admin, notifications_admin_db, notify_db_session):
with notifications_admin.test_client() as client:
with client.session_transaction() as session:
user = create_test_user()
session['user_id'] = user.id
verify_codes_dao.add_code(user_id=user.id, code='12345', code_type='sms')
client.post('/two-factor', data={'sms_code': '12345'})
response = client.post('/add-service', data={'service_name': 'testing the post'})
assert response.status_code == 302
assert response.location == 'http://localhost/dashboard'
saved_service = services_dao.find_service_by_service_name('testing the post')
assert saved_service is not None
def test_should_return_form_errors_when_service_name_is_empty(notifications_admin,
notifications_admin_db,
notify_db_session):
with notifications_admin.test_client() as client:
with client.session_transaction() as session:
user = create_test_user()
session['user_id'] = user.id
verify_codes_dao.add_code(user_id=user.id, code='12345', code_type='sms')
client.post('/two-factor', data={'sms_code': '12345'})
response = client.post('/add-service', data={})
assert response.status_code == 400
assert 'Please enter your service name' in response.get_data(as_text=True)