rest api basics working, still some old crap here

master
Anton Lydike 4 years ago
commit f2e8e074b6

2
.gitignore vendored

@ -0,0 +1,2 @@
__pycache__
venv

2
.idea/.gitignore vendored

@ -0,0 +1,2 @@
# Default ignored files
/workspace.xml

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (family)" project-jdk-type="Python SDK" />
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/totpal.iml" filepath="$PROJECT_DIR$/.idea/totpal.iml" />
</modules>
</component>
</project>

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="Flask">
<option name="enabled" value="true" />
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.8 (family)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/templates" />
</list>
</option>
</component>
</module>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1,11 @@
FROM python:3.8
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD [ "python", "app.py" ]

@ -0,0 +1 @@
# Family history mapping tool

@ -0,0 +1,26 @@
from os import getenv
from flask import Flask
from flask_socketio import SocketIO
from server import setup
import eventlet
eventlet.monkey_patch()
app = Flask(__name__)
app.secret_key = getenv('APP_SECRET')
if not app.secret_key:
print("Please set a secret key via environment variable APP_SECRET")
import sys
sys.exit(1)
socketio = SocketIO(app, async_mode='eventlet')
setup(app, socketio)
if __name__ == '__main__':
print("starting")
socketio.run(app, debug=(getenv('DEV', 'false') == 'true'), port=int(getenv('PORT', 5000)), host='0.0.0.0')

@ -0,0 +1,11 @@
with import <nixpkgs> {};
stdenv.mkDerivation rec {
name = "totpal-run-env";
buildInputs = [
(python3.withPackages (ps: with ps; [
flask flask-socketio eventlet pylint
]))
];
}

@ -0,0 +1,31 @@
version: '3'
services:
neo4j:
image: neo4j:4.1
volumes:
- 'db-data:/data'
environment:
NEO4J_AUTH: neo4j/my_password
ports:
- '127.0.0.1:7474:7474'
- '127.0.0.1:7687:7687'
python:
build: .
environment:
PORT: 5000
NEO4J_HOST: 'neo4j'
NEO4J_USER: 'neo4j'
NEO4J_PW: 'my_password'
UPLOAD_DIR: '/run/uploads'
DEV: 'true'
APP_SECRET: 'dev'
volumes:
- 'uploads:/run/uploads'
- './:/usr/src/app'
ports:
- '127.0.0.1:5000:5000'
volumes:
db-data:
uploads:

@ -0,0 +1,4 @@
flask
flask-socketio
eventlet
neomodel

@ -0,0 +1,15 @@
from os import getenv
from neomodel import config
pw = getenv('NEO4J_PW')
user = getenv('NEO4J_USER')
host = getenv('NEO4J_HOST')
port = getenv('NEO4J_PORT', 7687)
config.DATABASE_URL = f'bolt://{user}:{pw}@{host}:{port}'
from .api import attach_to_flask
def setup(app, socketio):
attach_to_flask(app)

@ -0,0 +1,120 @@
import sys
from .models import *
from flask import Flask, request, jsonify
from typing import Dict, Type, List, Final
from datetime import datetime
class ApiError(BaseException):
def __init__(self, msg, status=400):
super().__init__()
self.msg = msg
self.status = status
def connect_callback(attr, target):
"""returns a callback to connect attr with target"""
return lambda: attr.connect(target)
def construct_object_from_request(cls, uid=None):
if uid:
obj: Final = cls.nodes.get_or_none(uid=uid)
if not obj:
raise ApiError(f"No {cls.__name__} found with uid {request.form.get('uid')}!", status=404)
else:
obj: Final = cls()
relationship_attach_callbacks = list()
for name, value in cls.__dict__.items():
if name[0] == '_' or not name[0].islower():
continue
cls_name = value.__class__.__name__
convert_with = None
if cls_name == 'StringProperty':
convert_with = str
elif cls_name == 'DateProperty':
convert_with = datetime.fromisoformat
elif cls_name == 'IntegerProperty':
convert_with = int
elif cls_name == 'FloatProperty':
convert_with = float
elif cls_name == 'RelationshipDefinition':
if name not in request.form:
continue
target_cls = value._raw_class
# convert class string to class by loading it from module object
if isinstance(target_cls, str):
target_cls = getattr(cls.__module__, target_cls)
for val in request.form.getlist(name):
target = target_cls.nodes.first_or_none(uid=val)
if not target:
raise ApiError(f'Cannot find referenced object uid={val} of type {target_cls.__name__}!')
relationship_attach_callbacks.append(connect_callback(getattr(obj, name), target))
if not convert_with:
# skip this property
continue
if name in request.form:
setattr(obj, name, convert_with(request.form.get(name)))
return obj, lambda: [c() for c in relationship_attach_callbacks]
def func_api_obj(cls: Type):
"""basic method for handling /api/type calls"""
if request.method == 'GET':
return jsonify([p.json(include_relations=False) for p in cls.nodes.all()])
if request.method == 'POST':
p, attach_relationships = construct_object_from_request(cls)
p.save()
attach_relationships()
return jsonify(p.json())
def func_api_obj_id(cls: Type, uid: str):
if request.method == 'GET':
person = Person.nodes.get_or_none(uid=uid)
if not person:
return "Not found", 404
return jsonify(person.json())
if request.method == 'PUT':
p, attach_relationships = construct_object_from_request(cls, uid=uid)
p.save()
attach_relationships()
return jsonify(p.json())
def attach_to_flask(app: Flask):
@app.route('/api/person', methods=['POST', 'GET'])
def func_api_person():
return func_api_obj(Person)
@app.route('/api/person/<uid>', methods=['PUT', 'GET'])
def func_api_person_id(uid):
return func_api_obj_id(Person, uid)
@app.route('/api/event', methods=['POST', 'GET'])
def func_api_event():
return func_api_obj(Event)
@app.route('/api/event/<uid>', methods=['PUT', 'GET'])
def func_api_event_id(uid):
return func_api_obj_id(Event, uid)
@app.route('/api/place', methods=['POST', 'GET'])
def func_api_place():
return func_api_obj(Place)
@app.route('/api/place/<uid>', methods=['PUT', 'GET'])
def func_api_place_id(uid):
return func_api_obj_id(Place, uid)

@ -0,0 +1 @@
from .models import Place, Person, Event, Image, VisitRel

@ -0,0 +1,71 @@
from neomodel import StructuredNode, StringProperty, RelationshipTo, RelationshipFrom, \
UniqueIdProperty, DateProperty, StructuredRel, RelationshipManager
from neomodel.cardinality import ZeroOrOne, ZeroOrMore, One, OneOrMore
class Jsonifiable(object):
def json(self, include_relations=True):
json = dict()
for name, value in vars(self).items():
if name == 'id':
continue
print(name, value)
if isinstance(value, RelationshipManager):
if not include_relations:
continue
if isinstance(value, (One, ZeroOrOne)):
val = value.get_or_none()
print(val)
json[name] = val.json(include_relations=False) if val else None
elif isinstance(value, (OneOrMore, ZeroOrMore)):
json[name] = [val.json(include_relations=False) for val in value.all()]
else:
json[name] = value
return json
class VisitRel(StructuredRel, Jsonifiable):
title = StringProperty(required=True)
description = StringProperty()
date = DateProperty()
class Person(StructuredNode, Jsonifiable):
uid = UniqueIdProperty()
name = StringProperty(required=True)
father = RelationshipTo('Person', 'FATHER', cardinality=ZeroOrOne)
mother = RelationshipTo('Person', 'MOTHER', cardinality=ZeroOrOne)
birthdate = DateProperty()
deceased = DateProperty()
description = StringProperty()
picture = StringProperty()
places = RelationshipTo('Place', 'VISITOR', ZeroOrMore, model=VisitRel)
class Event(StructuredNode, Jsonifiable):
uid = UniqueIdProperty()
name = StringProperty(required=True)
participants = RelationshipTo(Person, 'PARTICIPANT', cardinality=ZeroOrMore)
places = RelationshipTo('Place', 'HAPPENED_AT', cardinality=ZeroOrMore)
date = DateProperty()
end_date = DateProperty()
related_events = RelationshipTo('Event', 'RELATED_EVENT')
class Place(StructuredNode, Jsonifiable):
uid = UniqueIdProperty()
name = StringProperty(required=True)
description = StringProperty()
visitors = RelationshipFrom(Person, 'VISITOR', ZeroOrMore, model=VisitRel)
class Image(StructuredNode, Jsonifiable):
uid = StringProperty(unique_index=True, required=True)
description = StringProperty()
pictured = RelationshipTo(Person, 'PICTURED')
place = RelationshipTo(Place, 'TAKEN_AT')
date = DateProperty()

@ -0,0 +1,21 @@
with import <nixpkgs> {};
let
extensions = (with pkgs.vscode-extensions; [
ms-python.python
]);
vscode-with-extensions = pkgs.vscode-with-extensions.override {
vscodeExtensions = extensions;
};
in
stdenv.mkDerivation rec {
name = "totpal-dev-env";
buildInputs = [
(python3.withPackages (ps: with ps; [
flask flask-socketio eventlet pylint
]))
git
openjdk11
jetbrains.pycharm-professional
];
}

@ -0,0 +1,20 @@
const $ = sel => document.querySelector(sel)
const $$ = sel => Array.from(document.querySelectorAll(sel))
const create_elm = str => {
const div = document.createElement('div')
div.innerHTML = str
return Array.from(div.childNodes)
}
const flash_msg = (message) => {
const elm = create_elm('<div class="flash-msg flex"><span class="flex-grow">' + message + '</span> <span class="close-btn">×</span></div>')[0]
$('main .container').prepend(elm)
elm.querySelector('.close-btn').addEventListener('click', e => elm.remove())
}
onload(() => {
// remove flash messages when close button is clicked
$$('.flash-msg .close-btn').forEach(elm => elm.addEventListener('click', e => {
elm.parentElement.remove()
}))
})

@ -0,0 +1,32 @@
onload(() => {
const socket = io.connect(document.location.host);
socket.on('flash_msg', flash_msg);
socket.on('player_state', (data) => {
if (!data.secret) {
// our secret seems to be to old
localStorage.removeItem('secret');
return register();
}
//flash_msg("Logged in!");
localStorage.setItem('secret', data.secret);
localStorage.setItem('name', data.username);
document.cookie = `secret=${data.secret}`;
window.username = data.username;
$('.username').innerText = 'playing as ' + window.username;
});
function register() {
const name = localStorage.getItem('name') || prompt("Whats your username?");
socket.emit('signup', name)
}
if (!localStorage.getItem('secret')) {
register()
} else {
socket.emit('resume', localStorage.getItem('secret'))
}
window.socket = socket;
});

@ -0,0 +1,83 @@
html, body {
width: 100%;
margin: 0;
padding: 0;
font-size: 16px;
display: flex;
flex-direction: column;
min-height: 100vh;
font-family: 'roboto', 'open-sans', sans-serif;
}
.flex {
display: flex;
}
.flex-grow {
flex-grow: 1;
}
.container {
width: 70%;
max-width: 1080px;
margin: auto;
}
footer {
padding: 8px 0;
}
/* specific styles */
html {
background: rgb(35, 36, 37);
color: #fff;
}
nav {
background: rgb(22, 22, 22);
margin-bottom: 32px;
font-size: 24px;
}
nav h1 {
padding: 0px;
margin: 0px;
font-size: inherit;
}
nav h1 span {
font-weight: 400;
}
nav .container div {
padding: 16px 0;
}
nav .container .username {
padding: 16px;
font-weight: 300;
}
a {
color: rgb(85, 150, 255);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.flash-msg {
border: 1px solid;
padding: 8px 16px;
border-radius: 3px;
margin: 16px;
margin-top: -16px;
margin-bottom: 32px;
background: rgb(48, 50, 51)
}
.flash-msg .close-btn {
cursor: pointer;
}

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script>(() => {onload_list = []; window.onload = (f) => {onload_list.push(f)}; window.__is_loaded = () => {onload_list.forEach(x => x());window.__is_loaded = () => undefined} })()</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.1/socket.io.js"></script>
<meta charset="UTF-8">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TOTPAL - {{ title }}</title>
<script src="{{ url_for('static', filename='site.js') }}"></script>
{% block head %}
{% endblock %}
</head>
<body>
<nav><div class="container flex">
{% block nav %}
<div class="">
<h1>TOTPAL <span>{{ title }}</span> </h1>
</div>
<div class="username flex-grow"></div>
<div>
{% block links %}
<a href="/">Home</a>
{% endblock %}
</div>
{% endblock %}
</div></nav>
<main class="flex-grow">
<div class="container">
{% for message in get_flashed_messages() %}
<div class="flash-msg flex"><span class="flex-grow">{{ message | safe }}</span> <span class="close-btn">&times;</span></div>
{% endfor %}
{% block content %}
No content
{% endblock %}
</div>
</main>
<footer>
<div class="container">
{% block footer %}
&copy; Copyright by AntonLydike - play at your own risk
{% endblock %}
</div>
</footer>
<script>
if (__is_loaded) {__is_loaded()}
</script>
</body>
</html>

@ -0,0 +1,6 @@
{% extends 'base.html' %}
{% block content %}
<h1>Error: {{ name }}</h1>
<p>{{ message }}</p>
{% endblock %}

@ -0,0 +1,78 @@
{% extends 'base.html' %}
{% block head %}
<script src="{{ url_for('static', filename='socket.js') }}"></script>
{% endblock %}
{% block content %}
<h1>Game phase: <span class="game-phase">None</span></h1>
<h2>Players:</h2>
<ul class="player-list"></ul>
<div class="state-show state-searching">
<button class="start-game">Start game</button>
</div>
<div class="state-show state-selecting">
<p>The player is <span class="guesser-name"></span></p>
<form id="select-url">
<input type="text" name="url" placeholder="url"/>
<input type="text" name="title" placeholder="title"/>
<button type="submit">Select</button>
</form>
</div>
<div class="state-show state-guessing state-resolution">
<p>The articles title is: <span class="article-title"></span>...</p>
<button class="show-resolution state-guessing state-show">We are done guessing</button>
</div>
<div class="state-show state-resolution">
<p>The article was submitted by <span class="selected-player"></span>!</p>
</div>
<script>
onload(() => {
socket.on('room_state', update_state);
function update_state(state) {
console.log(state);
print_players(state);
$('.article-title').innerText = state.article;
$('.selected-player').innerText = state.selected_player ? state.selected_player.name : '';
$('.game-phase').innerText = state.state.toLowerCase();
$('.guesser-name').innerText = state.guesser ? state.guesser.name : '';
$$('.state-show').forEach(elm => {
elm.hidden = !elm.classList.contains('state-' + state.state.toLowerCase());
})
}
function print_players(state) {
const root = $('.player-list');
root.innerHTML = "";
state.players.forEach(player => {
const elm = create_elm(`<li>${(player.online ? '&#9679;' : '&#9675;')} ${player.name}</li>`)[0]
root.appendChild(elm)
})
}
update_state({{ room | safe }});
$('form#select-url').addEventListener('submit', e => {
e.preventDefault();
let url = e.target.elements.url.value;
let title = e.target.elements.title.value;
socket.emit('add_article', title, url)
});
$('button.start-game').addEventListener('click', e => {
socket.emit('start_selecting')
});
$('button.show-resolution').addEventListener('click', e => {
socket.emit('start_resolving')
});
})
</script>
{% endblock %}

@ -0,0 +1,42 @@
{% extends 'base.html' %}
{% block head %}
<script src="{{ url_for('static', filename='socket.js') }}"></script>
{% endblock %}
{% block content %}
<h1>Games waiting:</h1>
<form action="/room" method="POST" id="create-room">
<input type="text" placeholder="Name" name="name">
<button class="create-room" type="submit">Create game</button>
<input type="hidden" name="secret" class="insert-secret"/>
</form>
<ul class="active-lobbies"></ul>
<script>
onload(() => {
socket.on('room_list', draw_rooms);
function draw_rooms(list) {
console.log(list);
const root = $('.active-lobbies');
root.innerHTML = "";
list.forEach(room => {
const elm = create_elm(`<li>${room.name} with ${room.players.map(x => (x.online ? '&#9679; ' : '&#9675; ') + x.name).join(", ")} <a href="/room/${room.id}">JOIN</a></li>`)[0]
root.appendChild(elm)
})
}
$('form#create-room').addEventListener('submit', e => {
e.target.elements.secret.value = localStorage.getItem('secret')
});
draw_rooms({{ rooms | safe }})
})
</script>
{% endblock %}
Loading…
Cancel
Save