initial broken commit
This commit is contained in:
commit
f431f7c4e6
|
@ -0,0 +1,10 @@
|
|||
__pycache__
|
||||
build/
|
||||
dist/
|
||||
node_modules/
|
||||
nodepupper.egg-info/
|
||||
pupper.db*
|
||||
styles/css/
|
||||
styles/dist/
|
||||
styles/mincss/
|
||||
testenv/
|
|
@ -0,0 +1,5 @@
|
|||
import ZODB
|
||||
import ZODB.FileStorage
|
||||
storage = ZODB.FileStorage.FileStorage("pupper.db")
|
||||
db = ZODB.DB(storage)
|
||||
print(db.open().root())
|
|
@ -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,0 +1,7 @@
|
|||
import hashlib
|
||||
|
||||
|
||||
def pwhash(password):
|
||||
h = hashlib.sha256()
|
||||
h.update(password.encode("UTF-8"))
|
||||
return h.hexdigest()
|
|
@ -0,0 +1,281 @@
|
|||
import os
|
||||
import cherrypy
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from nodepupper.nodeops import NodeOps, NObject
|
||||
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])
|
||||
|
||||
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
|
||||
"""
|
||||
#s = self.session()
|
||||
# all tags / albums with photos visible under the current auth context
|
||||
# tagq = s.query(Tag).join(TagItem).join(PhotoSet)
|
||||
# if not auth():
|
||||
# tagq = tagq.filter(PhotoSet.status == PhotoStatus.public)
|
||||
# tagq = tagq.filter(Tag.is_album == False).order_by(Tag.name).all() # pragma: manual auth
|
||||
|
||||
# albumq = s.query(Tag).join(TagItem).join(PhotoSet)
|
||||
# if not auth():
|
||||
# albumq = albumq.filter(PhotoSet.status == PhotoStatus.public)
|
||||
# albumq = albumq.filter(Tag.is_album == True).order_by(Tag.name).all() # pragma: manual auth
|
||||
|
||||
ret = {
|
||||
"all_tags": [], #tagq,
|
||||
"all_albums": [], #albumq,
|
||||
"path": cherrypy.request.path_info,
|
||||
"auth": auth()
|
||||
}
|
||||
#s.close()
|
||||
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:
|
||||
c.root.nodes[fqdn] = NObject(body)
|
||||
|
||||
with self.nodes.db.transaction() as c:
|
||||
yield self.render("node_edit.html", node_name=node, 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.items())
|
||||
# raise cherrypy.HTTPRedirect('feed', 302)
|
||||
|
||||
@cherrypy.expose
|
||||
def feed(self, page=0, pgsize=25):
|
||||
"""
|
||||
/feed - main photo feed - show photos sorted by date, newest first
|
||||
"""
|
||||
s = self.session()
|
||||
page, pgsize = int(page), int(pgsize)
|
||||
total_sets = photo_auth_filter(s.query(func.count(PhotoSet.id))).first()[0]
|
||||
images = photo_auth_filter(s.query(PhotoSet)).order_by(PhotoSet.date.desc()). \
|
||||
offset(pgsize * page).limit(pgsize).all()
|
||||
yield self.render("feed.html", images=[i for i in images], page=page, pgsize=int(pgsize), total_sets=total_sets)
|
||||
|
||||
@cherrypy.expose
|
||||
def stats(self):
|
||||
"""
|
||||
/stats - show server statistics
|
||||
"""
|
||||
s = self.session()
|
||||
images = photo_auth_filter(s.query(func.count(PhotoSet.uuid),
|
||||
func.strftime('%Y', PhotoSet.date).label('year'),
|
||||
func.strftime('%m', PhotoSet.date).label('month'))). \
|
||||
group_by('year', 'month').order_by(desc('year'), desc('month')).all()
|
||||
tsize = photo_auth_filter(s.query(func.sum(Photo.size)).join(PhotoSet)).scalar() # pragma: manual auth
|
||||
yield self.render("monthly.html", images=images, tsize=tsize)
|
||||
|
||||
@cherrypy.expose
|
||||
def map(self, i=None, a=None, zoom=3):
|
||||
"""
|
||||
/map - show all photos on the a map. Passing $i will show a single photo, or passing $a will show photos under
|
||||
the given tag.
|
||||
TODO using so many coordinates is slow in the browser. dedupe them somehow.
|
||||
"""
|
||||
s = self.session()
|
||||
query = photo_auth_filter(s.query(PhotoSet)).filter(PhotoSet.lat != 0, PhotoSet.lon != 0)
|
||||
if a:
|
||||
query = query.join(TagItem).join(Tag).filter(Tag.uuid == a)
|
||||
if i:
|
||||
query = query.filter(PhotoSet.uuid == i)
|
||||
yield self.render("map.html", images=query.all(), zoom=int(zoom))
|
||||
|
||||
@cherrypy.expose
|
||||
@require_auth
|
||||
def create_tags(self, fromdate=None, uuid=None, tag=None, newtag=None, remove=None):
|
||||
"""
|
||||
/create_tags - tag multiple items selected by day of photo
|
||||
:param fromdate: act upon photos taken on this day
|
||||
:param uuid: act upon a single photo with this uuid
|
||||
:param tag: target photos will have a tag specified by this uuid added
|
||||
:param remove: target photos will have the tag specified by this uuid removed
|
||||
:param newtag: new tag name to create
|
||||
"""
|
||||
s = self.session()
|
||||
|
||||
def get_photos():
|
||||
if fromdate:
|
||||
dt = datetime.strptime(fromdate, "%Y-%m-%d")
|
||||
dt_end = dt + timedelta(days=1)
|
||||
photos = s.query(PhotoSet).filter(and_(PhotoSet.date >= dt,
|
||||
PhotoSet.date < dt_end)).order_by(PhotoSet.date)
|
||||
num_photos = s.query(func.count(PhotoSet.id)). \
|
||||
filter(and_(PhotoSet.date >= dt, PhotoSet.date < dt_end)).order_by(PhotoSet.date).scalar()
|
||||
|
||||
if uuid:
|
||||
photos = s.query(PhotoSet).filter(PhotoSet.uuid == uuid)
|
||||
num_photos = s.query(func.count(PhotoSet.id)).filter(PhotoSet.uuid == uuid).scalar()
|
||||
return photos, num_photos
|
||||
|
||||
if remove:
|
||||
rmtag = s.query(Tag).filter(Tag.uuid == remove).first()
|
||||
photoq, _ = get_photos()
|
||||
for photo in photoq:
|
||||
s.query(TagItem).filter(TagItem.tag_id == rmtag.id, TagItem.set_id == photo.id).delete()
|
||||
s.commit()
|
||||
|
||||
if newtag:
|
||||
s.add(Tag(title=newtag.capitalize(), name=newtag, slug=slugify(newtag)))
|
||||
s.commit()
|
||||
|
||||
photos, num_photos = get_photos()
|
||||
|
||||
if tag: # Create the tag on all the photos
|
||||
tag = s.query(Tag).filter(Tag.uuid == tag).first()
|
||||
for photo in photos.all():
|
||||
if 0 == s.query(func.count(TagItem.id)).filter(TagItem.tag_id == tag.id,
|
||||
TagItem.set_id == photo.id).scalar():
|
||||
s.add(TagItem(tag_id=tag.id, set_id=photo.id))
|
||||
s.commit()
|
||||
|
||||
alltags = s.query(Tag).order_by(Tag.name).all()
|
||||
yield self.render("create_tags.html", images=photos, alltags=alltags,
|
||||
num_photos=num_photos, fromdate=fromdate, uuid=uuid)
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
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()
|
|
@ -0,0 +1,122 @@
|
|||
import ZODB
|
||||
import ZODB.FileStorage
|
||||
import persistent
|
||||
import persistent.list
|
||||
import BTrees.OOBTree
|
||||
|
||||
|
||||
class NObject(persistent.Persistent):
|
||||
|
||||
def __init__(self, body=None):
|
||||
self.parents = persistent.list.PersistentList()
|
||||
self.body = body
|
||||
|
||||
# def add_author(self, author):
|
||||
# self.authors.append(author)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
# def add_photoset(self, photoset):
|
||||
# """
|
||||
# Commit a populated photoset object to the library. The paths in the photoset's file list entries will be updated
|
||||
# as the file is moved to the library path.
|
||||
# """
|
||||
|
||||
# # Create target directory
|
||||
# path = os.path.join(self.path, self.get_datedir_path(photoset.date))
|
||||
# os.makedirs(path, exist_ok=True)
|
||||
|
||||
# moves = [] # Track files moved. If the sql transaction files, we'll undo these
|
||||
|
||||
# for file in photoset.files:
|
||||
# dest = os.path.join(path, os.path.basename(file.path))
|
||||
|
||||
# # Check if the name is already in use, rename new file if needed
|
||||
# dupe_rename = 1
|
||||
# while os.path.exists(dest):
|
||||
# fname = os.path.basename(file.path).split(".")
|
||||
# fname[-2] += "_{}".format(dupe_rename)
|
||||
# dest = os.path.join(path, '.'.join(fname))
|
||||
# dupe_rename += 1
|
||||
# os.rename(file.path, dest)
|
||||
# moves.append((file.path, dest))
|
||||
# file.path = dest.lstrip(self.path)
|
||||
|
||||
# s = self.session()
|
||||
# s.add(photoset)
|
||||
# try:
|
||||
# s.commit()
|
||||
# except IntegrityError:
|
||||
# # Commit failed, undo the moves
|
||||
# for move in moves:
|
||||
# os.rename(move[1], move[0])
|
||||
# raise
|
||||
|
||||
# def get_datedir_path(self, date):
|
||||
# """
|
||||
# Return a path like 2018/3/31 given a datetime object representing the same date
|
||||
# """
|
||||
# return os.path.join(str(date.year), str(date.month), str(date.day))
|
||||
|
||||
# def make_thumb(self, photo, style):
|
||||
# """
|
||||
# Create a thumbnail of the given photo, scaled/cropped to the given named style
|
||||
# :return: local path to thumbnail file or None if creation failed or was blocked
|
||||
# """
|
||||
# # style tuples: max x, max y, rotate ok)
|
||||
# # rotate ok means x and y maxes can be swapped if it fits the image's aspect ratio better
|
||||
# styles = {"tiny": (80, 80, False),
|
||||
# "small": (100, 100, False),
|
||||
# "feed": (250, 250, False),
|
||||
# "preview": (1024, 768, True),
|
||||
# "big": (2048, 1536, True)}
|
||||
# dest = os.path.join(self.cache_path, "thumbs", style, "{}.jpg".format(photo.uuid))
|
||||
# if os.path.exists(dest):
|
||||
# return os.path.abspath(dest)
|
||||
# if photo.width is None: # todo better detection of images that PIL can't open
|
||||
# return None
|
||||
# if photo.uuid not in self._failed_thumbs_cache[style]:
|
||||
# thumb_width, thumb_height, flip_ok = styles[style]
|
||||
# i_width = photo.width
|
||||
# i_height = photo.height
|
||||
# im_is_rotated = photo.orientation % 2 != 0 or i_height > i_width
|
||||
|
||||
# if im_is_rotated and flip_ok:
|
||||
# thumb_width, thumb_height = thumb_height, thumb_width
|
||||
|
||||
# thumb_width = min(thumb_width, i_width if i_width > 0 else 999999999) # TODO do we even have photo.width if PIL can't read the image?
|
||||
# thumb_height = min(thumb_height, i_height if i_height > 0 else 999999999) # TODO this seems bad
|
||||
|
||||
# p = Process(target=self.gen_thumb, args=(os.path.join(self.path, photo.path), dest, thumb_width, thumb_height, photo.orientation))
|
||||
# p.start()
|
||||
# p.join()
|
||||
# if p.exitcode != 0:
|
||||
# self._failed_thumbs_cache[style][photo.uuid] = True # dont retry failed generations
|
||||
# return None
|
||||
# return os.path.abspath(dest)
|
||||
# return None
|
||||
|
||||
# @staticmethod
|
||||
# def gen_thumb(src_img, dest_img, width, height, rotation):
|
||||
# try:
|
||||
# start = time()
|
||||
# # TODO lock around the dir creation
|
||||
# os.makedirs(os.path.split(dest_img)[0], exist_ok=True)
|
||||
# image = Image.open(src_img)
|
||||
# image = image.rotate(90 * rotation, expand=True)
|
||||
# thumb = ImageOps.fit(image, (width, height), Image.ANTIALIAS)
|
||||
# thumb.save(dest_img, 'JPEG')
|
||||
# print("Generated {} in {}s".format(dest_img, round(time() - start, 4)))
|
||||
# except:
|
||||
# traceback.print_exc()
|
||||
# if os.path.exists(dest_img):
|
||||
# os.unlink(dest_img)
|
||||
# sys.exit(1)
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
PYTHONPATH=. python3 nodepupper/daemon.py -p 8040 --debug
|
|
@ -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)
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -0,0 +1,34 @@
|
|||
{% extends "page.html" %}
|
||||
{% block title %}{% if node %}Edit{% else %}Create{% endif %} node{% endblock %}
|
||||
{% block subtitle %}{% if node %}Editing "{{ node_name }}"{% else %}New item{% endif %}{% endblock %}
|
||||
|
||||
{% block buttons %}
|
||||
{% if node is defined %}<a href="/nodes/{{ image.uuid }}"><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_name 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 %}
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "page.html" %}
|
||||
{% block title %}All nodes{% endblock %}
|
||||
{% block subtitle %}Subtitle{% endblock %}
|
||||
{% block buttons %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="nodes-all">
|
||||
{% for name, node in nodes %}
|
||||
<div class="node">
|
||||
<h2>{{ name }}</h2>
|
||||
<p>
|
||||
<a href="/node_edit?node={{ name }}">edit</a>
|
||||
</p>
|
||||
<pre>
|
||||
{{ node.body }}
|
||||
</pre>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,62 @@
|
|||
<!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="/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>
|
|
@ -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>
|
|
@ -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 }}°
|
||||
</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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
Loading…
Reference in New Issue