diff options
Diffstat (limited to 'fatcat')
| -rwxr-xr-x | fatcat/backend.py | 301 | ||||
| -rw-r--r-- | fatcat/static/robots.txt | 1 | ||||
| -rw-r--r-- | fatcat/templates/base.html | 70 | ||||
| -rw-r--r-- | fatcat/templates/home.html | 32 | ||||
| -rw-r--r-- | fatcat/templates/work_add.html | 215 | ||||
| -rw-r--r-- | fatcat/templates/work_view.html | 37 | ||||
| -rw-r--r-- | fatcat/test_backend.py | 53 | ||||
| -rwxr-xr-x | fatcat/webface.py | 87 | 
8 files changed, 796 insertions, 0 deletions
| diff --git a/fatcat/backend.py b/fatcat/backend.py new file mode 100755 index 00000000..a39ae790 --- /dev/null +++ b/fatcat/backend.py @@ -0,0 +1,301 @@ + +import argparse +from flask import Flask, render_template, send_from_directory, request, \ +    url_for, abort, g, redirect, jsonify +from sqlalchemy import create_engine, MetaData, Table + +app = Flask(__name__) +app.config.from_object(__name__) + +# Load default config and override config from an environment variable +app.config.update(dict( +    DATABASE_URI='sqlite://:memory:', +    SECRET_KEY='development-key', +    USERNAME='admin', +    PASSWORD='admin' +)) +app.config.from_envvar('FATCAT_BACKEND_CONFIG', silent=True) + +metadata = MetaData() + + +## SQL Schema ############################################################### + +import enum +from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey, \ +    Enum + +# TODO: http://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/mixins.html + +class IdState(enum.Enum): +    normal = 1 +    redirect = 2 +    removed = 3 + +work_id = Table('work_id', metadata, +    Column('id', Integer, primary_key=True, autoincrement=False), +    Column('revision', ForeignKey('work_revision.id')), +    ) + +work_revision = Table('work_revision', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('previous', ForeignKey('work_revision.id'), nullable=True), +    Column('state', Enum(IdState)), +    Column('redirect_id', ForeignKey('work_id.id'), nullable=True), +    Column('edit_id', ForeignKey('edit.id')), +    Column('extra_json', ForeignKey('extra_json.sha1'), nullable=True), + +    Column('title', String), +    Column('work_type', String), +    Column('date', String), +    ) + +release_id = Table('release_id', metadata, +    Column('id', Integer, primary_key=True, autoincrement=False), +    Column('revision', ForeignKey('release_revision.id')), +    ) + +release_revision = Table('release_revision', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('previous', ForeignKey('release_revision.id'), nullable=True), +    Column('state', Enum(IdState)), +    Column('redirect_id', ForeignKey('release_id.id'), nullable=True), +    Column('edit_id', ForeignKey('edit.id')), +    Column('extra_json', ForeignKey('extra_json.sha1'), nullable=True), + +    #Column('work', ForeignKey('work_id.id')), +    Column('container', ForeignKey('container_id.id')), +    Column('title', String), +    Column('license', String),          # TODO: oa status foreign key +    Column('release_type', String),     # TODO: foreign key +    Column('date', String),             # TODO: datetime +    Column('doi', String),              # TODO: identifier table +    ) + +creator_id = Table('creator_id', metadata, +    Column('id', Integer, primary_key=True, autoincrement=False), +    Column('revision', ForeignKey('creator_revision.id')), +    ) + +creator_revision = Table('creator_revision', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('previous', ForeignKey('creator_revision.id'), nullable=True), +    Column('state', Enum(IdState)), +    Column('redirect_id', ForeignKey('creator_id.id'), nullable=True), +    Column('edit_id', ForeignKey('edit.id')), +    Column('extra_json', ForeignKey('extra_json.sha1'), nullable=True), + +    Column('name', String), +    Column('sortname', String), +    Column('orcid', String),            # TODO: identifier table +    ) + +work_contrib = Table('work_contrib', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('work_rev', ForeignKey('work_revision.id'), nullable=False), +    Column('creator_id', ForeignKey('creator_id.id'), nullable=False), +    Column('stub', String, nullable=False), +    ) + +release_contrib = Table('release_contrib', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('release_rev', ForeignKey('release_revision.id'), nullable=False), +    Column('creator_id', ForeignKey('creator_id.id'), nullable=False), +    ) + +container_id = Table('container_id', metadata, +    Column('id', Integer, primary_key=True, autoincrement=False), +    Column('revision', ForeignKey('container_revision.id')), +    ) + +container_revision = Table('container_revision', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('previous', ForeignKey('container_revision.id'), nullable=True), +    Column('state', Enum(IdState)), +    Column('redirect_id', ForeignKey('container_id.id'), nullable=True), +    Column('edit_id', ForeignKey('edit.id')), +    Column('extra_json', ForeignKey('extra_json.sha1'), nullable=True), + +    Column('name', String), +    Column('container', ForeignKey('container_id.id')), +    Column('publisher', String),        # TODO: foreign key +    Column('sortname', String), +    Column('issn', String),             # TODO: identifier table +    ) + +file_id = Table('file_id', metadata, +    Column('id', Integer, primary_key=True, autoincrement=False), +    Column('revision', ForeignKey('container_revision.id')), +    ) + +file_revision = Table('file_revision', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('previous', ForeignKey('file_revision.id'), nullable=True), +    Column('state', Enum(IdState)), +    Column('redirect_id', ForeignKey('file_id.id'), nullable=True), +    Column('edit_id', ForeignKey('edit.id')), +    Column('extra_json', ForeignKey('extra_json.sha1'), nullable=True), + +    Column('size', Integer), +    Column('sha1', Integer),            # TODO: hash table... only or in addition? +    Column('url', Integer),             # TODO: URL table +    ) + +release_file= Table('release_file', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('release_rev', ForeignKey('release_revision.id'), nullable=False), +    Column('file_id', ForeignKey('file_id.id'), nullable=False), +    ) + +edit = Table('edit', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('edit_group', ForeignKey('edit_group.id')), +    Column('editor', ForeignKey('editor.id')), +    Column('description', String), +    ) + +edit_group = Table('edit_group', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('editor', ForeignKey('editor.id')), +    Column('description', String), +    ) + +editor = Table('editor', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('username', String), +    ) + +changelog = Table('changelog', metadata, +    Column('id', Integer, primary_key=True, autoincrement=True), +    Column('edit_id', ForeignKey('edit.id')), +    Column('timestamp', Integer), +    ) + +extra_json = Table('extra_json', metadata, +    Column('sha1', String, primary_key=True, autoincrement=True), +    Column('json', String), +    ) + +## Helpers ################################################################## + +def is_fcid(s): +    return len(s) == 26 and s.isalnum() + +# XXX: why isn't this running? +def test_is_fcid(): + +    for s in ("rzga5b9cd7efgh04iljk", "RZGA5B9CD7Efgh04iljk"): +        assert is_fcid() is True + +    for s in ("rzga59cd7efgh04iljk", "rzga.b9cd7efgh04iljk", "",  +            "rzga5b9cd7efgh04iljkz"): +        assert is_fcid() is False + +def release_list(id_list): +    # XXX: MOCK +    l = [] +    for i in id_list: +        l.append({ +            "id": i, +            "rev": "8fkj28fjhqkjdhkjkj9s", +            "previous": "0021jdfjhqkjdhkjkj9s", +            "state": "normal", +            "redirect_id": None, +            "edit_id": "932582iuhckjvssk", +            "extra_json": None, + +            "container_id": "0021jdfjhqkjdhkjkj9s", +            "title": "Mocks are great", +            "license": "CC-0", +            "release_type": "publication", +            "date": "2017-11-22", +            "doi": "10.1000/953kj.sdfkj", +        }) +    return l + +def release_hydrate(release_id): +    e = release_list([release_id])[0] +    e['container'] = container_hydrate(d['container_id']) +    e.pop('container_id') +    e['creators'] = [creator_hydrate(c['id']) for c in e['creator_ids']] +    return e + +def work_list(id_list): +    """This is the fast/light version: populates entity-specific lists (eg, +    identifiers), and any primaries, but doesn't transclude all other +    entities""" +    if len(id_list) == 0: +        return [] + +    l = [] +    for i in id_list: +        l.append({ +            "id": "rzga5b9cd7efgh04iljk", +            "rev": "8fkj28fjhqkjdhkjkj9s", +            "previous": "0021jdfjhqkjdhkjkj9s", +            "state": "normal", +            "redirect_id": None, +            "edit_id": "932582iuhckjvssk", +            "extra_json": None, + +            "title": "Mocks are great", +            "contributors": [], +            "work_type": "journal-article", +            "date": None, + +            "primary_release": release_list(["8fkj28fjhqkjdhkjkj9s"])[0], +        }) +    return l + +def work_hydrate(work_id): +    """This is the heavy/slowversion: everything from get_works(), but also +    recursively transcludes single-linked entities""" +    # XXX: +    return work_list([work_id])[0] + +## API Methods ############################################################## + +@app.route('/health', methods=['GET']) +def health(): +    return jsonify({'ok': True}) + + +@app.route('/v0/work/<work_id>', methods=['GET']) +def work_get(work_id): +    if not is_fcid(work_id): +        print("not fcid: {}".format(work_id)) +        return abort(404) +    work = work_hydrate(work_id) +    return jsonify(work) + +## Entry Point ############################################################## + +def main(): +    parser = argparse.ArgumentParser() +    parser.add_argument('--debug', +        action='store_true', +        help="enable debugging interface") +    parser.add_argument('--host', +        default="127.0.0.1", +        help="listen on this host/IP") +    parser.add_argument('--port', +        type=int, +        default=8040, +        help="listen on this port") +    parser.add_argument('--database-uri', +        default=app.config['DATABASE_URI'], +        help="sqlalchemy database string") +    args = parser.parse_args() + +    app.config['DATABASE_URI'] = args.database_uri +    app.conn = create_engine(app.config['DATABASE_URI'], convert_unicode=True) +    metadata.create_all(bind=engine) + +    # XXX: +    db_test_data() + +    app.run(debug=args.debug, host=args.host, port=args.port) + + +if __name__ == '__main__': +    main() diff --git a/fatcat/static/robots.txt b/fatcat/static/robots.txt new file mode 100644 index 00000000..a168f11b --- /dev/null +++ b/fatcat/static/robots.txt @@ -0,0 +1 @@ +# Hello friends! diff --git a/fatcat/templates/base.html b/fatcat/templates/base.html new file mode 100644 index 00000000..4e9dcd4b --- /dev/null +++ b/fatcat/templates/base.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html lang="en" style="position: relative; min-height: 100%; height: auto;"> +<head> +  <meta charset="utf-8" /> +  <meta name="viewport" content="width=device-width"> + +  <title>{% block title %}fatcat!{% endblock %}</title> +  <meta name="viewport" content="width=device-width, initial-scale=1"> +  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.2.13/dist/semantic.min.css"> +  <script +    src="https://code.jquery.com/jquery-3.1.1.min.js" +    integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" +    crossorigin="anonymous"></script> +  <script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.2.13/dist/semantic.min.js"></script> + +</head> +<body style="margin-bottom: 100px; height: auto;"> + +<header class="ui fixed inverted menu"> +  <div class="ui container"> +    <a href="/" class="header item"> +      <!-- <img class="logo" src="assets/images/logo.png"> --> +      fatcat! +    </a> +    <a href="/about" class="item">About</a> +    <a href="#" class="item">Guide</a> +    <div class="right menu"> +      <div class="item"> +        <div class="ui transparent inverted icon input"> +          <i class="search icon"></i> +          <input type="text" placeholder="Search..."> +        </div> +      </div> +      <div class="ui simple dropdown item"> +      acidburn <i class="dropdown icon"></i> +        <div class="menu"> +          <a class="item" href="#">Open Submissions</a> +          <a class="item" href="#">Edit History</a> +          <div class="divider"></div> +          <a class="item" href="/account">Account</a> +          <a class="item" href="/logout">Logout</a> +        </div> +      </div> + +    </div> +  </div> +</header> + +<main class="ui main text container" style="margin-top: 4em; margin-bottom: 2em;"> +{% block body %}Nothing to see here.{% endblock %} +</main> + + +<footer class="ui inverted vertical footer segment" style="margin-top: 2em; padding-top: 2em; padding-bottom:2em; position: absolute; bottom: 0px; width: 100%;"> +  <div class="ui center aligned container"> +    <div class="ui horizontal inverted small divided link list"> +      <span class="item">fatcat!</span> +      <a class="item" href="/about">About</a> +      <a class="item" href="/sources">Sources</a> +      <a class="item" href="#">Status</a> +      <a class="item" href="#">Datasets</a> +      <a class="item" href="https://git.bnewbold.net/fatcat/">Source Code</a> +    </div> +  </div> +</footer> + +{% block postscript %}{% endblock %} + +</body> +</html> diff --git a/fatcat/templates/home.html b/fatcat/templates/home.html new file mode 100644 index 00000000..d9cc34a2 --- /dev/null +++ b/fatcat/templates/home.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block body %} + +<h1>Salutations!</h1> + +Just mockups for now... + +<ul> +  <li><b>Work:</b> +    <a href="/work/create">Create</a>, +    <a href="/work/random">Random</a> +  <li><b><strike>Release:</strike></b> +    <a href="/release/create">Create</a>, +    <a href="/release/random">Random</a> +  <li><b><strike>File:</strike></b> +    <a href="/file/create">Create</a>, +    <a href="/file/random">Random</a> +  <li><b><strike>Contributor:</strike></b> +    <a href="/contrib/create">Create</a>, +    <a href="/contrib/random">Random</a> +  <li><b><strike>Container:</strike></b> +    <a href="/container/create">Create</a>, +    <a href="/container/random">Random</a> +  <li><b><strike>Publisher:</strike></b> +    <a href="/publisher/create">Create</a>, +    <a href="/publisher/random">Random</a> +  <li>Edit groups... +  <li>Changelog... +  <li>Login/Signup... +</ul> + +{% endblock %} diff --git a/fatcat/templates/work_add.html b/fatcat/templates/work_add.html new file mode 100644 index 00000000..ac8a8169 --- /dev/null +++ b/fatcat/templates/work_add.html @@ -0,0 +1,215 @@ +{% extends "base.html" %} +{% block body %} +<div class="ui segment"> +<h1 class="ui header">Adding a New Thing</h1> + +<form class="ui form" id="add_work_form"> + +  <h3 class="ui dividing header">The Basics</h3> + +  <div class="ui huge field required"> +    <label>Title</label> +    <input name="work_title" type="text" placeholder="Title of Work (in English)"> +  </div> + +  <div class="ui field required"> +    <label>Type of Work</label> +    <select class="ui dropdown" id="work_type"> +      <option value="">Primary Type</option> +      <option value="journal-article">Journal Article</option> +      <option value="book">Book</option> +      <option value="book-chapter">Book Chapter</option> +      <option value="dataset">Dataset</option> +      <option value="dissertation">Thesis or Dissertation</option> +      <option value="monograph">Monograph</option> +      <option value="proceedings-article">Conference Proceeding</option> +      <option value="report">Report</option> +      <option value="other">Other</option> +    </select> +  </div> + +  <!-- Primary Creators/Authors --> +  <div class="ui field search" id="work_creators"> +    <label>Primary Creator(s)</label> +    <div class="ui icon input"> +      <input class="prompt" type="text" placeholder="Search..."> +      <i class="search icon"></i> +    </div> +    <div class="results"></div> +  </div> + +  <!-- Description (not an abstract) --> +  <div class="ui field"> +    <label>Description</label> +    <div class="field"> +      <label>Not an abstract...</label> +      <textarea rows="2"></textarea> +    </div> +  </div> + +  <!-- Primary/Original Language --> +  <div class="field"> +    <label>Primary Language</label> +    <select class="ui search select dropdown" id="language-select"> +      <option value="">Select if Appropriate</option> +      <option value="en">English</option> +      <option value="es">Spanish</option> +    </select> +  </div> + +  <!-- Subject / Categorization / Tags --> +  <div class="field"> +    <label>Subject</label> +    <select multiple="" class="ui dropdown" id="subjects"> +      <option value="">Select Subject/Tags</option> +      <option value="AF">Afghanistan</option> +      <option value="AX">Ă…land Islands</option> +      <option value="AL">Albania</option> +      <option value="DZ">Algeria</option> +      <option value="AS">American Samoa</option> +      <option value="AD">Andorra</option> +      <option value="AO">Angola</option> +    </select> +  </div> + + +  <h3 class="ui dividing header">Primary Release / Edition</h3> + +  <!-- Contributors (and how) --> +  <div class="ui field search" id="release_creators"> +    <label>Primary Creator(s)</label> +    <div class="ui icon input"> +      <input class="prompt" type="text" placeholder="Search..."> +      <i class="search icon"></i> +    </div> +    <div class="results"></div> +  </div> + +  <!-- Date --> +  <!-- Container / Part-Of --> +  <!-- Publisher --> +  <!-- Identifier --> +  <!-- Language --> +  <!-- Type / Media --> +  <!-- Issue / Volume / Pages / Chapter --> + +  <!-- Anything Else? --> +  <h3 class="ui dividing header">Anything Else?</h3> + +  <!-- File / Copy / URL --> +  <!-- Citations --> + +<div class="ui submit button">Create Work</div> +</form> + +</div> +{% endblock %} + +{% block postscript %} +<script> +<!-- Form validation code --> +$(document).ready(function() { + +  $('#add_work_form') +    .form({ +        fields: { +        name: { +            identifier: 'name', +            rules: [ +            { +                type   : 'empty', +                prompt : 'Please enter your name' +            } +            ] +        }, +        skills: { +            identifier: 'skills', +            rules: [ +            { +                type   : 'minCount[2]', +                prompt : 'Please select at least two skills' +            } +            ] +        }, +        gender: { +            identifier: 'gender', +            rules: [ +            { +                type   : 'empty', +                prompt : 'Please select a gender' +            } +            ] +        }, +        username: { +            identifier: 'username', +            rules: [ +            { +                type   : 'empty', +                prompt : 'Please enter a username' +            } +            ] +        }, +        password: { +            identifier: 'password', +            rules: [ +            { +                type   : 'empty', +                prompt : 'Please enter a password' +            }, +            { +                type   : 'minLength[6]', +                prompt : 'Your password must be at least {ruleValue} characters' +            } +            ] +        }, +        terms: { +            identifier: 'terms', +            rules: [ +            { +                type   : 'checked', +                prompt : 'You must agree to the terms and conditions' +            } +            ] +        } +        } +    }) +    ; + +  var example_authors = [ +    { title: 'Andorra' }, +    { title: 'United Arab Emirates' }, +    { title: 'Afghanistan' }, +    { title: 'Antigua' }, +    { title: 'Anguilla' }, +    { title: 'Albania' }, +    { title: 'Armenia' }, +    { title: 'Netherlands Antilles' }, +    { title: 'Angola' }, +    { title: 'Argentina' }, +    { title: 'American Samoa' }, +    { title: 'Austria' }, +    { title: 'Australia' }, +    { title: 'Aruba' }, +  ]; + +  $('#work_creators') +    .search({ +        source: example_authors +    }) +  ; + +  $('#release_creators') +    .search({ +        source: example_authors +    }) +  ; + +  $('#work_type').dropdown(); +  $('#subjects').dropdown(); +  $('#language-select').dropdown(); + +  console.log("Page loaded"); + +}); +</script> +{% endblock %} diff --git a/fatcat/templates/work_view.html b/fatcat/templates/work_view.html new file mode 100644 index 00000000..8c5e955d --- /dev/null +++ b/fatcat/templates/work_view.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% block body %} + +<h1>{{ work.title }}</h1> + +<p>Work type: {{ work.type }} +<p><a href="/work/{{ work.id }}/history">History</a> +<p>Contributors: +{% for c in work.contributors %} {{ c.name }}; {% endfor %} + +{% if primary %} +<h2>Primary Release/Edition</h2> +<p>Title: {{ primary.title }} +<p>Date: {{ primary.date }} + +{% if primary.container %} +<p>Container: <a href="/container/{{ primary.container.id }}">{{ primary.container.title }}</a> +{% endif %} + +{% if primary.doi %} +<p>DOI: <a href="https://dx.doi.org/{{ primary.doi }}">{{ primary.doi }}</a> +{% endif %} + +{% else %} +<p>No primary release +{% endif %} + +{% if releases %} +<ul> +{% for r in releases %} +  <ul><a href="/release/{{ r.id }}">{{ r.title }}</a> ({{ y.date }} - {{ y.release_type }}) +{% endfor %} +</ul> +{% else %} +{% endif %} + +{% endblock %} diff --git a/fatcat/test_backend.py b/fatcat/test_backend.py new file mode 100644 index 00000000..429b5ae7 --- /dev/null +++ b/fatcat/test_backend.py @@ -0,0 +1,53 @@ + +import os +import json +import backend +import unittest +import tempfile +from nose.tools import * + +# TODO: replace all these "assert" with unit test version (which displays left +# and right on failure) + +# TODO: http://alextechrants.blogspot.com/2013/08/unit-testing-sqlalchemy-apps.html + +## Helpers ################################################################## + +def check_entity_fields(e): +    for key in ('id', 'rev', 'previous', 'state', 'redirect_id', 'edit_id', +            'extra_json'): +        assert_in(key, e) +    for key in ('id', 'rev'): +        assert_is_not_none(e[key]) + +## API Tests ################################################################ + +class BackendTestCase(unittest.TestCase): + +    def setUp(self): +        backend.app.config['DATABASE_URI'] = 'sqlite://:memory:' +        backend.app.testing = True +        self.app = backend.app.test_client() + +    def test_health(self): +        rv = self.app.get('/health') +        obj = json.loads(rv.data.decode('utf-8')) +        assert obj['ok'] + +    def test_works(self): + +        # Invalid Id +        rv = self.app.get('/v0/work/_') +        assert rv.status_code == 404 + +        # Missing Id (TODO) +        #rv = self.app.get('/v0/work/rzga5b9cd7efgh04iljk') +        #assert rv.status is 404 + +        # Valid Id +        rv = self.app.get('/v0/work/r3zga5b9cd7ef8gh084714iljk') +        assert rv.status_code == 200 +        obj = json.loads(rv.data.decode('utf-8')) +        check_entity_fields(obj) +        assert obj['title'] +        assert_equal(obj['work_type'], "journal-article") diff --git a/fatcat/webface.py b/fatcat/webface.py new file mode 100755 index 00000000..33833e25 --- /dev/null +++ b/fatcat/webface.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +import os +import argparse +import requests +from flask import Flask, render_template, send_from_directory, request, \ +    url_for, abort, g, redirect, jsonify + +app = Flask(__name__) +app.config.from_object(__name__) + + +### Views ################################################################### + +@app.route('/health', methods=['GET']) +def health(): +    return jsonify({'ok': True}) + +@app.route('/work/create', methods=['GET']) +def work_create(): +    return render_template('work_add.html') + +@app.route('/work/random', methods=['GET']) +def work_random(): +    work = { +        "title": "Structure and Interpretation", +        "work_type": "book", +        "date": None, +        "contributors": [ +            {"name": "Alyssa P. Hacker"}, +        ], +        "primary": { +            "title": "Structure and Interpretation", +            "release_type": "online", +            "date": "2000-01-01", +            "doi": "10.491/599.sdo14", +        }, +        "releases": [ +        ] +    } +    return render_template('work_view.html', work=work, primary=work['primary']) + +@app.route('/work/<work_id>/random', methods=['GET']) +def work_view(work_id): +    return render_template('work_view.html') + + +### Static Routes ########################################################### + +@app.route('/', methods=['GET']) +def homepage(): +    return render_template('home.html') + +@app.route('/about', methods=['GET']) +def aboutpage(): +    return render_template('about.html') + +@app.route('/robots.txt', methods=['GET']) +def robots(): +    return send_from_directory(os.path.join(app.root_path, 'static'), +                               'robots.txt', +                               mimetype='text/plain') + + +### Entry Point ############################################################# + +def main(): +    parser = argparse.ArgumentParser() +    parser.add_argument('--debug', +        action='store_true', +        help="enable debugging interface") +    parser.add_argument('--host', +        default="127.0.0.1", +        help="listen on this host/IP") +    parser.add_argument('--port', +        type=int, +        default=5050, +        help="listen on this port") +    parser.add_argument('--backend-api', +        default="localhost:6060", +        help="backend API to connect to") +    args = parser.parse_args() + +    app.run(debug=args.debug, host=args.host, port=args.port) + +if __name__ == '__main__': +    main() | 
