Support album descriptions and layout options, and add photo descriptionspull/2/head
@@ -28,11 +28,25 @@ def list_albums(): | |||
index = get_index() | |||
result = [] | |||
# Missing settings? | |||
if not "settings" in index: | |||
index["settings"] = dict() | |||
for album in index["album-order"]: | |||
if not album in index["settings"]: | |||
# Need to initialize its settings. | |||
index["settings"][album] = dict( | |||
format="classic", | |||
description="", | |||
) | |||
write_index(index) | |||
cover = index["covers"][album] | |||
pic = index["albums"][album][cover]["thumb"] | |||
result.append(dict( | |||
name=album, | |||
format=index["settings"][album]["format"], | |||
description=index["settings"][album]["description"], | |||
cover=pic, | |||
data=index["albums"][album], | |||
)) | |||
@@ -40,6 +54,68 @@ def list_albums(): | |||
return result | |||
def get_album(name): | |||
"""Get details about an album.""" | |||
index = get_index() | |||
result = [] | |||
if not name in index["albums"]: | |||
return None | |||
album = index["albums"][name] | |||
cover = index["covers"][name] | |||
return dict( | |||
name=name, | |||
format=index["settings"][name]["format"], | |||
description=index["settings"][name]["description"], | |||
cover=album[cover]["thumb"], | |||
) | |||
def rename_album(old_name, new_name): | |||
"""Rename an existing photo album. | |||
Returns True on success, False if the new name conflicts with another | |||
album's name.""" | |||
old_name = sanitize_name(old_name) | |||
newname = sanitize_name(new_name) | |||
index = get_index() | |||
# New name is unique? | |||
if new_name in index["albums"]: | |||
logger.error("Can't rename album: new name already exists!") | |||
return False | |||
def transfer_key(obj, old_key, new_key): | |||
# Reusable function to do a simple move on a dict key. | |||
obj[new_key] = obj[old_key] | |||
del obj[old_key] | |||
# Simple moves. | |||
transfer_key(index["albums"], old_name, new_name) | |||
transfer_key(index["covers"], old_name, new_name) | |||
transfer_key(index["photo-order"], old_name, new_name) | |||
transfer_key(index["settings"], old_name, new_name) | |||
# Update the photo -> album maps. | |||
for photo in index["map"]: | |||
if index["map"][photo] == old_name: | |||
index["map"][photo] = new_name | |||
# Fix the album ordering. | |||
new_order = list() | |||
for name in index["album-order"]: | |||
if name == old_name: | |||
name = new_name | |||
new_order.append(name) | |||
index["album-order"] = new_order | |||
# And save. | |||
write_index(index) | |||
return True | |||
def list_photos(album): | |||
"""List the photos in an album.""" | |||
album = sanitize_name(album) | |||
@@ -125,18 +201,18 @@ def get_image_dimensions(pic): | |||
return img.size | |||
def update_photo(album, key, data): | |||
"""Update photo meta-data in the album.""" | |||
index = get_index() | |||
# def update_photo(album, key, data): | |||
# """Update photo meta-data in the album.""" | |||
# index = get_index() | |||
if not album in index["albums"]: | |||
index["albums"][album] = {} | |||
if not key in index["albums"][album]: | |||
index["albums"][album][key] = {} | |||
# if not album in index["albums"]: | |||
# index["albums"][album] = {} | |||
# if not key in index["albums"][album]: | |||
# index["albums"][album][key] = {} | |||
# Update! | |||
index["albums"][album][key].update(data) | |||
write_index(index) | |||
# # Update! | |||
# index["albums"][album][key].update(data) | |||
# write_index(index) | |||
def crop_photo(key, x, y, length): | |||
@@ -201,6 +277,18 @@ def edit_photo(key, data): | |||
write_index(index) | |||
def edit_album(album, data): | |||
"""Update an album's settings (description, format, etc.)""" | |||
album = sanitize_name(album) | |||
index = get_index() | |||
if not album in index["albums"]: | |||
logger.error("Failed to edit album: not found!") | |||
return | |||
index["settings"][album].update(data) | |||
write_index(index) | |||
def rotate_photo(key, rotate): | |||
"""Rotate a photo 90 degrees to the left or right.""" | |||
photo = get_photo(key) | |||
@@ -404,6 +492,7 @@ def process_photo(form, filename): | |||
# What album are the photos going to? | |||
album = form.get("album", "") | |||
new_album = form.get("new-album", None) | |||
new_desc = form.get("new-description", None) | |||
if album == "" and new_album: | |||
album = new_album | |||
@@ -425,12 +514,18 @@ def process_photo(form, filename): | |||
# Update the photo data. | |||
if not album in index["albums"]: | |||
index["albums"][album] = {} | |||
if not album in index["settings"]: | |||
index["settings"][album] = { | |||
"format": "classic", | |||
"description": new_desc, | |||
} | |||
index["albums"][album][key] = dict( | |||
ip=request.remote_addr, | |||
author=g.info["session"]["uid"], | |||
uploaded=int(time.time()), | |||
caption=form.get("caption", ""), | |||
description=form.get("description", ""), | |||
**sizes | |||
) | |||
@@ -6,7 +6,8 @@ from flask import Blueprint, g, request, redirect, url_for, session, flash | |||
import rophako.model.user as User | |||
import rophako.model.photo as Photo | |||
from rophako.utils import template, pretty_time, login_required, ajax_response | |||
from rophako.utils import (template, pretty_time, render_markdown, | |||
login_required, ajax_response) | |||
from rophako.plugin import load_plugin | |||
from rophako.log import logger | |||
from config import * | |||
@@ -40,8 +41,15 @@ def album_index(name): | |||
flash("That album doesn't exist.") | |||
return redirect(url_for(".albums")) | |||
g.info["album"] = name | |||
g.info["photos"] = photos | |||
g.info["album"] = name | |||
g.info["album_info"] = Photo.get_album(name) | |||
g.info["markdown"] = render_markdown(g.info["album_info"]["description"]) | |||
g.info["photos"] = photos | |||
# Render Markdown descriptions for photos. | |||
for photo in g.info["photos"]: | |||
photo["data"]["markdown"] = render_markdown(photo["data"].get("description", "")) | |||
return template("photos/album.html") | |||
@@ -61,6 +69,7 @@ def view_photo(key): | |||
g.info["photo"] = photo | |||
g.info["photo"]["key"] = key | |||
g.info["photo"]["pretty_time"] = pretty_time(PHOTO_TIME_FORMAT, photo["uploaded"]) | |||
g.info["photo"]["markdown"] = render_markdown(photo.get("description", "")) | |||
return template("photos/view.html") | |||
@@ -196,9 +205,10 @@ def edit(key): | |||
return redirect(url_for(".albums")) | |||
if request.method == "POST": | |||
caption = request.form.get("caption", "") | |||
rotate = request.form.get("rotate", "") | |||
Photo.edit_photo(key, dict(caption=caption)) | |||
caption = request.form.get("caption", "") | |||
description = request.form.get("description", "") | |||
rotate = request.form.get("rotate", "") | |||
Photo.edit_photo(key, dict(caption=caption, description=description)) | |||
# Rotating the photo? | |||
if rotate in ["left", "right", "180"]: | |||
@@ -234,6 +244,43 @@ def delete(key): | |||
return template("photos/delete.html") | |||
@mod.route("/edit_album/<album>", methods=["GET", "POST"]) | |||
@login_required | |||
def edit_album(album): | |||
photos = Photo.list_photos(album) | |||
if photos is None: | |||
flash("That album doesn't exist.") | |||
return redirect(url_for(".albums")) | |||
if request.method == "POST": | |||
# Collect the form details. | |||
new_name = request.form["name"] | |||
description = request.form["description"] | |||
layout = request.form["format"] | |||
# Renaming the album? | |||
if new_name != album: | |||
ok = Photo.rename_album(album, new_name) | |||
if not ok: | |||
flash("Failed to rename album: already exists?") | |||
return redirect(url_for(".edit_album", album=album)) | |||
album = new_name | |||
# Update album settings. | |||
Photo.edit_album(album, dict( | |||
description=description, | |||
format=layout, | |||
)) | |||
return redirect(url_for(".albums")) | |||
g.info["album"] = album | |||
g.info["album_info"] = Photo.get_album(album) | |||
g.info["photos"] = photos | |||
return template("photos/edit_album.html") | |||
@mod.route("/arrange_albums", methods=["GET", "POST"]) | |||
@login_required | |||
def arrange_albums(): | |||
@@ -253,6 +300,34 @@ def arrange_albums(): | |||
return template("photos/arrange_albums.html") | |||
@mod.route("/edit_captions/<album>", methods=["GET", "POST"]) | |||
@login_required | |||
def bulk_captions(album): | |||
"""Bulk edit captions and titles in an album.""" | |||
photos = Photo.list_photos(album) | |||
if photos is None: | |||
flash("That album doesn't exist.") | |||
return redirect(url_for(".albums")) | |||
if request.method == "POST": | |||
# Do it. | |||
for photo in photos: | |||
caption_key = "{}:caption".format(photo["key"]) | |||
desc_key = "{}:description".format(photo["key"]) | |||
if caption_key in request.form and desc_key in request.form: | |||
caption = request.form[caption_key] | |||
description = request.form[desc_key] | |||
Photo.edit_photo(photo['key'], dict(caption=caption, description=description)) | |||
flash("The photos have been updated.") | |||
return redirect(url_for(".albums")) | |||
g.info["album"] = album | |||
g.info["photos"] = photos | |||
return template("photos/edit_captions.html") | |||
@mod.route("/delete_album/<album>", methods=["GET", "POST"]) | |||
@login_required | |||
def delete_album(album): | |||
@@ -4,22 +4,45 @@ | |||
<h1>Album: {{ album }}</h1> | |||
<ul class="photo-grid"> | |||
{% if markdown %} | |||
{{ markdown|safe }}<p> | |||
{% endif %} | |||
{% if album_info["format"] == "vertical" %} | |||
{% for photo in photos %} | |||
<li class="portrait"> | |||
<div class="dummy"></div> | |||
<div class="photo-grid-item"> | |||
<a href="{{ url_for('photo.view_photo', key=photo['key']) }}"> | |||
<img src="{{ app['photo_url'] }}/{{ photo['data']['thumb'] }}" width="100%" height="100%"> | |||
<span class="name">{{ photo["data"]["caption"] }}</span> | |||
</a> | |||
</div> | |||
</li> | |||
{% set data = photo["data"] %} | |||
{% if data["caption"] %} | |||
<h2>{{ data["caption"] }}</h2> | |||
{% endif %} | |||
<a href="{{ url_for('photo.view_photo', key=photo['key']) }}"> | |||
<img src="{{ app['photo_url'] }}/{{ data['large'] }}" class="portrait"> | |||
</a><p> | |||
{% if data["description"] %} | |||
<div class="photo-description">{{ data["markdown"]|safe }}</div> | |||
{% endif %} | |||
{% if loop.index < photos|length %}<hr>{% endif %} | |||
{% endfor %} | |||
{% else %}{# classic layout #} | |||
<ul class="photo-grid"> | |||
{% for photo in photos %} | |||
<li class="portrait"> | |||
<div class="dummy"></div> | |||
<div class="photo-grid-item"> | |||
<a href="{{ url_for('photo.view_photo', key=photo['key']) }}"> | |||
<img src="{{ app['photo_url'] }}/{{ photo['data']['thumb'] }}" width="100%" height="100%"> | |||
<span class="name">{{ photo["data"]["caption"] }}</span> | |||
</a> | |||
</div> | |||
</li> | |||
{% endfor %} | |||
</ul> | |||
<div class="clear"></div> | |||
</ul> | |||
<div class="clear"></div> | |||
{% endif %} | |||
{% if session["login"] %} | |||
<h1>Administrative Options</h1> | |||
@@ -27,6 +50,8 @@ | |||
<ul> | |||
<li><a href="{{ url_for('photo.upload') }}">Upload a Photo</a></li> | |||
{% if photos|length > 0 %} | |||
<li><a href="{{ url_for('photo.edit_album', album=album) }}">Edit Album Settings</a></li> | |||
<li><a href="{{ url_for('photo.bulk_captions', album=album) }}">Edit Image Titles/Descriptions</a></li> | |||
<li><a href="{{ url_for('photo.arrange_photos', album=album) }}">Rearrange Photos</a></li> | |||
<li><a href="{{ url_for('photo.delete_album', album=album) }}">Delete Album</a></li> | |||
{% endif %} | |||
@@ -12,6 +12,10 @@ | |||
<strong>Photo Caption:</strong><br> | |||
<input type="text" size="40" name="caption" value="{{ photo['caption'] }}"><p> | |||
<strong>Description:</strong><br> | |||
<textarea cols="50" rows="6" name="description">{{ photo['description'] }}</textarea><br> | |||
<small>Use <a href="/markdown">Markdown</a> syntax.</small><p> | |||
Rotate: | |||
<label> | |||
<input type="radio" name="rotate" value="" checked> Leave alone | |||
@@ -0,0 +1,31 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Edit Album{% endblock %} | |||
{% block content %} | |||
<h1>Edit Album: {{ album }}</h1> | |||
<form id="album-editor" action="{{ url_for('photo.edit_album', album=album) }}" method="POST"> | |||
<input type="hidden" name="token" value="{{ csrf_token() }}"> | |||
<strong>Album Title:</strong><br> | |||
<input type="text" size="40" name="name" value="{{ album }}"><p> | |||
<strong>Description:</strong><br> | |||
<textarea cols="50" rows="6" name="description">{{ album_info["description"] }}</textarea><br> | |||
<small>Use <a href="/markdown" target="_blank">Markdown</a> syntax.</small><p> | |||
<strong>Display Format:</strong><br> | |||
<label> | |||
<input type="radio" name="format" value="classic"{% if album_info["format"] == "classic" %} checked{% endif %}> | |||
<strong>Classic:</strong> Display a grid of thumbnails that must be clicked to view full size images. | |||
</label><br> | |||
<label> | |||
<input type="radio" name="format" value="vertical"{% if album_info["format"] == "vertical" %} checked{% endif %}> | |||
<strong>Vertical:</strong> Display all full size photos in one vertical view. | |||
</label><p> | |||
<button type="submit">Save Changes</button> | |||
</form> | |||
{% endblock %} |
@@ -0,0 +1,33 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Edit Captions{% endblock %} | |||
{% block content %} | |||
<h1>Edit Captions in {{ album }}</h1> | |||
All captions use <a href="/markdown">Markdown</a> syntax.<p> | |||
<form id="caption-editor" action="{{ url_for('photo.bulk_captions', album=album) }}" method="POST"> | |||
<input type="hidden" name="token" value="{{ csrf_token() }}"> | |||
<table width="100%" border="0" cellspacing="4" cellpadding="4"> | |||
{% for photo in photos %} | |||
<tr> | |||
<td width="100" align="center" valign="top"> | |||
<img src="{{ app['photo_url'] }}/{{ photo['data']['avatar'] }}" alt="Photo"> | |||
</td> | |||
<td align="left" valign="top"> | |||
<strong>Caption:</strong><br> | |||
<input type="text" size="40" name="{{ photo['key'] }}:caption" value="{{ photo['data']['caption'] }}"><p> | |||
<strong>Description:</strong><br> | |||
<textarea cols="50" rows="6" name="{{ photo['key'] }}:description">{{ photo['data']['description'] }}</textarea> | |||
</td> | |||
</tr> | |||
{% endfor %} | |||
</table><p> | |||
<button type="submit">Save Changes</button> | |||
</form> | |||
{% endblock %} |
@@ -50,15 +50,17 @@ somewhere else on the Internet. | |||
{% endfor %} | |||
</optgroup> | |||
<option value="">Create a new album</option> | |||
</select><p> | |||
</select> | |||
<blockquote id="create-album"> | |||
<strong>New album:</strong><br> | |||
<input type="text" size="20" id="new-album" name="new-album"> | |||
</blockquote> | |||
<input type="text" size="20" id="new-album" name="new-album"><p> | |||
<strong>Caption:</strong><br> | |||
<input type="text" size="40" name="caption"> | |||
<strong>Album Description:</strong><br> | |||
<textarea cols="50" rows="6" name="new-description"></textarea><br> | |||
<small>Shows up at the top of the album. | |||
Use <a href="/markdown" target="_blank">Markdown</a> formatting.</small> | |||
</blockquote> | |||
</fieldset> | |||
<p> | |||
@@ -22,15 +22,17 @@ | |||
{{ nav_links() }} | |||
<div class="center"> | |||
<a href="{{ url_for('photo.view_photo', key=photo['next']) }}"> | |||
<img src="{{ app['photo_url'] }}/{{ photo['large'] }}" class="portrait"> | |||
</a><br> | |||
<strong>{{ photo["caption"] }}</strong> | |||
</div> | |||
<p> | |||
Uploaded by {{ author["name"] }} on {{ photo["pretty_time"] }}. | |||
{% if photo["caption"] %} | |||
<h2>{{ photo["caption"] }}</h2> | |||
{% endif %} | |||
<a href="{{ url_for('photo.view_photo', key=photo['next']) }}"> | |||
<img src="{{ app['photo_url'] }}/{{ photo['large'] }}" class="portrait"> | |||
</a><p> | |||
{% if photo["markdown"] %} | |||
<div class="photo-description">{{ photo["markdown"]|safe }}</div> | |||
{% endif %} | |||
<em>Uploaded by {{ author["name"] }} on {{ photo["pretty_time"] }}.</em> | |||
{{ nav_links() }} | |||
@@ -219,6 +219,18 @@ ul.photo-grid li .dummy { | |||
box-shadow: 0px 0px 4px #FF4444; | |||
} | |||
/* Photo description blocks */ | |||
.photo-description { | |||
display: block; | |||
border: 1px solid #000000; | |||
box-shadow: 0px 0px 4px #000000; | |||
padding: 10px; | |||
margin: 20px 0px; | |||
background-color: #646464; | |||
color: #FFFFFF; | |||
width: 790px; | |||
} | |||
/* Blog titles when shown on index view */ | |||
a.blog-title-index:link, a.blog-title-index:visited { | |||
font-size: 32pt; | |||