initial broken commit

This commit is contained in:
dave 2018-10-08 22:31:05 -07:00
commit e96c0f533c
31 changed files with 2891 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
__pycache__
build/
dist/
node_modules/
nodepupper.egg-info/
pupper.db*
styles/css/
styles/dist/
styles/mincss/
testenv/

13
checkdb.py Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env python3
import ZODB
import ZODB.FileStorage
def main():
storage = ZODB.FileStorage.FileStorage("pupper.db")
db = ZODB.DB(storage)
for k, v in db.open().root.nodes.items():
print(k, v.name, ":", v, "\n\t", v.body, "\n")
if __name__ == "__main__":
main()

46
gruntfile.js Normal file
View File

@ -0,0 +1,46 @@
module.exports = function(grunt) {
grunt.initConfig({
less: {
website: {
files: {
'styles/css/main.css': 'styles/less/main.less'
}
}
},
cssmin: {
website: {
files: {
'styles/mincss/pure.css': 'node_modules/purecss/build/pure.css',
'styles/mincss/grids-responsive-min.css': 'node_modules/purecss/build/grids-responsive.css',
'styles/mincss/main.css': 'styles/css/main.css'
}
}
},
concat: {
dist: {
src: [
'styles/mincss/pure.css',
'styles/mincss/grids-responsive-min.css',
'styles/mincss/main.css'
],
dest: 'styles/dist/style.css',
},
},
watch: {
less: {
files: ['styles/less/{,*/}*.less'],
tasks: ['less:website', 'cssmin:website', 'concat:dist'],
options: {
spawn: false
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.registerTask('default', ['less:website', 'cssmin:website', 'concat:dist']);
};

0
nodepupper/__init__.py Normal file
View File

7
nodepupper/common.py Normal file
View File

@ -0,0 +1,7 @@
import hashlib
def pwhash(password):
h = hashlib.sha256()
h.update(password.encode("UTF-8"))
return h.hexdigest()

212
nodepupper/daemon.py Normal file
View File

@ -0,0 +1,212 @@
import os
import cherrypy
import logging
from datetime import datetime, timedelta
from nodepupper.nodeops import NodeOps, NObject, NClass, NClassAttachment
from jinja2 import Environment, FileSystemLoader, select_autoescape
from nodepupper.common import pwhash
import math
from urllib.parse import urlparse
APPROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))
def auth():
"""
Return the currently authorized username (per request) or None
"""
return cherrypy.session.get('authed', None)
def require_auth(func):
"""
Decorator: raise 403 unless session is authed
"""
def wrapped(*args, **kwargs):
if not auth():
raise cherrypy.HTTPError(403)
return func(*args, **kwargs)
return wrapped
def slugify(words):
return ''.join(letter for letter in '-'.join(words.lower().split())
if ('a' <= letter <= 'z') or ('0' <= letter <= '9') or letter == '-')
class AppWeb(object):
def __init__(self, nodedb, template_dir):
self.nodes = nodedb
self.tpl = Environment(loader=FileSystemLoader(template_dir),
autoescape=select_autoescape(['html', 'xml']))
self.tpl.filters.update(basename=os.path.basename,
ceil=math.ceil,
statusstr=lambda x: str(x).split(".")[-1])
self.node = NodesWeb(self)
self.classes = ClassWeb(self)
def render(self, template, **kwargs):
"""
Render a template
"""
return self.tpl.get_template(template).render(**kwargs, **self.get_default_vars())
def get_default_vars(self):
"""
Return a dict containing variables expected to be on every page
"""
with self.nodes.db.transaction() as c:
ret = {
"classes": c.root.classes,
# "all_albums": [],
"path": cherrypy.request.path_info,
"auth": True or auth()
}
return ret
@cherrypy.expose
def node_edit(self, node=None, op=None, body=None, fqdn=None):
print(op, body, fqdn)
if op in ("Edit", "Create") and body and fqdn:
with self.nodes.db.transaction() as c:
obj = c.root.nodes[fqdn] if fqdn in c.root.nodes else NObject(fqdn, body)
obj.body = body
c.root.nodes[fqdn] = obj
raise cherrypy.HTTPRedirect("node/{}".format(fqdn), 302)
with self.nodes.db.transaction() as c:
yield self.render("node_edit.html", node=c.root.nodes.get(node, None))
@cherrypy.expose
def index(self):
"""
"""
with self.nodes.db.transaction() as c:
yield self.render("nodes.html", nodes=c.root.nodes.values())
# raise cherrypy.HTTPRedirect('feed', 302)
@cherrypy.expose
def login(self):
"""
/login - enable super features by logging into the app
"""
cherrypy.session['authed'] = cherrypy.request.login
dest = "/feed" if "Referer" not in cherrypy.request.headers \
else urlparse(cherrypy.request.headers["Referer"]).path
raise cherrypy.HTTPRedirect(dest, 302)
@cherrypy.expose
def logout(self):
"""
/logout
"""
cherrypy.session.clear()
dest = "/feed" if "Referer" not in cherrypy.request.headers \
else urlparse(cherrypy.request.headers["Referer"]).path
raise cherrypy.HTTPRedirect(dest, 302)
@cherrypy.expose
def error(self, status, message, traceback, version):
yield self.render("error.html", status=status, message=message, traceback=traceback)
@cherrypy.popargs("node")
class NodesWeb(object):
def __init__(self, root):
self.root = root
self.nodes = root.nodes
self.render = root.render
@cherrypy.expose
def index(self, node):
with self.nodes.db.transaction() as c:
yield self.render("node.html", node=c.root.nodes[node])
@cherrypy.popargs("cls")
class ClassWeb(object):
def __init__(self, root):
self.root = root
self.nodes = root.nodes
self.render = root.render
@cherrypy.expose
def index(self, cls):
# with self.nodes.db.transaction() as c:
yield self.render("classes.html")
@cherrypy.expose
def op(self, cls, op=None, name=None):
# with self.nodes.db.transaction() as c:
yield self.render("classes.html")
def main():
import argparse
import signal
parser = argparse.ArgumentParser(description="Photod photo server")
parser.add_argument('-p', '--port', default=8080, type=int, help="tcp port to listen on")
# parser.add_argument('-l', '--library', default="./library", help="library path")
# parser.add_argument('-c', '--cache', default="./cache", help="cache path")
parser.add_argument('-s', '--database', default="./pupper.db", help="path to persistent sqlite database")
parser.add_argument('--debug', action="store_true", help="enable development options")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO if args.debug else logging.WARNING,
format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s")
library = NodeOps(args.database)
tpl_dir = os.path.join(APPROOT, "templates") if not args.debug else "templates"
web = AppWeb(library, tpl_dir)
def validate_password(realm, username, password):
s = library.session()
if s.query(User).filter(User.name == username, User.password == pwhash(password)).first():
return True
return False
cherrypy.tree.mount(web, '/', {'/': {'tools.trailing_slash.on': False,
'error_page.403': web.error,
'error_page.404': web.error},
'/static': {"tools.staticdir.on": True,
"tools.staticdir.dir": os.path.join(APPROOT, "styles/dist")
if not args.debug else os.path.abspath("styles/dist")},
'/login': {'tools.auth_basic.on': True,
'tools.auth_basic.realm': 'webapp',
'tools.auth_basic.checkpassword': validate_password}})
cherrypy.config.update({
'tools.sessions.on': True,
'tools.sessions.locking': 'explicit',
'tools.sessions.timeout': 525600,
'request.show_tracebacks': True,
'server.socket_port': args.port,
'server.thread_pool': 25,
'server.socket_host': '0.0.0.0',
'server.show_tracebacks': True,
'log.screen': False,
'engine.autoreload.on': args.debug
})
def signal_handler(signum, stack):
logging.critical('Got sig {}, exiting...'.format(signum))
cherrypy.engine.exit()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
try:
cherrypy.engine.start()
cherrypy.engine.block()
finally:
logging.info("API has shut down")
cherrypy.engine.exit()
if __name__ == '__main__':
main()

45
nodepupper/nodeops.py Normal file
View File

@ -0,0 +1,45 @@
import ZODB
import ZODB.FileStorage
import persistent
import persistent.list
import persistent.mapping
import BTrees.OOBTree
def plist():
return persistent.list.PersistentList()
def pmap():
return persistent.mapping.PersistentMapping()
class NObject(persistent.Persistent):
def __init__(self, fqdn, body):
self.fqdn = fqdn
self.parents = plist()
self.classes = pmap()
self.body = body
class NClass(persistent.Persistent):
def __init__(self, name):
self.name = name
class NClassAttachment(persistent.Persistent):
def __init__(self, cls, conf):
self.cls = cls
self.conf = conf
class NodeOps(object):
def __init__(self, db_path):
self.storage = ZODB.FileStorage.FileStorage(db_path)
self.db = ZODB.DB(self.storage)
with self.db.transaction() as c:
if "nodes" not in c.root():
c.root.nodes = BTrees.OOBTree.BTree()
if "classes" not in c.root():
c.root.classes = BTrees.OOBTree.BTree()

1528
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "nodepupper",
"version": "1.0.0",
"description": "",
"main": null,
"dependencies": {
"grunt": "^1.0.3",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-cssmin": "^3.0.0",
"grunt-contrib-less": "^2.0.0",
"grunt-contrib-watch": "^1.1.0",
"purecss": "^1.0.0"
},
"devDependencies": {},
"scripts": {},
"author": "",
"license": "ISC"
}

19
requirements.txt Normal file
View File

@ -0,0 +1,19 @@
backports.functools-lru-cache==1.5
BTrees==4.5.1
cheroot==6.5.2
CherryPy==18.0.1
jaraco.functools==1.20
Jinja2==2.10
MarkupSafe==1.0
more-itertools==4.3.0
persistent==4.4.2
portend==2.3
pytz==2018.5
six==1.11.0
tempora==1.13
transaction==2.2.1
zc.lockfile==1.3.0
ZConfig==3.3.0
ZODB==5.4.0
zodbpickle==1.0.2
zope.interface==4.5.0

3
run.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
PYTHONPATH=. python3 nodepupper/daemon.py -p 8040 --debug

26
setup.py Normal file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
from setuptools import setup
__version__ = "0.0.0"
setup(name='nodepupper',
version=__version__,
description='very good boy',
url='',
author='dpedu',
author_email='dave@davepedu.com',
packages=['nodepupper'],
install_requires=[],
entry_points={
"console_scripts": [
"nodepupperd = nodepupper.daemon:main"
]
},
include_package_data=True,
package_data={'nodepupper': ['../templates/*.html',
'../templates/fragments/*.html',
'../styles/dist/*']},
zip_safe=False)

283
styles/less/main.less Normal file
View File

@ -0,0 +1,283 @@
* {
box-sizing: border-box;
}
@linkcolor: #1b98f8;
a {
text-decoration: none;
color: @linkcolor;
}
#layout, #nav, #list, #main {
margin: 0;
padding: 0;
}
/* make the navigation 100% width on phones */
#nav {
width: 100%;
height: 40px;
position: relative;
background: rgb(37, 42, 58);
text-align: center;
.user-status {
padding: 25px 0px;
color: #fff;
span {
font-family: monospace;
}
}
/* show the "Menu" button on phones */
.nav-menu-button {
display: block;
top: 0.5em;
right: 0.5em;
position: absolute;
}
&.active {
height: 80%;
}
}
/* when "Menu" is clicked, the navbar should be 80% height */
/* don't show the navigation items until the "Menu" button is clicked */
.nav-inner {
display: none;
}
#nav.active .nav-inner {
display: block;
padding: 2em 0;
}
#nav {
.pure-menu {
background: transparent;
border: none;
text-align: left;
}
.pure-menu-link:hover,
.pure-menu-link:focus {
background: rgb(55, 60, 90);
}
.pure-menu-link {
color: #fff;
margin-left: 0.5em;
}
.pure-menu-heading {
border-bottom: none;
font-size:110%;
color: rgb(75, 113, 151);
}
}
.tag-icon {
width: 15px;
height: 15px;
display: inline-block;
margin-right: 0.5em;
border-radius: 3px;
}
.tag-icon-mod-6-0 {
background: #ffc94c;
}
.tag-icon-mod-6-1 {
background: #41ccb4;
}
.tag-icon-mod-6-2 {
background: #40c365;
}
.tag-icon-mod-6-3 {
background: #ffc900;
}
.tag-icon-mod-6-4 {
background: #4100b4;
}
.tag-icon-mod-6-5 {
background: #00c365;
}
.email-content {
padding: 0em 4em;
}
.email-content-header, .email-content-body, .email-content-footer {
padding: 1em 0em;
}
.email-content-header {
border-bottom: 1px solid #ddd;
}
.email-content-title {
margin: 0.5em 0 0;
}
.email-content-subtitle {
font-size: 1em;
margin: 0;
font-weight: normal;
}
.email-content-controls {
margin-top: 2em;
text-align: right;
.secondary-button {
margin-bottom: 0.3em;
}
form {
display: inline-block;
}
}
.email-avatar {
width: 40px;
height: 40px;
}
@media (min-width: 40em) {
#layout {
padding-left: 300px; /* "left col (nav + list)" width */
position: relative;
}
/* These are position:fixed; elements that will be in the left 500px of the screen */
#nav, #list {
position: fixed;
top: 0;
bottom: 0;
overflow: auto;
}
#nav {
margin-left:-300px; /* "left col (nav + list)" width */
width:300px;
height: 100%;
}
.nav-inner {
display: block;
padding: 1.5em 0;
}
#nav .nav-menu-button {
display: none;
}
#main {
position: fixed;
top: 33%;
right: 0;
bottom: 0;
left: 150px;
overflow: auto;
}
}
@media (min-width: 60em) {
/* This will now take up it's own column, so don't need position: fixed; */
#main {
position: static;
margin: 0;
padding: 0;
}
}
.photo-feed {
.photo {
float: left;
width: 250px;
height: 250px; // matches the "feed" thumb size in library.py
overflow: hidden;
margin: 0px 20px 20px 0px;
img {
display: block;
}
&:hover {
border: 3px solid #252a3a;
width: 256px;
height: 256px;
margin: -3px 17px 17px -3px;
}
}
.feed-divider {
clear: both;
padding: 10px 0px;
}
}
.photo-view {
.photo-preview img {
display: block;
width: 100%;
max-height: 100%;
box-sizing: border-box;
}
.photo-info {
padding: 0px 0px 0px 25px;
}
.photo-formats {
ul {
padding: 0;
}
li {
margin-bottom: 15px;
}
img {
// float: left;
max-width: 100px;
}
}
.photo-tags {
h2 a {
font-weight: normal;
font-size: 14px;
}
}
}
.photo-tagging {
img {
float: left;
padding: 0px 20px 20px 0px;
}
}
.tags-picker {
padding: 0;
li {
display: inline-block;
padding-right: 25px;
}
input.submit-link {
padding: 0;
border: 0;
background: none;
color: @linkcolor;
&:hover {
text-decoration: underline;
}
}
}
ul.pager {
padding: 0;
li {
display: inline-block;
&.current {
font-weight: bold;
}
}
}
.date-feed {
a {
padding-right: 15px;
}
.many {
font-weight: bold;
}
}

57
templates/album.html Normal file
View File

@ -0,0 +1,57 @@
{% extends "page.html" %}
{% block title %}{{ tag.title or tag.name }}{% endblock %}
{% block subtitle %}{{ tag.description }}{% endblock %}
{% block buttons %}
<form action="/tag/{{ tag.uuid }}/op" method="post">
{% if tag.is_album %}<input type="submit" class="secondary-button pure-button" name="op" value="Demote to tag" />{% else %}
<input type="submit" class="secondary-button pure-button" name="op" value="Promote to album" />{% endif %}
<input type="submit" class="secondary-button pure-button" name="op" value="Make all public" />
<input type="submit" class="secondary-button pure-button" name="op" value="Make all private" />
<input type="submit" class="secondary-button pure-button" name="op" value="Delete tag" />
</form>
<a href="/map?zoom=6&a={{ tag.uuid }}"><button class="secondary-button pure-button">Map</button></a>
<a href="/tag/{{ tag.uuid }}/edit"><button class="secondary-button pure-button">Edit</button></a>
{% endblock %}
{% block body %}
{% set locals = namespace() %}
{% set total_pages = (total_items/pgsize)|ceil %}
<div class="photo-feed">
{% set locals.im_date = "" %}
{% for item in images %}
{% set newdate = item.date.strftime("%b %d, %Y") %}
{% if newdate != locals.im_date %}
{% set locals.im_date = newdate %}
<div class="feed-divider year"><h4>{{ locals.im_date }}</h4></div>
{% endif %}
{% include "fragments/feed-photo.html" %}
{% endfor %}
<br style="clear:both" />
<div class="pager">
<h6>Page</h6>
{% if page > 0 %}
<div class="nav-prev">
<a href="{{path}}?page={{ page - 1 }}">Previous</a>
</div>
{% endif %}
<div class="pages">
<ul class="pager">
{% for pgnum in range(0, total_pages) %}
<li{% if pgnum == page %} class="current"{% endif %}>
<a href="{{path}}?page={{ pgnum }}">{{ pgnum }}</a>
</li>
{% endfor %}
</ul>
</div>
{% if page + 1 < total_pages %}
<div class="nav-next">
<a href="{{path}}?page={{ page + 1 }}">Next</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}

22
templates/classes.html Normal file
View File

@ -0,0 +1,22 @@
{% extends "page.html" %}
{% block title %}All classes{% endblock %}
{% block subtitle %}<a href="/node_edit">Add</a>{% endblock %}
{% block buttons %}{% endblock %}
{% block body %}
<div class="classes-all">
{% for node in classes %}
<div class="class">
<h2>{{ node.fqdn }}</h2>
<p>
<a href="/node/{{ node.fqdn }}">view</a> <a href="/node_edit?node={{ node.fqdn }}">edit</a>
</p>
<pre>
{{ node.body }}
</pre>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends "page.html" %}
{% block title %}Tagging {{ num_photos }} photo{% if num_photos > 1 %}s{% endif %}{% endblock %}
{% block subtitle %}{% endblock %}
{% block buttons %}{% endblock %}
{% block body %}
{% set preview = 9 %}
<div class="photo-tagging pure-g">
<div class="current-tags pure-u-1-3">
<h2>Images ({{ num_photos }})</h2>
<div>
{% for image in images %}{% if loop.index <= preview %}
<a href="/photo/{{ image.uuid }}">
<img src="/thumb/set/small/{{ image.uuid }}.jpg" />
</a>
{% endif %}{% endfor %}
</div>
{% if num_photos > preview %}<br clear="both" /><p>...and {{ num_photos - preview }} more</p>{% endif %}
<h2>Current Tags</h2>
<ul class="tags-picker">
{% for tagi in images[0].tags %}
<li>
<form action="/create_tags" method="post">
{% if fromdate %}<input type="hidden" name="fromdate" value="{{ fromdate }}" />{% endif %}
{% if uuid %}<input type="hidden" name="uuid" value="{{ uuid }}" />{% endif %}
<input type="hidden" name="remove" value="{{ tagi.tag.uuid }}" />
<input class="submit-link" type="submit" value="{{ tagi.tag.name }}" />
</form>
</li>
{% endfor %}
</ul>
</div>
<div class="all-tags pure-u-1-3">
<h2>All tags</h2>
<ul class="tags-picker">
{% for tag in alltags %}
<li>
<form action="/create_tags" method="post">
{% if fromdate %}<input type="hidden" name="fromdate" value="{{ fromdate }}" />{% endif %}
{% if uuid %}<input type="hidden" name="uuid" value="{{ uuid }}" />{% endif %}
<input type="hidden" name="tag" value="{{ tag.uuid }}" />
<input class="submit-link" type="submit" value="{{ tag.name }}" />
</form>
</li>
{% endfor %}
</ul>
</div>
<div class="add-tags pure-u-1-3">
<h2>Add tag</h2>
<form action="/create_tags" method="post" class="pure-form">
{% if fromdate %}<input type="hidden" name="fromdate" value="{{ fromdate }}" />{% endif %}
{% if uuid %}<input type="hidden" name="uuid" value="{{ uuid }}" />{% endif %}
<input type="text" name="newtag" placeholder="new tag name" />
<input type="submit" value="Add" class="pure-button pure-button-primary" />
</form>
</div>
</div>
{% endblock %}

55
templates/date.html Normal file
View File

@ -0,0 +1,55 @@
{% extends "page.html" %}
{% block title %}{{ "Photos on {}".format(date.strftime("%b %d, %Y")) }}{% endblock %}
{% block subtitle %}{% endblock %}
{% block buttons %}
<form action="/create_tags" method="post">
<input type="hidden" name="fromdate" value="{{ date.strftime("%Y-%m-%d") }}">
<input type="submit" class="secondary-button pure-button" value="Tag all" />
</form>
{% endblock %}
{% block body %}
{% set locals = namespace() %}
{% set total_pages = (total_sets/pgsize)|ceil %}
<div class="photo-feed">
{% set locals.im_date = "" %}
{% for item in images %}
{% set newdate = item.date.strftime("%b %d, %Y") %}
{% if newdate != locals.im_date %}
{% set locals.im_date = newdate %}
<div class="feed-divider year"><h4>{{ locals.im_date }}</h4></div>
{% endif %}
{% include "fragments/feed-photo.html" %}
{% endfor %}
<br style="clear:both" />
<div class="pager">
<h6>Page</h6>
{% if page > 0 %}
<div class="nav-prev">
<a href="{{path}}?page={{ page - 1 }}">Previous</a>
</div>
{% endif %}
<div class="pages">
<ul class="pager">
{% for pgnum in range(0, total_pages) %}
<li{% if pgnum == page %} class="current"{% endif %}>
<a href="{{path}}?page={{ pgnum }}">{{ pgnum }}</a>
</li>
{% endfor %}
</ul>
</div>
{% if page + 1 < total_pages %}
<div class="nav-next">
<a href="{{path}}?page={{ page + 1 }}">Next</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}

29
templates/dates.html Normal file
View File

@ -0,0 +1,29 @@
{% extends "page.html" %}
{% block title %}Photo Dates{% endblock %}
{% block subtitle %}Count per day{% endblock %}
{% block buttons %}
xxx
{% endblock %}
{% block body %}
{% set locals = namespace() %}
<div class="date-feed">
{% set locals.year = "" %}
{% set locals.month = "" %}
{% for item, date, count, _, _, _ in images %}
{% if item.date.year != locals.year %}
{% set locals.year = item.date.year %}
<div class="feed-divider year"><h4>{{ item.date.year }}</h4></div>
{% endif %}
{% if item.date.month != locals.month %}
{% set locals.month = item.date.month %}
<div class="feed-divider month"><h4>{{ item.date.strftime("%B") }}</h4></div>
{% endif %}
<a class="date-item{% if count > 50 %} many{% endif %}" href="/date/{{ date }}">{{ date }} ({{ count }})</a>
{% endfor %}
</div>
{% endblock %}

17
templates/error.html Normal file
View File

@ -0,0 +1,17 @@
{% extends "page.html" %}
{% block title %}{{ status }}{% endblock %}
{% block subtitle %}{% endblock %}
{% block buttons %}{% endblock %}
{% block body %}
<div class="photo-feed">
<div>
<p>{{ message }}</p>
</div>
<div>
<pre>{{ traceback }}</pre>
</div>
</div>
{% endblock %}

26
templates/feed.html Normal file
View File

@ -0,0 +1,26 @@
{% extends "page.html" %}
{% block title %}Photos by date{% endblock %}
{% block subtitle %}By date, descending{% endblock %}
{% block buttons %}{% endblock %}
{% block body %}
{% set locals = namespace() %}
{% set total_pages = (total_sets/pgsize)|ceil %}
<div class="photo-feed">
{% set locals.im_date = "" %}
{% for item in images %}
{% set newdate = item.date.strftime("%b %d, %Y") %}
{% if newdate != locals.im_date %}
{% set locals.im_date = newdate %}
<div class="feed-divider year"><h4>{{ locals.im_date }}</h4></div>
{% endif %}
{% include "fragments/feed-photo.html" %}
{% endfor %}
<br style="clear:both" />
{% include "pager.html" %}
</div>
{% endblock %}

View File

@ -0,0 +1,5 @@
<div class="photo">
<a href="/photo/{{ item.slug or item.uuid }}">
<img src="/thumb/set/feed/{{ item.uuid }}.jpg" />
</a>
</div>

35
templates/map.html Normal file
View File

@ -0,0 +1,35 @@
{% extends "page.html" %}
{% block title %}Photo map{% endblock %}
{% block subtitle %}GPS data{% endblock %}
{% block buttons %}{% endblock %}
{% block body %}
<div class="photo-map">
<div id="mapdiv" style="height: 900px"></div>
<script src="http://www.openlayers.org/api/OpenLayers.js"></script>
<script>
<!-- https://wiki.openstreetmap.org/wiki/OpenLayers_Marker_Example -->
var points = [
{%- for item in images -%}
[{{item.lon}}, {{item.lat}}],
{%- endfor -%}
]
var map = new OpenLayers.Map("mapdiv");
map.addLayer(new OpenLayers.Layer.OSM());
var markers = new OpenLayers.Layer.Markers( "Markers" );
for(var i=0;i<points.length;i++) {
var point = points[i]
var lonLat = new OpenLayers.LonLat(point[0], point[1])
.transform(new OpenLayers.Projection("EPSG:4326"),
map.getProjectionObject());
var marker = new OpenLayers.Marker(lonLat)
markers.addMarker(marker);
}
map.addLayer(markers);
var zoom={{ zoom or 3 }};
map.setCenter(lonLat, zoom);
</script>
</div>
{% endblock %}

36
templates/monthly.html Normal file
View File

@ -0,0 +1,36 @@
{% extends "page.html" %}
{% block title %}Server statistics{% endblock %}
{% block subtitle %}{% endblock %}
{% block buttons %}{% endblock %}
{% block body %}
{% set locals = namespace() %}
{% set locals.total_images = 0 %}
<div>
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th>year</th>
<th>month</th>
<th>count</th>
</tr>
</thead>
<tbody>
{% for row in images %}
<tr>
<td>{{row[1]}}</td>
<td>{{row[2]}}</td>
<td>{{"{:,}".format(row[0])}}{% set locals.total_images = locals.total_images + row[0]|int %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>{{ "{:,}".format(locals.total_images) }} Files - {{ tsize | filesizeformat }}</p>
</div>
{% endblock %}

40
templates/node.html Normal file
View File

@ -0,0 +1,40 @@
{% extends "page.html" %}
{% block title %}{{ node.fqdn }}{% endblock %}
{% block subtitle %}placehoolder{% endblock %}
{% block buttons %}
<!--<form action="/node/{{ node.fqdn }}/op" method="post">
<input type="submit" class="secondary-button pure-button" name="op" value="Make all public" />
<input type="submit" class="secondary-button pure-button" name="op" value="Make all private" />
<input type="submit" class="secondary-button pure-button" name="op" value="Delete tag" />
</form>-->
<a href="//node/{{ node.fqdn }}/attach"><button class="secondary-button pure-button">Attach class</button></a>
{% endblock %}
{% block body %}
<div class="nodes-single">
<div class="node">
<div class="node_info">
<h2>{{ node.name }}</h2>
<p>
</p>
<pre>
{{ node.body }}
</pre>
</div>
<div class="node-classes">
<h2>Classes</h2>
<div class="class-list">
{% for class in node.classes %}
<div class="class">
{{ class.cls.name }}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}

34
templates/node_edit.html Normal file
View File

@ -0,0 +1,34 @@
{% extends "page.html" %}
{% block title %}{% if node %}Edit{% else %}Create{% endif %} node{% endblock %}
{% block subtitle %}{% if node %}Editing "{{ node.fqdn }}"{% else %}New item{% endif %}{% endblock %}
{% block buttons %}
{% if node is defined %}<a href="/nodes/{{ node.fqdn }}"><button class="secondary-button pure-button">Back</button></a>{% endif %}
{% endblock %}
{% block body %}
<div class="photo-view pure-g">
<div class="photo-form pure-u-2-3">
<form action="/node_edit" method="post" class="pure-form pure-form-stacked">
<fieldset>
<fieldset class="pure-group pure-u-1">
<input name="fqdn" type="text" class="pure-input-1" placeholder="FQDN" value="{{ node.fqdn or '' }}" />
<textarea name="body" class="pure-input-1" placeholder="Body" rows="15">{{ node and node.body or '' }}</textarea>
</fieldset>
<!-- <div class="pure-u-1">
<label for="offset">Offset (minutes)</label>
<input id="offset" class="pure-u-1-2" type="text" name="offset" value="xxx" />
</div> -->
<div class="pure-u-1">
<input type="submit" class="pure-button pure-button-primary" name="op" value="{% if node %}Edit{% else %}Create{% endif %}" />
</div>
</fieldset>
</form>
</div>
<div class="photo-info pure-u-1-3">
xxx
</div>
</div>
{% endblock %}

22
templates/nodes.html Normal file
View File

@ -0,0 +1,22 @@
{% extends "page.html" %}
{% block title %}All nodes{% endblock %}
{% block subtitle %}<a href="/node_edit">Add</a>{% endblock %}
{% block buttons %}{% endblock %}
{% block body %}
<div class="nodes-all">
{% for node in nodes %}
<div class="node">
<h2>{{ node.fqdn }}</h2>
<p>
<a href="/node/{{ node.fqdn }}">view</a> <a href="/node_edit?node={{ node.fqdn }}">edit</a>
</p>
<pre>
{{ node.body }}
</pre>
</div>
{% endfor %}
</div>
{% endblock %}

63
templates/page.html Normal file
View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% if title %}{{ title }} :: {% endif %}Photo App</title>
<link rel="stylesheet" href="/static/style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="layout" class="content pure-g">
<div id="nav" class="pure-u">
<a href="#" class="nav-menu-button">Menu</a>
<div class="nav-inner">
<div class="pure-menu">
<ul class="pure-menu-list">
<li class="pure-menu-item"><a href="/" class="pure-menu-link">Nodes</a></li>
<li class="pure-menu-item"><a href="/objects" class="pure-menu-link">Objects</a></li>
<li class="pure-menu-item"><a href="/classes" class="pure-menu-link">Classes</a></li>
<li class="pure-menu-item"><a href="/lookup" class="pure-menu-link">Lookup</a></li>
<li class="pure-menu-heading">Albums</li>
{% for tag in all_albums %}
<li class="pure-menu-item"><a href="/album/{{ tag.slug }}" class="pure-menu-link"><span class="tag-icon tag-icon-mod-6-{{ tag.id % 6 }}"></span>{{ tag.name }}</a></li>
{% endfor %}
<li class="pure-menu-heading">Tags</li>
{% for tag in all_tags %}
<li class="pure-menu-item"><a href="/tag/{{ tag.slug }}" class="pure-menu-link"><span class="tag-icon tag-icon-mod-6-{{ tag.id % 6 }}"></span>{{ tag.name }}</a></li>
{% endfor %}
</ul>
</div>
<div class="user-status">
{% if auth %}
<p>Authed as <span>{{ auth }}</span></p>
<p><a href="/logout">Log out</a></p>
{% else %}
<p>Browsing as a guest</p>
<p><a href="/login">Log in</a></p>
{% endif %}
</div>
</div>
</div>
<div id="main" class="pure-u-1">
<div class="email-content">
<div class="email-content-header pure-g">
<div class="pure-u-1-2">
<h1 class="email-content-title">{% block title %}DEFAULT TITLE{% endblock %}</h1>
<p class="email-content-subtitle">
{% block subtitle %}DEFAULT SUBTITLE{% endblock %}
</p>
</div>
{% if auth %}
<div class="email-content-controls pure-u-1-2">
{% block buttons %}{% endblock %}
</div>
{% endif %}
</div>
<div class="email-content-body">
{% block body %}default body{% endblock %}
</div>
</div>
</div>
</div>
</body>
</html>

22
templates/pager.html Normal file
View File

@ -0,0 +1,22 @@
<div class="pager">
<h6>Page</h6>
{% if page > 0 %}
<div class="nav-prev">
<a href="{{path}}?page={{ page - 1 }}">Previous</a>
</div>
{% endif %}
<div class="pages">
<ul class="pager">
{% for pgnum in range(0, total_pages) %}
<li{% if pgnum == page %} class="current"{% endif %}>
<a href="{{path}}?page={{ pgnum }}">{{ pgnum }}</a>
</li>
{% endfor %}
</ul>
</div>
{% if page + 1 < total_pages %}
<div class="nav-next">
<a href="{{path}}?page={{ page + 1 }}">Next</a>
</div>
{% endif %}
</div>

106
templates/photo.html Normal file
View File

@ -0,0 +1,106 @@
{% extends "page.html" %}
{% block title %}{{ image.title or image.uuid }}{% endblock %}
{% block subtitle %}{{ image.date }}{% endblock %}
{% block buttons %}
<form action="/photo/{{ image.uuid }}/op" method="post">
{% if image.status == PhotoStatus.private %}
<input type="submit" class="secondary-button pure-button" name="op" value="Make public" />
{% else %}
<input type="submit" class="secondary-button pure-button" name="op" value="Make private" />
{% endif %}
</form>
<a href="/photo/{{ image.uuid }}/edit"><button class="secondary-button pure-button">Edit</button></a>
{% endblock %}
{% block body %}
<div class="photo-view pure-g">
<div class="photo-preview pure-u-2-3">
<a href="/thumb/set/big/{{ image.uuid }}.jpg">
<img src="/thumb/set/preview/{{ image.uuid }}.jpg" />
</a>
</div>
<div class="photo-info pure-u-1-3">
{% if image.description %}
<div class="photo-description">
<h2>Description</h2>
<p>{{ image.description }}</p>
</div>
{% endif %}
<div class="photo-metadata">
<h2>Information</h2>
<ul>
<li>
<strong>Date:</strong> {{ image.date }}
</li>
{% if image.date_offset %}
<li>
<strong>Time offset: </strong> {{ image.date_offset }}m
</li>
<li>
<strong>Embedded date: </strong>{{ image.date_real }}
</li>
{% endif %}
<li>
<strong>Status: </strong>{{ image.status | statusstr }}
</li>
<li>
<strong>Versions:</strong> {{ image.files|length }}
</li>
{% if image.lat != 0 %}
<li>
<strong>Coordinates:</strong> <a href="/map?zoom=13&i={{ image.uuid }}">{{ image.lat }}, {{ image.lon }}</a>
</li>
{% endif %}
</ul>
</div>
<div class="photo-formats">
<h2>Versions</h2>
<ul class="pure-g">
{% for img in image.files %}
<li class="pure-u-1 pure-g">
<a href="/thumb/one/big/{{ img.uuid }}.jpg" class="pure-g-1-4">
<img src="/thumb/one/small/{{ img.uuid }}.jpg" />
</a>
<div class="pure-u-3-4">
<div>
{{ img.uuid }}
</div>
<div>
{{ img.path | basename }}
</div>
<div>
{{ img.size | filesizeformat }}{% if img.width %} - {{ img.width }} x {{ img.height }}{% endif %}
</div>
{% if img.orientation > 0 %}
<div>
Rotation: {{ img.orientation * 90 }}&deg;
</div>
{% endif %}
<div>
{{ img.format }}
</div>
<div>
<a href="/download/one/{{ img.uuid }}">download</a>
<a href="/download/one/{{ img.uuid }}.{{ img.format | mime2ext }}?preview=true">preview</a>
</div>
</div>
</li>
{% endfor %}
</ul>
</div>
<div class="photo-tags">
<h2>Tags{% if auth %} <a href="/create_tags?uuid={{ image.uuid }}">add</a>{% endif %}</h2>
<ul class="tags-picker">
{% for tagi in image.tags %}
<li>
<a href="/tag/{{ tagi.tag.slug }}">{{ tagi.tag.name }}</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}

27
templates/tag_edit.html Normal file
View File

@ -0,0 +1,27 @@
{% extends "page.html" %}
{% block title %}Editing {{ tag.uuid }}{% endblock %}
{% block subtitle %}{% endblock %}
{% block buttons %}
<a href="/tag/{{ tag.slug or tag.uuid }}"><button class="secondary-button pure-button">Back</button></a>
{% endblock %}
{% block body %}
<div class="photo-view pure-g">
<div class="photo-form pure-u-2-3">
<form action="/tag/{{ tag.uuid }}/op" method="post" class="pure-form pure-form-stacked">
<fieldset>
<fieldset class="pure-group pure-u-1">
<input name="title" type="text" class="pure-input-1" placeholder="Image title" value="{{ tag.title or "" }}" />
<textarea name="description" class="pure-input-1" placeholder="Description" rows="15">{{ tag.description or "" }}</textarea>
</fieldset>
<div class="pure-u-1">
<input type="submit" class="pure-button pure-button-primary" name="op" value="Save" />
</div>
</fieldset>
</form>
</div>
</div>
{% endblock %}

24
templates/untagged.html Normal file
View File

@ -0,0 +1,24 @@
{% extends "page.html" %}
{% block title %}Untagged photos{% endblock %}
{% block subtitle %}By date, descending{% endblock %}
{% block body %}
{% set locals = namespace() %}
{% set total_pages = (total_items/pgsize)|ceil %}
<div class="photo-feed">
{% set locals.im_date = "" %}
{% for item in images %}
{% set newdate = item.date.strftime("%b %d, %Y") %}
{% if newdate != locals.im_date %}
{% set locals.im_date = newdate %}
<div class="feed-divider year"><h4><a href="/date/{{ item.date.strftime("%Y-%m-%d") }}">{{ locals.im_date }}</a></h4></div>
{% endif %}
{% include "fragments/feed-photo.html" %}
{% endfor %}
<br style="clear:both" />
{% include "pager.html" %}
</div>
{% endblock %}