basic auth for endpoints
This commit is contained in:
parent
346f0a7944
commit
3035d117b5
|
@ -3,7 +3,7 @@ import cherrypy
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from photoapp.library import PhotoLibrary
|
from photoapp.library import PhotoLibrary
|
||||||
from photoapp.types import Photo, PhotoSet, Tag, TagItem
|
from photoapp.types import Photo, PhotoSet, Tag, TagItem, PhotoStatus
|
||||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
from sqlalchemy import desc
|
from sqlalchemy import desc
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
@ -15,17 +15,43 @@ APPROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))
|
||||||
|
|
||||||
|
|
||||||
def auth():
|
def auth():
|
||||||
|
"""
|
||||||
|
Return the currently authorized username (per request) or None
|
||||||
|
"""
|
||||||
return cherrypy.session.get('authed', None)
|
return cherrypy.session.get('authed', None)
|
||||||
|
|
||||||
|
|
||||||
def mime2ext(mime):
|
def mime2ext(mime):
|
||||||
return {"image/png": "png",
|
"""
|
||||||
"image/jpeg": "jpg",
|
Given a mime type return the canonical file extension
|
||||||
"image/gif": "gif",
|
"""
|
||||||
"application/octet-stream-xmp": "xmp",
|
return {"image/png": "png",
|
||||||
"image/x-canon-cr2": "cr2",
|
"image/jpeg": "jpg",
|
||||||
"video/mp4": "mp4",
|
"image/gif": "gif",
|
||||||
"video/quicktime": "mov"}[mime]
|
"application/octet-stream-xmp": "xmp",
|
||||||
|
"image/x-canon-cr2": "cr2",
|
||||||
|
"video/mp4": "mp4",
|
||||||
|
"video/quicktime": "mov"}[mime]
|
||||||
|
|
||||||
|
|
||||||
|
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 photo_auth_filter(query):
|
||||||
|
"""
|
||||||
|
Sqlalchemy helper: filter the given PhotoSet query to items that match the authorized user's PhotoStatus access
|
||||||
|
level. Currently, authed users can access ALL photos, and unauthed users can access only PhotoStatus.public
|
||||||
|
status items.
|
||||||
|
"""
|
||||||
|
return query.filter(PhotoSet.status == PhotoStatus.public) if not auth() else query
|
||||||
|
|
||||||
|
|
||||||
class PhotosWeb(object):
|
class PhotosWeb(object):
|
||||||
|
@ -57,11 +83,23 @@ class PhotosWeb(object):
|
||||||
Return a dict containing variables expected to be on every page
|
Return a dict containing variables expected to be on every page
|
||||||
"""
|
"""
|
||||||
s = self.session()
|
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.order_by(Tag.title).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.title).all() # pragma: manual auth
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
"all_tags": s.query(Tag).order_by(Tag.title).all(),
|
"all_tags": tagq,
|
||||||
"all_albums": s.query(Tag).filter(Tag.is_album == True).order_by(Tag.title).all(),
|
"all_albums": albumq,
|
||||||
"path": cherrypy.request.path_info,
|
"path": cherrypy.request.path_info,
|
||||||
"auth": auth()
|
"auth": auth(),
|
||||||
|
"PhotoStatus": PhotoStatus
|
||||||
}
|
}
|
||||||
s.close()
|
s.close()
|
||||||
return ret
|
return ret
|
||||||
|
@ -86,8 +124,9 @@ class PhotosWeb(object):
|
||||||
"""
|
"""
|
||||||
s = self.session()
|
s = self.session()
|
||||||
page, pgsize = int(page), int(pgsize)
|
page, pgsize = int(page), int(pgsize)
|
||||||
total_sets = s.query(func.count(PhotoSet.id)).first()[0]
|
total_sets = photo_auth_filter(s.query(func.count(PhotoSet.id))).first()[0]
|
||||||
images = s.query(PhotoSet).order_by(PhotoSet.date.desc()).offset(pgsize * page).limit(pgsize).all()
|
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)
|
yield self.render("feed.html", images=[i for i in images], page=page, pgsize=int(pgsize), total_sets=total_sets)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@ -96,11 +135,11 @@ class PhotosWeb(object):
|
||||||
/stats - show server statistics
|
/stats - show server statistics
|
||||||
"""
|
"""
|
||||||
s = self.session()
|
s = self.session()
|
||||||
images = s.query(func.count(PhotoSet.uuid),
|
images = photo_auth_filter(s.query(func.count(PhotoSet.uuid),
|
||||||
func.strftime('%Y', PhotoSet.date).label('year'),
|
func.strftime('%Y', PhotoSet.date).label('year'),
|
||||||
func.strftime('%m', PhotoSet.date).label('month')). \
|
func.strftime('%m', PhotoSet.date).label('month'))). \
|
||||||
group_by('year', 'month').order_by(desc('year'), desc('month')).all()
|
group_by('year', 'month').order_by(desc('year'), desc('month')).all()
|
||||||
tsize = s.query(func.sum(Photo.size)).scalar()
|
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)
|
yield self.render("monthly.html", images=images, tsize=tsize)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@ -110,12 +149,13 @@ class PhotosWeb(object):
|
||||||
TODO using so many coordinates is slow in the browser. dedupe them somehow.
|
TODO using so many coordinates is slow in the browser. dedupe them somehow.
|
||||||
"""
|
"""
|
||||||
s = self.session()
|
s = self.session()
|
||||||
query = s.query(PhotoSet).filter(PhotoSet.lat != 0, PhotoSet.lon != 0)
|
query = photo_auth_filter(s.query(PhotoSet)).filter(PhotoSet.lat != 0, PhotoSet.lon != 0)
|
||||||
if i:
|
if i:
|
||||||
query = query.filter(PhotoSet.uuid == i)
|
query = query.filter(PhotoSet.uuid == i)
|
||||||
yield self.render("map.html", images=query.all(), zoom=int(zoom))
|
yield self.render("map.html", images=query.all(), zoom=int(zoom))
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@require_auth
|
||||||
def create_tags(self, fromdate=None, uuid=None, tag=None, newtag=None, remove=None):
|
def create_tags(self, fromdate=None, uuid=None, tag=None, newtag=None, remove=None):
|
||||||
"""
|
"""
|
||||||
/create_tags - tag multiple items selected by day of photo
|
/create_tags - tag multiple items selected by day of photo
|
||||||
|
@ -178,11 +218,12 @@ class PhotosWeb(object):
|
||||||
raise cherrypy.HTTPRedirect('/feed', 302)
|
raise cherrypy.HTTPRedirect('/feed', 302)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def sess(self):
|
def logout(self):
|
||||||
"""
|
"""
|
||||||
/sess - TODO DELETE ME dump session contents for debugging purposes
|
/login - enable super features by logging into the app
|
||||||
"""
|
"""
|
||||||
yield cherrypy.session['authed']
|
cherrypy.session.clear()
|
||||||
|
raise cherrypy.HTTPRedirect('/feed', 302)
|
||||||
|
|
||||||
|
|
||||||
@cherrypy.popargs('date')
|
@cherrypy.popargs('date')
|
||||||
|
@ -201,20 +242,20 @@ class DateView(object):
|
||||||
pgsize = 100
|
pgsize = 100
|
||||||
dt = datetime.strptime(date, "%Y-%m-%d")
|
dt = datetime.strptime(date, "%Y-%m-%d")
|
||||||
dt_end = dt + timedelta(days=1)
|
dt_end = dt + timedelta(days=1)
|
||||||
total_sets = s.query(func.count(PhotoSet.id)). \
|
total_sets = photo_auth_filter(s.query(func.count(PhotoSet.id))). \
|
||||||
filter(and_(PhotoSet.date >= dt, PhotoSet.date < dt_end)).first()[0]
|
filter(and_(PhotoSet.date >= dt, PhotoSet.date < dt_end)).first()[0]
|
||||||
images = s.query(PhotoSet).filter(and_(PhotoSet.date >= dt,
|
images = photo_auth_filter(s.query(PhotoSet)).filter(and_(PhotoSet.date >= dt,
|
||||||
PhotoSet.date < dt_end)).order_by(PhotoSet.date). \
|
PhotoSet.date < dt_end)).order_by(PhotoSet.date). \
|
||||||
offset(page * pgsize).limit(pgsize).all()
|
offset(page * pgsize).limit(pgsize).all()
|
||||||
yield self.master.render("date.html", page=page, pgsize=pgsize, total_sets=total_sets,
|
yield self.master.render("date.html", page=page, pgsize=pgsize, total_sets=total_sets,
|
||||||
images=[i for i in images], date=dt)
|
images=[i for i in images], date=dt)
|
||||||
return
|
return
|
||||||
images = s.query(PhotoSet, func.strftime('%Y-%m-%d',
|
images = photo_auth_filter(s.query(PhotoSet, func.strftime('%Y-%m-%d',
|
||||||
PhotoSet.date).label('gdate'),
|
PhotoSet.date).label('gdate'),
|
||||||
func.count('photos.id'),
|
func.count('photos.id'),
|
||||||
func.strftime('%Y', PhotoSet.date).label('year'),
|
func.strftime('%Y', PhotoSet.date).label('year'),
|
||||||
func.strftime('%m', PhotoSet.date).label('month'),
|
func.strftime('%m', PhotoSet.date).label('month'),
|
||||||
func.strftime('%d', PhotoSet.date).label('day')). \
|
func.strftime('%d', PhotoSet.date).label('day'))). \
|
||||||
group_by('gdate').order_by(desc('year'), 'month', 'day').all()
|
group_by('gdate').order_by(desc('year'), 'month', 'day').all()
|
||||||
yield self.master.render("dates.html", images=images)
|
yield self.master.render("dates.html", images=images)
|
||||||
|
|
||||||
|
@ -233,10 +274,12 @@ class ThumbnailView(object):
|
||||||
uuid = uuid.split(".")[0]
|
uuid = uuid.split(".")[0]
|
||||||
s = self.master.session()
|
s = self.master.session()
|
||||||
|
|
||||||
query = s.query(Photo).filter(Photo.set.has(uuid=uuid)) if item_type == "set" \
|
query = photo_auth_filter(s.query(Photo)).filter(Photo.set.has(uuid=uuid)) if item_type == "set" \
|
||||||
else s.query(Photo).filter(Photo.uuid == uuid) if item_type == "one" \
|
else photo_auth_filter(s.query(Photo)).filter(Photo.uuid == uuid) if item_type == "one" \
|
||||||
else None
|
else None
|
||||||
|
|
||||||
|
assert query
|
||||||
|
|
||||||
# prefer making thumbs from jpeg to avoid loading large raws
|
# prefer making thumbs from jpeg to avoid loading large raws
|
||||||
first = None
|
first = None
|
||||||
best = None
|
best = None
|
||||||
|
@ -248,7 +291,7 @@ class ThumbnailView(object):
|
||||||
break
|
break
|
||||||
thumb_from = best or first
|
thumb_from = best or first
|
||||||
if not thumb_from:
|
if not thumb_from:
|
||||||
raise Exception("404") # TODO it right
|
raise Exception("404")
|
||||||
# TODO some lock around calls to this based on uuid
|
# TODO some lock around calls to this based on uuid
|
||||||
thumb_path = self.master.library.make_thumb(thumb_from, thumb_size)
|
thumb_path = self.master.library.make_thumb(thumb_from, thumb_size)
|
||||||
if thumb_path:
|
if thumb_path:
|
||||||
|
@ -272,7 +315,7 @@ class DownloadView(object):
|
||||||
s = self.master.session()
|
s = self.master.session()
|
||||||
|
|
||||||
query = None if item_type == "set" \
|
query = None if item_type == "set" \
|
||||||
else s.query(Photo).filter(Photo.uuid == uuid) if item_type == "one" \
|
else photo_auth_filter(s.query(Photo)).filter(Photo.uuid == uuid) if item_type == "one" \
|
||||||
else None # TODO set download query
|
else None # TODO set download query
|
||||||
|
|
||||||
item = query.first()
|
item = query.first()
|
||||||
|
@ -296,9 +339,21 @@ class PhotoView(object):
|
||||||
def index(self, uuid):
|
def index(self, uuid):
|
||||||
uuid = uuid.split(".")[0]
|
uuid = uuid.split(".")[0]
|
||||||
s = self.master.session()
|
s = self.master.session()
|
||||||
photo = s.query(PhotoSet).filter(PhotoSet.uuid == uuid).first()
|
photo = photo_auth_filter(s.query(PhotoSet)).filter(PhotoSet.uuid == uuid).first()
|
||||||
yield self.master.render("photo.html", image=photo)
|
yield self.master.render("photo.html", image=photo)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@require_auth
|
||||||
|
def op(self, uuid, op):
|
||||||
|
s = self.master.session()
|
||||||
|
photo = s.query(PhotoSet).filter(PhotoSet.uuid == uuid).first()
|
||||||
|
if op == "Make public":
|
||||||
|
photo.status = PhotoStatus.public
|
||||||
|
elif op == "Make private":
|
||||||
|
photo.status = PhotoStatus.private
|
||||||
|
s.commit()
|
||||||
|
raise cherrypy.HTTPRedirect('/photo/{}'.format(photo.uuid), 302)
|
||||||
|
|
||||||
|
|
||||||
@cherrypy.popargs('uuid')
|
@cherrypy.popargs('uuid')
|
||||||
class TagView(object):
|
class TagView(object):
|
||||||
|
@ -316,19 +371,22 @@ class TagView(object):
|
||||||
s = self.master.session()
|
s = self.master.session()
|
||||||
|
|
||||||
if uuid == "untagged":
|
if uuid == "untagged":
|
||||||
numphotos = s.query(func.count(PhotoSet.id)).filter(~PhotoSet.id.in_(s.query(TagItem.set_id))).scalar()
|
numphotos = photo_auth_filter(s.query(func.count(PhotoSet.id))). \
|
||||||
photos = s.query(PhotoSet).filter(~PhotoSet.id.in_(s.query(TagItem.set_id))).\
|
filter(~PhotoSet.id.in_(s.query(TagItem.set_id))).scalar()
|
||||||
|
photos = photo_auth_filter(s.query(PhotoSet)).filter(~PhotoSet.id.in_(s.query(TagItem.set_id))).\
|
||||||
offset(page * pgsize).limit(pgsize).all()
|
offset(page * pgsize).limit(pgsize).all()
|
||||||
yield self.master.render("untagged.html", images=photos, total_items=numphotos, pgsize=pgsize, page=page)
|
yield self.master.render("untagged.html", images=photos, total_items=numphotos, pgsize=pgsize, page=page)
|
||||||
else:
|
else:
|
||||||
tag = s.query(Tag).filter(Tag.uuid == uuid).first()
|
tag = s.query(Tag).filter(Tag.uuid == uuid).first()
|
||||||
numphotos = s.query(func.count(Tag.id)).join(TagItem).join(PhotoSet).filter(Tag.uuid == uuid).scalar()
|
numphotos = photo_auth_filter(s.query(func.count(Tag.id)).join(TagItem).join(PhotoSet)). \
|
||||||
photos = s.query(PhotoSet).join(TagItem).join(Tag).filter(Tag.uuid == uuid). \
|
filter(Tag.uuid == uuid).scalar()
|
||||||
|
photos = photo_auth_filter(s.query(PhotoSet)).join(TagItem).join(Tag).filter(Tag.uuid == uuid). \
|
||||||
order_by(PhotoSet.date.desc()).offset(page * pgsize).limit(pgsize).all()
|
order_by(PhotoSet.date.desc()).offset(page * pgsize).limit(pgsize).all()
|
||||||
yield self.master.render("album.html", tag=tag, images=photos,
|
yield self.master.render("album.html", tag=tag, images=photos,
|
||||||
total_items=numphotos, pgsize=pgsize, page=page)
|
total_items=numphotos, pgsize=pgsize, page=page)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@require_auth
|
||||||
def op(self, uuid, op):
|
def op(self, uuid, op):
|
||||||
s = self.master.session()
|
s = self.master.session()
|
||||||
tag = s.query(Tag).filter(Tag.uuid == uuid).first()
|
tag = s.query(Tag).filter(Tag.uuid == uuid).first()
|
||||||
|
|
|
@ -22,19 +22,29 @@ a {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: rgb(37, 42, 58);
|
background: rgb(37, 42, 58);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
|
||||||
/* show the "Menu" button on phones */
|
.user-status {
|
||||||
#nav .nav-menu-button {
|
padding: 25px 0px;
|
||||||
display: block;
|
color: #fff;
|
||||||
top: 0.5em;
|
|
||||||
right: 0.5em;
|
span {
|
||||||
position: absolute;
|
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 */
|
/* when "Menu" is clicked, the navbar should be 80% height */
|
||||||
#nav.active {
|
|
||||||
height: 80%;
|
|
||||||
}
|
|
||||||
/* don't show the navigation items until the "Menu" button is clicked */
|
/* don't show the navigation items until the "Menu" button is clicked */
|
||||||
.nav-inner {
|
.nav-inner {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -45,24 +55,28 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#nav .pure-menu {
|
#nav {
|
||||||
background: transparent;
|
.pure-menu {
|
||||||
border: none;
|
background: transparent;
|
||||||
text-align: left;
|
border: none;
|
||||||
}
|
text-align: left;
|
||||||
#nav .pure-menu-link:hover,
|
}
|
||||||
#nav .pure-menu-link:focus {
|
|
||||||
|
.pure-menu-link:hover,
|
||||||
|
.pure-menu-link:focus {
|
||||||
background: rgb(55, 60, 90);
|
background: rgb(55, 60, 90);
|
||||||
}
|
}
|
||||||
#nav .pure-menu-link {
|
.pure-menu-link {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
#nav .pure-menu-heading {
|
.pure-menu-heading {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
font-size:110%;
|
font-size:110%;
|
||||||
color: rgb(75, 113, 151);
|
color: rgb(75, 113, 151);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.tag-icon {
|
.tag-icon {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<li class="pure-menu-item"><a href="/" class="pure-menu-link">All photos</a></li>
|
<li class="pure-menu-item"><a href="/" class="pure-menu-link">All photos</a></li>
|
||||||
<li class="pure-menu-item"><a href="/albums" class="pure-menu-link">Albums</a></li>
|
<li class="pure-menu-item"><a href="/albums" class="pure-menu-link">Albums</a></li>
|
||||||
<li class="pure-menu-item"><a href="/date" class="pure-menu-link">Dates</a></li>
|
<li class="pure-menu-item"><a href="/date" class="pure-menu-link">Dates</a></li>
|
||||||
<li class="pure-menu-item"><a href="/monthly" class="pure-menu-link">Stats</a></li>
|
<li class="pure-menu-item"><a href="/stats" class="pure-menu-link">Stats</a></li>
|
||||||
<li class="pure-menu-item"><a href="/map" class="pure-menu-link">Map</a></li>
|
<li class="pure-menu-item"><a href="/map" class="pure-menu-link">Map</a></li>
|
||||||
<li class="pure-menu-item"><a href="/tag/untagged" class="pure-menu-link">Untagged</a></li>
|
<li class="pure-menu-item"><a href="/tag/untagged" class="pure-menu-link">Untagged</a></li>
|
||||||
<li class="pure-menu-item"><a href="/admin/trash" class="pure-menu-link">Trash</a></li>
|
<li class="pure-menu-item"><a href="/admin/trash" class="pure-menu-link">Trash</a></li>
|
||||||
|
@ -30,6 +30,15 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div id="main" class="pure-u-1">
|
<div id="main" class="pure-u-1">
|
||||||
|
@ -41,13 +50,15 @@
|
||||||
{{ subtitle }}
|
{{ subtitle }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="email-content-controls pure-u-1-2">
|
{% if auth %}
|
||||||
{% block buttons %}
|
<div class="email-content-controls pure-u-1-2">
|
||||||
<button class="secondary-button pure-button">420</button>
|
{% block buttons %}
|
||||||
<button class="secondary-button pure-button">Blaze</button>
|
<button class="secondary-button pure-button">420</button>
|
||||||
<button class="secondary-button pure-button">It</button>
|
<button class="secondary-button pure-button">Blaze</button>
|
||||||
{% endblock %}
|
<button class="secondary-button pure-button">It</button>
|
||||||
</div>
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="email-content-body">
|
<div class="email-content-body">
|
||||||
{% block body %}default body{% endblock %}
|
{% block body %}default body{% endblock %}
|
||||||
|
|
|
@ -3,7 +3,13 @@
|
||||||
{% set subtitle = image.uuid %}
|
{% set subtitle = image.uuid %}
|
||||||
|
|
||||||
{% block buttons %}
|
{% block buttons %}
|
||||||
xxx
|
<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>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
Loading…
Reference in New Issue