checkpoint

Add Bookmark class; Add user.html; Add more commit loggers; Fix warning_bag attributeerror when it was None
This commit is contained in:
voussoir 2017-02-04 18:30:02 -08:00
parent 109d5feef1
commit 8b05a26ff7
14 changed files with 300 additions and 40 deletions

View file

@ -37,6 +37,12 @@ SQL_ALBUM_COLUMNS = [
'description',
'associated_directory',
]
SQL_BOOKMARK_COLUMNS = [
'id',
'title',
'url',
'author_id',
]
SQL_PHOTO_COLUMNS = [
'id',
'filepath',
@ -82,6 +88,7 @@ SQL_USER_COLUMNS = [
_sql_dictify = lambda columns: {key:index for (index, key) in enumerate(columns)}
SQL_ALBUM = _sql_dictify(SQL_ALBUM_COLUMNS)
SQL_BOOKMARK = _sql_dictify(SQL_BOOKMARK_COLUMNS)
SQL_ALBUMPHOTO = _sql_dictify(SQL_ALBUMPHOTO_COLUMNS)
SQL_LASTID = _sql_dictify(SQL_LASTID_COLUMNS)
SQL_PHOTO = _sql_dictify(SQL_PHOTO_COLUMNS)

View file

@ -24,6 +24,8 @@ site.config.update(
TEMPLATES_AUTO_RELOAD=True,
)
site.jinja_env.add_extension('jinja2.ext.do')
site.jinja_env.trim_blocks = True
site.jinja_env.lstrip_blocks = True
site.debug = True
P = phototagger.PhotoDB()
@ -83,6 +85,12 @@ def P_tag(tagname):
except exceptions.NoSuchTag as e:
flask.abort(404, 'That tag doesnt exist: %s' % e)
def P_user(username):
try:
return P.get_user(username=username)
except exceptions.NoSuchUser as e:
flask.abort(404, 'That user doesnt exist: %s' % e)
def send_file(filepath):
'''
Range-enabled file sending.
@ -279,12 +287,14 @@ def get_album_zip(albumid):
recursive = request.args.get('recursive', True)
recursive = helpers.truthystring(recursive)
arcnames = helpers.album_zip_filenames(album, recursive=recursive)
streamed_zip = zipstream.ZipFile()
for (real_filepath, arcname) in arcnames.items():
streamed_zip.write(real_filepath, arcname=arcname)
# Add the album metadata as an {id}.txt file within each directory.
directories = helpers.album_zip_directories(album, recursive=recursive)
for (inner_album, directory) in directories.items():
text = []
@ -337,7 +347,8 @@ def get_albums_json():
@session_manager.give_token
def get_bookmarks():
session = session_manager.get(request)
return flask.render_template('bookmarks.html', session=session)
bookmarks = list(P.get_bookmarks())
return flask.render_template('bookmarks.html', bookmarks=bookmarks, session=session)
@site.route('/file/<photoid>')
@ -584,6 +595,26 @@ def get_thumbnail(photoid):
return send_file(path)
def get_user_core(username):
user = P_user(username)
return user
@site.route('/user/<username>', methods=['GET'])
@session_manager.give_token
def get_user_html(username):
user = get_user_core(username)
session = session_manager.get(request)
return flask.render_template('user.html', user=user, session=session)
@site.route('/user/<username>.json', methods=['GET'])
@session_manager.give_token
def get_user_json(username):
user = get_user_core(username)
user = jsonify.user(user)
user = jsonify.make_json_response(user)
return user
@site.route('/album/<albumid>', methods=['POST'])
@site.route('/album/<albumid>.json', methods=['POST'])
@session_manager.give_token

View file

@ -39,6 +39,22 @@ def upgrade_3_to_4(sql):
cur.execute('ALTER TABLE photos ADD COLUMN author_id TEXT')
cur.execute('CREATE INDEX IF NOT EXISTS index_photo_author on photos(author_id)')
def upgrade_4_to_5(sql):
'''
Add table `bookmarks` and its indices.
'''
cur = sql.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS bookmarks(
id TEXT,
title TEXT,
url TEXT,
author_id TEXT
)
''')
cur.execute('CREATE INDEX IF NOT EXISTS index_bookmark_id on bookmarks(id)')
cur.execute('CREATE INDEX IF NOT EXISTS index_bookmark_author on bookmarks(author_id)')
def upgrade_all(database_filename):
'''
Given the filename of a phototagger database, apply all of the needed

View file

@ -2,6 +2,9 @@
class NoSuchAlbum(Exception):
pass
class NoSuchBookmark(Exception):
pass
class NoSuchGroup(Exception):
pass

View file

@ -55,3 +55,11 @@ def tag(t):
'qualified_name': t.qualified_name(),
}
return j
def user(u):
j = {
'id': u.id,
'username': u.username,
'created': u.created,
}
return j

View file

@ -61,7 +61,7 @@ class GroupableMixin:
self.photodb._cached_frozen_children = None
cur.execute('INSERT INTO tag_group_rel VALUES(?, ?)', [self.id, member.id])
if commit:
self.photodb.log.debug('Commiting - add to group')
self.photodb.log.debug('Committing - add to group')
self.photodb.commit()
def children(self):
@ -266,6 +266,7 @@ class Album(ObjectBase, GroupableMixin):
[self.id, photo.id]
)
if commit:
self.photodb.log.debug('Committing - remove photo from album')
self.photodb.commit()
def walk_photos(self):
@ -277,6 +278,44 @@ class Album(ObjectBase, GroupableMixin):
print(child)
yield from child.walk_photos()
class Bookmark(ObjectBase):
def __init__(self, photodb, row_tuple):
self.photodb = photodb
if isinstance(row_tuple, (list, tuple)):
row_tuple = {constants.SQL_BOOKMARK_COLUMNS[index]: value for (index, value) in enumerate(row_tuple)}
self.id = row_tuple['id']
self.title = row_tuple['title']
self.url = row_tuple['url']
self.author_id = row_tuple['author_id']
def __repr__(self):
return 'Bookmark:{id}'.format(id=self.id)
def delete(self, *, commit=True):
cur = self.photodb.sql.cursor()
cur.execute('DELETE FROM bookmarks WHERE id == ?', [self.id])
if commit:
self.photodb.sql.commit()
def edit(self, title=None, url=None, *, commit=True):
if title is None and url is None:
return
if title is not None:
self.title = title
if url is not None:
self.url = url
cur = self.photodb.sql.cursor()
cur.execute('UPDATE bookmarks SET title = ?, url = ? WHERE id == ?', [self.title, self.url, self.id])
if commit:
self.photodb.log.debug('Committing - edit bookmark')
self.photodb.sql.commit()
class Photo(ObjectBase):
'''
A PhotoDB entry containing information about an image file.
@ -528,6 +567,8 @@ class Photo(ObjectBase):
self.ratio = None
self.duration = None
self.photodb.log.debug('Reloading metadata for {photo:r}'.format(photo=self))
if self.mimetype == 'image':
try:
image = PIL.Image.open(self.real_filepath)

View file

@ -28,7 +28,7 @@ logging.getLogger('PIL.PngImagePlugin').setLevel(logging.WARNING)
# Note: Setting user_version pragma in init sequence is safe because it only
# happens after the out-of-date check occurs, so no chance of accidentally
# overwriting it.
DATABASE_VERSION = 4
DATABASE_VERSION = 5
DB_INIT = '''
PRAGMA count_changes = OFF;
PRAGMA cache_size = 10000;
@ -39,6 +39,12 @@ CREATE TABLE IF NOT EXISTS albums(
description TEXT,
associated_directory TEXT COLLATE NOCASE
);
CREATE TABLE IF NOT EXISTS bookmarks(
id TEXT,
title TEXT,
url TEXT,
author_id TEXT
);
CREATE TABLE IF NOT EXISTS photos(
id TEXT,
filepath TEXT COLLATE NOCASE,
@ -91,6 +97,10 @@ CREATE INDEX IF NOT EXISTS index_album_id on albums(id);
CREATE INDEX IF NOT EXISTS index_albumrel_albumid on album_photo_rel(albumid);
CREATE INDEX IF NOT EXISTS index_albumrel_photoid on album_photo_rel(photoid);
-- Bookmark
CREATE INDEX IF NOT EXISTS index_bookmark_id on bookmarks(id);
CREATE INDEX IF NOT EXISTS index_bookmark_author on bookmarks(author_id);
-- Photo
CREATE INDEX IF NOT EXISTS index_photo_id on photos(id);
CREATE INDEX IF NOT EXISTS index_photo_path on photos(filepath COLLATE NOCASE);
@ -379,6 +389,48 @@ class PDBAlbumMixin:
return album
class PDBBookmarkMixin:
def get_bookmark(self, id):
cur = self.sql.cursor()
cur.execute('SELECT * FROM bookmarks WHERE id == ?', [id])
fetch = cur.fetchone()
if fetch is None:
raise exceptions.NoSuchBookmark(id)
bookmark = objects.Bookmark(self, fetch)
return bookmark
def get_bookmarks(self):
yield from self.get_things(thing_type='bookmark')
def new_bookmark(self, url, title=None, *, author=None, commit=True):
if not url:
raise ValueError('Must provide a URL')
bookmark_id = self.generate_id('bookmarks')
title = title or None
author_id = self.get_user_id_or_none(author)
# To do: NORMALIZATION AND VALIDATION
data = {
'author_id': author_id,
'id': bookmark_id,
'title': title,
'url': url,
}
(qmarks, bindings) = helpers.binding_filler(constants.SQL_BOOKMARK_COLUMNS, data)
query = 'INSERT INTO bookmarks VALUES(%s)' % qmarks
cur = self.sql.cursor()
cur.execute(query, bindings)
bookmark = objects.Bookmark(self, data)
if commit:
self.log.debug('Committing - new Bookmark')
self.sql.commit()
return bookmark
class PDBPhotoMixin:
def get_photo(self, photoid):
return self.get_thing_by_id('photo', photoid)
@ -452,15 +504,7 @@ class PDBPhotoMixin:
exc.photo = existing
raise exc
if isinstance(author, objects.User):
if author.photodb != self:
raise ValueError('That user does not belong to this photodb')
author_id = author.id
elif author is not None:
# Just to confirm
author_id = self.get_user(id=author).id
else:
author_id = None
author_id = self.get_user_id_or_none(author)
extension = os.path.splitext(filename)[1]
extension = extension.replace('.', '')
@ -503,11 +547,11 @@ class PDBPhotoMixin:
photo.add_tag(tag, commit=False)
if commit:
self.log.debug('Commiting - new_photo')
self.log.debug('Committing - new_photo')
self.commit()
return photo
def purge_deleted_files(self):
def purge_deleted_files(self, *, commit=True):
'''
Remove Photo entries if their corresponding file is no longer found.
'''
@ -515,14 +559,20 @@ class PDBPhotoMixin:
for photo in photos:
if os.path.exists(photo.real_filepath):
continue
photo.delete()
photo.delete(commit=False)
if commit:
self.log.debug('Committing - purge deleted photos')
self.sql.commit()
def purge_empty_albums(self):
def purge_empty_albums(self, *, commit=True):
albums = self.get_albums()
for album in albums:
if album.children() or album.photos():
continue
album.delete()
album.delete(commit=False)
if commit:
self.log.debug('Committing - purge empty albums')
self.sql.commit()
def search(
self,
@ -559,7 +609,7 @@ class PDBPhotoMixin:
TAGS AND FILTERS
authors:
A list of User object or users IDs.
A list of User objects, or usernames, or user ids.
created:
A hyphen_range string respresenting min and max. Or just a number for lower bound.
@ -813,7 +863,7 @@ class PDBPhotoMixin:
photos_received += 1
yield photo
if warning_bag.warnings:
if warning_bag and warning_bag.warnings:
yield warning_bag
end_time = time.time()
@ -894,7 +944,7 @@ class PDBTagMixin:
cur = self.sql.cursor()
cur.execute('INSERT INTO tags VALUES(?, ?)', [tagid, tagname])
if commit:
self.log.debug('Commiting - new_tag')
self.log.debug('Committing - new_tag')
self.commit()
tag = objects.Tag(self, [tagid, tagname])
return tag
@ -955,6 +1005,23 @@ class PDBUserMixin:
else:
raise exceptions.NoSuchUser(username)
def get_user_id_or_none(self, user):
'''
For methods that create photos, albums, etc., we sometimes associate
them with an author but sometimes not. This method hides validation
that those methods would otherwise have to duplicate.
'''
if isinstance(user, objects.User):
if user.photodb != self:
raise ValueError('That user does not belong to this photodb')
author_id = user.id
elif user is not None:
# Confirm that this string is an ID and not junk.
author_id = self.get_user(id=user).id
else:
author_id = None
return author_id
def login(self, user_id, password):
cur = self.sql.cursor()
cur.execute('SELECT * FROM users WHERE id == ?', [user_id])
@ -1018,7 +1085,7 @@ class PDBUserMixin:
return objects.User(self, data)
class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin, PDBUserMixin):
class PhotoDB(PDBAlbumMixin, PDBBookmarkMixin, PDBPhotoMixin, PDBTagMixin, PDBUserMixin):
'''
This class represents an SQLite3 database containing the following tables:
@ -1189,7 +1256,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin, PDBUserMixin):
current_album.add_photo(photo, commit=False)
if commit:
self.log.debug('Commiting - digest')
self.log.debug('Committing - digest')
self.commit()
return album
@ -1318,7 +1385,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin, PDBUserMixin):
ID is actually used.
'''
table = table.lower()
if table not in ['photos', 'tags', 'groups']:
if table not in ['photos', 'tags', 'groups', 'bookmarks']:
raise ValueError('Invalid table requested: %s.', table)
cur = self.sql.cursor()
@ -1377,6 +1444,12 @@ _THING_CLASSES = {
'exception': exceptions.NoSuchAlbum,
'table': 'albums',
},
'bookmark':
{
'class': objects.Bookmark,
'exception': exceptions.NoSuchBookmark,
'table': 'bookmarks',
},
'photo':
{
'class': objects.Photo,

View file

@ -26,6 +26,17 @@ def build_query(orderby):
query += ' ORDER BY %s' % orderby
return query
def get_user(photodb, username_or_id):
try:
user = photodb.get_user(username=username_or_id)
except exceptions.NoSuchUser:
try:
user = photodb.get_user(id=username_or_id)
except exceptions.NoSuchUser:
raise
return user
def minmax(key, value, minimums, maximums, warning_bag=None):
'''
Dissects a hyphenated range string and inserts the correct k:v pair into
@ -69,6 +80,14 @@ def minmax(key, value, minimums, maximums, warning_bag=None):
maximums[key] = high
def normalize_authors(authors, photodb, warning_bag=None):
'''
Either:
- A string, where the usernames are separated by commas
- An iterable containing usernames
- An iterable containing User objects.
Returns: A set of user IDs.
'''
if not authors:
return None
@ -84,7 +103,7 @@ def normalize_authors(authors, photodb, warning_bag=None):
requested_author = requested_author.username
try:
user = photodb.get_user(username=requested_author)
user = get_user(photodb, requested_author)
except exceptions.NoSuchUser:
if warning_bag:
warning_bag.add(constants.WARNING_NO_SUCH_USER.format(username=requested_author))

View file

@ -138,16 +138,6 @@ li
right: 8px;
font-size: 0.8em;
}
.photo_card_grid_info a
{
position: absolute;
max-height: 30px;
overflow: hidden;
}
.photo_card_grid_info a:hover
{
max-height: 100%;
}
.photo_card_grid_file_metadata
{
position: absolute;
@ -156,8 +146,15 @@ li
}
.photo_card_grid_filename
{
position: absolute;
max-height: 30px;
overflow: hidden;
word-break:break-word;
}
.photo_card_grid_filename:hover
{
max-height: 100%;
}
.photo_card_grid_tags
{
position: absolute;

View file

@ -36,7 +36,7 @@ p
{% if sub_albums %}
<h3>Sub-albums</h3>
<ul>
{% for sub_album in sub_albums %}
{% for sub_album in sub_albums|sort(attribute='title') %}
<li><a href="/album/{{sub_album.id}}">
{% if sub_album.title %}
{{sub_album.title}}

View file

@ -8,6 +8,26 @@
<script src="/static/common.js"></script>
<style>
#bookmarks
{
display: flex;
flex: 0 0 auto;
flex-direction: column;
}
.bookmark_card
{
background-color: #ffffd4;
display: inline-flex;
flex: 0 0 auto;
flex-direction: column;
padding: 8px;
margin: 8px;
border-radius: 8px;
}
.bookmark_card .bookmark_url
{
color: #aaa;
}
</style>
</head>
@ -15,7 +35,20 @@
<body>
{{header.make_header(session=session)}}
<div id="content_body">
<a href="/search?has_tags=no&orderby=random-desc&mimetype=image">Needs tagging</a>
<div id="bookmarks">
{% for bookmark in bookmarks %}
<div class="bookmark_card">
<a href="{{bookmark.url}}" class="bookmark_title">
{% if bookmark.title %}
{{bookmark.title}}
{% else %}
{{bookmark.id}}
{% endif %}
</a>
<a href="{{bookmark.url}}" class="bookmark_url">{{bookmark.url}}</a>
</div>
{% endfor %}
</div>
</div>
</body>

View file

@ -45,7 +45,7 @@
{% if photo.duration %}
{{photo.duration_string()}},
{% endif %}
{{photo.bytestring()}}
<a target="_blank" href="/file/{{photo.id}}.{{photo.extension}}">{{photo.bytestring()}}</a>
</span>
<span class="photo_card_grid_tags">
{% set tags = photo.tags() %}

View file

@ -12,6 +12,7 @@
<body>
{{header.make_header(session=session)}}
<div id="content_body">
<p>test</p>
</div>

31
templates/user.html Normal file
View file

@ -0,0 +1,31 @@
<!DOCTYPE html5>
<html>
<head>
{% import "header.html" as header %}
<title>User {{user.username}}</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css">
<style>
#content_body
{
/* overriding common.css here */
display: block;
}
</style>
</head>
<body>
{{header.make_header(session=session)}}
<div id="content_body">
<h2>{{user.username}}</h2>
<p>ID: {{user.id}}</p>
<p><a href="/search?author={{user.username}}">Photos by {{user.username}}</a></p>
</div>
</body>
<script type="text/javascript">
</script>
</html>