From 17df983c6aa53302114eb27b957ce0041a80e2db Mon Sep 17 00:00:00 2001 From: Viktor Roytman Date: Mon, 6 Feb 2017 11:41:43 -0500 Subject: [PATCH] Squashed commit of the following: commit 5d2e2717769266e2c2be024637967db9dc9647de Author: Viktor Roytman Date: Mon Feb 6 11:40:16 2017 -0500 Add customer (list/add/remove/write ID card) commit c1cc6f1c2eedd3fc38351bb472be7dadb6583715 Author: Viktor Roytman Date: Mon Feb 6 11:33:50 2017 -0500 Add dummy write ID card buttons for vendors commit c86604b08124b1ac04324380188699e6b28e2ce5 Author: Viktor Roytman Date: Fri Feb 3 16:48:11 2017 -0500 Add tests for vendors page commit d0f35a05ee31e9658d8b68ce1d06b02448f8857a Author: Viktor Roytman Date: Fri Feb 3 16:40:53 2017 -0500 Add tests for vendors commit afe1b3c215ae74f136ea0e43d84476a99e5795ac Author: Viktor Roytman Date: Fri Feb 3 15:37:35 2017 -0500 Add vendors commit bbc5d2128719f8da9d028acca3cd1f239aaf231e Author: Viktor Roytman Date: Fri Feb 3 12:33:23 2017 -0500 Add dummy write credit card functionality commit 828a4894d0085dd47d7768e6a538120bed764974 Author: Viktor Roytman Date: Thu Feb 2 16:49:07 2017 -0500 Add work-in-progress minigrid-specific functionality commit 8935da32c4966928a6005b84dd494c3ce294c5ba Author: Viktor Roytman Date: Thu Feb 2 15:54:04 2017 -0500 Add dummy page for reading and validating cards commit a549fe21c867715202b639591e520c15e1aac4ee Author: Viktor Roytman Date: Thu Feb 2 15:42:02 2017 -0500 Add dummy device management page commit 6851dc7792e5c95cf696aab4a4ee5399ade47a69 Author: Viktor Roytman Date: Thu Feb 2 15:11:31 2017 -0500 Add dummy aggregate data plots commit a6a1921298ceec4279cc28147e3eaa03cb71d062 Author: Viktor Roytman Date: Thu Feb 2 12:15:13 2017 -0500 Add dummy page for writing technician ID card commit c64b0dc23ac1db6a110f5185a0a31b77c2cff5b6 Author: Viktor Roytman Date: Thu Feb 2 11:57:06 2017 -0500 Minor changes commit 61377b236bc31dd33682492b60dfe83d3d3ed9bb Author: Viktor Roytman Date: Thu Feb 2 11:53:49 2017 -0500 Add page for writing technician ID card commit 1f80e85a01b09aa9d68a1cd33d7899dbfdd226dc Author: Viktor Roytman Date: Thu Feb 2 11:36:37 2017 -0500 Add tests for create minigrid functionality commit cf1aa53e3951ffe7fd616b61769c485fd891da39 Author: Viktor Roytman Date: Thu Feb 2 11:11:45 2017 -0500 Change reason to message in templates commit bab53deefce666709444f3111dc8381369156c8b Author: Viktor Roytman Date: Thu Feb 2 10:54:10 2017 -0500 Fix tests commit d332cfde9b50508f29c95029af79b058d09fc66b Author: Viktor Roytman Date: Thu Feb 2 10:46:38 2017 -0500 Temporary workaround for Travis commit dbf2c20ff38714dbe5877d6f7da545d9de749554 Author: Viktor Roytman Date: Wed Feb 1 16:45:16 2017 -0500 Add minigrid creation Still need tests commit 183b88534d5aa4c2f766a6150cd23cd47995e282 Author: Viktor Roytman Date: Wed Feb 1 16:45:05 2017 -0500 Update version number --- .travis.yml | 4 +- minigrid/handlers.py | 222 +++++++-- minigrid/models.py | 69 ++- minigrid/templates/cards.html | 13 + minigrid/templates/device.html | 13 + minigrid/templates/index-logged-out.html | 4 +- minigrid/templates/index-minigrid-list.html | 28 +- minigrid/templates/minigrid.html | 13 +- minigrid/templates/minigrid_customers.html | 44 ++ minigrid/templates/minigrid_vendors.html | 44 ++ minigrid/templates/minigrid_write_credit.html | 18 + minigrid/templates/tariff_form.html | 2 +- minigrid/templates/tariffs.html | 4 +- minigrid/templates/technician.html | 13 + minigrid/templates/users.html | 4 +- prod/docker-compose.yml | 2 +- prod/install.sh | 6 +- tests/python/coverage_run.sh | 1 + tests/python/test_handlers.py | 432 +++++++++++++++++- tests/python/test_models.py | 28 ++ 20 files changed, 903 insertions(+), 61 deletions(-) create mode 100644 minigrid/templates/cards.html create mode 100644 minigrid/templates/device.html create mode 100644 minigrid/templates/minigrid_customers.html create mode 100644 minigrid/templates/minigrid_vendors.html create mode 100644 minigrid/templates/minigrid_write_credit.html create mode 100644 minigrid/templates/technician.html diff --git a/.travis.yml b/.travis.yml index efa97fb..65033d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,10 @@ python: - "3.6" cache: pip addons: - postgresql: "9.4" + postgresql: "9.5" firefox: "49.0" +dist: trusty +sudo: false before_install: - pip install -r dev/requirements.txt diff --git a/minigrid/handlers.py b/minigrid/handlers.py index f31dbd2..9c62d0f 100644 --- a/minigrid/handlers.py +++ b/minigrid/handlers.py @@ -5,7 +5,7 @@ from sqlalchemy.dialects.postgresql import insert from sqlalchemy.exc import DataError, IntegrityError -from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.orm.exc import NoResultFound, UnmappedInstanceError import tornado.web @@ -41,12 +41,18 @@ def write_error(self, status_code, **kwargs): error = kwargs['exc_info'][1] if isinstance(error, minigrid.error.MinigridHTTPError): self.set_status(error.status_code, reason=error.reason) + message = getattr(error, 'message', error.reason) self.render( - error.template_name, reason=error.reason, - **error.template_kwargs) + error.template_name, message=message, **error.template_kwargs) return super().write_error(status_code, **kwargs) + def render(self, *args, **kwargs): + """Override default render to include a message of None.""" + if 'message' not in kwargs: + kwargs['message'] = None + super().render(*args, **kwargs) + class MainHandler(BaseHandler): """Handlers for the site index.""" @@ -55,13 +61,11 @@ def get(self): """Render the homepage.""" if self.current_user: system = self.session.query(models.System).one_or_none() - minigrids = ( - self.session - .query(models.Minigrid).order_by(models.Minigrid.name)) + minigrids = models.get_minigrids(self.session) self.render( 'index-minigrid-list.html', system=system, minigrids=minigrids) return - self.render('index-logged-out.html', reason=None) + self.render('index-logged-out.html') def post(self): """Send login information to the portier broker.""" @@ -93,7 +97,7 @@ def get(self): except NoResultFound: self.redirect('/') return - self.render('tariffs.html', system=system, reason=None) + self.render('tariffs.html', system=system) @tornado.web.authenticated def post(self): @@ -112,7 +116,7 @@ def post(self): try: with models.transaction(self.session) as session: session.execute(statement) - reason = 'Updated tariff information' + message = 'Updated tariff information' except (IntegrityError, DataError) as error: if 'minigrid_system_check' in error.orig.pgerror: message = ( @@ -128,19 +132,50 @@ def post(self): message = ' '.join(error.orig.pgerror.split()) raise minigrid.error.MinigridHTTPError( message, 400, 'tariffs.html', system=self.system) - self.set_status(200) self.render( 'tariffs.html', system=self.session.query(models.System).one_or_none(), - reason=reason) + message=message) + + +class MinigridsHandler(BaseHandler): + """Handlers for minigrids.""" + + @tornado.web.authenticated + def get(self): + """Redirect to index.""" + self.redirect('/') + + @tornado.web.authenticated + def post(self): + """Create a new minigrid model.""" + try: + with models.transaction(self.session) as session: + session.add(models.Minigrid( + minigrid_name=self.get_argument('minigrid_name'), + aes_key=self.get_argument('minigrid_aes_key'))) + except (IntegrityError, DataError) as error: + if 'minigrid_name_key' in error.orig.pgerror: + message = 'A minigrid with that name already exists' + else: + message = ' '.join(error.orig.pgerror.split()) + raise minigrid.error.MinigridHTTPError( + message, 400, 'index-minigrid-list.html', + system=self.session.query(models.System).one_or_none(), + minigrids=models.get_minigrids(session)) + self.set_status(201) + self.render( + 'index-minigrid-list.html', + system=self.session.query(models.System).one_or_none(), + minigrids=models.get_minigrids(session)) class UsersHandler(BaseHandler): """Handlers for user management.""" - def _render_users(self, reason=None): + def _render_users(self, message=None): users = self.session.query(models.User).order_by('email') - self.render('users.html', users=users, reason=reason) + self.render('users.html', users=users, message=message) @tornado.web.authenticated def get(self): @@ -151,16 +186,46 @@ def get(self): def post(self): """Create a new user model.""" email = self.get_argument('email') - reason = None + message = None try: with models.transaction(self.session) as session: session.add(models.User(email=email)) except IntegrityError as error: if 'user_email_check' in error.orig.pgerror: - reason = f'{email} is not a valid e-mail address' + message = f'{email} is not a valid e-mail address' else: - reason = f'Account for {email} already exists' - self._render_users(reason=reason) + message = f'Account for {email} already exists' + self._render_users(message=message) + + +# TODO: this button should do something +class TechnicianHandler(BaseHandler): + """Handlers for technician view.""" + + @tornado.web.authenticated + def get(self): + """Render the technician ID writing form.""" + self.render('technician.html') + + +# TODO: this button should do something +class DeviceHandler(BaseHandler): + """Handlers for device view.""" + + @tornado.web.authenticated + def get(self): + """Render the device form.""" + self.render('device.html') + + +# TODO: this button should do something +class CardsHandler(BaseHandler): + """Handlers for cards view.""" + + @tornado.web.authenticated + def get(self): + """Render the cards form.""" + self.render('cards.html') class MinigridHandler(BaseHandler): @@ -169,15 +234,111 @@ class MinigridHandler(BaseHandler): @tornado.web.authenticated def get(self, minigrid_id): """Render the view for a minigrid record.""" - try: - minigrid = ( - self.session - .query(models.Minigrid) - .filter_by(minigrid_id=minigrid_id) - .one()) - except (NoResultFound, DataError): - raise tornado.web.HTTPError(404) - self.render('minigrid.html', minigrid=minigrid) + self.render( + 'minigrid.html', + minigrid=models.get_minigrid(self.session, minigrid_id)) + + +# TODO: this button should do something +class MinigridWriteCreditHandler(BaseHandler): + """Handlers for writing credit cards view.""" + + @tornado.web.authenticated + def get(self, minigrid_id): + """Render the write credit card form.""" + self.render( + 'minigrid_write_credit.html', + minigrid=models.get_minigrid(self.session, minigrid_id)) + + +class MinigridVendorsHandler(BaseHandler): + """Handlers for vendors view.""" + + @tornado.web.authenticated + def get(self, minigrid_id): + """Render the vendors view.""" + self.render( + 'minigrid_vendors.html', + minigrid=models.get_minigrid(self.session, minigrid_id)) + + @tornado.web.authenticated + def post(self, minigrid_id): + """Add a vendor.""" + grid = models.get_minigrid(self.session, minigrid_id) + action = self.get_argument('action') + if action == 'create': + try: + with models.transaction(self.session) as session: + grid.vendors.append(models.Vendor( + vendor_name=self.get_argument('vendor_name'))) + except (IntegrityError, DataError) as error: + if 'vendor_name_key' in error.orig.pgerror: + message = 'A vendor with that name already exists' + else: + message = ' '.join(error.orig.pgerror.split()) + raise minigrid.error.MinigridHTTPError( + message, 400, 'minigrid_vendors.html', minigrid=grid) + self.set_status(201) + elif action == 'remove': + vendor_id = self.get_argument('vendor_id') + try: + with models.transaction(self.session) as session: + vendor = session.query(models.Vendor).get(vendor_id) + session.delete(vendor) + message = f'Vendor {vendor.vendor_name} removed' + except UnmappedInstanceError: + message = 'The requested vendor no longer exists' + self.render( + 'minigrid_vendors.html', minigrid=grid, message=message) + return + else: + raise tornado.web.HTTPError(400, 'Bad Request (invalid action)') + self.render('minigrid_vendors.html', minigrid=grid) + + +class MinigridCustomersHandler(BaseHandler): + """Handlers for customers view.""" + + @tornado.web.authenticated + def get(self, minigrid_id): + """Render the customers view.""" + self.render( + 'minigrid_customers.html', + minigrid=models.get_minigrid(self.session, minigrid_id)) + + @tornado.web.authenticated + def post(self, minigrid_id): + """Add a customer.""" + grid = models.get_minigrid(self.session, minigrid_id) + action = self.get_argument('action') + if action == 'create': + try: + with models.transaction(self.session) as session: + grid.customers.append(models.Customer( + customer_name=self.get_argument('customer_name'))) + except (IntegrityError, DataError) as error: + if 'customer_name_key' in error.orig.pgerror: + message = 'A customer with that name already exists' + else: + message = ' '.join(error.orig.pgerror.split()) + raise minigrid.error.MinigridHTTPError( + message, 400, 'minigrid_customers.html', minigrid=grid) + self.set_status(201) + elif action == 'remove': + customer_id = self.get_argument('customer_id') + try: + with models.transaction(self.session) as session: + customer = session.query(models.Customer).get(customer_id) + session.delete(customer) + message = f'Customer {customer.customer_name} removed' + except UnmappedInstanceError: + message = 'The requested customer no longer exists' + self.render( + 'minigrid_customers.html', minigrid=grid, message=message) + return + else: + raise tornado.web.HTTPError(400, 'Bad Request (invalid action)') + self.render('minigrid_customers.html', minigrid=grid) class VerifyLoginHandler(BaseHandler): @@ -230,8 +391,15 @@ def post(self): application_urls = [ (r'/', MainHandler), - (r'/minigrid/(.+)?', MinigridHandler), + (r'/minigrids/(.{36})/?', MinigridHandler), + (r'/minigrids/(.{36})/vendors/?', MinigridVendorsHandler), + (r'/minigrids/(.{36})/customers/?', MinigridCustomersHandler), + (r'/minigrids/(.{36})/write_credit/?', MinigridWriteCreditHandler), (r'/tariffs/?', TariffsHandler), + (r'/minigrids/?', MinigridsHandler), (r'/users/?', UsersHandler), + (r'/technician/?', TechnicianHandler), + (r'/device/?', DeviceHandler), + (r'/cards/?', CardsHandler), (r'/verify/?', VerifyLoginHandler), (r'/logout/?', LogoutHandler)] diff --git a/minigrid/models.py b/minigrid/models.py index fea708d..7806bf3 100644 --- a/minigrid/models.py +++ b/minigrid/models.py @@ -3,9 +3,14 @@ import sqlalchemy as sa from sqlalchemy.dialects import postgresql as pg +from sqlalchemy.exc import DataError from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship +from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.sql import func +import tornado.web + from minigrid.options import options @@ -52,6 +57,12 @@ def pk(): pg.UUID, primary_key=True, server_default=func.uuid_generate_v4()) +def fk(foreign_column): + """Return a foreign key.""" + return sa.Column( + pg.UUID, sa.ForeignKey(foreign_column)) + + def json_column(column_name, default=None): """Return a JSONB column that is a dictionary at the top level.""" return sa.Column( @@ -61,6 +72,24 @@ def json_column(column_name, default=None): server_default=default) +def get_minigrids(session): + """Return the minigrids ordered by name.""" + return session.query(Minigrid).order_by(Minigrid.minigrid_name) + + +def get_minigrid(session, minigrid_id, exception=tornado.web.HTTPError(404)): + """Return a minigrid by ID, if it exists.""" + try: + with transaction(session) as tx_session: + return ( + tx_session.query(Minigrid) + .filter_by(minigrid_id=minigrid_id).one()) + except (NoResultFound, DataError): + if exception is None: + raise + raise exception + + class User(Base): """The model for a registered user.""" @@ -103,8 +132,44 @@ class Minigrid(Base): __tablename__ = 'minigrid' minigrid_id = pk() - name = sa.Column( - pg.TEXT, sa.CheckConstraint("name != ''"), + minigrid_name = sa.Column( + pg.TEXT, sa.CheckConstraint("minigrid_name != ''"), nullable=False, unique=True) + aes_key = sa.Column( + pg.TEXT, sa.CheckConstraint("aes_key != ''"), + nullable=False) error_code = json_column('error_code', default='{}') status = json_column('status', default='{}') + + vendors = relationship( + 'Vendor', backref='minigrid', order_by='Vendor.vendor_name') + customers = relationship( + 'Customer', backref='minigrid', order_by='Customer.customer_name') + + +class Vendor(Base): + """The model for a Vendor.""" + + __tablename__ = 'vendor' + vendor_id = pk() + vendor_minigrid_id = fk('minigrid.minigrid_id') + vendor_name = sa.Column( + pg.TEXT, sa.CheckConstraint("vendor_name != ''"), + nullable=False) + + __table_args__ = ( + sa.UniqueConstraint('vendor_minigrid_id', 'vendor_name'),) + + +class Customer(Base): + """The model for a customer.""" + + __tablename__ = 'customer' + customer_id = pk() + customer_minigrid_id = fk('minigrid.minigrid_id') + customer_name = sa.Column( + pg.TEXT, sa.CheckConstraint("customer_name != ''"), + nullable=False) + + __table_args__ = ( + sa.UniqueConstraint('customer_minigrid_id', 'customer_name'),) diff --git a/minigrid/templates/cards.html b/minigrid/templates/cards.html new file mode 100644 index 0000000..da05e16 --- /dev/null +++ b/minigrid/templates/cards.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block body %} + {% if message is not None %} +

{{ message }}

+ {% end %} +

Read and validate cards:

+
+ {% module xsrf_form_html() %} + +
+ +{% end %} diff --git a/minigrid/templates/device.html b/minigrid/templates/device.html new file mode 100644 index 0000000..ccbd9c3 --- /dev/null +++ b/minigrid/templates/device.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block body %} + {% if message is not None %} +

{{ message }}

+ {% end %} +

Add device:

+
+ {% module xsrf_form_html() %} + +
+ +{% end %} diff --git a/minigrid/templates/index-logged-out.html b/minigrid/templates/index-logged-out.html index b09f7ce..1d38dbb 100644 --- a/minigrid/templates/index-logged-out.html +++ b/minigrid/templates/index-logged-out.html @@ -1,8 +1,8 @@ {% extends 'base.html' %} {% block body %} - {% if reason is not None %} -

Login unsuccessful: {{ reason }}

+ {% if message is not None %} +

Login unsuccessful: {{ message }}

{% end %}
{% module xsrf_form_html() %} diff --git a/minigrid/templates/index-minigrid-list.html b/minigrid/templates/index-minigrid-list.html index 50a91e0..d700b75 100644 --- a/minigrid/templates/index-minigrid-list.html +++ b/minigrid/templates/index-minigrid-list.html @@ -10,18 +10,30 @@
{% else %}

Manage tariffs »

+

Write technician ID card »

+

Manage devices »

+

Read and validate cards »

+ +

Minigrids:

    + {% set zero_minigrids = True %} {% for grid in minigrids %} -
  • -

    {{ grid.name }} »

    -

    Status: {{ grid.status }}

    -
  • - {% else %} -

    No minigrids

    + {% set zero_minigrids = False %} +
  • +

    {{ grid.minigrid_name }} »

    +

    Status: {{ grid.status }}

    +
  • {% end %}
+ {% if zero_minigrids %} +

No minigrids

+ {% end %} +
+ {% if message is not None %} +

Could not create minigrid: {{ message }}

+ {% end %}

Add minigrid:

{% module xsrf_form_html() %} @@ -29,5 +41,9 @@
+
+ +

Aggregate data plots:

+

TODO: What data? From where?

{% end %} {% end %} diff --git a/minigrid/templates/minigrid.html b/minigrid/templates/minigrid.html index 7826470..dafac6e 100644 --- a/minigrid/templates/minigrid.html +++ b/minigrid/templates/minigrid.html @@ -1,9 +1,20 @@ {% extends 'base.html' %} {% block body %} -

Minigrid Name: {{ minigrid.name }}

+

Minigrid Name: {{ minigrid.minigrid_name }}

ID: {{ minigrid.minigrid_id }}

Error code: {{ minigrid.error_code }}

Status: {{ minigrid.status }}

+
+ +

Vendor list »

+

Customer list »

+

Write credit card »

+
+ +

Data:

+

TODO: What data? From where?

+
+ {% end %} diff --git a/minigrid/templates/minigrid_customers.html b/minigrid/templates/minigrid_customers.html new file mode 100644 index 0000000..13ea79b --- /dev/null +++ b/minigrid/templates/minigrid_customers.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} + +{% block body %} + {% if message is not None %} +

{{ message }}

+ {% end %} +

Minigrid Name: {{ minigrid.minigrid_name }}

+

ID: {{ minigrid.minigrid_id }}

+

Customers:

+
    + {% set zero_customers = True %} + {% for customer in minigrid.customers %} + {% set zero_customers = False %} +
  • +

    Name: {{ customer.customer_name }}

    +
    + {% module xsrf_form_html() %} + +
    +
    + {% module xsrf_form_html() %} + +
    +
  • + {% end %} +
+ {% if zero_customers %} +

No customers

+ {% end %} +
+ +

Add customer:

+
+ {% module xsrf_form_html() %} +
+ +
+
+ + +{% end %} diff --git a/minigrid/templates/minigrid_vendors.html b/minigrid/templates/minigrid_vendors.html new file mode 100644 index 0000000..dee52af --- /dev/null +++ b/minigrid/templates/minigrid_vendors.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} + +{% block body %} + {% if message is not None %} +

{{ message }}

+ {% end %} +

Minigrid Name: {{ minigrid.minigrid_name }}

+

ID: {{ minigrid.minigrid_id }}

+

Vendors:

+
    + {% set zero_vendors = True %} + {% for vendor in minigrid.vendors %} + {% set zero_vendors = False %} +
  • +

    Name: {{ vendor.vendor_name }}

    +
    + {% module xsrf_form_html() %} + +
    +
    + {% module xsrf_form_html() %} + +
    +
  • + {% end %} +
+ {% if zero_vendors %} +

No vendors

+ {% end %} +
+ +

Add vendor:

+
+ {% module xsrf_form_html() %} +
+ +
+
+ + +{% end %} diff --git a/minigrid/templates/minigrid_write_credit.html b/minigrid/templates/minigrid_write_credit.html new file mode 100644 index 0000000..721c0b1 --- /dev/null +++ b/minigrid/templates/minigrid_write_credit.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block body %} + {% if message is not None %} +

{{ message }}

+ {% end %} +

Minigrid Name: {{ minigrid.minigrid_name }}

+

ID: {{ minigrid.minigrid_id }}

+

Write credit card:

+
+ {% module xsrf_form_html() %} + +
+ +{% end %} diff --git a/minigrid/templates/tariff_form.html b/minigrid/templates/tariff_form.html index 87c546f..cf4b2c8 100644 --- a/minigrid/templates/tariff_form.html +++ b/minigrid/templates/tariff_form.html @@ -2,4 +2,4 @@


- + diff --git a/minigrid/templates/tariffs.html b/minigrid/templates/tariffs.html index 8214e81..f93716d 100644 --- a/minigrid/templates/tariffs.html +++ b/minigrid/templates/tariffs.html @@ -1,8 +1,8 @@ {% extends 'base.html' %} {% block body %} - {% if reason is not None %} -

{{ reason }}

+ {% if message is not None %} +

{{ message }}

{% end %}

Tariff information:

diff --git a/minigrid/templates/technician.html b/minigrid/templates/technician.html new file mode 100644 index 0000000..cfaee32 --- /dev/null +++ b/minigrid/templates/technician.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block body %} + {% if message is not None %} +

{{ message }}

+ {% end %} +

Write technician ID card:

+ + {% module xsrf_form_html() %} + +
+ +{% end %} diff --git a/minigrid/templates/users.html b/minigrid/templates/users.html index 581ef8b..a1f49bd 100644 --- a/minigrid/templates/users.html +++ b/minigrid/templates/users.html @@ -15,8 +15,8 @@ - {% if reason is not None %} -

Could not create user account: {{ reason }}

+ {% if message is not None %} +

Could not create user account: {{ message }}

{% end %} {% end %} diff --git a/prod/docker-compose.yml b/prod/docker-compose.yml index 9b0b9f7..6e1dd2e 100644 --- a/prod/docker-compose.yml +++ b/prod/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: minigrid: - image: selcolumbia/minigrid-server:0.1.3 + image: selcolumbia/minigrid-server:0.1.4 command: ./prod/run.sh --db_host=db --redis_url=redis://redis:6379/0 --minigrid-website-url=https://www.example.com depends_on: - redis diff --git a/prod/install.sh b/prod/install.sh index 33a3bc4..5f9a858 100755 --- a/prod/install.sh +++ b/prod/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# Minigrid Server installer for version 0.1.3 +# Minigrid Server installer for version 0.1.4 set -e # Do you have docker installed? @@ -108,8 +108,8 @@ $SUDO openssl dhparam -out /etc/letsencrypt/live/$LETSENCRYPT_DIR/dhparam.pem 20 printf "========================================\n" printf " Generating configuration \n" printf "========================================\n" -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.3/prod/docker-compose.yml > docker-compose.yml -$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.3/prod/nginx.conf > nginx.conf +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.4/prod/docker-compose.yml > docker-compose.yml +$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.4/prod/nginx.conf > nginx.conf sed -i s/www.example.com/$LETSENCRYPT_DIR/g docker-compose.yml sed -i s/www.example.com/$LETSENCRYPT_DIR/g nginx.conf diff --git a/tests/python/coverage_run.sh b/tests/python/coverage_run.sh index 3313031..9b057c7 100755 --- a/tests/python/coverage_run.sh +++ b/tests/python/coverage_run.sh @@ -1,5 +1,6 @@ #!/usr/bin/env sh set -e +bash -c "psql -d minigrid -c 'drop schema if exists minigrid_test cascade;' -U postgres 1&>/dev/null" coverage erase coverage run --source=minigrid,server.py --branch -m unittest ${@:-discover tests.python} coverage html diff --git a/tests/python/test_handlers.py b/tests/python/test_handlers.py index 1b62751..a90fc50 100644 --- a/tests/python/test_handlers.py +++ b/tests/python/test_handlers.py @@ -81,8 +81,8 @@ def setUp(self): session.add(self.user) session.add(models.System(day_tariff=1, night_tariff=1)) self.minigrids = ( - models.Minigrid(name='a'), - models.Minigrid(name='b'), + models.Minigrid(minigrid_name='a', aes_key='a'), + models.Minigrid(minigrid_name='b', aes_key='a'), ) session.add_all(self.minigrids) @@ -108,12 +108,13 @@ def test_get_logged_in(self, get_current_user): self.assertEqual(len(minigrids), 2) self.assertEqual( minigrids[0].a['href'], - '/minigrid/' + self.minigrids[0].minigrid_id, + '/minigrids/' + self.minigrids[0].minigrid_id, ) - self.assertEqual(minigrids[0].a.text, self.minigrids[0].name + ' »') + self.assertEqual( + minigrids[0].a.text, self.minigrids[0].minigrid_name + ' »') -class TestMinigridView(HTTPTest): +class TestMinigridHandler(HTTPTest): def setUp(self): super().setUp() with models.transaction(self.session) as session: @@ -121,14 +122,14 @@ def setUp(self): session.add(self.user) session.add(models.System(day_tariff=1, night_tariff=1)) self.minigrids = ( - models.Minigrid(name='a'), - models.Minigrid(name='b'), + models.Minigrid(minigrid_name='a', aes_key='a'), + models.Minigrid(minigrid_name='b', aes_key='a'), ) session.add_all(self.minigrids) def test_get_not_logged_in(self): response = self.fetch( - '/minigrid/' + self.minigrids[0].minigrid_id, + '/minigrids/' + self.minigrids[0].minigrid_id, follow_redirects=False, ) self.assertResponseCode(response, 302) @@ -137,26 +138,26 @@ def test_get_not_logged_in(self): def test_get_malformed_id(self, get_current_user): get_current_user.return_value = self.user with ExpectLog('tornado.access', '404'): - response = self.fetch('/minigrid/' + 'nope') + response = self.fetch('/minigrids/' + 'nope') self.assertResponseCode(response, 404) @patch('minigrid.handlers.BaseHandler.get_current_user') def test_get_nonexistent_id(self, get_current_user): get_current_user.return_value = self.user with ExpectLog('tornado.access', '404'): - response = self.fetch('/minigrid/' + str(uuid.uuid4())) + response = self.fetch('/minigrids/' + str(uuid.uuid4())) self.assertResponseCode(response, 404) @patch('minigrid.handlers.BaseHandler.get_current_user') def test_get_success(self, get_current_user): get_current_user.return_value = self.user - response = self.fetch('/minigrid/' + self.minigrids[0].minigrid_id) + response = self.fetch('/minigrids/' + self.minigrids[0].minigrid_id) self.assertResponseCode(response, 200) body = BeautifulSoup(response.body) self.assertIn('Minigrid Name: a', body.h1) -class TestUsersView(HTTPTest): +class TestUsersHandler(HTTPTest): def setUp(self): super().setUp() with models.transaction(self.session) as session: @@ -217,7 +218,7 @@ def test_post_success(self, get_current_user): self.session.query(models.User).filter_by(email='ba@a.com').one()) -class TestTariffsView(HTTPTest): +class TestTariffsHandler(HTTPTest): def setUp(self): super().setUp() with models.transaction(self.session) as session: @@ -352,6 +353,411 @@ class FakePGError: self.assertIn('This should show up', response.body.decode()) +class TestMinigridsHandler(HTTPTest): + def setUp(self): + super().setUp() + with models.transaction(self.session) as session: + self.user = models.User(email='a@a.com') + session.add(self.user) + session.add(models.System(day_tariff=1, night_tariff=1)) + + def test_get_minigrids_not_logged_in(self): + response = self.fetch('/minigrids') + self.assertResponseCode(response, 200) + self.assertNotIn('user', response.headers['Set-Cookie']) + self.assertIn('Log In', response.body.decode()) + self.assertNotIn('Log Out', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_minigrids_logged_in_redirect(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch('/minigrids', follow_redirects=False) + self.assertResponseCode(response, 302) + self.assertEqual(response.headers['Location'], '/') + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_minigrids_logged_in_result(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch('/minigrids') + self.assertIn('No minigrids', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_post_minigrids_success(self, get_current_user): + get_current_user.return_value = self.user + self.assertIsNone(self.session.query(models.Minigrid).one_or_none()) + response = self.fetch( + '/minigrids?minigrid_name=a&minigrid_aes_key=a', + method='POST', body='') + self.assertResponseCode(response, 201) + minigrid = self.session.query(models.Minigrid).one() + self.assertEqual(minigrid.minigrid_name, 'a') + self.assertEqual(minigrid.aes_key, 'a') + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_post_minigrids_missing_field(self, get_current_user): + get_current_user.return_value = self.user + log_1 = ExpectLog( + 'tornado.general', ".*Missing argument minigrid_name") + log_2 = ExpectLog('tornado.access', '400') + with log_1, log_2: + response = self.fetch( + '/minigrids?minigrid_aes_key=a', + method='POST', body='') + self.assertResponseCode(response, 400) + self.assertIsNone(self.session.query(models.Minigrid).one_or_none()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_post_minigrids_duplicate_name(self, get_current_user): + get_current_user.return_value = self.user + with models.transaction(self.session) as session: + session.add(models.Minigrid(minigrid_name='a', aes_key='a')) + with ExpectLog('tornado.access', '400'): + response = self.fetch( + '/minigrids?minigrid_name=a&minigrid_aes_key=b', + method='POST', body='') + self.assertResponseCode(response, 400) + self.assertIn( + 'A minigrid with that name already exists', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_post_minigrids_empty_aes_key(self, get_current_user): + get_current_user.return_value = self.user + with ExpectLog('tornado.access', '400'): + response = self.fetch( + '/minigrids?minigrid_name=a&minigrid_aes_key=', + method='POST', body='') + self.assertResponseCode(response, 400) + self.assertIn('minigrid_aes_key_check', response.body.decode()) + + +class TestTechnicianHandler(HTTPTest): + def setUp(self): + super().setUp() + with models.transaction(self.session) as session: + self.user = models.User(email='a@a.com') + session.add(self.user) + session.add(models.System(day_tariff=1, night_tariff=1)) + + def test_get_technician_not_logged_in(self): + response = self.fetch('/technician') + self.assertResponseCode(response, 200) + self.assertNotIn('user', response.headers['Set-Cookie']) + self.assertIn('Log In', response.body.decode()) + self.assertNotIn('Log Out', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_technician_logged_in(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch('/technician') + self.assertResponseCode(response, 200) + self.assertIn('Write technician ID card', response.body.decode()) + + +class TestDeviceHandler(HTTPTest): + def setUp(self): + super().setUp() + with models.transaction(self.session) as session: + self.user = models.User(email='a@a.com') + session.add(self.user) + session.add(models.System(day_tariff=1, night_tariff=1)) + + def test_get_device_not_logged_in(self): + response = self.fetch('/device') + self.assertResponseCode(response, 200) + self.assertNotIn('user', response.headers['Set-Cookie']) + self.assertIn('Log In', response.body.decode()) + self.assertNotIn('Log Out', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_device_logged_in(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch('/device') + self.assertResponseCode(response, 200) + self.assertIn('Add device:', response.body.decode()) + + +class TestCardsHandler(HTTPTest): + def setUp(self): + super().setUp() + with models.transaction(self.session) as session: + self.user = models.User(email='a@a.com') + session.add(self.user) + session.add(models.System(day_tariff=1, night_tariff=1)) + + def test_get_cards_not_logged_in(self): + response = self.fetch('/cards') + self.assertResponseCode(response, 200) + self.assertNotIn('user', response.headers['Set-Cookie']) + self.assertIn('Log In', response.body.decode()) + self.assertNotIn('Log Out', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_cards_logged_in(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch('/cards') + self.assertResponseCode(response, 200) + self.assertIn('Read and validate cards:', response.body.decode()) + + +class TestWriteCreditCardHandler(HTTPTest): + def setUp(self): + super().setUp() + with models.transaction(self.session) as session: + self.user = models.User(email='a@a.com') + session.add(self.user) + session.add(models.System(day_tariff=1, night_tariff=1)) + self.minigrid = models.Minigrid(minigrid_name='a', aes_key='a') + session.add(self.minigrid) + + def test_get_write_credit_cards_not_logged_in(self): + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/write_credit') + self.assertResponseCode(response, 200) + self.assertNotIn('user', response.headers['Set-Cookie']) + self.assertIn('Log In', response.body.decode()) + self.assertNotIn('Log Out', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_write_credit_cards_logged_in(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/write_credit') + self.assertResponseCode(response, 200) + self.assertIn('Write credit card:', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_write_credit_cards_404(self, get_current_user): + get_current_user.return_value = self.user + with ExpectLog('tornado.access', '404'): + response = self.fetch( + f'/minigrids/{self.user.user_id}/write_credit') + self.assertResponseCode(response, 404) + + +class TestMinigridVendorsHandler(HTTPTest): + def setUp(self): + super().setUp() + with models.transaction(self.session) as session: + self.user = models.User(email='a@a.com') + session.add(self.user) + session.add(models.System(day_tariff=1, night_tariff=1)) + self.minigrid = models.Minigrid(minigrid_name='a', aes_key='a') + self.vendor = models.Vendor(vendor_name='v') + self.minigrid.vendors.append(self.vendor) + session.add(self.minigrid) + + def test_get_not_logged_in(self): + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/vendors') + self.assertResponseCode(response, 200) + self.assertNotIn('user', response.headers['Set-Cookie']) + self.assertIn('Log In', response.body.decode()) + self.assertNotIn('Log Out', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_logged_in(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/vendors') + self.assertResponseCode(response, 200) + self.assertNotIn('Log In', response.body.decode()) + self.assertIn('Log Out', response.body.decode()) + self.assertNotIn( + 'You must initialize the system tariff information.', + response.body.decode()) + body = BeautifulSoup(response.body) + minigrids = body.ul.findAll('li') + self.assertEqual(len(minigrids), 1) + self.assertEqual(minigrids[0].p.contents[0], 'Name: v') + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_create_vendor(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/vendors?' + 'action=create&vendor_name=v2', method='POST', body='') + self.assertResponseCode(response, 201) + vendor = ( + self.session.query(models.Vendor) + .filter_by(vendor_name='v2').one()) + self.assertEqual(vendor.vendor_name, 'v2') + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_create_duplicate_vendor(self, get_current_user): + get_current_user.return_value = self.user + with ExpectLog('tornado.access', '400'): + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/vendors?' + 'action=create&vendor_name=v', method='POST', body='') + self.assertResponseCode(response, 400) + self.assertIn( + 'A vendor with that name already exists', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_create_missing_name(self, get_current_user): + get_current_user.return_value = self.user + with ExpectLog('tornado.access', '400'): + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/vendors?' + 'action=create&vendor_name=', method='POST', body='') + self.assertResponseCode(response, 400) + self.assertIn('vendor_vendor_name_check', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_remove_vendor(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/vendors?' + f'action=remove&vendor_id={self.vendor.vendor_id}', + method='POST', body='') + self.assertResponseCode(response, 200) + vendor = self.session.query(models.Vendor).one_or_none() + self.assertIsNone(vendor) + self.assertIn('Vendor v removed', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_remove_vendor_twice(self, get_current_user): + get_current_user.return_value = self.user + self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/vendors?' + f'action=remove&vendor_id={self.vendor.vendor_id}', + method='POST', body='') + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/vendors?' + f'action=remove&vendor_id={self.vendor.vendor_id}', + method='POST', body='') + self.assertResponseCode(response, 200) + vendor = self.session.query(models.Vendor).one_or_none() + self.assertIsNone(vendor) + self.assertIn( + 'The requested vendor no longer exists', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_bad_action(self, get_current_user): + get_current_user.return_value = self.user + log_1 = ExpectLog( + 'tornado.general', '.*Bad Request \(invalid action\)') + log_2 = ExpectLog('tornado.access', '400') + with log_1, log_2: + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/vendors?' + 'action=something', method='POST', body='') + self.assertResponseCode(response, 400) + self.assertIn('Bad Request', response.body.decode()) + + +class TestMinigridCustomersHandler(HTTPTest): + def setUp(self): + super().setUp() + with models.transaction(self.session) as session: + self.user = models.User(email='a@a.com') + session.add(self.user) + session.add(models.System(day_tariff=1, night_tariff=1)) + self.minigrid = models.Minigrid(minigrid_name='a', aes_key='a') + self.customer = models.Customer(customer_name='c') + self.minigrid.customers.append(self.customer) + session.add(self.minigrid) + + def test_get_not_logged_in(self): + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/customers') + self.assertResponseCode(response, 200) + self.assertNotIn('user', response.headers['Set-Cookie']) + self.assertIn('Log In', response.body.decode()) + self.assertNotIn('Log Out', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_get_logged_in(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/customers') + self.assertResponseCode(response, 200) + self.assertNotIn('Log In', response.body.decode()) + self.assertIn('Log Out', response.body.decode()) + self.assertNotIn( + 'You must initialize the system tariff information.', + response.body.decode()) + body = BeautifulSoup(response.body) + minigrids = body.ul.findAll('li') + self.assertEqual(len(minigrids), 1) + self.assertEqual(minigrids[0].p.contents[0], 'Name: c') + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_create_customer(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/customers?' + 'action=create&customer_name=c2', method='POST', body='') + self.assertResponseCode(response, 201) + customer = ( + self.session.query(models.Customer) + .filter_by(customer_name='c2').one()) + self.assertEqual(customer.customer_name, 'c2') + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_create_duplicate_customer(self, get_current_user): + get_current_user.return_value = self.user + with ExpectLog('tornado.access', '400'): + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/customers?' + 'action=create&customer_name=c', method='POST', body='') + self.assertResponseCode(response, 400) + self.assertIn( + 'A customer with that name already exists', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_create_missing_name(self, get_current_user): + get_current_user.return_value = self.user + with ExpectLog('tornado.access', '400'): + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/customers?' + 'action=create&customer_name=', method='POST', body='') + self.assertResponseCode(response, 400) + self.assertIn('customer_customer_name_check', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_remove_customer(self, get_current_user): + get_current_user.return_value = self.user + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/customers?' + f'action=remove&customer_id={self.customer.customer_id}', + method='POST', body='') + self.assertResponseCode(response, 200) + customer = self.session.query(models.Customer).one_or_none() + self.assertIsNone(customer) + self.assertIn('Customer c removed', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_remove_customer_twice(self, get_current_user): + get_current_user.return_value = self.user + self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/customers?' + f'action=remove&customer_id={self.customer.customer_id}', + method='POST', body='') + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/customers?' + f'action=remove&customer_id={self.customer.customer_id}', + method='POST', body='') + self.assertResponseCode(response, 200) + customer = self.session.query(models.Customer).one_or_none() + self.assertIsNone(customer) + self.assertIn( + 'The requested customer no longer exists', response.body.decode()) + + @patch('minigrid.handlers.BaseHandler.get_current_user') + def test_bad_action(self, get_current_user): + get_current_user.return_value = self.user + log_1 = ExpectLog( + 'tornado.general', '.*Bad Request \(invalid action\)') + log_2 = ExpectLog('tornado.access', '400') + with log_1, log_2: + response = self.fetch( + f'/minigrids/{self.minigrid.minigrid_id}/customers?' + 'action=something', method='POST', body='') + self.assertResponseCode(response, 400) + self.assertIn('Bad Request', response.body.decode()) + + class TestXSRF(HTTPTest): def get_app(self): self.app = Application(self.session) diff --git a/tests/python/test_models.py b/tests/python/test_models.py index 7506806..9396a40 100644 --- a/tests/python/test_models.py +++ b/tests/python/test_models.py @@ -1,3 +1,9 @@ +import uuid + +from sqlalchemy.orm.exc import NoResultFound + +from tornado.web import HTTPError + from tests.python.util import Test from minigrid import models @@ -9,3 +15,25 @@ def test_create(self): session.add(models.User(email='a@b.com')) user = self.session.query(models.User).one() self.assertEqual(user.email, 'a@b.com') + + +class TestMinigrid(Test): + def setUp(self): + super().setUp() + with models.transaction(self.session) as session: + session.add(models.Minigrid(minigrid_name='a', aes_key='b')) + + def test_get_minigrid_regular(self): + minigrid = self.session.query(models.Minigrid).one() + grid = models.get_minigrid(self.session, minigrid.minigrid_id) + self.assertIs(grid, minigrid) + + def test_get_minigrid_raise_404(self): + with self.assertRaises(HTTPError) as error: + models.get_minigrid(self.session, str(uuid.uuid4())) + self.assertEqual(error.exception.status_code, 404) + + def test_get_minigrid_reraise(self): + with self.assertRaises(NoResultFound): + models.get_minigrid( + self.session, str(uuid.uuid4()), exception=None)