diff --git a/app/main/__init__.py b/app/main/__init__.py index 2f617456e..4025b6dd1 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -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 diff --git a/app/main/dao/services_dao.py b/app/main/dao/services_dao.py new file mode 100644 index 000000000..9881d6689 --- /dev/null +++ b/app/main/dao/services_dao.py @@ -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() diff --git a/app/main/forms.py b/app/main/forms.py index 8d94fdaf7..300b2600f 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -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 diff --git a/app/main/views/add_service.py b/app/main/views/add_service.py new file mode 100644 index 000000000..4d03be4f8 --- /dev/null +++ b/app/main/views/add_service.py @@ -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 diff --git a/app/main/views/index.py b/app/main/views/index.py index f1525d4cd..485b4d2d6 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -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') diff --git a/app/models.py b/app/models.py index cb99c26a0..d420c9070 100644 --- a/app/models.py +++ b/app/models.py @@ -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()) diff --git a/app/templates/views/add-service.html b/app/templates/views/add-service.html index 130247a0d..3b50f3371 100644 --- a/app/templates/views/add-service.html +++ b/app/templates/views/add-service.html @@ -16,15 +16,17 @@ GOV.UK Notify | Set up service
  • as your email sender name
  • -

    - -
    - For example, 'Vehicle tax' or 'Carer's allowance' -

    +
    + {{ form.hidden_tag() }} + + {{ form.service_name(class="form-control-2-3", autocomplete="off") }}
    + For example, 'Vehicle tax' or 'Carer's allowance' -

    - Continue -

    +

    + +

    +
    + diff --git a/migrations/versions/60_add_service.py b/migrations/versions/60_add_service.py new file mode 100644 index 000000000..5dd28284d --- /dev/null +++ b/migrations/versions/60_add_service.py @@ -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 ### diff --git a/tests/app/main/dao/test_service_dao.py b/tests/app/main/dao/test_service_dao.py new file mode 100644 index 000000000..e8918eb1f --- /dev/null +++ b/tests/app/main/dao/test_service_dao.py @@ -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 diff --git a/tests/app/main/test_add_service_form.py b/tests/app/main/test_add_service_form.py new file mode 100644 index 000000000..87f460c22 --- /dev/null +++ b/tests/app/main/test_add_service_form.py @@ -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 diff --git a/tests/app/main/views/test_add_service.py b/tests/app/main/views/test_add_service.py new file mode 100644 index 000000000..8ac5755d4 --- /dev/null +++ b/tests/app/main/views/test_add_service.py @@ -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)