From 8b05a26ff75d4c9a869bf6f2a6bf22961da192da Mon Sep 17 00:00:00 2001 From: Ethan Dalool Date: Sat, 4 Feb 2017 18:30:02 -0800 Subject: [PATCH] checkpoint Add Bookmark class; Add user.html; Add more commit loggers; Fix warning_bag attributeerror when it was None --- constants.py | 7 +++ etiquette.py | 33 ++++++++++- etiquette_upgrader.py | 16 ++++++ exceptions.py | 3 + jsonify.py | 8 +++ objects.py | 43 +++++++++++++- phototagger.py | 115 +++++++++++++++++++++++++++++++------- searchhelpers.py | 21 ++++++- static/common.css | 17 +++--- templates/album.html | 2 +- templates/bookmarks.html | 35 +++++++++++- templates/photo_card.html | 2 +- templates/template.html | 7 ++- templates/user.html | 31 ++++++++++ 14 files changed, 300 insertions(+), 40 deletions(-) create mode 100644 templates/user.html diff --git a/constants.py b/constants.py index 41e63a5..fad0ca9 100644 --- a/constants.py +++ b/constants.py @@ -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) diff --git a/etiquette.py b/etiquette.py index f49e88b..6a8d7b1 100644 --- a/etiquette.py +++ b/etiquette.py @@ -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/') @@ -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/', 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/.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/', methods=['POST']) @site.route('/album/.json', methods=['POST']) @session_manager.give_token diff --git a/etiquette_upgrader.py b/etiquette_upgrader.py index 31f9c12..d42a84d 100644 --- a/etiquette_upgrader.py +++ b/etiquette_upgrader.py @@ -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 diff --git a/exceptions.py b/exceptions.py index 8471d07..80e1363 100644 --- a/exceptions.py +++ b/exceptions.py @@ -2,6 +2,9 @@ class NoSuchAlbum(Exception): pass +class NoSuchBookmark(Exception): + pass + class NoSuchGroup(Exception): pass diff --git a/jsonify.py b/jsonify.py index 779c577..8ed286c 100644 --- a/jsonify.py +++ b/jsonify.py @@ -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 diff --git a/objects.py b/objects.py index b8c5074..1ccd369 100644 --- a/objects.py +++ b/objects.py @@ -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) diff --git a/phototagger.py b/phototagger.py index 3543d7c..885efb8 100644 --- a/phototagger.py +++ b/phototagger.py @@ -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, diff --git a/searchhelpers.py b/searchhelpers.py index a1bcf62..25d8f26 100644 --- a/searchhelpers.py +++ b/searchhelpers.py @@ -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)) diff --git a/static/common.css b/static/common.css index 2e5de5e..05c870f 100644 --- a/static/common.css +++ b/static/common.css @@ -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; diff --git a/templates/album.html b/templates/album.html index a3aa873..76429ef 100644 --- a/templates/album.html +++ b/templates/album.html @@ -36,7 +36,7 @@ p {% if sub_albums %}

Sub-albums