summaryrefslogtreecommitdiffstats
path: root/fatcat
diff options
context:
space:
mode:
Diffstat (limited to 'fatcat')
-rwxr-xr-xfatcat/backend.py301
-rw-r--r--fatcat/static/robots.txt1
-rw-r--r--fatcat/templates/base.html70
-rw-r--r--fatcat/templates/home.html32
-rw-r--r--fatcat/templates/work_add.html215
-rw-r--r--fatcat/templates/work_view.html37
-rw-r--r--fatcat/test_backend.py53
-rwxr-xr-xfatcat/webface.py87
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()