Enhanced photo album features
Support album descriptions and layout options, and add photo descriptions
This commit is contained in:
parent
1035fe287c
commit
1c32d08ab8
|
@ -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>
|
||||
{% 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 %}
|
||||
|
|
|
@ -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
|
||||
|
|
31
rophako/modules/photo/templates/photos/edit_album.html
Normal file
31
rophako/modules/photo/templates/photos/edit_album.html
Normal 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 %}
|
33
rophako/modules/photo/templates/photos/edit_captions.html
Normal file
33
rophako/modules/photo/templates/photos/edit_captions.html
Normal 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 %}
|
|
@ -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>
|
||||
{% 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() }}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue
Block a user