Skip to content

Commit

Permalink
starting simple message board
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin committed Sep 23, 2016
1 parent 2d21eab commit 6c1104b
Show file tree
Hide file tree
Showing 13 changed files with 228 additions and 39 deletions.
11 changes: 7 additions & 4 deletions aiohttp_devtools/cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from pathlib import Path

import click

from .exceptions import AiohttpDevException
Expand Down Expand Up @@ -48,15 +50,16 @@ def runserver(**config):
@cli.command()
@click.argument('path', type=_path_type, required=True)
@click.argument('name', required=False)
@click.option('--template-engine', type=click.Choice(Options.TEMPLATE_ENG_CHOICES), default=Options.TEMPLATE_ENG_JINJA)
@click.option('--template-engine', type=click.Choice(Options.TEMPLATE_ENG_CHOICES), default=Options.TEMPLATE_ENG_JINJA2)
@click.option('--session', type=click.Choice(Options.SESSION_CHOICES), default=Options.SESSION_SECURE)
@click.option('--database', type=click.Choice(Options.DB_CHOICES), default=Options.DB_PG_SA)
def start(**config):
def start(*, path, name, template_engine, session, database):
"""
Create a new aiohttp app.
"""
config['name'] = config['name'] or click.prompt('Enter a name to give your app')
if name is None:
name = Path(path).name
try:
StartProject(**config)
StartProject(path=path, name=name, template_engine=template_engine, session=session, database=database)
except AiohttpDevException as e:
raise click.BadParameter(e)
4 changes: 4 additions & 0 deletions aiohttp_devtools/runserver/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pathlib import Path
from pprint import pformat

from multiprocessing import set_start_method
from watchdog.observers import Observer

from .logs import AuxiliaryLogHandler, dft_logger
Expand All @@ -10,6 +11,9 @@


def runserver(**config):
# force a full reload to interpret an updated version of code, this must be called only once
set_start_method('spawn')

_, code_path = import_string(config['app_path'], config['app_factory'])
static_path = config.pop('static_path')
config.update(
Expand Down
11 changes: 4 additions & 7 deletions aiohttp_devtools/runserver/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,12 @@ def modify_main_app(app, **config):
livereload_enabled = config['livereload']
aux_logger.debug('livereload enabled: %s', '✓' if livereload_enabled else '✖')

if JINJA_ENV in app:
static_url = '{}/{}'.format(aux_server, config['static_url'].strip('/'))
# if a jinja environment is setup add a global variable `static_url`
# which can be used as in `src="{{ static_url }}/foobar.css"`
app[JINJA_ENV].globals['static_url'] = static_url
aux_logger.debug('global environment variable static_url="%s" added to jinja environment', static_url)
static_url = '{}/{}'.format(aux_server, config['static_url'].strip('/'))
app['static_url'] = static_url
aux_logger.debug('global environment variable static_url="%s" added to app as "static_url"', static_url)

async def on_prepare(request, response):
if livereload_enabled and 'text/html' in response.content_type:
if request.path.startswith('/_debugtoolbar') and livereload_enabled and 'text/html' in response.content_type:
response.body += live_reload_snippet
app.on_response_prepare.append(on_prepare)

Expand Down
5 changes: 1 addition & 4 deletions aiohttp_devtools/runserver/watch.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import signal
from datetime import datetime
from multiprocessing import Process, set_start_method
from multiprocessing import Process

from watchdog.events import PatternMatchingEventHandler, match_any_paths, unicode_paths

Expand All @@ -11,9 +11,6 @@
# specific to jetbrains I think, very annoying if not ignored
JB_BACKUP_FILE = '*___jb_???___'

# force a full reload to interpret an updated version of code:
set_start_method('spawn')


class _BaseEventHandler(PatternMatchingEventHandler):
patterns = ['*.*']
Expand Down
16 changes: 9 additions & 7 deletions aiohttp_devtools/start/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
THIS_DIR = Path(__file__).parent
TEMPLATE_DIR = THIS_DIR / 'template' # type: Path

PY_REGEXES = [(re.compile(p), r) for p, r in [
('\n# *\n', '\n\n'), # blank comment
('\n# *$', '\n'), # blank comment on last line
('\n{4,}', '\n\n\n') # more than 2 empty lines
PY_REGEXES = [(re.compile(p, f), r) for p, r, f in [
('^ *# *\n', '', re.M), # blank comments
('\n *# *$', '', 0), # blank comment at end of fie
('\n{4,}', '\n\n\n', 0) # more than 2 empty lines
]]


class Options:
# could use Enums here but they wouldn't play well with click
NONE = 'none'

TEMPLATE_ENG_JINJA = 'jinja'
TEMPLATE_ENG_CHOICES = (NONE, TEMPLATE_ENG_JINJA)
TEMPLATE_ENG_JINJA2 = 'jinja2'
TEMPLATE_ENG_CHOICES = (NONE, TEMPLATE_ENG_JINJA2)

SESSION_SECURE = 'secure'
SESSION_VANILLA = 'vanilla'
Expand Down Expand Up @@ -52,6 +52,8 @@ def __init__(self, *, path: str, name: str, template_engine: str, session: str,
'database': {'is_' + o: database == o for o in Options.DB_CHOICES},
}
self.generate_directory(TEMPLATE_DIR)
display_path = self.project_root.relative_to(Path('.').resolve())
print('New aiohttp project "{name}" started at ./{path}'.format(name=name, path=display_path))

def generate_directory(self, p: Path):
for pp in p.iterdir():
Expand All @@ -72,7 +74,7 @@ def generate_file(self, p: Path):
return

if p.name == 'requirements.txt':
lines = set(text.split('\n'))
lines = set(filter(bool, text.split('\n')))
text = '\n'.join(sorted(lines))
elif p.suffix == '.py':
# helpful when debugging: print(text.replace(' ', '·').replace('\n', '⏎\n'))
Expand Down
47 changes: 45 additions & 2 deletions aiohttp_devtools/start/template/app/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,51 @@
from pathlib import Path
# {% if template_engine.is_jinja2 %}
import aiohttp_jinja2
from aiohttp_jinja2 import APP_KEY as JINJA2_APP_KEY
import jinja2
# {% endif %}
from aiohttp import web
from .routes import routes

from .routes import setup_routes

THIS_DIR = Path(__file__).parent
# {% if template_engine.is_jinja2 %}

@jinja2.contextfilter
def reverse_url(context, name, **parts):
app = context['app']

kwargs = {}
if 'query' in parts:
kwargs['query'] = parts.pop('query')
if parts:
kwargs['parts'] = parts
return app.router[name].url(**kwargs)


@jinja2.contextfilter
def static_url(context, static_file):
app = context['app']
try:
static_url = app['static_url']
except KeyError:
raise RuntimeError('app does not define a static root url "static_url"')
return '{}/{}'.format(static_url.rstrip('/'), static_file.lstrip('/'))
# {% endif %}


def create_app(loop):
app = web.Application(loop=loop)
[app.router.add_route(*args) for args in routes]
app['name'] = '{{ name }}'
# {% if template_engine.is_jinja2 %}

jinja2_loader = jinja2.FileSystemLoader(str(THIS_DIR / 'templates'))
aiohttp_jinja2.setup(app, loader=jinja2_loader, app_key=JINJA2_APP_KEY)
app[JINJA2_APP_KEY].filters.update(
url=reverse_url,
static=static_url,
)
# {% endif %}

setup_routes(app)
return app
9 changes: 4 additions & 5 deletions aiohttp_devtools/start/template/app/routes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from .views import index

routes = [
('/', index),
]
from .views import index, messages


def setup_routes(app):
app.router.add_get('/', index, name='index')
app.router.add_route('*', '/messages', messages, name='messages')
10 changes: 6 additions & 4 deletions aiohttp_devtools/start/template/app/templates/base.jinja
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
{% if template_engine.is_jinja %}
{% if template_engine.is_jinja2 %}
{% raw %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ name }}</title>
<title>{{ app.name }}</title>
<link href="{{ 'styles.css'|static }}" rel="stylesheet">
</head>
<body>
{% block content %}{% endblock %}
<main>
<h1>{{ title }}</h1>
{% block content %}{% endblock %}
</main>
</body>
</html>
{% endraw %}
Expand Down
10 changes: 8 additions & 2 deletions aiohttp_devtools/start/template/app/templates/index.jinja
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
{% if template_engine.is_jinja %}
{% if template_engine.is_jinja2 %}
{% raw %}
{% extends 'base.jinja' %}

{% block content %}
hello
<p>{{ message }}</p>
<p>
To demonstrate a little of the functionality of aiohttp this app implements a very simple message board.
</p>
<b>
<a href="{{ 'messages'|url }}">View and add messages</a>
</b>
{% endblock %}
{% endraw %}
{% endif %}
32 changes: 32 additions & 0 deletions aiohttp_devtools/start/template/app/templates/messages.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% if template_engine.is_jinja2 %}
{% raw %}
{% extends 'base.jinja' %}

{% block content %}
<h2>Add a new message:</h2>
<form method="post" action="{{ 'messages'|url }}">
{% if form_errors %}
<div class="form-errors">
{{ form_errors }}
</div>
{% endif %}
<p>
<label for="username">Your name:</label>
<input type="text" name="username" id="username" placeholder="fred blogs">
<label for="message">Message:</label>
<input type="text" name="message" id="message" placeholder="hello there">
</p>
<button type="submit">Post Message</button>
</form>

<h2>Messages:</h2>
<ul>
{% for message in messages %}
<li><b>{{ message.username }}:</b> {{ message.message }}, ({{ message.timestamp }})</li>
{% else %}
<li>No messages found.</li>
{% endfor %}
</ul>
{% endblock %}
{% endraw %}
{% endif %}
91 changes: 88 additions & 3 deletions aiohttp_devtools/start/template/app/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,100 @@
from datetime import datetime
from pathlib import Path

from aiohttp import web
# {% if template_engine.is_jinja %}
from aiohttp.hdrs import METH_POST
# {% if template_engine.is_jinja2 %}
from aiohttp.web_exceptions import HTTPFound
from aiohttp_jinja2 import template
# {% endif %}

# {% if template_engine.is_jinja %}
# if no database is available we use a plain old file to store messages. Don't do this kind of thing in production!
MESSAGE_FILE = Path('messages.txt')

# {% if template_engine.is_jinja2 %}

@template('index.jinja')
async def index(request):
return {'foo': 'bar'}
"""
This is the view handler for the "/" url.
:param request: the request object see http://aiohttp.readthedocs.io/en/stable/web_reference.html#request
:return: context for the template. Not: we return a dict not a response because of the @template decorator
"""
return {
'title': request.app['name'],
'message': "Success! you've setup a basic aiohttp app.",
}


# {% else %}

async def index(request):
"""
This is the view handler for the "/" url.
:param request: the request object see http://aiohttp.readthedocs.io/en/stable/web_reference.html#request
:return: aiohttp.web.Response object
"""
content = """\
<!DOCTYPE html>
<head>
<title>{title}</title>
<link href="{styles_css}" rel="stylesheet">
</head>
<body>
<h1>{title}</h1>
<p>{message}</p>
</body>"""
return web.Response(text='<body>hello</body>', content_type='text/html')
# {% endif %}


async def process_form(request):
new_message, missing_fields = {}, []
fields = ['username', 'message']
data = await request.post()
for f in fields:
new_message[f] = data.get(f)
if not new_message[f]:
missing_fields.append(f)

if missing_fields:
return 'Invalid form submission, missing fields: {}'.format(', '.join(missing_fields))

# hack: this very simple storage uses "|" to split fields so we need to replace it in username
new_message['username'] = new_message['username'].replace('|', '')
with MESSAGE_FILE.open('a') as f:
now = datetime.now().isoformat()
f.write('{username}|{timestamp:%Y-%m-%d %H:%M}|{message}'.format(timestamp=now, **new_message))
raise HTTPFound(request.app.router['messages'].url())


# {% if template_engine.is_jinja2 %}
@template('messages.jinja')
# {% endif %}
async def messages(request):
if request.method == METH_POST:
# the 302 redirect is processed as an exception, so if this coroutine returns there's a form error
form_errors = await process_form(request)
else:
form_errors = None

messages = []
if MESSAGE_FILE.exists():
lines = MESSAGE_FILE.read_text().split('\n')
for line in reversed(lines):
if not line:
continue
username, ts, message = line.split('|', 2)
ts = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.strptime(ts, '%Y-%m-%dT%H:%M:%S.%f'))
messages.append({'username': username, 'timestamp': ts, 'message': message})
# {% if template_engine.is_jinja2 %}
return {
'title': 'Message board',
'form_errors': form_errors,
'messages': messages,
}
# {% else %}
raise NotImplementedError()
# {% endif %}
2 changes: 1 addition & 1 deletion aiohttp_devtools/start/template/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{# This file is special: lines are made unique and sorted before the new requirements.tzt file is created #}
{% if template_engine.is_jinja %}
{% if template_engine.is_jinja2 %}
aiohttp-jinja2==0.8.0
{% endif %}

Expand Down
19 changes: 19 additions & 0 deletions aiohttp_devtools/start/template/static/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
html, body {
font-family: Garamond, Georgia, serif;
margin: 0;
}

main {
margin: 0 auto;
max-width: 940px;
background-color: white;
padding: 20px 20px;
}

.form-errors {
background-color: #f2dede;
padding: 5px 10px;
border: 1px solid red;
border-radius: 3px;
color: #79312f;
}

0 comments on commit 6c1104b

Please sign in to comment.