Enhanced photo album features

Support album descriptions and layout options, and add photo
descriptions
This commit is contained in:
Noah 2014-08-26 18:11:34 -07:00
parent 1035fe287c
commit 1c32d08ab8
9 changed files with 321 additions and 42 deletions

View File

@ -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
)

View File

@ -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):

View File

@ -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>
{% endfor %}
{% set data = photo["data"] %}
{% if data["caption"] %}
<h2>{{ data["caption"] }}</h2>
{% endif %}
</ul>
<div class="clear"></div>
<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>
{% 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 %}

View File

@ -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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>
{% 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 %}
Uploaded by {{ author["name"] }} on {{ photo["pretty_time"] }}.
<em>Uploaded by {{ author["name"] }} on {{ photo["pretty_time"] }}.</em>
{{ nav_links() }}

View File

@ -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;