Skip to content

Commit

Permalink
Merge pull request #7 from SEL-Columbia/user-and-minigrid-list
Browse files Browse the repository at this point in the history
Version 0.1.1
  • Loading branch information
vr2262 authored Dec 19, 2016
2 parents c746297 + 5a6fa67 commit 696a00f
Show file tree
Hide file tree
Showing 21 changed files with 410 additions and 42 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ script:
- xvfb-run --server-args="-screen 0, 1280x1280x16" tests/python/coverage_run.sh

after_success:
- coveralls
- codecov

notifications:
email:
Expand Down
14 changes: 8 additions & 6 deletions dev/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@
sys.path.insert(1, os.path.join(sys.path[0], '..'))


def createdb():
def createdb(ensure=True):
"""Create the schema and tables and return a Session."""
from minigrid import models
engine = models.create_engine()
models.Base.metadata.create_all(engine)
return sessionmaker(bind=engine, autocommit=True)()
if ensure:
models.Base.metadata.create_all(engine)
print('Created schema {}'.format(models.Base.metadata.schema))
return sessionmaker(bind=engine)()


def create_user(*, email):
"""Create a user with the given e-mail."""
from minigrid import models
session = createdb()
with session.begin():
session = createdb(ensure=False)
with session.begin_nested():
session.add(models.User(email=email))
print('Created user with e-mail ' + email)

Expand All @@ -47,7 +49,7 @@ def main():
from minigrid.options import parse_command_line
parse_command_line([None] + others)
from minigrid.options import options
if 'db_schema' not in others:
if not any(other.startswith('--db_schema=') for other in others):
options.db_schema = 'minigrid_dev'
try:
command = globals()[args.command_name]
Expand Down
1 change: 1 addition & 0 deletions dev/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
codecov
fakeredis==0.8.1
flake8==3.2.1
pydocstyle==1.1.1
Expand Down
2 changes: 1 addition & 1 deletion minigrid/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class LoginError(MinigridHTTPError):

def __init__(self,
status_code=400,
template_name='index.html',
template_name='index-logged-out.html',
*, reason):
"""Create a login error (400 by default)."""
super().__init__(reason, status_code, template_name)
59 changes: 57 additions & 2 deletions minigrid/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from urllib.parse import urlencode
from uuid import uuid4

from sqlalchemy.exc import DataError, IntegrityError
from sqlalchemy.orm.exc import NoResultFound

import tornado.web
Expand All @@ -18,7 +19,7 @@ class BaseHandler(tornado.web.RequestHandler):

@property
def session(self):
"""The database session. Use session.begin() for transactions."""
"""The db session. Use session.begin_nested() for transactions."""
return self.application.session

def get_current_user(self):
Expand Down Expand Up @@ -46,7 +47,13 @@ class MainHandler(BaseHandler):

def get(self):
"""Render the homepage."""
self.render('index.html', reason=None)
if self.current_user:
minigrids = (
self.session
.query(models.Minigrid).order_by(models.Minigrid.name))
self.render('index-minigrid-list.html', minigrids=minigrids)
return
self.render('index-logged-out.html', reason=None)

def post(self):
"""Send login information to the portier broker."""
Expand All @@ -64,6 +71,52 @@ def post(self):
self.redirect('https://broker.portier.io/auth?' + query_args)


class UsersHandler(BaseHandler):
"""Handlers for user management."""

def _render_users(self, reason=None):
users = self.session.query(models.User).order_by('email')
self.render('users.html', users=users, reason=reason)

@tornado.web.authenticated
def get(self):
"""Render the view for user management."""
self._render_users()

@tornado.web.authenticated
def post(self):
"""Create a new user model."""
email = self.get_argument('email')
reason = None
try:
with self.session.begin_nested():
self.session.add(models.User(email=email))
except IntegrityError as error:
if 'user_email_check' in error.orig.pgerror:
reason = '{} is not a valid e-mail address'.format(email)
else:
reason = 'Account for {} already exists'.format(email)
self._render_users(reason=reason)


class MinigridHandler(BaseHandler):
"""Handlers for a minigrid view."""

@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)


class VerifyLoginHandler(BaseHandler):
"""Handlers for portier verification."""

Expand Down Expand Up @@ -115,6 +168,8 @@ def post(self):

application_urls = [
(r'/', MainHandler),
(r'/minigrid/(.+)?', MinigridHandler),
(r'/users/?', UsersHandler),
(r'/verify/?', VerifyLoginHandler),
(r'/logout/?', LogoutHandler),
]
40 changes: 40 additions & 0 deletions minigrid/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql as pg
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql.functions import current_timestamp
from sqlalchemy.sql import func

from minigrid.options import options
Expand Down Expand Up @@ -36,6 +37,25 @@ def pk():
pg.UUID, primary_key=True, server_default=func.uuid_generate_v4())


def update_time():
"""Return a timestamp column set to CURRENT_TIMESTAMP by default."""
return sa.Column(
pg.TIMESTAMP(timezone=True),
nullable=False,
server_default=current_timestamp(),
)


def json_column(column_name, default=None):
"""Return a JSONB column that is a dictionary at the top level."""
return sa.Column(
pg.json.JSONB,
sa.CheckConstraint("{} @> '{{}}'".format(column_name)),
nullable=False,
server_default=default,
)


class User(Base):
"""The model for a registered user."""

Expand All @@ -45,3 +65,23 @@ class User(Base):
pg.TEXT, sa.CheckConstraint("email ~ '.*@.*'"),
nullable=False, unique=True,
)


class Minigrid(Base):
"""The model for a minigrid."""

__tablename__ = 'minigrid'
minigrid_id = pk()
name = sa.Column(
pg.TEXT, sa.CheckConstraint("name != ''"),
nullable=False, unique=True)
day_tariff = sa.Column(
pg.NUMERIC,
sa.CheckConstraint('day_tariff > 0'), nullable=False)
day_tariff_update_time = update_time()
night_tariff = sa.Column(
pg.NUMERIC,
sa.CheckConstraint('night_tariff > 0'), nullable=False)
night_tariff_update_time = update_time()
error_code = json_column('error_code', default='{}')
status = json_column('status', default='{}')
12 changes: 12 additions & 0 deletions minigrid/templates/index-logged-out.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% extends 'base.html' %}

{% block body %}
{% if reason is not None %}
<p><strong>Login unsuccessful: {{ reason }}</strong></p>
{% end %}
<form action="/" method="POST">
{% module xsrf_form_html() %}
E-mail: <input type="email" name="email" />
<input type="submit" value="Log In" />
</form>
{% end %}
14 changes: 14 additions & 0 deletions minigrid/templates/index-minigrid-list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% extends 'base.html' %}

{% block body %}
<p><a href="/users">Manage users &raquo;</a></p>
<p>Minigrids:</p>
<ul>
{% for grid in minigrids %}
<li>
<p><a href="/minigrid/{{ grid.minigrid_id }}">{{ grid.name }} &raquo;</a></p>
<p>Status: {{ grid.status }}</p>
</li>
{% end %}
</ul>
{% end %}
15 changes: 0 additions & 15 deletions minigrid/templates/index.html

This file was deleted.

13 changes: 13 additions & 0 deletions minigrid/templates/minigrid.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends 'base.html' %}

{% block body %}
<h1>Minigrid Name: {{ minigrid.name }}</h1>
<p>ID: {{ minigrid.minigrid_id }}</p>
<p>Day tariff: {{ minigrid.day_tariff }}</p>
<p>Last update of day tariff: {{ minigrid.day_tariff_update_time }}</p>
<p>Night tariff: {{ minigrid.night_tariff }}</p>
<p>Last update of night tariff: {{ minigrid.night_tariff_update_time }}</p>
<p>Error code: {{ minigrid.error_code }}</p>
<p>Status: {{ minigrid.status }}</p>
<footer><a href="/">&laquo; Return to minigrid index</a></footer>
{% end %}
22 changes: 22 additions & 0 deletions minigrid/templates/users.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends 'base.html' %}

{% block body %}
<p>Users:</p>
<ul>
{% for user in users %}
<li>
<p><a href="mailto:{{ user.email }}">{{ user.email }}</a></p>
</li>
{% end %}
</ul>
<p>Add user account:</p>
<form action="{{ request.uri }}" method="POST">
{% module xsrf_form_html() %}
E-mail: <input type="email" name="email" />
<input type="submit" value="Add User" />
</form>
{% if reason is not None %}
<p><strong>Could not create user account: {{ reason }}</strong></p>
{% end %}
<footer><a href="/">&laquo; Return to minigrid index</a></footer>
{% end %}
43 changes: 43 additions & 0 deletions prod/create_initial_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env python
"""Create the initial administrator for the application."""
import argparse
import os
import sys
from time import sleep

from sqlalchemy import func
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import sessionmaker

sys.path.insert(1, os.path.join(sys.path[0], '..'))


def main():
"""Supply the administrator's e-mail"""
parser = argparse.ArgumentParser()
parser.add_argument('email')
args, others = parser.parse_known_args()
from minigrid.options import parse_command_line
parse_command_line([None] + others)
from minigrid.options import options
if not any(other.startswith('--db_schema=') for other in others):
options.db_schema = 'minigrid'
from minigrid import models
engine = models.create_engine()
session = sessionmaker(bind=engine)()
try:
users = session.query(func.count(models.User.user_id)).scalar()
except OperationalError:
print('Database connection failed... trying again in 5 seconds.')
sleep(5)
users = session.query(func.count(models.User.user_id)).scalar()
if users:
print('At least one user already exists. Log in as that user.')
sys.exit(1)
with session.begin_nested():
session.add(models.User(email=args.email))
print('Created initial user with e-mail', args.email)


if __name__ == '__main__':
main()
2 changes: 1 addition & 1 deletion prod/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: '2'
services:
minigrid:
image: selcolumbia/minigrid-server:0.0.7
image: selcolumbia/minigrid-server:0.1.1
command: ./prod/run.sh --db_host=db --redis_url=redis://redis:6379/0 --minigrid-website-url=https://www.example.com
depends_on:
- redis
Expand Down
22 changes: 16 additions & 6 deletions prod/install.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env sh
# Minigrid Server installer for version 0.0.7
# Minigrid Server installer for version 0.1.1
set -e

# Do you have docker installed?
Expand Down Expand Up @@ -106,14 +106,21 @@ $SUDO openssl dhparam -out /etc/letsencrypt/live/$LETSENCRYPT_DIR/dhparam.pem 20

# Download the configuration files
printf "========================================\n"
printf " Downloading configuration files \n"
printf " Generating configuration \n"
printf "========================================\n"
$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.0.7/prod/docker-compose.yml > docker-compose.yml
$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.0.7/prod/nginx.conf > nginx.conf
$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.1/prod/docker-compose.yml > docker-compose.yml
$CURL -L https://raw.githubusercontent.com/SEL-Columbia/minigrid-server/0.1.1/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

printf "\n"
printf "Please enter an e-mail address for the \n"
printf "administrator. This will be the only \n"
printf "account that can log in at first. \n"
printf "Administrator e-mail address:\n>>> "
read ADMIN_EMAIL

# Bring up the server
printf "========================================\n"
printf " Starting minigrid server. \n"
Expand All @@ -126,7 +133,10 @@ if [ -f /etc/redhat-release ] ; then
chcon -Rt svirt_sandbox_file_t .
fi
$DOCKER_COMPOSE up -d
NGINX_CONTAINER_NAME=$($DOCKER_COMPOSE ps | grep nginx | cut -d' ' -f1)
MINIGRID_CONTAINER_NAME=$($DOCKER_COMPOSE ps | grep _minigrid_ | cut -d' ' -f1)
sleep 1
docker exec $MINIGRID_CONTAINER_NAME ""prod/create_initial_user.py --db-host=db $ADMIN_EMAIL""
NGINX_CONTAINER_NAME=$($DOCKER_COMPOSE ps | grep _nginx_ | cut -d' ' -f1)

# Let's Encrypt auto-renew (for now this is a cron job).
printf "========================================\n"
Expand All @@ -146,6 +156,6 @@ CRON_CMD="mkdir -p /tmp/letsencrypt && "\
"if [ -f /tmp/renewed ] ; then docker restart $NGINX_CONTAINER_NAME ; fi ; "\
"rm -f /tmp/renewed"
# https://certbot.eff.org/#ubuntuxenial-nginx recommends running this twice a day on random minute within the hour
CRON_JOB="00 01,13 * * * sleep \$(expr \$RANDOM \% 59); $CRON_CMD"
CRON_JOB="00 01,13 * * * sleep \$(expr \$RANDOM \% 59 \* 60); $CRON_CMD"
crontab -l | fgrep -i -v "$CRON_CMD" | { cat; echo "$CRON_JOB"; } | crontab -
crontab -l
Loading

0 comments on commit 696a00f

Please sign in to comment.