Compare commits
No commits in common. "8b4fa133d1e76a61ac69354d79dd5019a507151b" and "da5c1ee0081f1117fc8448bf83eb50982e0532c6" have entirely different histories.
8b4fa133d1
...
da5c1ee008
35 changed files with 274 additions and 1371 deletions
|
|
@ -41,7 +41,7 @@ ffmpeg = _load_ffmpeg()
|
||||||
|
|
||||||
# Database #########################################################################################
|
# Database #########################################################################################
|
||||||
|
|
||||||
DATABASE_VERSION = 26
|
DATABASE_VERSION = 25
|
||||||
|
|
||||||
DB_INIT = '''
|
DB_INIT = '''
|
||||||
CREATE TABLE IF NOT EXISTS albums(
|
CREATE TABLE IF NOT EXISTS albums(
|
||||||
|
|
@ -204,15 +204,6 @@ CREATE TABLE IF NOT EXISTS tag_synonyms(
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS index_tag_synonyms_name on tag_synonyms(name);
|
CREATE INDEX IF NOT EXISTS index_tag_synonyms_name on tag_synonyms(name);
|
||||||
CREATE INDEX IF NOT EXISTS index_tag_synonyms_mastername on tag_synonyms(mastername);
|
CREATE INDEX IF NOT EXISTS index_tag_synonyms_mastername on tag_synonyms(mastername);
|
||||||
----------------------------------------------------------------------------------------------------
|
|
||||||
CREATE TABLE IF NOT EXISTS user_permissions(
|
|
||||||
userid TEXT NOT NULL,
|
|
||||||
permission TEXT NOT NULL,
|
|
||||||
created INT,
|
|
||||||
PRIMARY KEY(userid, permission),
|
|
||||||
FOREIGN KEY(userid) REFERENCES users(id)
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS index_user_permissions_userid on user_permissions(userid);
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
SQL_COLUMNS = sqlhelpers.extract_table_column_map(DB_INIT)
|
SQL_COLUMNS = sqlhelpers.extract_table_column_map(DB_INIT)
|
||||||
|
|
@ -318,7 +309,6 @@ DEFAULT_DATADIR = '_etiquette'
|
||||||
DEFAULT_DBNAME = 'phototagger.db'
|
DEFAULT_DBNAME = 'phototagger.db'
|
||||||
DEFAULT_CONFIGNAME = 'config.json'
|
DEFAULT_CONFIGNAME = 'config.json'
|
||||||
DEFAULT_THUMBDIR = 'thumbnails'
|
DEFAULT_THUMBDIR = 'thumbnails'
|
||||||
DEFAULT_UPLOADS_DIR = 'uploads'
|
|
||||||
|
|
||||||
DEFAULT_CONFIGURATION = {
|
DEFAULT_CONFIGURATION = {
|
||||||
'cache_size': {
|
'cache_size': {
|
||||||
|
|
@ -332,30 +322,25 @@ DEFAULT_CONFIGURATION = {
|
||||||
'enable_feature': {
|
'enable_feature': {
|
||||||
'album': {
|
'album': {
|
||||||
'edit': True,
|
'edit': True,
|
||||||
'delete': True,
|
|
||||||
'new': True,
|
'new': True,
|
||||||
},
|
},
|
||||||
'bookmark': {
|
'bookmark': {
|
||||||
'edit': True,
|
'edit': True,
|
||||||
'delete': True,
|
|
||||||
'new': True,
|
'new': True,
|
||||||
},
|
},
|
||||||
'photo': {
|
'photo': {
|
||||||
'add_remove_tag': True,
|
'add_remove_tag': True,
|
||||||
'new': True,
|
'new': True,
|
||||||
'edit': True,
|
'edit': True,
|
||||||
'delete': True,
|
|
||||||
'generate_thumbnail': True,
|
'generate_thumbnail': True,
|
||||||
'reload_metadata': True,
|
'reload_metadata': True,
|
||||||
},
|
},
|
||||||
'tag': {
|
'tag': {
|
||||||
'edit': True,
|
'edit': True,
|
||||||
'delete': True,
|
|
||||||
'new': True,
|
'new': True,
|
||||||
},
|
},
|
||||||
'user': {
|
'user': {
|
||||||
'edit': True,
|
'edit': True,
|
||||||
'delete': True,
|
|
||||||
'login': True,
|
'login': True,
|
||||||
'new': True,
|
'new': True,
|
||||||
},
|
},
|
||||||
|
|
@ -398,74 +383,3 @@ DEFAULT_CONFIGURATION = {
|
||||||
'Good morning, Paul. What will your first sequence of the day be?',
|
'Good morning, Paul. What will your first sequence of the day be?',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Permissions ######################################################################################
|
|
||||||
|
|
||||||
ANONYMOUS_USER_ID = 0
|
|
||||||
|
|
||||||
PERMISSION_ADMIN = 'admin'
|
|
||||||
PERMISSION_DEPUTY = 'deputy'
|
|
||||||
|
|
||||||
PERMISSION_ALBUM_CREATE = 'album_create'
|
|
||||||
PERMISSION_ALBUM_DELETE_ALL = 'album_delete_all'
|
|
||||||
PERMISSION_ALBUM_DELETE_OWN = 'album_delete_own'
|
|
||||||
PERMISSION_ALBUM_EDIT_ALL = 'album_edit_all'
|
|
||||||
PERMISSION_ALBUM_EDIT_OWN = 'album_edit_own'
|
|
||||||
|
|
||||||
PERMISSION_BOOKMARK_CREATE = 'bookmark_create'
|
|
||||||
PERMISSION_BOOKMARK_DELETE_ALL = 'bookmark_delete_all'
|
|
||||||
PERMISSION_BOOKMARK_DELETE_OWN = 'bookmark_delete_own'
|
|
||||||
PERMISSION_BOOKMARK_EDIT_ALL = 'bookmark_edit_all'
|
|
||||||
PERMISSION_BOOKMARK_EDIT_OWN = 'bookmark_edit_own'
|
|
||||||
|
|
||||||
PERMISSION_PHOTO_CREATE = 'photo_create'
|
|
||||||
PERMISSION_PHOTO_DELETE_ALL = 'photo_delete_all'
|
|
||||||
PERMISSION_PHOTO_DELETE_OWN = 'photo_delete_own'
|
|
||||||
PERMISSION_PHOTO_EDIT_ALL = 'photo_edit_all'
|
|
||||||
PERMISSION_PHOTO_EDIT_OWN = 'photo_edit_own'
|
|
||||||
|
|
||||||
PERMISSION_TAG_CREATE = 'tag_create'
|
|
||||||
PERMISSION_TAG_DELETE_ALL = 'tag_delete_all'
|
|
||||||
PERMISSION_TAG_DELETE_OWN = 'tag_delete_own'
|
|
||||||
PERMISSION_TAG_EDIT_ALL = 'tag_edit_all'
|
|
||||||
PERMISSION_TAG_EDIT_OWN = 'tag_edit_own'
|
|
||||||
|
|
||||||
ALL_PERMISSIONS = {
|
|
||||||
PERMISSION_ADMIN,
|
|
||||||
# deputy is omitted intentionally
|
|
||||||
PERMISSION_ALBUM_CREATE,
|
|
||||||
PERMISSION_ALBUM_DELETE_ALL,
|
|
||||||
PERMISSION_ALBUM_DELETE_OWN,
|
|
||||||
PERMISSION_ALBUM_EDIT_ALL,
|
|
||||||
PERMISSION_ALBUM_EDIT_OWN,
|
|
||||||
PERMISSION_BOOKMARK_CREATE,
|
|
||||||
PERMISSION_BOOKMARK_DELETE_ALL,
|
|
||||||
PERMISSION_BOOKMARK_DELETE_OWN,
|
|
||||||
PERMISSION_BOOKMARK_EDIT_ALL,
|
|
||||||
PERMISSION_BOOKMARK_EDIT_OWN,
|
|
||||||
PERMISSION_PHOTO_CREATE,
|
|
||||||
PERMISSION_PHOTO_DELETE_ALL,
|
|
||||||
PERMISSION_PHOTO_DELETE_OWN,
|
|
||||||
PERMISSION_PHOTO_EDIT_ALL,
|
|
||||||
PERMISSION_PHOTO_EDIT_OWN,
|
|
||||||
PERMISSION_TAG_CREATE,
|
|
||||||
PERMISSION_TAG_DELETE_ALL,
|
|
||||||
PERMISSION_TAG_DELETE_OWN,
|
|
||||||
PERMISSION_TAG_EDIT_ALL,
|
|
||||||
PERMISSION_TAG_EDIT_OWN,
|
|
||||||
}
|
|
||||||
|
|
||||||
NEW_USER_PERMISSIONS = {
|
|
||||||
PERMISSION_ALBUM_CREATE,
|
|
||||||
PERMISSION_ALBUM_DELETE_OWN,
|
|
||||||
PERMISSION_ALBUM_EDIT_OWN,
|
|
||||||
PERMISSION_BOOKMARK_CREATE,
|
|
||||||
PERMISSION_BOOKMARK_DELETE_OWN,
|
|
||||||
PERMISSION_BOOKMARK_EDIT_OWN,
|
|
||||||
PERMISSION_PHOTO_CREATE,
|
|
||||||
PERMISSION_PHOTO_DELETE_OWN,
|
|
||||||
PERMISSION_PHOTO_EDIT_OWN,
|
|
||||||
PERMISSION_TAG_CREATE,
|
|
||||||
PERMISSION_TAG_DELETE_OWN,
|
|
||||||
PERMISSION_TAG_EDIT_OWN,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -157,14 +157,12 @@ class UsernameTooShort(InvalidUsername):
|
||||||
class DisplayNameTooLong(EtiquetteException):
|
class DisplayNameTooLong(EtiquetteException):
|
||||||
error_message = 'Display name "{display_name}" is longer than maximum of {max_length}.'
|
error_message = 'Display name "{display_name}" is longer than maximum of {max_length}.'
|
||||||
|
|
||||||
class WrongLogin(EtiquetteException):
|
|
||||||
error_message = 'Wrong username-password combination.'
|
|
||||||
|
|
||||||
# PERMISSION ERRORS ################################################################################
|
|
||||||
|
|
||||||
class Unauthorized(EtiquetteException):
|
class Unauthorized(EtiquetteException):
|
||||||
error_message = 'You\'re not allowed to do that.'
|
error_message = 'You\'re not allowed to do that.'
|
||||||
|
|
||||||
|
class WrongLogin(EtiquetteException):
|
||||||
|
error_message = 'Wrong username-password combination.'
|
||||||
|
|
||||||
# GENERAL ERRORS ###################################################################################
|
# GENERAL ERRORS ###################################################################################
|
||||||
|
|
||||||
class BadDataDirectory(EtiquetteException):
|
class BadDataDirectory(EtiquetteException):
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ def image_is_mostly_black(image):
|
||||||
|
|
||||||
return (black_count / len(pixels)) > 0.5
|
return (black_count / len(pixels)) > 0.5
|
||||||
|
|
||||||
def generate_video_thumbnail(filepath, width, height, special={}) -> PIL.Image:
|
def generate_video_thumbnail(filepath, width, height, **special) -> PIL.Image:
|
||||||
file = pathclass.Path(filepath)
|
file = pathclass.Path(filepath)
|
||||||
file.assert_is_file()
|
file.assert_is_file()
|
||||||
probe = constants.ffmpeg.probe(filepath)
|
probe = constants.ffmpeg.probe(filepath)
|
||||||
|
|
@ -284,16 +284,16 @@ def generate_video_thumbnail(filepath, width, height, special={}) -> PIL.Image:
|
||||||
frame_width=width,
|
frame_width=width,
|
||||||
frame_height=height,
|
frame_height=height,
|
||||||
)
|
)
|
||||||
duration = probe.video.duration or probe.format.duration
|
duration = probe.video.duration
|
||||||
|
|
||||||
if 'timestamp' in special:
|
if 'timestamp' in special:
|
||||||
timestamp_choices = [float(special['timestamp'])]
|
timestamp_choices = [special['timestamp']]
|
||||||
else:
|
else:
|
||||||
timestamp_choices = list(range(0, int(duration), 3))
|
timestamp_choices = list(range(0, int(duration), 3))
|
||||||
|
|
||||||
image = None
|
image = None
|
||||||
for this_time in timestamp_choices:
|
for this_time in timestamp_choices:
|
||||||
log.debug('Attempting video thumbnail at t=%s', this_time)
|
log.debug('Attempting video thumbnail at t=%d', this_time)
|
||||||
command = kkroening_ffmpeg.input(file.absolute_path, ss=this_time)
|
command = kkroening_ffmpeg.input(file.absolute_path, ss=this_time)
|
||||||
command = command.filter('scale', size[0], size[1])
|
command = command.filter('scale', size[0], size[1])
|
||||||
command = command.output('pipe:', vcodec='bmp', format='image2pipe', vframes=1)
|
command = command.output('pipe:', vcodec='bmp', format='image2pipe', vframes=1)
|
||||||
|
|
@ -317,7 +317,7 @@ def hash_photoset(photos) -> str:
|
||||||
|
|
||||||
photo_ids = sorted(set(p.id for p in photos))
|
photo_ids = sorted(set(p.id for p in photos))
|
||||||
for photo_id in photo_ids:
|
for photo_id in photo_ids:
|
||||||
hasher.update(str(photo_id).encode('utf-8'))
|
hasher.update(photo_id.encode('utf-8'))
|
||||||
|
|
||||||
return hasher.hexdigest()
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -273,10 +273,6 @@ class Album(ObjectBase, GroupableMixin):
|
||||||
table = 'albums'
|
table = 'albums'
|
||||||
group_table = 'album_group_rel'
|
group_table = 'album_group_rel'
|
||||||
no_such_exception = exceptions.NoSuchAlbum
|
no_such_exception = exceptions.NoSuchAlbum
|
||||||
permission_edit_all = constants.PERMISSION_ALBUM_EDIT_ALL
|
|
||||||
permission_edit_own = constants.PERMISSION_ALBUM_EDIT_OWN
|
|
||||||
permission_delete_all = constants.PERMISSION_ALBUM_DELETE_ALL
|
|
||||||
permission_delete_own = constants.PERMISSION_ALBUM_DELETE_OWN
|
|
||||||
|
|
||||||
def __init__(self, photodb, db_row):
|
def __init__(self, photodb, db_row):
|
||||||
super().__init__(photodb)
|
super().__init__(photodb)
|
||||||
|
|
@ -472,7 +468,7 @@ class Album(ObjectBase, GroupableMixin):
|
||||||
|
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
@decorators.required_feature('album.delete')
|
@decorators.required_feature('album.edit')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def delete(self, *, delete_children=False) -> None:
|
def delete(self, *, delete_children=False) -> None:
|
||||||
log.info('Deleting %s.', self)
|
log.info('Deleting %s.', self)
|
||||||
|
|
@ -779,10 +775,6 @@ class Album(ObjectBase, GroupableMixin):
|
||||||
class Bookmark(ObjectBase):
|
class Bookmark(ObjectBase):
|
||||||
table = 'bookmarks'
|
table = 'bookmarks'
|
||||||
no_such_exception = exceptions.NoSuchBookmark
|
no_such_exception = exceptions.NoSuchBookmark
|
||||||
permission_edit_all = constants.PERMISSION_BOOKMARK_EDIT_ALL
|
|
||||||
permission_edit_own = constants.PERMISSION_BOOKMARK_EDIT_OWN
|
|
||||||
permission_delete_all = constants.PERMISSION_BOOKMARK_DELETE_ALL
|
|
||||||
permission_delete_own = constants.PERMISSION_BOOKMARK_DELETE_OWN
|
|
||||||
|
|
||||||
def __init__(self, photodb, db_row):
|
def __init__(self, photodb, db_row):
|
||||||
super().__init__(photodb)
|
super().__init__(photodb)
|
||||||
|
|
@ -862,7 +854,7 @@ class Bookmark(ObjectBase):
|
||||||
|
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
@decorators.required_feature('bookmark.delete')
|
@decorators.required_feature('bookmark.edit')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
self.photodb.delete(table=Bookmark, pairs={'id': self.id})
|
self.photodb.delete(table=Bookmark, pairs={'id': self.id})
|
||||||
|
|
@ -923,10 +915,6 @@ class Photo(ObjectBase):
|
||||||
'''
|
'''
|
||||||
table = 'photos'
|
table = 'photos'
|
||||||
no_such_exception = exceptions.NoSuchPhoto
|
no_such_exception = exceptions.NoSuchPhoto
|
||||||
permission_edit_all = constants.PERMISSION_PHOTO_EDIT_ALL
|
|
||||||
permission_edit_own = constants.PERMISSION_PHOTO_EDIT_OWN
|
|
||||||
permission_delete_all = constants.PERMISSION_PHOTO_DELETE_ALL
|
|
||||||
permission_delete_own = constants.PERMISSION_PHOTO_DELETE_OWN
|
|
||||||
|
|
||||||
def __init__(self, photodb, db_row):
|
def __init__(self, photodb, db_row):
|
||||||
super().__init__(photodb)
|
super().__init__(photodb)
|
||||||
|
|
@ -1014,11 +1002,6 @@ class Photo(ObjectBase):
|
||||||
@decorators.required_feature('photo.add_remove_tag')
|
@decorators.required_feature('photo.add_remove_tag')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def add_tag(self, tag, timestamp=None):
|
def add_tag(self, tag, timestamp=None):
|
||||||
if isinstance(tag, PhotoTagRel):
|
|
||||||
tag = tag.tag
|
|
||||||
elif isinstance(tag, Tag):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
tag = self.photodb.get_tag(name=tag)
|
tag = self.photodb.get_tag(name=tag)
|
||||||
|
|
||||||
existing = self.has_tag(tag, check_children=False, match_timestamp=timestamp)
|
existing = self.has_tag(tag, check_children=False, match_timestamp=timestamp)
|
||||||
|
|
@ -1092,9 +1075,9 @@ class Photo(ObjectBase):
|
||||||
Take all of the tags owned by other_photo and apply them to this photo.
|
Take all of the tags owned by other_photo and apply them to this photo.
|
||||||
'''
|
'''
|
||||||
for tag in other_photo.get_tags():
|
for tag in other_photo.get_tags():
|
||||||
self.add_tag(tag, timestamp=tag.timestamp)
|
self.add_tag(tag)
|
||||||
|
|
||||||
@decorators.required_feature('photo.delete')
|
@decorators.required_feature('photo.edit')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def delete(self, *, delete_file=False) -> None:
|
def delete(self, *, delete_file=False) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -1130,11 +1113,11 @@ class Photo(ObjectBase):
|
||||||
|
|
||||||
@decorators.required_feature('photo.generate_thumbnail')
|
@decorators.required_feature('photo.generate_thumbnail')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def generate_thumbnail(self, trusted_file=False, special={}):
|
def generate_thumbnail(self, trusted_file=False, **special):
|
||||||
'''
|
'''
|
||||||
special:
|
special:
|
||||||
You can provide `max_width` and/or `max_height` to override the
|
For images, you can provide `max_width` and/or `max_height` to
|
||||||
config file.
|
override the config file.
|
||||||
For videos, you can provide a `timestamp` to take the thumbnail at.
|
For videos, you can provide a `timestamp` to take the thumbnail at.
|
||||||
'''
|
'''
|
||||||
image = None
|
image = None
|
||||||
|
|
@ -1157,9 +1140,9 @@ class Photo(ObjectBase):
|
||||||
try:
|
try:
|
||||||
image = helpers.generate_video_thumbnail(
|
image = helpers.generate_video_thumbnail(
|
||||||
self.real_path.absolute_path,
|
self.real_path.absolute_path,
|
||||||
width=special.get('max_width', self.photodb.config['thumbnail_width']),
|
width=self.photodb.config['thumbnail_width'],
|
||||||
height=special.get('max_height', self.photodb.config['thumbnail_height']),
|
height=self.photodb.config['thumbnail_height'],
|
||||||
special=special,
|
**special
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning(traceback.format_exc())
|
log.warning(traceback.format_exc())
|
||||||
|
|
@ -1392,7 +1375,6 @@ class Photo(ObjectBase):
|
||||||
'bytes': self.bytes,
|
'bytes': self.bytes,
|
||||||
}
|
}
|
||||||
self.photodb.update(table=Photo, pairs=data, where_key='id')
|
self.photodb.update(table=Photo, pairs=data, where_key='id')
|
||||||
self.__reinit__()
|
|
||||||
|
|
||||||
@decorators.required_feature('photo.edit')
|
@decorators.required_feature('photo.edit')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
|
|
@ -2116,10 +2098,6 @@ class Tag(ObjectBase, GroupableMixin):
|
||||||
table = 'tags'
|
table = 'tags'
|
||||||
group_table = 'tag_group_rel'
|
group_table = 'tag_group_rel'
|
||||||
no_such_exception = exceptions.NoSuchTag
|
no_such_exception = exceptions.NoSuchTag
|
||||||
permission_edit_all = constants.PERMISSION_TAG_EDIT_ALL
|
|
||||||
permission_edit_own = constants.PERMISSION_TAG_EDIT_OWN
|
|
||||||
permission_delete_all = constants.PERMISSION_TAG_DELETE_ALL
|
|
||||||
permission_delete_own = constants.PERMISSION_TAG_DELETE_OWN
|
|
||||||
|
|
||||||
def __init__(self, photodb, db_row):
|
def __init__(self, photodb, db_row):
|
||||||
super().__init__(photodb)
|
super().__init__(photodb)
|
||||||
|
|
@ -2313,7 +2291,7 @@ class Tag(ObjectBase, GroupableMixin):
|
||||||
# Enjoy your new life as a monk.
|
# Enjoy your new life as a monk.
|
||||||
mastertag.add_synonym(self.name)
|
mastertag.add_synonym(self.name)
|
||||||
|
|
||||||
@decorators.required_feature('tag.delete')
|
@decorators.required_feature('tag.edit')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def delete(self, *, delete_children=False) -> None:
|
def delete(self, *, delete_children=False) -> None:
|
||||||
log.info('Deleting %s.', self)
|
log.info('Deleting %s.', self)
|
||||||
|
|
@ -2522,15 +2500,6 @@ class User(ObjectBase):
|
||||||
def _uncache(self):
|
def _uncache(self):
|
||||||
self.photodb.caches[User].remove(self.id)
|
self.photodb.caches[User].remove(self.id)
|
||||||
|
|
||||||
@decorators.required_feature('user.edit')
|
|
||||||
def add_permission(self, permission):
|
|
||||||
pairs = {
|
|
||||||
'userid': self.id,
|
|
||||||
'permission': permission,
|
|
||||||
'created': timetools.now().timestamp(),
|
|
||||||
}
|
|
||||||
self.photodb.insert(table='user_permissions', pairs=pairs, ignore_duplicate=True)
|
|
||||||
|
|
||||||
@decorators.required_feature('user.login')
|
@decorators.required_feature('user.login')
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
if not isinstance(password, bytes):
|
if not isinstance(password, bytes):
|
||||||
|
|
@ -2541,7 +2510,7 @@ class User(ObjectBase):
|
||||||
raise exceptions.WrongLogin()
|
raise exceptions.WrongLogin()
|
||||||
return success
|
return success
|
||||||
|
|
||||||
@decorators.required_feature('user.delete')
|
@decorators.required_feature('user.edit')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def delete(self, *, disown_authored_things) -> None:
|
def delete(self, *, disown_authored_things) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -2605,13 +2574,6 @@ class User(ObjectBase):
|
||||||
[self.id]
|
[self.id]
|
||||||
)
|
)
|
||||||
|
|
||||||
@decorators.cache_until_commit
|
|
||||||
def get_permissions(self):
|
|
||||||
return set(self.photodb.select_column(
|
|
||||||
'SELECT permission FROM user_permissions WHERE userid == ?',
|
|
||||||
[self.id],
|
|
||||||
))
|
|
||||||
|
|
||||||
def get_photos(self, *, direction='asc') -> typing.Iterable[Photo]:
|
def get_photos(self, *, direction='asc') -> typing.Iterable[Photo]:
|
||||||
'''
|
'''
|
||||||
Raises ValueError if direction is not asc or desc.
|
Raises ValueError if direction is not asc or desc.
|
||||||
|
|
@ -2656,41 +2618,6 @@ class User(ObjectBase):
|
||||||
exists = self.photodb.select_one_value(query, [self.id])
|
exists = self.photodb.select_one_value(query, [self.id])
|
||||||
return exists is not None
|
return exists is not None
|
||||||
|
|
||||||
def has_object_permission(self, thing, edit_or_delete) -> bool:
|
|
||||||
my_permissions = self.get_permissions()
|
|
||||||
if constants.PERMISSION_ADMIN in my_permissions:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if edit_or_delete == 'edit' and thing.permission_edit_all in my_permissions:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if edit_or_delete == 'delete' and thing.permission_delete_all in my_permissions:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# If this user does not have ADMIN or ALL permission, then they must be
|
|
||||||
# the owner or deputy of the owner. So if the thing has no owner then
|
|
||||||
# there's no way.
|
|
||||||
thing_author = thing.author
|
|
||||||
if not thing_author:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if edit_or_delete == 'edit':
|
|
||||||
own_permission = thing.permission_edit_own
|
|
||||||
elif edit_or_delete == 'delete':
|
|
||||||
own_permission = thing.permission_delete_own
|
|
||||||
|
|
||||||
if thing_author == self and own_permission in my_permissions:
|
|
||||||
return True
|
|
||||||
|
|
||||||
deputy_permission = constants.PERMISSION_DEPUTY + f':{thing_author.id}'
|
|
||||||
if deputy_permission in my_permissions and own_permission in thing_author.get_permissions():
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def has_permission(self, permission_string):
|
|
||||||
return permission_string in self.get_permissions()
|
|
||||||
|
|
||||||
def jsonify(self) -> dict:
|
def jsonify(self) -> dict:
|
||||||
j = {
|
j = {
|
||||||
'type': 'user',
|
'type': 'user',
|
||||||
|
|
@ -2704,14 +2631,6 @@ class User(ObjectBase):
|
||||||
|
|
||||||
return j
|
return j
|
||||||
|
|
||||||
@decorators.required_feature('user.edit')
|
|
||||||
def remove_permission(self, permission):
|
|
||||||
pairs = {
|
|
||||||
'userid': self.id,
|
|
||||||
'permission': permission,
|
|
||||||
}
|
|
||||||
self.photodb.delete(table='user_permissions', pairs=pairs)
|
|
||||||
|
|
||||||
@decorators.required_feature('user.edit')
|
@decorators.required_feature('user.edit')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def set_display_name(self, display_name) -> None:
|
def set_display_name(self, display_name) -> None:
|
||||||
|
|
|
||||||
|
|
@ -315,7 +315,6 @@ class PDBPhotoMixin:
|
||||||
do_thumbnail=True,
|
do_thumbnail=True,
|
||||||
hash_kwargs=None,
|
hash_kwargs=None,
|
||||||
known_hash=None,
|
known_hash=None,
|
||||||
override_filename=None,
|
|
||||||
searchhidden=False,
|
searchhidden=False,
|
||||||
tags=None,
|
tags=None,
|
||||||
trusted_file=False,
|
trusted_file=False,
|
||||||
|
|
@ -357,7 +356,7 @@ class PDBPhotoMixin:
|
||||||
data = {
|
data = {
|
||||||
'id': photo_id,
|
'id': photo_id,
|
||||||
'filepath': filepath.absolute_path,
|
'filepath': filepath.absolute_path,
|
||||||
'override_filename': override_filename,
|
'override_filename': None,
|
||||||
'created': timetools.now().timestamp(),
|
'created': timetools.now().timestamp(),
|
||||||
'tagged_at': None,
|
'tagged_at': None,
|
||||||
'author_id': author_id,
|
'author_id': author_id,
|
||||||
|
|
@ -683,14 +682,7 @@ class PDBUserMixin:
|
||||||
|
|
||||||
@decorators.required_feature('user.new')
|
@decorators.required_feature('user.new')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def new_user(
|
def new_user(self, username, password, *, display_name=None) -> objects.User:
|
||||||
self,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
*,
|
|
||||||
display_name=None,
|
|
||||||
permissions=constants.NEW_USER_PERMISSIONS,
|
|
||||||
) -> objects.User:
|
|
||||||
# These might raise exceptions.
|
# These might raise exceptions.
|
||||||
self.assert_valid_username(username)
|
self.assert_valid_username(username)
|
||||||
self.assert_no_such_user(username=username)
|
self.assert_no_such_user(username=username)
|
||||||
|
|
@ -711,24 +703,15 @@ class PDBUserMixin:
|
||||||
|
|
||||||
hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())
|
hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())
|
||||||
|
|
||||||
now = timetools.now().timestamp()
|
|
||||||
data = {
|
data = {
|
||||||
'id': user_id,
|
'id': user_id,
|
||||||
'username': username,
|
'username': username,
|
||||||
'password': hashed_password,
|
'password': hashed_password,
|
||||||
'display_name': display_name,
|
'display_name': display_name,
|
||||||
'created': now,
|
'created': timetools.now().timestamp(),
|
||||||
}
|
}
|
||||||
self.insert(table=objects.User, pairs=data)
|
self.insert(table=objects.User, pairs=data)
|
||||||
|
|
||||||
for permission in set(permissions):
|
|
||||||
permission_row = {
|
|
||||||
'userid': user_id,
|
|
||||||
'permission': permission,
|
|
||||||
'created': now,
|
|
||||||
}
|
|
||||||
self.insert(table='user_permissions', pairs=permission_row)
|
|
||||||
|
|
||||||
return self.get_cached_instance(objects.User, data)
|
return self.get_cached_instance(objects.User, data)
|
||||||
|
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
|
|
@ -742,7 +725,6 @@ class PDBUtilMixin:
|
||||||
self,
|
self,
|
||||||
directory,
|
directory,
|
||||||
*,
|
*,
|
||||||
check_existing=True,
|
|
||||||
exclude_directories=None,
|
exclude_directories=None,
|
||||||
exclude_filenames=None,
|
exclude_filenames=None,
|
||||||
glob_directories=None,
|
glob_directories=None,
|
||||||
|
|
@ -763,21 +745,6 @@ class PDBUtilMixin:
|
||||||
If a Photo object already exists for a file, it will be added to the
|
If a Photo object already exists for a file, it will be added to the
|
||||||
correct album.
|
correct album.
|
||||||
|
|
||||||
check_existing:
|
|
||||||
If True, we will check each file to see if it is already in the
|
|
||||||
database. We will also use the file hash to see if each file is a
|
|
||||||
move/rename of a file in the database. This is helpful when we are
|
|
||||||
re-digesting a folder we've already digested before, looking for new
|
|
||||||
files and skipping the existing ones, and updating the database to
|
|
||||||
match out of band moves/renames.
|
|
||||||
If False, we will not check for existing records, and treat every
|
|
||||||
file as brand new. This saves some processing time during the
|
|
||||||
digest, but could lead to duplicate Photo records if you are not
|
|
||||||
careful. This can be helpful when processing a user's upload job
|
|
||||||
because it is guaranteed we've never processed that folder before,
|
|
||||||
and because we'd want to consider incoming uploads new and unique
|
|
||||||
even if they are a hash match for an existing file.
|
|
||||||
|
|
||||||
exclude_directories:
|
exclude_directories:
|
||||||
A list of basenames or absolute paths of directories to ignore.
|
A list of basenames or absolute paths of directories to ignore.
|
||||||
This list works in addition to, not instead of, the
|
This list works in addition to, not instead of, the
|
||||||
|
|
@ -914,17 +881,6 @@ class PDBUtilMixin:
|
||||||
# hash work by passing this as the known_hash to new_photo.
|
# hash work by passing this as the known_hash to new_photo.
|
||||||
return {'sha256': sha256}
|
return {'sha256': sha256}
|
||||||
|
|
||||||
def create_photo(filepath):
|
|
||||||
'''
|
|
||||||
Create a Photo from this filepath without attempting to check for
|
|
||||||
existing copies or hash matches.
|
|
||||||
'''
|
|
||||||
photo = self.new_photo(filepath, **new_photo_kwargs)
|
|
||||||
if new_photo_ratelimit is not None:
|
|
||||||
new_photo_ratelimit.limit()
|
|
||||||
|
|
||||||
return (photo, True)
|
|
||||||
|
|
||||||
def create_or_fetch_photo(filepath):
|
def create_or_fetch_photo(filepath):
|
||||||
'''
|
'''
|
||||||
Given a filepath, find the corresponding Photo object if it exists,
|
Given a filepath, find the corresponding Photo object if it exists,
|
||||||
|
|
@ -1005,10 +961,7 @@ class PDBUtilMixin:
|
||||||
if natural_sort:
|
if natural_sort:
|
||||||
files = sorted(files, key=lambda f: stringtools.natural_sorter(f.basename))
|
files = sorted(files, key=lambda f: stringtools.natural_sorter(f.basename))
|
||||||
|
|
||||||
if check_existing:
|
|
||||||
photos = [create_or_fetch_photo(file) for file in files]
|
photos = [create_or_fetch_photo(file) for file in files]
|
||||||
else:
|
|
||||||
photos = [create_photo(file) for file in files]
|
|
||||||
|
|
||||||
# Note, this means that empty folders will not get an Album.
|
# Note, this means that empty folders will not get an Album.
|
||||||
# At this time this behavior is intentional. Furthermore, due to
|
# At this time this behavior is intentional. Furthermore, due to
|
||||||
|
|
@ -1155,9 +1108,6 @@ class PhotoDB(
|
||||||
self.thumbnail_directory = self.data_directory.with_child(constants.DEFAULT_THUMBDIR)
|
self.thumbnail_directory = self.data_directory.with_child(constants.DEFAULT_THUMBDIR)
|
||||||
self.thumbnail_directory.makedirs(exist_ok=True)
|
self.thumbnail_directory.makedirs(exist_ok=True)
|
||||||
|
|
||||||
self.uploads_directory = self.data_directory.with_child(constants.DEFAULT_UPLOADS_DIR)
|
|
||||||
self.uploads_directory.makedirs(exist_ok=True)
|
|
||||||
|
|
||||||
# CONFIG
|
# CONFIG
|
||||||
self.config_filepath = self.data_directory.with_child(constants.DEFAULT_CONFIGNAME)
|
self.config_filepath = self.data_directory.with_child(constants.DEFAULT_CONFIGNAME)
|
||||||
self.load_config()
|
self.load_config()
|
||||||
|
|
|
||||||
|
|
@ -409,7 +409,7 @@ def purge_deleted_files_argparse(args):
|
||||||
if args.any_photo_args:
|
if args.any_photo_args:
|
||||||
photos = get_photos_from_args(args)
|
photos = get_photos_from_args(args)
|
||||||
else:
|
else:
|
||||||
photos = list(photodb.get_photos())
|
photos = search_in_cwd(yield_photos=True, yield_albums=False)
|
||||||
|
|
||||||
need_commit = False
|
need_commit = False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ from voussoirkit import bytestring
|
||||||
from voussoirkit import configlayers
|
from voussoirkit import configlayers
|
||||||
from voussoirkit import flasktools
|
from voussoirkit import flasktools
|
||||||
from voussoirkit import pathclass
|
from voussoirkit import pathclass
|
||||||
from voussoirkit import timetools
|
|
||||||
from voussoirkit import vlogging
|
from voussoirkit import vlogging
|
||||||
|
|
||||||
import etiquette
|
import etiquette
|
||||||
|
|
@ -27,8 +26,8 @@ log = vlogging.getLogger(__name__)
|
||||||
# Constants ########################################################################################
|
# Constants ########################################################################################
|
||||||
|
|
||||||
DEFAULT_SERVER_CONFIG = {
|
DEFAULT_SERVER_CONFIG = {
|
||||||
'anonymous_read': False,
|
'anonymous_read': True,
|
||||||
'registration_enabled': False,
|
'anonymous_write': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
BROWSER_CACHE_DURATION = 180
|
BROWSER_CACHE_DURATION = 180
|
||||||
|
|
@ -45,7 +44,6 @@ TEMPLATE_DIR = root_dir.with_child('templates')
|
||||||
STATIC_DIR = root_dir.with_child('static')
|
STATIC_DIR = root_dir.with_child('static')
|
||||||
FAVICON_PATH = STATIC_DIR.with_child('favicon.png')
|
FAVICON_PATH = STATIC_DIR.with_child('favicon.png')
|
||||||
SERVER_CONFIG_FILENAME = 'etiquette_flask_config.json'
|
SERVER_CONFIG_FILENAME = 'etiquette_flask_config.json'
|
||||||
SESSIONS_STATE_FILENAME = 'etiquette_flask_sessions.json'
|
|
||||||
|
|
||||||
site = flask.Flask(
|
site = flask.Flask(
|
||||||
__name__,
|
__name__,
|
||||||
|
|
@ -63,7 +61,6 @@ site.jinja_env.lstrip_blocks = True
|
||||||
jinja_filters.register_all(site)
|
jinja_filters.register_all(site)
|
||||||
site.localhost_only = False
|
site.localhost_only = False
|
||||||
|
|
||||||
# state_file will be set later
|
|
||||||
session_manager = sessions.SessionManager(maxlen=10000)
|
session_manager = sessions.SessionManager(maxlen=10000)
|
||||||
file_etag_manager = client_caching.FileEtagManager(
|
file_etag_manager = client_caching.FileEtagManager(
|
||||||
maxlen=10000,
|
maxlen=10000,
|
||||||
|
|
@ -84,9 +81,7 @@ def catch_etiquette_exception(endpoint):
|
||||||
try:
|
try:
|
||||||
return endpoint(*args, **kwargs)
|
return endpoint(*args, **kwargs)
|
||||||
except etiquette.exceptions.EtiquetteException as exc:
|
except etiquette.exceptions.EtiquetteException as exc:
|
||||||
if isinstance(exc, etiquette.exceptions.Unauthorized):
|
if isinstance(exc, etiquette.exceptions.NoSuch):
|
||||||
status = 403
|
|
||||||
elif isinstance(exc, etiquette.exceptions.NoSuch):
|
|
||||||
status = 404
|
status = 404
|
||||||
else:
|
else:
|
||||||
status = 400
|
status = 400
|
||||||
|
|
@ -100,9 +95,7 @@ def before_request():
|
||||||
# visitors, make sure your reverse proxy is properly setting X-Forwarded-For
|
# visitors, make sure your reverse proxy is properly setting X-Forwarded-For
|
||||||
# so that werkzeug's proxyfix can set that as the remote_addr.
|
# so that werkzeug's proxyfix can set that as the remote_addr.
|
||||||
# In NGINX: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
# In NGINX: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
request.client_address = request.remote_addr
|
|
||||||
request.is_localhost = (request.remote_addr == '127.0.0.1')
|
request.is_localhost = (request.remote_addr == '127.0.0.1')
|
||||||
|
|
||||||
if site.localhost_only and not request.is_localhost:
|
if site.localhost_only and not request.is_localhost:
|
||||||
return flask.abort(403)
|
return flask.abort(403)
|
||||||
|
|
||||||
|
|
@ -149,9 +142,7 @@ def P_wrapper(function):
|
||||||
return function(thingid)
|
return function(thingid)
|
||||||
|
|
||||||
except etiquette.exceptions.EtiquetteException as exc:
|
except etiquette.exceptions.EtiquetteException as exc:
|
||||||
if isinstance(exc, etiquette.exceptions.Unauthorized):
|
if isinstance(exc, etiquette.exceptions.NoSuch):
|
||||||
status = 403
|
|
||||||
elif isinstance(exc, etiquette.exceptions.NoSuch):
|
|
||||||
status = 404
|
status = 404
|
||||||
else:
|
else:
|
||||||
status = 400
|
status = 400
|
||||||
|
|
@ -317,7 +308,6 @@ def init_photodb(*args, **kwargs):
|
||||||
global P
|
global P
|
||||||
P = etiquette.photodb.PhotoDB.closest_photodb(*args, **kwargs)
|
P = etiquette.photodb.PhotoDB.closest_photodb(*args, **kwargs)
|
||||||
load_config()
|
load_config()
|
||||||
load_sessions()
|
|
||||||
|
|
||||||
def load_config() -> None:
|
def load_config() -> None:
|
||||||
log.debug('Loading server config file.')
|
log.debug('Loading server config file.')
|
||||||
|
|
@ -331,30 +321,6 @@ def load_config() -> None:
|
||||||
if needs_rewrite:
|
if needs_rewrite:
|
||||||
save_config()
|
save_config()
|
||||||
|
|
||||||
def load_sessions():
|
|
||||||
state_file = P.data_directory.with_child(SESSIONS_STATE_FILENAME)
|
|
||||||
session_manager.state_file = state_file
|
|
||||||
if not state_file.exists:
|
|
||||||
return
|
|
||||||
log.debug('Loading sessions from state file')
|
|
||||||
j = json.loads(state_file.read('r'))
|
|
||||||
for session in j:
|
|
||||||
if session['userid'] is None:
|
|
||||||
user = None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
user = P.get_user(id=session['userid'])
|
|
||||||
except etiquette.exceptions.NoSuchUser:
|
|
||||||
continue
|
|
||||||
session = sessions.Session(
|
|
||||||
session_manager=session_manager,
|
|
||||||
user=user,
|
|
||||||
token=session['token'],
|
|
||||||
ip_address=session['ip_address'],
|
|
||||||
user_agent=session['user_agent'],
|
|
||||||
last_activity=timetools.fromtimestamp(session['last_activity']),
|
|
||||||
)
|
|
||||||
|
|
||||||
def save_config() -> None:
|
def save_config() -> None:
|
||||||
log.debug('Saving server config file.')
|
log.debug('Saving server config file.')
|
||||||
config_file = P.data_directory.with_child(SERVER_CONFIG_FILENAME)
|
config_file = P.data_directory.with_child(SERVER_CONFIG_FILENAME)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import flask; from flask import request
|
import flask; from flask import request
|
||||||
import json
|
|
||||||
|
|
||||||
from voussoirkit import dotdict
|
from voussoirkit import dotdict
|
||||||
from voussoirkit import flasktools
|
from voussoirkit import flasktools
|
||||||
|
|
@ -16,7 +15,7 @@ session_manager = common.session_manager
|
||||||
|
|
||||||
@site.route('/admin')
|
@site.route('/admin')
|
||||||
def get_admin():
|
def get_admin():
|
||||||
common.permission_manager.admin_only()
|
common.permission_manager.admin()
|
||||||
|
|
||||||
counts = dotdict.DotDict({
|
counts = dotdict.DotDict({
|
||||||
'albums': common.P.get_album_count(),
|
'albums': common.P.get_album_count(),
|
||||||
|
|
@ -32,20 +31,11 @@ def get_admin():
|
||||||
'tags': len(common.P.caches[etiquette.objects.Tag]),
|
'tags': len(common.P.caches[etiquette.objects.Tag]),
|
||||||
'users': len(common.P.caches[etiquette.objects.User]),
|
'users': len(common.P.caches[etiquette.objects.User]),
|
||||||
})
|
})
|
||||||
return common.render_template(
|
return common.render_template(request, 'admin.html', cached=cached, counts=counts)
|
||||||
request,
|
|
||||||
'admin.html',
|
|
||||||
etq_config=json.dumps(common.P.config, indent=4, sort_keys=True),
|
|
||||||
server_config=json.dumps(common.site.server_config, indent=4, sort_keys=True),
|
|
||||||
cached=cached,
|
|
||||||
counts=counts,
|
|
||||||
users=list(common.P.get_users()),
|
|
||||||
sessions=list(session_manager.sessions.items()),
|
|
||||||
)
|
|
||||||
|
|
||||||
@site.route('/admin/dbdownload')
|
@site.route('/admin/dbdownload')
|
||||||
def get_dbdump():
|
def get_dbdump():
|
||||||
common.permission_manager.admin_only()
|
common.permission_manager.admin()
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
binary = common.P.database_filepath.read('rb')
|
binary = common.P.database_filepath.read('rb')
|
||||||
|
|
@ -60,24 +50,14 @@ def get_dbdump():
|
||||||
|
|
||||||
@site.route('/admin/clear_sessions', methods=['POST'])
|
@site.route('/admin/clear_sessions', methods=['POST'])
|
||||||
def post_clear_sessions():
|
def post_clear_sessions():
|
||||||
common.permission_manager.admin_only()
|
common.permission_manager.admin()
|
||||||
|
|
||||||
session_manager.clear()
|
session_manager.clear()
|
||||||
session_manager.save_state()
|
|
||||||
return flasktools.json_response({})
|
|
||||||
|
|
||||||
@site.route('/admin/remove_session', methods=['POST'])
|
|
||||||
@flasktools.required_fields(['token'], forbid_whitespace=True)
|
|
||||||
def post_remove_session():
|
|
||||||
common.permission_manager.admin_only()
|
|
||||||
|
|
||||||
token = request.form['token']
|
|
||||||
session_manager.remove(token)
|
|
||||||
return flasktools.json_response({})
|
return flasktools.json_response({})
|
||||||
|
|
||||||
@site.route('/admin/reload_config', methods=['POST'])
|
@site.route('/admin/reload_config', methods=['POST'])
|
||||||
def post_reload_config():
|
def post_reload_config():
|
||||||
common.permission_manager.admin_only()
|
common.permission_manager.admin()
|
||||||
|
|
||||||
common.P.load_config()
|
common.P.load_config()
|
||||||
common.load_config()
|
common.load_config()
|
||||||
|
|
@ -86,7 +66,7 @@ def post_reload_config():
|
||||||
|
|
||||||
@site.route('/admin/uncache', methods=['POST'])
|
@site.route('/admin/uncache', methods=['POST'])
|
||||||
def post_uncache():
|
def post_uncache():
|
||||||
common.permission_manager.admin_only()
|
common.permission_manager.admin()
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
for cache in common.P.caches.values():
|
for cache in common.P.caches.values():
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ session_manager = common.session_manager
|
||||||
|
|
||||||
@site.route('/album/<album_id>')
|
@site.route('/album/<album_id>')
|
||||||
def get_album_html(album_id):
|
def get_album_html(album_id):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
album = common.P_album(album_id, response_type='html')
|
album = common.P_album(album_id, response_type='html')
|
||||||
response = common.render_template(
|
response = common.render_template(
|
||||||
request,
|
request,
|
||||||
|
|
@ -29,14 +29,14 @@ def get_album_html(album_id):
|
||||||
|
|
||||||
@site.route('/album/<album_id>.json')
|
@site.route('/album/<album_id>.json')
|
||||||
def get_album_json(album_id):
|
def get_album_json(album_id):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
album = common.P_album(album_id, response_type='json')
|
album = common.P_album(album_id, response_type='json')
|
||||||
album = album.jsonify()
|
album = album.jsonify()
|
||||||
return flasktools.json_response(album)
|
return flasktools.json_response(album)
|
||||||
|
|
||||||
@site.route('/album/<album_id>.zip')
|
@site.route('/album/<album_id>.zip')
|
||||||
def get_album_zip(album_id):
|
def get_album_zip(album_id):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
album = common.P_album(album_id, response_type='html')
|
album = common.P_album(album_id, response_type='html')
|
||||||
|
|
||||||
recursive = request.args.get('recursive', True)
|
recursive = request.args.get('recursive', True)
|
||||||
|
|
@ -45,7 +45,7 @@ def get_album_zip(album_id):
|
||||||
streamed_zip = etiquette.helpers.zip_album(album, recursive=recursive)
|
streamed_zip = etiquette.helpers.zip_album(album, recursive=recursive)
|
||||||
|
|
||||||
if album.title:
|
if album.title:
|
||||||
download_as = f'{album.title} ({album.id}).zip'
|
download_as = f'album {album.id} - {album.title}.zip'
|
||||||
else:
|
else:
|
||||||
download_as = f'album {album.id}.zip'
|
download_as = f'album {album.id}.zip'
|
||||||
|
|
||||||
|
|
@ -61,10 +61,10 @@ def get_album_zip(album_id):
|
||||||
@site.route('/album/<album_id>/add_child', methods=['POST'])
|
@site.route('/album/<album_id>/add_child', methods=['POST'])
|
||||||
@flasktools.required_fields(['child_id'], forbid_whitespace=True)
|
@flasktools.required_fields(['child_id'], forbid_whitespace=True)
|
||||||
def post_album_add_child(album_id):
|
def post_album_add_child(album_id):
|
||||||
album = common.P_album(album_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(album)
|
|
||||||
child_ids = stringtools.comma_space_split(request.form['child_id'])
|
child_ids = stringtools.comma_space_split(request.form['child_id'])
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
album = common.P_album(album_id, response_type='json')
|
||||||
children = list(common.P_albums(child_ids, response_type='json'))
|
children = list(common.P_albums(child_ids, response_type='json'))
|
||||||
print(children)
|
print(children)
|
||||||
album.add_children(children)
|
album.add_children(children)
|
||||||
|
|
@ -75,10 +75,10 @@ def post_album_add_child(album_id):
|
||||||
@site.route('/album/<album_id>/remove_child', methods=['POST'])
|
@site.route('/album/<album_id>/remove_child', methods=['POST'])
|
||||||
@flasktools.required_fields(['child_id'], forbid_whitespace=True)
|
@flasktools.required_fields(['child_id'], forbid_whitespace=True)
|
||||||
def post_album_remove_child(album_id):
|
def post_album_remove_child(album_id):
|
||||||
album = common.P_album(album_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(album)
|
|
||||||
child_ids = stringtools.comma_space_split(request.form['child_id'])
|
child_ids = stringtools.comma_space_split(request.form['child_id'])
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
album = common.P_album(album_id, response_type='json')
|
||||||
children = list(common.P_albums(child_ids, response_type='json'))
|
children = list(common.P_albums(child_ids, response_type='json'))
|
||||||
album.remove_children(children)
|
album.remove_children(children)
|
||||||
response = album.jsonify()
|
response = album.jsonify()
|
||||||
|
|
@ -86,18 +86,17 @@ def post_album_remove_child(album_id):
|
||||||
|
|
||||||
@site.route('/album/<album_id>/remove_thumbnail_photo', methods=['POST'])
|
@site.route('/album/<album_id>/remove_thumbnail_photo', methods=['POST'])
|
||||||
def post_album_remove_thumbnail_photo(album_id):
|
def post_album_remove_thumbnail_photo(album_id):
|
||||||
album = common.P_album(album_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(album)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
album = common.P_album(album_id, response_type='json')
|
||||||
album.set_thumbnail_photo(None)
|
album.set_thumbnail_photo(None)
|
||||||
return flasktools.json_response(album.jsonify())
|
return flasktools.json_response(album.jsonify())
|
||||||
|
|
||||||
@site.route('/album/<album_id>/refresh_directories', methods=['POST'])
|
@site.route('/album/<album_id>/refresh_directories', methods=['POST'])
|
||||||
def post_album_refresh_directories(album_id):
|
def post_album_refresh_directories(album_id):
|
||||||
album = common.P_album(album_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(album)
|
|
||||||
common.permission_manager.permission_string(etiquette.constants.PERMISSION_PHOTO_CREATE)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
album = common.P_album(album_id, response_type='json')
|
||||||
for directory in album.get_associated_directories():
|
for directory in album.get_associated_directories():
|
||||||
if not directory.is_dir:
|
if not directory.is_dir:
|
||||||
continue
|
continue
|
||||||
|
|
@ -108,9 +107,9 @@ def post_album_refresh_directories(album_id):
|
||||||
@site.route('/album/<album_id>/set_thumbnail_photo', methods=['POST'])
|
@site.route('/album/<album_id>/set_thumbnail_photo', methods=['POST'])
|
||||||
@flasktools.required_fields(['photo_id'], forbid_whitespace=True)
|
@flasktools.required_fields(['photo_id'], forbid_whitespace=True)
|
||||||
def post_album_set_thumbnail_photo(album_id):
|
def post_album_set_thumbnail_photo(album_id):
|
||||||
album = common.P_album(album_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(album)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
album = common.P_album(album_id, response_type='json')
|
||||||
photo = common.P_photo(request.form['photo_id'], response_type='json')
|
photo = common.P_photo(request.form['photo_id'], response_type='json')
|
||||||
album.set_thumbnail_photo(photo)
|
album.set_thumbnail_photo(photo)
|
||||||
return flasktools.json_response(album.jsonify())
|
return flasktools.json_response(album.jsonify())
|
||||||
|
|
@ -123,10 +122,10 @@ def post_album_add_photo(album_id):
|
||||||
'''
|
'''
|
||||||
Add a photo or photos to this album.
|
Add a photo or photos to this album.
|
||||||
'''
|
'''
|
||||||
album = common.P_album(album_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(album)
|
|
||||||
photo_ids = stringtools.comma_space_split(request.form['photo_id'])
|
photo_ids = stringtools.comma_space_split(request.form['photo_id'])
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
album = common.P_album(album_id, response_type='json')
|
||||||
photos = list(common.P_photos(photo_ids, response_type='json'))
|
photos = list(common.P_photos(photo_ids, response_type='json'))
|
||||||
album.add_photos(photos)
|
album.add_photos(photos)
|
||||||
response = album.jsonify()
|
response = album.jsonify()
|
||||||
|
|
@ -138,10 +137,10 @@ def post_album_remove_photo(album_id):
|
||||||
'''
|
'''
|
||||||
Remove a photo or photos from this album.
|
Remove a photo or photos from this album.
|
||||||
'''
|
'''
|
||||||
album = common.P_album(album_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(album)
|
|
||||||
photo_ids = stringtools.comma_space_split(request.form['photo_id'])
|
photo_ids = stringtools.comma_space_split(request.form['photo_id'])
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
album = common.P_album(album_id, response_type='json')
|
||||||
photos = list(common.P_photos(photo_ids, response_type='json'))
|
photos = list(common.P_photos(photo_ids, response_type='json'))
|
||||||
album.remove_photos(photos)
|
album.remove_photos(photos)
|
||||||
response = album.jsonify()
|
response = album.jsonify()
|
||||||
|
|
@ -154,7 +153,7 @@ def post_album_add_tag(album_id):
|
||||||
'''
|
'''
|
||||||
Apply a tag to every photo in the album.
|
Apply a tag to every photo in the album.
|
||||||
'''
|
'''
|
||||||
common.permission_manager.admin_only()
|
common.permission_manager.basic()
|
||||||
response = {}
|
response = {}
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
album = common.P_album(album_id, response_type='json')
|
album = common.P_album(album_id, response_type='json')
|
||||||
|
|
@ -179,12 +178,12 @@ def post_album_edit(album_id):
|
||||||
'''
|
'''
|
||||||
Edit the title / description.
|
Edit the title / description.
|
||||||
'''
|
'''
|
||||||
album = common.P_album(album_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(album)
|
|
||||||
title = request.form.get('title', None)
|
title = request.form.get('title', None)
|
||||||
description = request.form.get('description', None)
|
description = request.form.get('description', None)
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
album = common.P_album(album_id, response_type='json')
|
||||||
album.edit(title=title, description=description)
|
album.edit(title=title, description=description)
|
||||||
|
|
||||||
response = album.jsonify(
|
response = album.jsonify(
|
||||||
|
|
@ -196,7 +195,9 @@ def post_album_edit(album_id):
|
||||||
|
|
||||||
@site.route('/album/<album_id>/show_in_folder', methods=['POST'])
|
@site.route('/album/<album_id>/show_in_folder', methods=['POST'])
|
||||||
def post_album_show_in_folder(album_id):
|
def post_album_show_in_folder(album_id):
|
||||||
common.permission_manager.localhost_only()
|
common.permission_manager.basic()
|
||||||
|
if not request.is_localhost:
|
||||||
|
flask.abort(403)
|
||||||
|
|
||||||
album = common.P_album(album_id, response_type='json')
|
album = common.P_album(album_id, response_type='json')
|
||||||
directories = album.get_associated_directories()
|
directories = album.get_associated_directories()
|
||||||
|
|
@ -214,7 +215,7 @@ def post_album_show_in_folder(album_id):
|
||||||
# Album listings ###################################################################################
|
# Album listings ###################################################################################
|
||||||
|
|
||||||
@site.route('/all_albums.json')
|
@site.route('/all_albums.json')
|
||||||
@common.permission_manager.read_decorator
|
@common.permission_manager.basic_decorator
|
||||||
@flasktools.cached_endpoint(max_age=15)
|
@flasktools.cached_endpoint(max_age=15)
|
||||||
def get_all_album_names():
|
def get_all_album_names():
|
||||||
all_albums = {album.id: album.display_name for album in common.P.get_albums()}
|
all_albums = {album.id: album.display_name for album in common.P.get_albums()}
|
||||||
|
|
@ -223,7 +224,7 @@ def get_all_album_names():
|
||||||
|
|
||||||
@site.route('/albums')
|
@site.route('/albums')
|
||||||
def get_albums_html():
|
def get_albums_html():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
albums = list(common.P.get_root_albums())
|
albums = list(common.P.get_root_albums())
|
||||||
albums.sort(key=lambda x: x.display_name.lower())
|
albums.sort(key=lambda x: x.display_name.lower())
|
||||||
response = common.render_template(
|
response = common.render_template(
|
||||||
|
|
@ -236,7 +237,7 @@ def get_albums_html():
|
||||||
|
|
||||||
@site.route('/albums.json')
|
@site.route('/albums.json')
|
||||||
def get_albums_json():
|
def get_albums_json():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
albums = list(common.P.get_albums())
|
albums = list(common.P.get_albums())
|
||||||
albums.sort(key=lambda x: x.display_name.lower())
|
albums.sort(key=lambda x: x.display_name.lower())
|
||||||
albums = [album.jsonify(include_photos=False) for album in albums]
|
albums = [album.jsonify(include_photos=False) for album in albums]
|
||||||
|
|
@ -246,7 +247,7 @@ def get_albums_json():
|
||||||
|
|
||||||
@site.route('/albums/create_album', methods=['POST'])
|
@site.route('/albums/create_album', methods=['POST'])
|
||||||
def post_albums_create():
|
def post_albums_create():
|
||||||
common.permission_manager.permission_string(etiquette.constants.PERMISSION_ALBUM_CREATE)
|
common.permission_manager.basic()
|
||||||
title = request.form.get('title', None)
|
title = request.form.get('title', None)
|
||||||
description = request.form.get('description', None)
|
description = request.form.get('description', None)
|
||||||
parent_id = request.form.get('parent_id', None)
|
parent_id = request.form.get('parent_id', None)
|
||||||
|
|
@ -269,8 +270,8 @@ def post_albums_create():
|
||||||
|
|
||||||
@site.route('/album/<album_id>/delete', methods=['POST'])
|
@site.route('/album/<album_id>/delete', methods=['POST'])
|
||||||
def post_album_delete(album_id):
|
def post_album_delete(album_id):
|
||||||
album = common.P_album(album_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.delete_thing(album)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
album = common.P_album(album_id, response_type='json')
|
||||||
album.delete()
|
album.delete()
|
||||||
return flasktools.json_response({})
|
return flasktools.json_response({})
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,7 @@ session_manager = common.session_manager
|
||||||
def root():
|
def root():
|
||||||
common.permission_manager.global_public()
|
common.permission_manager.global_public()
|
||||||
motd = random.choice(common.P.config['motd_strings'])
|
motd = random.choice(common.P.config['motd_strings'])
|
||||||
return common.render_template(
|
return common.render_template(request, 'root.html', motd=motd)
|
||||||
request,
|
|
||||||
'root.html',
|
|
||||||
motd=motd,
|
|
||||||
anonymous_read=common.site.server_config['anonymous_read'],
|
|
||||||
)
|
|
||||||
|
|
||||||
@site.route('/favicon.ico')
|
@site.route('/favicon.ico')
|
||||||
@site.route('/favicon.png')
|
@site.route('/favicon.png')
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,16 @@ session_manager = common.session_manager
|
||||||
|
|
||||||
@site.route('/bookmark/<bookmark_id>.json')
|
@site.route('/bookmark/<bookmark_id>.json')
|
||||||
def get_bookmark_json(bookmark_id):
|
def get_bookmark_json(bookmark_id):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
bookmark = common.P_bookmark(bookmark_id, response_type='json')
|
bookmark = common.P_bookmark(bookmark_id, response_type='json')
|
||||||
response = bookmark.jsonify()
|
response = bookmark.jsonify()
|
||||||
return flasktools.json_response(response)
|
return flasktools.json_response(response)
|
||||||
|
|
||||||
@site.route('/bookmark/<bookmark_id>/edit', methods=['POST'])
|
@site.route('/bookmark/<bookmark_id>/edit', methods=['POST'])
|
||||||
def post_bookmark_edit(bookmark_id):
|
def post_bookmark_edit(bookmark_id):
|
||||||
bookmark = common.P_bookmark(bookmark_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(bookmark)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
bookmark = common.P_bookmark(bookmark_id, response_type='json')
|
||||||
# Emptystring is okay for titles, but not for URL.
|
# Emptystring is okay for titles, but not for URL.
|
||||||
title = request.form.get('title', None)
|
title = request.form.get('title', None)
|
||||||
url = request.form.get('url', None) or None
|
url = request.form.get('url', None) or None
|
||||||
|
|
@ -36,7 +36,7 @@ def post_bookmark_edit(bookmark_id):
|
||||||
|
|
||||||
@site.route('/bookmarks.atom')
|
@site.route('/bookmarks.atom')
|
||||||
def get_bookmarks_atom():
|
def get_bookmarks_atom():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
bookmarks = common.P.get_bookmarks()
|
bookmarks = common.P.get_bookmarks()
|
||||||
response = etiquette.helpers.make_atom_feed(
|
response = etiquette.helpers.make_atom_feed(
|
||||||
bookmarks,
|
bookmarks,
|
||||||
|
|
@ -48,13 +48,13 @@ def get_bookmarks_atom():
|
||||||
|
|
||||||
@site.route('/bookmarks')
|
@site.route('/bookmarks')
|
||||||
def get_bookmarks_html():
|
def get_bookmarks_html():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
bookmarks = list(common.P.get_bookmarks())
|
bookmarks = list(common.P.get_bookmarks())
|
||||||
return common.render_template(request, 'bookmarks.html', bookmarks=bookmarks)
|
return common.render_template(request, 'bookmarks.html', bookmarks=bookmarks)
|
||||||
|
|
||||||
@site.route('/bookmarks.json')
|
@site.route('/bookmarks.json')
|
||||||
def get_bookmarks_json():
|
def get_bookmarks_json():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
bookmarks = [b.jsonify() for b in common.P.get_bookmarks()]
|
bookmarks = [b.jsonify() for b in common.P.get_bookmarks()]
|
||||||
return flasktools.json_response(bookmarks)
|
return flasktools.json_response(bookmarks)
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ def get_bookmarks_json():
|
||||||
@site.route('/bookmarks/create_bookmark', methods=['POST'])
|
@site.route('/bookmarks/create_bookmark', methods=['POST'])
|
||||||
@flasktools.required_fields(['url'], forbid_whitespace=True)
|
@flasktools.required_fields(['url'], forbid_whitespace=True)
|
||||||
def post_bookmark_create():
|
def post_bookmark_create():
|
||||||
common.permission_manager.permission_string(etiquette.constants.PERMISSION_BOOKMARK_CREATE)
|
common.permission_manager.basic()
|
||||||
url = request.form['url']
|
url = request.form['url']
|
||||||
title = request.form.get('title', None)
|
title = request.form.get('title', None)
|
||||||
user = session_manager.get(request).user
|
user = session_manager.get(request).user
|
||||||
|
|
@ -75,8 +75,8 @@ def post_bookmark_create():
|
||||||
|
|
||||||
@site.route('/bookmark/<bookmark_id>/delete', methods=['POST'])
|
@site.route('/bookmark/<bookmark_id>/delete', methods=['POST'])
|
||||||
def post_bookmark_delete(bookmark_id):
|
def post_bookmark_delete(bookmark_id):
|
||||||
bookmark = common.P_bookmark(bookmark_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.delete_thing(bookmark)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
bookmark = common.P_bookmark(bookmark_id, response_type='json')
|
||||||
bookmark.delete()
|
bookmark.delete()
|
||||||
return flasktools.json_response({})
|
return flasktools.json_response({})
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import gevent
|
|
||||||
import flask; from flask import request
|
import flask; from flask import request
|
||||||
import os
|
import os
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
@ -11,7 +8,6 @@ from voussoirkit import cacheclass
|
||||||
from voussoirkit import flasktools
|
from voussoirkit import flasktools
|
||||||
from voussoirkit import pathclass
|
from voussoirkit import pathclass
|
||||||
from voussoirkit import stringtools
|
from voussoirkit import stringtools
|
||||||
from voussoirkit import timetools
|
|
||||||
from voussoirkit import vlogging
|
from voussoirkit import vlogging
|
||||||
|
|
||||||
log = vlogging.get_logger(__name__)
|
log = vlogging.get_logger(__name__)
|
||||||
|
|
@ -21,8 +17,6 @@ import etiquette
|
||||||
from .. import common
|
from .. import common
|
||||||
from .. import helpers
|
from .. import helpers
|
||||||
|
|
||||||
RNG = random.SystemRandom()
|
|
||||||
|
|
||||||
site = common.site
|
site = common.site
|
||||||
session_manager = common.session_manager
|
session_manager = common.session_manager
|
||||||
photo_download_zip_tokens = cacheclass.Cache(maxlen=100)
|
photo_download_zip_tokens = cacheclass.Cache(maxlen=100)
|
||||||
|
|
@ -31,13 +25,13 @@ photo_download_zip_tokens = cacheclass.Cache(maxlen=100)
|
||||||
|
|
||||||
@site.route('/photo/<photo_id>')
|
@site.route('/photo/<photo_id>')
|
||||||
def get_photo_html(photo_id):
|
def get_photo_html(photo_id):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
photo = common.P_photo(photo_id, response_type='html')
|
photo = common.P_photo(photo_id, response_type='html')
|
||||||
return common.render_template(request, 'photo.html', photo=photo)
|
return common.render_template(request, 'photo.html', photo=photo)
|
||||||
|
|
||||||
@site.route('/photo/<photo_id>.json')
|
@site.route('/photo/<photo_id>.json')
|
||||||
def get_photo_json(photo_id):
|
def get_photo_json(photo_id):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
photo = common.P_photo(photo_id, response_type='json')
|
photo = common.P_photo(photo_id, response_type='json')
|
||||||
photo = photo.jsonify()
|
photo = photo.jsonify()
|
||||||
photo = flasktools.json_response(photo)
|
photo = flasktools.json_response(photo)
|
||||||
|
|
@ -46,7 +40,7 @@ def get_photo_json(photo_id):
|
||||||
@site.route('/photo/<photo_id>/download')
|
@site.route('/photo/<photo_id>/download')
|
||||||
@site.route('/photo/<photo_id>/download/<basename>')
|
@site.route('/photo/<photo_id>/download/<basename>')
|
||||||
def get_file(photo_id, basename=None):
|
def get_file(photo_id, basename=None):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
photo_id = photo_id.split('.')[0]
|
photo_id = photo_id.split('.')[0]
|
||||||
photo = common.P.get_photo(photo_id)
|
photo = common.P.get_photo(photo_id)
|
||||||
|
|
||||||
|
|
@ -72,7 +66,7 @@ def get_file(photo_id, basename=None):
|
||||||
|
|
||||||
@site.route('/photo/<photo_id>/thumbnail')
|
@site.route('/photo/<photo_id>/thumbnail')
|
||||||
@site.route('/photo/<photo_id>/thumbnail/<basename>')
|
@site.route('/photo/<photo_id>/thumbnail/<basename>')
|
||||||
@common.permission_manager.read_decorator
|
@common.permission_manager.basic_decorator
|
||||||
@flasktools.cached_endpoint(max_age=common.BROWSER_CACHE_DURATION, etag_function=lambda: common.P.last_commit_id)
|
@flasktools.cached_endpoint(max_age=common.BROWSER_CACHE_DURATION, etag_function=lambda: common.P.last_commit_id)
|
||||||
def get_thumbnail(photo_id, basename=None):
|
def get_thumbnail(photo_id, basename=None):
|
||||||
photo_id = photo_id.split('.')[0]
|
photo_id = photo_id.split('.')[0]
|
||||||
|
|
@ -95,11 +89,11 @@ def get_thumbnail(photo_id, basename=None):
|
||||||
|
|
||||||
@site.route('/photo/<photo_id>/delete', methods=['POST'])
|
@site.route('/photo/<photo_id>/delete', methods=['POST'])
|
||||||
def post_photo_delete(photo_id):
|
def post_photo_delete(photo_id):
|
||||||
photo = common.P_photo(photo_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.delete_thing(photo)
|
|
||||||
delete_file = request.form.get('delete_file', False)
|
delete_file = request.form.get('delete_file', False)
|
||||||
delete_file = stringtools.truthystring(delete_file)
|
delete_file = stringtools.truthystring(delete_file)
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
photo = common.P_photo(photo_id, response_type='json')
|
||||||
photo.delete(delete_file=delete_file)
|
photo.delete(delete_file=delete_file)
|
||||||
return flasktools.json_response({})
|
return flasktools.json_response({})
|
||||||
|
|
||||||
|
|
@ -112,9 +106,6 @@ def post_photo_add_remove_tag_core(photo_ids, tagname, add_or_remove, timestamp=
|
||||||
photos = list(common.P_photos(photo_ids, response_type='json'))
|
photos = list(common.P_photos(photo_ids, response_type='json'))
|
||||||
tag = common.P_tag(tagname, response_type='json')
|
tag = common.P_tag(tagname, response_type='json')
|
||||||
|
|
||||||
for photo in photos:
|
|
||||||
common.permission_manager.edit_thing(photo)
|
|
||||||
|
|
||||||
response = {'action': add_or_remove, 'tagname': tag.name}
|
response = {'action': add_or_remove, 'tagname': tag.name}
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
for photo in photos:
|
for photo in photos:
|
||||||
|
|
@ -132,8 +123,8 @@ def post_photo_add_tag(photo_id):
|
||||||
'''
|
'''
|
||||||
Add a tag to this photo.
|
Add a tag to this photo.
|
||||||
'''
|
'''
|
||||||
|
common.permission_manager.basic()
|
||||||
photo = common.P_photo(photo_id, response_type='json')
|
photo = common.P_photo(photo_id, response_type='json')
|
||||||
common.permission_manager.edit_thing(photo)
|
|
||||||
tag = common.P_tag(request.form['tagname'], response_type='json')
|
tag = common.P_tag(request.form['tagname'], response_type='json')
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
|
@ -147,9 +138,9 @@ def post_photo_copy_tags(photo_id):
|
||||||
'''
|
'''
|
||||||
Copy the tags from another photo.
|
Copy the tags from another photo.
|
||||||
'''
|
'''
|
||||||
photo = common.P_photo(photo_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(photo)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
photo = common.P_photo(photo_id, response_type='json')
|
||||||
other = common.P_photo(request.form['other_photo'], response_type='json')
|
other = common.P_photo(request.form['other_photo'], response_type='json')
|
||||||
photo.copy_tags(other)
|
photo.copy_tags(other)
|
||||||
return flasktools.json_response([tag.jsonify() for tag in photo.get_tags()])
|
return flasktools.json_response([tag.jsonify() for tag in photo.get_tags()])
|
||||||
|
|
@ -160,7 +151,7 @@ def post_photo_remove_tag(photo_id):
|
||||||
'''
|
'''
|
||||||
Remove a tag from this photo.
|
Remove a tag from this photo.
|
||||||
'''
|
'''
|
||||||
common.permission_manager.edit_thing(photo)
|
common.permission_manager.basic()
|
||||||
response = post_photo_add_remove_tag_core(
|
response = post_photo_add_remove_tag_core(
|
||||||
photo_ids=photo_id,
|
photo_ids=photo_id,
|
||||||
tagname=request.form['tagname'],
|
tagname=request.form['tagname'],
|
||||||
|
|
@ -173,15 +164,16 @@ def post_photo_tag_rel_delete(photo_tag_rel_id):
|
||||||
'''
|
'''
|
||||||
Remove a tag from a photo.
|
Remove a tag from a photo.
|
||||||
'''
|
'''
|
||||||
photo_tag = common.P.get_object_by_id(etiquette.objects.PhotoTagRel, photo_tag_rel_id)
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(photo_tag.photo)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
photo_tag = common.P.get_object_by_id(etiquette.objects.PhotoTagRel, photo_tag_rel_id)
|
||||||
photo_tag.delete()
|
photo_tag.delete()
|
||||||
return flasktools.json_response(photo_tag.jsonify())
|
return flasktools.json_response(photo_tag.jsonify())
|
||||||
|
|
||||||
@site.route('/batch/photos/add_tag', methods=['POST'])
|
@site.route('/batch/photos/add_tag', methods=['POST'])
|
||||||
@flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True)
|
@flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True)
|
||||||
def post_batch_photos_add_tag():
|
def post_batch_photos_add_tag():
|
||||||
|
common.permission_manager.basic()
|
||||||
response = post_photo_add_remove_tag_core(
|
response = post_photo_add_remove_tag_core(
|
||||||
photo_ids=request.form['photo_ids'],
|
photo_ids=request.form['photo_ids'],
|
||||||
tagname=request.form['tagname'],
|
tagname=request.form['tagname'],
|
||||||
|
|
@ -192,6 +184,7 @@ def post_batch_photos_add_tag():
|
||||||
@site.route('/batch/photos/remove_tag', methods=['POST'])
|
@site.route('/batch/photos/remove_tag', methods=['POST'])
|
||||||
@flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True)
|
@flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True)
|
||||||
def post_batch_photos_remove_tag():
|
def post_batch_photos_remove_tag():
|
||||||
|
common.permission_manager.basic()
|
||||||
response = post_photo_add_remove_tag_core(
|
response = post_photo_add_remove_tag_core(
|
||||||
photo_ids=request.form['photo_ids'],
|
photo_ids=request.form['photo_ids'],
|
||||||
tagname=request.form['tagname'],
|
tagname=request.form['tagname'],
|
||||||
|
|
@ -205,15 +198,14 @@ def post_photo_generate_thumbnail_core(photo_ids, special={}):
|
||||||
if isinstance(photo_ids, str):
|
if isinstance(photo_ids, str):
|
||||||
photo_ids = stringtools.comma_space_split(photo_ids)
|
photo_ids = stringtools.comma_space_split(photo_ids)
|
||||||
|
|
||||||
photos = list(common.P_photos(photo_ids, response_type='json'))
|
|
||||||
for photo in photos:
|
|
||||||
common.permission_manager.edit_thing(photo)
|
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
photos = list(common.P_photos(photo_ids, response_type='json'))
|
||||||
|
|
||||||
for photo in photos:
|
for photo in photos:
|
||||||
photo._uncache()
|
photo._uncache()
|
||||||
|
photo = common.P_photo(photo.id, response_type='json')
|
||||||
try:
|
try:
|
||||||
photo.generate_thumbnail(special=special)
|
photo.generate_thumbnail()
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning(traceback.format_exc())
|
log.warning(traceback.format_exc())
|
||||||
|
|
||||||
|
|
@ -221,14 +213,14 @@ def post_photo_generate_thumbnail_core(photo_ids, special={}):
|
||||||
|
|
||||||
@site.route('/photo/<photo_id>/generate_thumbnail', methods=['POST'])
|
@site.route('/photo/<photo_id>/generate_thumbnail', methods=['POST'])
|
||||||
def post_photo_generate_thumbnail(photo_id):
|
def post_photo_generate_thumbnail(photo_id):
|
||||||
common.permission_manager.early_read()
|
common.permission_manager.basic()
|
||||||
special = request.form.to_dict()
|
special = request.form.to_dict()
|
||||||
response = post_photo_generate_thumbnail_core(photo_ids=photo_id, special=special)
|
response = post_photo_generate_thumbnail_core(photo_ids=photo_id, special=special)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@site.route('/batch/photos/generate_thumbnail', methods=['POST'])
|
@site.route('/batch/photos/generate_thumbnail', methods=['POST'])
|
||||||
def post_batch_photos_generate_thumbnail():
|
def post_batch_photos_generate_thumbnail():
|
||||||
common.permission_manager.early_read()
|
common.permission_manager.basic()
|
||||||
special = request.form.to_dict()
|
special = request.form.to_dict()
|
||||||
response = post_photo_generate_thumbnail_core(photo_ids=request.form['photo_ids'], special=special)
|
response = post_photo_generate_thumbnail_core(photo_ids=request.form['photo_ids'], special=special)
|
||||||
return response
|
return response
|
||||||
|
|
@ -237,18 +229,14 @@ def post_photo_refresh_metadata_core(photo_ids):
|
||||||
if isinstance(photo_ids, str):
|
if isinstance(photo_ids, str):
|
||||||
photo_ids = stringtools.comma_space_split(photo_ids)
|
photo_ids = stringtools.comma_space_split(photo_ids)
|
||||||
|
|
||||||
photos = list(common.P_photos(photo_ids, response_type='json'))
|
|
||||||
for photo in photos:
|
|
||||||
common.permission_manager.edit_thing(photo)
|
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
photos = list(common.P_photos(photo_ids, response_type='json'))
|
||||||
|
|
||||||
for photo in photos:
|
for photo in photos:
|
||||||
photo._uncache()
|
photo._uncache()
|
||||||
photo = common.P_photo(photo.id, response_type='json')
|
photo = common.P_photo(photo.id, response_type='json')
|
||||||
try:
|
try:
|
||||||
photo.reload_metadata()
|
photo.reload_metadata()
|
||||||
gevent.sleep(0.01)
|
|
||||||
except pathclass.NotFile:
|
except pathclass.NotFile:
|
||||||
flask.abort(404)
|
flask.abort(404)
|
||||||
|
|
||||||
|
|
@ -262,36 +250,50 @@ def post_photo_refresh_metadata_core(photo_ids):
|
||||||
|
|
||||||
@site.route('/photo/<photo_id>/refresh_metadata', methods=['POST'])
|
@site.route('/photo/<photo_id>/refresh_metadata', methods=['POST'])
|
||||||
def post_photo_refresh_metadata(photo_id):
|
def post_photo_refresh_metadata(photo_id):
|
||||||
common.permission_manager.early_read()
|
common.permission_manager.basic()
|
||||||
response = post_photo_refresh_metadata_core(photo_ids=photo_id)
|
response = post_photo_refresh_metadata_core(photo_ids=photo_id)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@site.route('/batch/photos/refresh_metadata', methods=['POST'])
|
@site.route('/batch/photos/refresh_metadata', methods=['POST'])
|
||||||
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
|
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
|
||||||
def post_batch_photos_refresh_metadata():
|
def post_batch_photos_refresh_metadata():
|
||||||
common.permission_manager.early_read()
|
common.permission_manager.basic()
|
||||||
response = post_photo_refresh_metadata_core(photo_ids=request.form['photo_ids'])
|
response = post_photo_refresh_metadata_core(photo_ids=request.form['photo_ids'])
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@site.route('/photo/<photo_id>/set_searchhidden', methods=['POST'])
|
@site.route('/photo/<photo_id>/set_searchhidden', methods=['POST'])
|
||||||
def post_photo_set_searchhidden(photo_id):
|
def post_photo_set_searchhidden(photo_id):
|
||||||
photo = common.P_photo(photo_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(photo)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
photo = common.P_photo(photo_id, response_type='json')
|
||||||
photo.set_searchhidden(True)
|
photo.set_searchhidden(True)
|
||||||
return flasktools.json_response({})
|
return flasktools.json_response({})
|
||||||
|
|
||||||
@site.route('/photo/<photo_id>/unset_searchhidden', methods=['POST'])
|
@site.route('/photo/<photo_id>/unset_searchhidden', methods=['POST'])
|
||||||
def post_photo_unset_searchhidden(photo_id):
|
def post_photo_unset_searchhidden(photo_id):
|
||||||
photo = common.P_photo(photo_id, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(photo)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
photo = common.P_photo(photo_id, response_type='json')
|
||||||
photo.set_searchhidden(False)
|
photo.set_searchhidden(False)
|
||||||
return flasktools.json_response({})
|
return flasktools.json_response({})
|
||||||
|
|
||||||
|
def post_batch_photos_searchhidden_core(photo_ids, searchhidden):
|
||||||
|
if isinstance(photo_ids, str):
|
||||||
|
photo_ids = stringtools.comma_space_split(photo_ids)
|
||||||
|
|
||||||
|
with common.P.transaction:
|
||||||
|
photos = list(common.P_photos(photo_ids, response_type='json'))
|
||||||
|
|
||||||
|
for photo in photos:
|
||||||
|
photo.set_searchhidden(searchhidden)
|
||||||
|
|
||||||
|
return flasktools.json_response({})
|
||||||
|
|
||||||
@site.route('/photo/<photo_id>/show_in_folder', methods=['POST'])
|
@site.route('/photo/<photo_id>/show_in_folder', methods=['POST'])
|
||||||
def post_photo_show_in_folder(photo_id):
|
def post_photo_show_in_folder(photo_id):
|
||||||
common.permission_manager.localhost_only()
|
common.permission_manager.basic()
|
||||||
|
if not request.is_localhost:
|
||||||
|
flask.abort(403)
|
||||||
|
|
||||||
photo = common.P_photo(photo_id, response_type='json')
|
photo = common.P_photo(photo_id, response_type='json')
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
|
|
@ -305,25 +307,10 @@ def post_photo_show_in_folder(photo_id):
|
||||||
|
|
||||||
flask.abort(501)
|
flask.abort(501)
|
||||||
|
|
||||||
def post_batch_photos_searchhidden_core(photo_ids, searchhidden):
|
|
||||||
if isinstance(photo_ids, str):
|
|
||||||
photo_ids = stringtools.comma_space_split(photo_ids)
|
|
||||||
|
|
||||||
photos = list(common.P_photos(photo_ids, response_type='json'))
|
|
||||||
for photo in photos:
|
|
||||||
common.permission_manager.edit_thing(photo)
|
|
||||||
|
|
||||||
with common.P.transaction:
|
|
||||||
|
|
||||||
for photo in photos:
|
|
||||||
photo.set_searchhidden(searchhidden)
|
|
||||||
|
|
||||||
return flasktools.json_response({})
|
|
||||||
|
|
||||||
@site.route('/batch/photos/set_searchhidden', methods=['POST'])
|
@site.route('/batch/photos/set_searchhidden', methods=['POST'])
|
||||||
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
|
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
|
||||||
def post_batch_photos_set_searchhidden():
|
def post_batch_photos_set_searchhidden():
|
||||||
common.permission_manager.early_read()
|
common.permission_manager.basic()
|
||||||
photo_ids = request.form['photo_ids']
|
photo_ids = request.form['photo_ids']
|
||||||
response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=True)
|
response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=True)
|
||||||
return response
|
return response
|
||||||
|
|
@ -331,46 +318,16 @@ def post_batch_photos_set_searchhidden():
|
||||||
@site.route('/batch/photos/unset_searchhidden', methods=['POST'])
|
@site.route('/batch/photos/unset_searchhidden', methods=['POST'])
|
||||||
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
|
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
|
||||||
def post_batch_photos_unset_searchhidden():
|
def post_batch_photos_unset_searchhidden():
|
||||||
common.permission_manager.early_read()
|
common.permission_manager.basic()
|
||||||
photo_ids = request.form['photo_ids']
|
photo_ids = request.form['photo_ids']
|
||||||
response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=False)
|
response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=False)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def post_batch_photos_delete_core(photo_ids, delete_file):
|
|
||||||
if isinstance(photo_ids, str):
|
|
||||||
photo_ids = stringtools.comma_space_split(photo_ids)
|
|
||||||
|
|
||||||
photos = list(common.P_photos(photo_ids, response_type='json'))
|
|
||||||
for photo in photos:
|
|
||||||
common.permission_manager.delete_thing(photo)
|
|
||||||
|
|
||||||
with common.P.transaction:
|
|
||||||
for photo in photos:
|
|
||||||
photo.delete(delete_file=delete_file)
|
|
||||||
|
|
||||||
return flasktools.json_response({})
|
|
||||||
|
|
||||||
@site.route('/batch/photos/soft_delete', methods=['POST'])
|
|
||||||
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
|
|
||||||
def post_batch_photos_soft_delete():
|
|
||||||
common.permission_manager.early_read()
|
|
||||||
photo_ids = request.form['photo_ids']
|
|
||||||
response = post_batch_photos_delete_core(photo_ids=photo_ids, delete_file=False)
|
|
||||||
return response
|
|
||||||
|
|
||||||
@site.route('/batch/photos/hard_delete', methods=['POST'])
|
|
||||||
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
|
|
||||||
def post_batch_photos_hard_delete():
|
|
||||||
common.permission_manager.early_read()
|
|
||||||
photo_ids = request.form['photo_ids']
|
|
||||||
response = post_batch_photos_delete_core(photo_ids=photo_ids, delete_file=True)
|
|
||||||
return response
|
|
||||||
|
|
||||||
# Clipboard ########################################################################################
|
# Clipboard ########################################################################################
|
||||||
|
|
||||||
@site.route('/clipboard')
|
@site.route('/clipboard')
|
||||||
def get_clipboard_page():
|
def get_clipboard_page():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
return common.render_template(request, 'clipboard.html')
|
return common.render_template(request, 'clipboard.html')
|
||||||
|
|
||||||
@site.route('/batch/photos', methods=['POST'])
|
@site.route('/batch/photos', methods=['POST'])
|
||||||
|
|
@ -379,7 +336,7 @@ def post_batch_photos():
|
||||||
'''
|
'''
|
||||||
Return a list of photo.jsonify() for each requested photo id.
|
Return a list of photo.jsonify() for each requested photo id.
|
||||||
'''
|
'''
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
photo_ids = request.form['photo_ids']
|
photo_ids = request.form['photo_ids']
|
||||||
|
|
||||||
photo_ids = stringtools.comma_space_split(photo_ids)
|
photo_ids = stringtools.comma_space_split(photo_ids)
|
||||||
|
|
@ -392,7 +349,7 @@ def post_batch_photos():
|
||||||
@site.route('/batch/photos/photo_card', methods=['POST'])
|
@site.route('/batch/photos/photo_card', methods=['POST'])
|
||||||
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
|
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
|
||||||
def post_batch_photos_photo_cards():
|
def post_batch_photos_photo_cards():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
photo_ids = request.form['photo_ids']
|
photo_ids = request.form['photo_ids']
|
||||||
|
|
||||||
photo_ids = stringtools.comma_space_split(photo_ids)
|
photo_ids = stringtools.comma_space_split(photo_ids)
|
||||||
|
|
@ -424,7 +381,7 @@ def get_batch_photos_download_zip(zip_token):
|
||||||
After the user has generated their zip token, they can retrieve
|
After the user has generated their zip token, they can retrieve
|
||||||
that zip file.
|
that zip file.
|
||||||
'''
|
'''
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
zip_token = zip_token.split('.')[0]
|
zip_token = zip_token.split('.')[0]
|
||||||
try:
|
try:
|
||||||
photo_ids = photo_download_zip_tokens[zip_token]
|
photo_ids = photo_download_zip_tokens[zip_token]
|
||||||
|
|
@ -454,7 +411,7 @@ def post_batch_photos_download_zip():
|
||||||
so the way this works is we generate a token representing the photoset
|
so the way this works is we generate a token representing the photoset
|
||||||
that they want, and then they can retrieve the zip itself via GET.
|
that they want, and then they can retrieve the zip itself via GET.
|
||||||
'''
|
'''
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
photo_ids = request.form['photo_ids']
|
photo_ids = request.form['photo_ids']
|
||||||
photo_ids = stringtools.comma_space_split(photo_ids)
|
photo_ids = stringtools.comma_space_split(photo_ids)
|
||||||
|
|
||||||
|
|
@ -529,7 +486,7 @@ def get_search_core():
|
||||||
|
|
||||||
@site.route('/search_embed')
|
@site.route('/search_embed')
|
||||||
def get_search_embed():
|
def get_search_embed():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
search = get_search_core()
|
search = get_search_core()
|
||||||
response = common.render_template(
|
response = common.render_template(
|
||||||
request,
|
request,
|
||||||
|
|
@ -541,7 +498,7 @@ def get_search_embed():
|
||||||
|
|
||||||
@site.route('/search')
|
@site.route('/search')
|
||||||
def get_search_html():
|
def get_search_html():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
|
|
||||||
search = get_search_core()
|
search = get_search_core()
|
||||||
search.kwargs.view = request.args.get('view', 'grid')
|
search.kwargs.view = request.args.get('view', 'grid')
|
||||||
|
|
@ -592,7 +549,7 @@ def get_search_html():
|
||||||
|
|
||||||
@site.route('/search.atom')
|
@site.route('/search.atom')
|
||||||
def get_search_atom():
|
def get_search_atom():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
search = get_search_core()
|
search = get_search_core()
|
||||||
soup = etiquette.helpers.make_atom_feed(
|
soup = etiquette.helpers.make_atom_feed(
|
||||||
search.results,
|
search.results,
|
||||||
|
|
@ -605,7 +562,7 @@ def get_search_atom():
|
||||||
|
|
||||||
@site.route('/search.json')
|
@site.route('/search.json')
|
||||||
def get_search_json():
|
def get_search_json():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
search = get_search_core()
|
search = get_search_core()
|
||||||
response = search.jsonify()
|
response = search.jsonify()
|
||||||
return flasktools.json_response(response)
|
return flasktools.json_response(response)
|
||||||
|
|
@ -614,92 +571,6 @@ def get_search_json():
|
||||||
|
|
||||||
@site.route('/swipe')
|
@site.route('/swipe')
|
||||||
def get_swipe():
|
def get_swipe():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
response = common.render_template(request, 'swipe.html')
|
response = common.render_template(request, 'swipe.html')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Upload ###########################################################################################
|
|
||||||
|
|
||||||
@site.route('/upload')
|
|
||||||
def get_upload_page():
|
|
||||||
common.permission_manager.permission_string(etiquette.constants.PERMISSION_PHOTO_CREATE)
|
|
||||||
response = common.render_template(request, 'upload.html')
|
|
||||||
return response
|
|
||||||
|
|
||||||
@site.route('/photo/upload', methods=['POST'])
|
|
||||||
def post_photo_upload():
|
|
||||||
common.permission_manager.permission_string(etiquette.constants.PERMISSION_PHOTO_CREATE)
|
|
||||||
|
|
||||||
files = request.files.getlist('file')
|
|
||||||
print(files)
|
|
||||||
if len(files) == 0:
|
|
||||||
return flask.abort(400)
|
|
||||||
|
|
||||||
job_id = f'{int(timetools.now().timestamp() * 1000)}_{RNG.getrandbits(32)}'
|
|
||||||
folder = common.P.uploads_directory.with_child(job_id)
|
|
||||||
folder.makedirs()
|
|
||||||
|
|
||||||
# All uploading/saving must be done before the transaction so the database
|
|
||||||
# is not locked up by a slow uploader.
|
|
||||||
diskpaths = []
|
|
||||||
for (index, file) in enumerate(files):
|
|
||||||
log.debug('Receiving uploaded file %s.' % file.filename)
|
|
||||||
unsafepath = file.filename
|
|
||||||
unsafepath = unsafepath.replace('\\', '/')
|
|
||||||
unsafepath = re.sub(r'//+', '/', unsafepath)
|
|
||||||
unsafepath = unsafepath.strip('/')
|
|
||||||
|
|
||||||
extension = pathclass.Path(unsafepath).extension
|
|
||||||
if len(extension) > 30:
|
|
||||||
extension = ''
|
|
||||||
|
|
||||||
diskpath = folder.with_child(f'{job_id}_{index}').add_extension(extension)
|
|
||||||
file.save(diskpath.absolute_path)
|
|
||||||
|
|
||||||
diskpaths.append(diskpath)
|
|
||||||
diskpath._unsafepath = unsafepath
|
|
||||||
|
|
||||||
diskpaths.sort(key=lambda x: x._unsafepath)
|
|
||||||
|
|
||||||
with common.P.transaction:
|
|
||||||
albums_by_path = {}
|
|
||||||
for diskpath in diskpaths:
|
|
||||||
print(diskpath._unsafepath)
|
|
||||||
photo = common.P.new_photo(
|
|
||||||
diskpath,
|
|
||||||
author=request.session.user,
|
|
||||||
override_filename=diskpath._unsafepath.split('/')[-1],
|
|
||||||
)
|
|
||||||
|
|
||||||
if '/' not in diskpath._unsafepath:
|
|
||||||
continue
|
|
||||||
|
|
||||||
(parentpath, basename) = diskpath._unsafepath.rsplit('/', 1)
|
|
||||||
if parentpath in albums_by_path:
|
|
||||||
parentalbum = albums_by_path[parentpath]
|
|
||||||
else:
|
|
||||||
parentname = parentpath.rsplit('/', 1)[-1]
|
|
||||||
parentalbum = common.P.new_album(parentname)
|
|
||||||
albums_by_path[parentpath] = parentalbum
|
|
||||||
|
|
||||||
parentalbum.add_photo(photo)
|
|
||||||
|
|
||||||
# Build the Album tree
|
|
||||||
to_check = set(albums_by_path.keys())
|
|
||||||
while len(to_check) > 0:
|
|
||||||
key = to_check.pop()
|
|
||||||
album = albums_by_path[key]
|
|
||||||
print(key, album)
|
|
||||||
|
|
||||||
if '/' not in key:
|
|
||||||
continue
|
|
||||||
|
|
||||||
(parentkey, parentname) = key.rsplit('/', 1)
|
|
||||||
if parentkey in albums_by_path:
|
|
||||||
parentalbum = albums_by_path[parentkey]
|
|
||||||
else:
|
|
||||||
parentalbum = common.P.new_album(parentname)
|
|
||||||
albums_by_path[parentkey] = parentalbum
|
|
||||||
to_check.add(parentkey)
|
|
||||||
|
|
||||||
parentalbum.add_child(album)
|
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@ session_manager = common.session_manager
|
||||||
@site.route('/tags/<specific_tag>')
|
@site.route('/tags/<specific_tag>')
|
||||||
@site.route('/tags/<specific_tag>.json')
|
@site.route('/tags/<specific_tag>.json')
|
||||||
def get_tags_specific_redirect(specific_tag):
|
def get_tags_specific_redirect(specific_tag):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
return flask.redirect(request.url.replace('/tags/', '/tag/'))
|
return flask.redirect(request.url.replace('/tags/', '/tag/'))
|
||||||
|
|
||||||
@site.route('/tagid/<tag_id>')
|
@site.route('/tagid/<tag_id>')
|
||||||
@site.route('/tagid/<tag_id>.json')
|
@site.route('/tagid/<tag_id>.json')
|
||||||
def get_tag_id_redirect(tag_id):
|
def get_tag_id_redirect(tag_id):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
if request.path.endswith('.json'):
|
if request.path.endswith('.json'):
|
||||||
tag = common.P_tag_id(tag_id, response_type='json')
|
tag = common.P_tag_id(tag_id, response_type='json')
|
||||||
else:
|
else:
|
||||||
|
|
@ -33,7 +33,7 @@ def get_tag_id_redirect(tag_id):
|
||||||
|
|
||||||
@site.route('/tag/<specific_tag_name>.json')
|
@site.route('/tag/<specific_tag_name>.json')
|
||||||
def get_tag_json(specific_tag_name):
|
def get_tag_json(specific_tag_name):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
specific_tag = common.P_tag(specific_tag_name, response_type='json')
|
specific_tag = common.P_tag(specific_tag_name, response_type='json')
|
||||||
if specific_tag.name != specific_tag_name:
|
if specific_tag.name != specific_tag_name:
|
||||||
new_url = f'/tag/{specific_tag.name}.json' + request.query_string.decode('utf-8')
|
new_url = f'/tag/{specific_tag.name}.json' + request.query_string.decode('utf-8')
|
||||||
|
|
@ -47,9 +47,9 @@ def get_tag_json(specific_tag_name):
|
||||||
|
|
||||||
@site.route('/tag/<tagname>/edit', methods=['POST'])
|
@site.route('/tag/<tagname>/edit', methods=['POST'])
|
||||||
def post_tag_edit(tagname):
|
def post_tag_edit(tagname):
|
||||||
tag = common.P_tag(tagname, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(tag)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
tag = common.P_tag(tagname, response_type='json')
|
||||||
name = request.form.get('name', '').strip()
|
name = request.form.get('name', '').strip()
|
||||||
if name:
|
if name:
|
||||||
tag.rename(name)
|
tag.rename(name)
|
||||||
|
|
@ -63,13 +63,10 @@ def post_tag_edit(tagname):
|
||||||
@site.route('/tag/<tagname>/add_child', methods=['POST'])
|
@site.route('/tag/<tagname>/add_child', methods=['POST'])
|
||||||
@flasktools.required_fields(['child_name'], forbid_whitespace=True)
|
@flasktools.required_fields(['child_name'], forbid_whitespace=True)
|
||||||
def post_tag_add_child(tagname):
|
def post_tag_add_child(tagname):
|
||||||
parent = common.P_tag(tagname, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(parent)
|
|
||||||
|
|
||||||
child = common.P_tag(request.form['child_name'], response_type='json')
|
|
||||||
common.permission_manager.edit_thing(child)
|
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
parent = common.P_tag(tagname, response_type='json')
|
||||||
|
child = common.P_tag(request.form['child_name'], response_type='json')
|
||||||
parent.add_child(child)
|
parent.add_child(child)
|
||||||
response = {'action': 'add_child', 'tagname': f'{parent.name}.{child.name}'}
|
response = {'action': 'add_child', 'tagname': f'{parent.name}.{child.name}'}
|
||||||
return flasktools.json_response(response)
|
return flasktools.json_response(response)
|
||||||
|
|
@ -77,11 +74,11 @@ def post_tag_add_child(tagname):
|
||||||
@site.route('/tag/<tagname>/add_synonym', methods=['POST'])
|
@site.route('/tag/<tagname>/add_synonym', methods=['POST'])
|
||||||
@flasktools.required_fields(['syn_name'], forbid_whitespace=True)
|
@flasktools.required_fields(['syn_name'], forbid_whitespace=True)
|
||||||
def post_tag_add_synonym(tagname):
|
def post_tag_add_synonym(tagname):
|
||||||
master_tag = common.P_tag(tagname, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(master_tag)
|
|
||||||
syn_name = request.form['syn_name']
|
syn_name = request.form['syn_name']
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
master_tag = common.P_tag(tagname, response_type='json')
|
||||||
syn_name = master_tag.add_synonym(syn_name)
|
syn_name = master_tag.add_synonym(syn_name)
|
||||||
|
|
||||||
response = {'action': 'add_synonym', 'synonym': syn_name}
|
response = {'action': 'add_synonym', 'synonym': syn_name}
|
||||||
|
|
@ -90,13 +87,10 @@ def post_tag_add_synonym(tagname):
|
||||||
@site.route('/tag/<tagname>/remove_child', methods=['POST'])
|
@site.route('/tag/<tagname>/remove_child', methods=['POST'])
|
||||||
@flasktools.required_fields(['child_name'], forbid_whitespace=True)
|
@flasktools.required_fields(['child_name'], forbid_whitespace=True)
|
||||||
def post_tag_remove_child(tagname):
|
def post_tag_remove_child(tagname):
|
||||||
parent = common.P_tag(tagname, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(parent)
|
|
||||||
|
|
||||||
child = common.P_tag(request.form['child_name'], response_type='json')
|
|
||||||
common.permission_manager.edit_thing(child)
|
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
parent = common.P_tag(tagname, response_type='json')
|
||||||
|
child = common.P_tag(request.form['child_name'], response_type='json')
|
||||||
parent.remove_child(child)
|
parent.remove_child(child)
|
||||||
response = {'action': 'remove_child', 'tagname': f'{parent.name}.{child.name}'}
|
response = {'action': 'remove_child', 'tagname': f'{parent.name}.{child.name}'}
|
||||||
return flasktools.json_response(response)
|
return flasktools.json_response(response)
|
||||||
|
|
@ -104,11 +98,11 @@ def post_tag_remove_child(tagname):
|
||||||
@site.route('/tag/<tagname>/remove_synonym', methods=['POST'])
|
@site.route('/tag/<tagname>/remove_synonym', methods=['POST'])
|
||||||
@flasktools.required_fields(['syn_name'], forbid_whitespace=True)
|
@flasktools.required_fields(['syn_name'], forbid_whitespace=True)
|
||||||
def post_tag_remove_synonym(tagname):
|
def post_tag_remove_synonym(tagname):
|
||||||
master_tag = common.P_tag(tagname, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.edit_thing(master_tag)
|
|
||||||
syn_name = request.form['syn_name']
|
syn_name = request.form['syn_name']
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
master_tag = common.P_tag(tagname, response_type='json')
|
||||||
syn_name = master_tag.remove_synonym(syn_name)
|
syn_name = master_tag.remove_synonym(syn_name)
|
||||||
|
|
||||||
response = {'action': 'delete_synonym', 'synonym': syn_name}
|
response = {'action': 'delete_synonym', 'synonym': syn_name}
|
||||||
|
|
@ -117,7 +111,7 @@ def post_tag_remove_synonym(tagname):
|
||||||
# Tag listings #####################################################################################
|
# Tag listings #####################################################################################
|
||||||
|
|
||||||
@site.route('/all_tags.json')
|
@site.route('/all_tags.json')
|
||||||
@common.permission_manager.read_decorator
|
@common.permission_manager.basic_decorator
|
||||||
@flasktools.cached_endpoint(max_age=15)
|
@flasktools.cached_endpoint(max_age=15)
|
||||||
def get_all_tag_names():
|
def get_all_tag_names():
|
||||||
all_tags = list(sorted(common.P.get_all_tag_names()))
|
all_tags = list(sorted(common.P.get_all_tag_names()))
|
||||||
|
|
@ -128,7 +122,7 @@ def get_all_tag_names():
|
||||||
@site.route('/tag/<specific_tag_name>')
|
@site.route('/tag/<specific_tag_name>')
|
||||||
@site.route('/tags')
|
@site.route('/tags')
|
||||||
def get_tags_html(specific_tag_name=None):
|
def get_tags_html(specific_tag_name=None):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
if specific_tag_name is None:
|
if specific_tag_name is None:
|
||||||
specific_tag = None
|
specific_tag = None
|
||||||
else:
|
else:
|
||||||
|
|
@ -167,7 +161,7 @@ def get_tags_html(specific_tag_name=None):
|
||||||
|
|
||||||
@site.route('/tags.json')
|
@site.route('/tags.json')
|
||||||
def get_tags_json():
|
def get_tags_json():
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
include_synonyms = request.args.get('synonyms')
|
include_synonyms = request.args.get('synonyms')
|
||||||
include_synonyms = include_synonyms is None or stringtools.truthystring(include_synonyms)
|
include_synonyms = include_synonyms is None or stringtools.truthystring(include_synonyms)
|
||||||
|
|
||||||
|
|
@ -181,8 +175,7 @@ def get_tags_json():
|
||||||
@site.route('/tags/create_tag', methods=['POST'])
|
@site.route('/tags/create_tag', methods=['POST'])
|
||||||
@flasktools.required_fields(['name'], forbid_whitespace=True)
|
@flasktools.required_fields(['name'], forbid_whitespace=True)
|
||||||
def post_tag_create():
|
def post_tag_create():
|
||||||
common.permission_manager.permission_string(etiquette.constants.PERMISSION_TAG_CREATE)
|
common.permission_manager.basic()
|
||||||
|
|
||||||
name = request.form['name']
|
name = request.form['name']
|
||||||
description = request.form.get('description', None)
|
description = request.form.get('description', None)
|
||||||
|
|
||||||
|
|
@ -194,7 +187,7 @@ def post_tag_create():
|
||||||
@site.route('/tags/easybake', methods=['POST'])
|
@site.route('/tags/easybake', methods=['POST'])
|
||||||
@flasktools.required_fields(['easybake_string'], forbid_whitespace=True)
|
@flasktools.required_fields(['easybake_string'], forbid_whitespace=True)
|
||||||
def post_tag_easybake():
|
def post_tag_easybake():
|
||||||
common.permission_manager.admin_only()
|
common.permission_manager.basic()
|
||||||
easybake_string = request.form['easybake_string']
|
easybake_string = request.form['easybake_string']
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
|
@ -204,9 +197,9 @@ def post_tag_easybake():
|
||||||
|
|
||||||
@site.route('/tag/<tagname>/delete', methods=['POST'])
|
@site.route('/tag/<tagname>/delete', methods=['POST'])
|
||||||
def post_tag_delete(tagname):
|
def post_tag_delete(tagname):
|
||||||
tag = common.P_tag(tagname, response_type='json')
|
common.permission_manager.basic()
|
||||||
common.permission_manager.delete_thing(tag)
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
tag = common.P_tag(tagname, response_type='json')
|
||||||
tag.delete()
|
tag.delete()
|
||||||
response = {'action': 'delete_tag', 'tagname': tag.name}
|
response = {'action': 'delete_tag', 'tagname': tag.name}
|
||||||
return flasktools.json_response(response)
|
return flasktools.json_response(response)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import flask; from flask import request
|
import flask; from flask import request
|
||||||
|
|
||||||
from voussoirkit import flasktools
|
from voussoirkit import flasktools
|
||||||
from voussoirkit import stringtools
|
|
||||||
|
|
||||||
import etiquette
|
import etiquette
|
||||||
|
|
||||||
|
|
@ -15,19 +14,13 @@ session_manager = common.session_manager
|
||||||
|
|
||||||
@site.route('/user/<username>')
|
@site.route('/user/<username>')
|
||||||
def get_user_html(username):
|
def get_user_html(username):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
user = common.P_user(username, response_type='html')
|
user = common.P_user(username, response_type='html')
|
||||||
return common.render_template(
|
return common.render_template(request, 'user.html', user=user)
|
||||||
request,
|
|
||||||
'user.html',
|
|
||||||
user=user,
|
|
||||||
user_permissions=user.get_permissions(),
|
|
||||||
constants_all_permissions=etiquette.constants.ALL_PERMISSIONS,
|
|
||||||
)
|
|
||||||
|
|
||||||
@site.route('/user/<username>.json')
|
@site.route('/user/<username>.json')
|
||||||
def get_user_json(username):
|
def get_user_json(username):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
user = common.P_user(username, response_type='json')
|
user = common.P_user(username, response_type='json')
|
||||||
user = user.jsonify()
|
user = user.jsonify()
|
||||||
return flasktools.json_response(user)
|
return flasktools.json_response(user)
|
||||||
|
|
@ -35,7 +28,7 @@ def get_user_json(username):
|
||||||
@site.route('/userid/<user_id>')
|
@site.route('/userid/<user_id>')
|
||||||
@site.route('/userid/<user_id>.json')
|
@site.route('/userid/<user_id>.json')
|
||||||
def get_user_id_redirect(user_id):
|
def get_user_id_redirect(user_id):
|
||||||
common.permission_manager.read()
|
common.permission_manager.basic()
|
||||||
if request.path.endswith('.json'):
|
if request.path.endswith('.json'):
|
||||||
user = common.P_user_id(user_id, response_type='json')
|
user = common.P_user_id(user_id, response_type='json')
|
||||||
else:
|
else:
|
||||||
|
|
@ -47,8 +40,12 @@ def get_user_id_redirect(user_id):
|
||||||
|
|
||||||
@site.route('/user/<username>/edit', methods=['POST'])
|
@site.route('/user/<username>/edit', methods=['POST'])
|
||||||
def post_user_edit(username):
|
def post_user_edit(username):
|
||||||
|
common.permission_manager.basic()
|
||||||
|
if not request.session:
|
||||||
|
return flasktools.json_response(etiquette.exceptions.Unauthorized().jsonify(), status=403)
|
||||||
user = common.P_user(username, response_type='json')
|
user = common.P_user(username, response_type='json')
|
||||||
common.permission_manager.logged_in(user)
|
if request.session.user != user:
|
||||||
|
return flasktools.json_response(etiquette.exceptions.Unauthorized().jsonify(), status=403)
|
||||||
|
|
||||||
display_name = request.form.get('display_name')
|
display_name = request.form.get('display_name')
|
||||||
if display_name is not None:
|
if display_name is not None:
|
||||||
|
|
@ -57,54 +54,6 @@ def post_user_edit(username):
|
||||||
|
|
||||||
return flasktools.json_response(user.jsonify())
|
return flasktools.json_response(user.jsonify())
|
||||||
|
|
||||||
@site.route('/user/<username>/set_password', methods=['POST'])
|
|
||||||
@flasktools.required_fields(['current_password', 'password_1', 'password_2'])
|
|
||||||
def post_user_set_password(username):
|
|
||||||
user = common.P_user(username, response_type='json')
|
|
||||||
common.permission_manager.logged_in(user)
|
|
||||||
|
|
||||||
current_password = request.form.get('current_password')
|
|
||||||
try:
|
|
||||||
user.check_password(current_password)
|
|
||||||
except (etiquette.exceptions.WrongLogin):
|
|
||||||
exc = etiquette.exceptions.WrongLogin()
|
|
||||||
response = exc.jsonify()
|
|
||||||
return flasktools.json_response(response, status=422)
|
|
||||||
except etiquette.exceptions.FeatureDisabled as exc:
|
|
||||||
response = exc.jsonify()
|
|
||||||
return flasktools.json_response(response, status=400)
|
|
||||||
|
|
||||||
password_1 = request.form.get('password_1')
|
|
||||||
password_2 = request.form.get('password_2')
|
|
||||||
if password_1 != password_2:
|
|
||||||
response = {
|
|
||||||
'error_type': 'PASSWORDS_DONT_MATCH',
|
|
||||||
'error_message': 'Passwords do not match.',
|
|
||||||
}
|
|
||||||
return flasktools.json_response(response, status=422)
|
|
||||||
|
|
||||||
with common.P.transaction:
|
|
||||||
user.set_password(password_1)
|
|
||||||
|
|
||||||
sessions = list(session_manager.sessions.items())
|
|
||||||
for (token, session) in sessions:
|
|
||||||
if session.user == user and token != request.session.token:
|
|
||||||
session_manager.remove(token)
|
|
||||||
|
|
||||||
@site.route('/user/<username>/set_permission', methods=['POST'])
|
|
||||||
@flasktools.required_fields(['permission', 'value'])
|
|
||||||
def post_user_set_permission(username):
|
|
||||||
common.permission_manager.admin_only()
|
|
||||||
permission_string = request.form['permission']
|
|
||||||
permission_value = stringtools.truthystring(request.form['value'])
|
|
||||||
user = common.P_user(username, response_type='json')
|
|
||||||
with common.P.transaction:
|
|
||||||
if permission_value:
|
|
||||||
user.add_permission(permission_string)
|
|
||||||
else:
|
|
||||||
user.remove_permission(permission_string)
|
|
||||||
return flasktools.json_response(user.jsonify())
|
|
||||||
|
|
||||||
# Login and logout #################################################################################
|
# Login and logout #################################################################################
|
||||||
|
|
||||||
@site.route('/login', methods=['GET'])
|
@site.route('/login', methods=['GET'])
|
||||||
|
|
@ -115,7 +64,7 @@ def get_login():
|
||||||
'login.html',
|
'login.html',
|
||||||
min_username_length=common.P.config['user']['min_username_length'],
|
min_username_length=common.P.config['user']['min_username_length'],
|
||||||
min_password_length=common.P.config['user']['min_password_length'],
|
min_password_length=common.P.config['user']['min_password_length'],
|
||||||
registration_enabled=common.site.server_config['registration_enabled'],
|
registration_enabled=common.P.config['enable_feature']['user']['new'],
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
@ -147,13 +96,13 @@ def post_login():
|
||||||
response = exc.jsonify()
|
response = exc.jsonify()
|
||||||
return flasktools.json_response(response, status=400)
|
return flasktools.json_response(response, status=400)
|
||||||
|
|
||||||
request.session = sessions.Session.from_request(session_manager=session_manager, request=request, user=user)
|
request.session = sessions.Session(request, user)
|
||||||
session_manager.save_state()
|
session_manager.add(request.session)
|
||||||
return flasktools.json_response({})
|
return flasktools.json_response({})
|
||||||
|
|
||||||
@site.route('/logout', methods=['POST'])
|
@site.route('/logout', methods=['POST'])
|
||||||
def post_logout():
|
def post_logout():
|
||||||
common.permission_manager.logged_in()
|
common.permission_manager.basic()
|
||||||
session_manager.remove(request)
|
session_manager.remove(request)
|
||||||
response = flasktools.json_response({})
|
response = flasktools.json_response({})
|
||||||
return response
|
return response
|
||||||
|
|
@ -189,5 +138,6 @@ def post_register():
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
user = common.P.new_user(username, password_1, display_name=display_name)
|
user = common.P.new_user(username, password_1, display_name=display_name)
|
||||||
|
|
||||||
request.session = sessions.Session.from_request(session_manager=session_manager, request=request, user=user)
|
request.session = sessions.Session(request, user)
|
||||||
|
session_manager.add(request.session)
|
||||||
return flasktools.json_response({})
|
return flasktools.json_response({})
|
||||||
|
|
|
||||||
|
|
@ -3,124 +3,41 @@ import functools
|
||||||
|
|
||||||
from voussoirkit import vlogging
|
from voussoirkit import vlogging
|
||||||
|
|
||||||
import etiquette
|
|
||||||
|
|
||||||
log = vlogging.getLogger(__name__)
|
log = vlogging.getLogger(__name__)
|
||||||
|
|
||||||
class PermissionManager:
|
class PermissionManager:
|
||||||
def __init__(self, site):
|
def __init__(self, site):
|
||||||
self.site = site
|
self.site = site
|
||||||
|
|
||||||
def admin_only(self):
|
def admin(self):
|
||||||
if request.is_localhost:
|
|
||||||
request.checked_permissions = True
|
|
||||||
return True
|
|
||||||
elif request.session.user and request.session.user.has_permission(etiquette.constants.PERMISSION_ADMIN):
|
|
||||||
request.checked_permissions = True
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise etiquette.exceptions.Unauthorized()
|
|
||||||
|
|
||||||
def localhost_only(self):
|
|
||||||
if request.is_localhost:
|
if request.is_localhost:
|
||||||
request.checked_permissions = True
|
request.checked_permissions = True
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise etiquette.exceptions.Unauthorized()
|
return flask.abort(403)
|
||||||
|
|
||||||
def logged_in(self, user=None):
|
def basic(self):
|
||||||
'''
|
if request.method not in {'GET', 'POST'}:
|
||||||
Require that the visitor be logged in as any user, or as one
|
return flask.abort(405)
|
||||||
specific user.
|
elif request.is_localhost:
|
||||||
'''
|
|
||||||
if request.session and request.session.user and user is None:
|
|
||||||
request.checked_permissions = True
|
request.checked_permissions = True
|
||||||
return True
|
return True
|
||||||
if request.session and request.session.user and request.session.user == user:
|
elif request.method == 'GET' and self.site.server_config['anonymous_read'] or request.session.user:
|
||||||
|
request.checked_permissions = True
|
||||||
|
return True
|
||||||
|
elif request.method == 'POST' and self.site.server_config['anonymous_write'] or request.session.user:
|
||||||
request.checked_permissions = True
|
request.checked_permissions = True
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise etiquette.exceptions.Unauthorized()
|
return flask.abort(403)
|
||||||
|
|
||||||
def early_read(self):
|
def basic_decorator(self, endpoint):
|
||||||
'''
|
|
||||||
This method does not set request.checked_permissions and must be used
|
|
||||||
along with one of the other checks. However, this check can act as a
|
|
||||||
cheap blocker against logged-out users before the caller wastes any time
|
|
||||||
loading items from the database to check more specific permissions.
|
|
||||||
For example, it is used by several of the bulk endpoints before checking
|
|
||||||
any of the individual items.
|
|
||||||
'''
|
|
||||||
if request.is_localhost:
|
|
||||||
return True
|
|
||||||
elif self.site.server_config['anonymous_read'] or request.session.user:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise etiquette.exceptions.Unauthorized()
|
|
||||||
|
|
||||||
def edit_thing(self, thing):
|
|
||||||
if request.is_localhost:
|
|
||||||
request.checked_permissions = True
|
|
||||||
return True
|
|
||||||
elif request.session.user and request.session.user.has_object_permission(thing, 'edit'):
|
|
||||||
request.checked_permissions = True
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise etiquette.exceptions.Unauthorized()
|
|
||||||
|
|
||||||
def delete_thing(self, thing):
|
|
||||||
if request.is_localhost:
|
|
||||||
request.checked_permissions = True
|
|
||||||
return True
|
|
||||||
elif request.session.user and request.session.user.has_object_permission(thing, 'delete'):
|
|
||||||
request.checked_permissions = True
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise etiquette.exceptions.Unauthorized()
|
|
||||||
|
|
||||||
def permission_string(self, permission_string):
|
|
||||||
'''
|
|
||||||
Require that the user has this specific permission string (mostly for
|
|
||||||
the CREATE permissions rather than edit/delete permissions).
|
|
||||||
'''
|
|
||||||
if request.is_localhost:
|
|
||||||
request.checked_permissions = True
|
|
||||||
return True
|
|
||||||
elif request.session.user and permission_string in request.session.user.get_permissions():
|
|
||||||
request.checked_permissions = True
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise etiquette.exceptions.Unauthorized()
|
|
||||||
|
|
||||||
def read(self):
|
|
||||||
'''
|
|
||||||
BE CAREFUL WITH CACHED ENDPOINTS. Use read_decorator instead.
|
|
||||||
'''
|
|
||||||
if request.is_localhost:
|
|
||||||
request.checked_permissions = True
|
|
||||||
return True
|
|
||||||
elif self.site.server_config['anonymous_read'] or request.session.user:
|
|
||||||
request.checked_permissions = True
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise etiquette.exceptions.Unauthorized()
|
|
||||||
|
|
||||||
def read_decorator(self, endpoint):
|
|
||||||
'''
|
|
||||||
Make sure to place read_decorator ABOVE the cached_endpoint decorator so
|
|
||||||
Python runs this one first.
|
|
||||||
'''
|
|
||||||
log.debug('Decorating %s with basic_decorator.', endpoint)
|
log.debug('Decorating %s with basic_decorator.', endpoint)
|
||||||
@functools.wraps(endpoint)
|
@functools.wraps(endpoint)
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
self.read()
|
self.basic()
|
||||||
return endpoint(*args, **kwargs)
|
return endpoint(*args, **kwargs)
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
def global_public(self):
|
def global_public(self):
|
||||||
'''
|
|
||||||
This check always passes. Use this for the root and login page so people
|
|
||||||
can log in.
|
|
||||||
'''
|
|
||||||
request.checked_permissions = True
|
request.checked_permissions = True
|
||||||
return True
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,18 @@
|
||||||
import datetime
|
|
||||||
import flask; from flask import request
|
import flask; from flask import request
|
||||||
import functools
|
import functools
|
||||||
import json
|
|
||||||
import random
|
|
||||||
import werkzeug.datastructures
|
import werkzeug.datastructures
|
||||||
|
|
||||||
from voussoirkit import cacheclass
|
from voussoirkit import cacheclass
|
||||||
from voussoirkit import flasktools
|
from voussoirkit import flasktools
|
||||||
|
from voussoirkit import passwordy
|
||||||
from voussoirkit import timetools
|
from voussoirkit import timetools
|
||||||
from voussoirkit import vlogging
|
|
||||||
|
|
||||||
log = vlogging.getLogger(__name__, 'sessions')
|
|
||||||
|
|
||||||
import etiquette
|
import etiquette
|
||||||
|
|
||||||
RNG = random.SystemRandom()
|
|
||||||
|
|
||||||
SESSION_MAX_AGE = 86400
|
SESSION_MAX_AGE = 86400
|
||||||
SAVE_STATE_INTERVAL = 60
|
|
||||||
|
|
||||||
def _generate_token() -> str:
|
def _generate_token(length=32):
|
||||||
return str(RNG.getrandbits(128))
|
return passwordy.random_hex(length=length)
|
||||||
|
|
||||||
def _normalize_token(token):
|
def _normalize_token(token):
|
||||||
if isinstance(token, flasktools.REQUEST_TYPES):
|
if isinstance(token, flasktools.REQUEST_TYPES):
|
||||||
|
|
@ -39,10 +31,8 @@ def _normalize_token(token):
|
||||||
return token
|
return token
|
||||||
|
|
||||||
class SessionManager:
|
class SessionManager:
|
||||||
def __init__(self, maxlen=None, state_file=None):
|
def __init__(self, maxlen=None):
|
||||||
self.sessions = cacheclass.Cache(maxlen=maxlen)
|
self.sessions = cacheclass.Cache(maxlen=maxlen)
|
||||||
self.last_activity = timetools.now()
|
|
||||||
self.last_save_state = timetools.now()
|
|
||||||
|
|
||||||
def _before_request(self, request):
|
def _before_request(self, request):
|
||||||
# Inject new token so the function doesn't know the difference
|
# Inject new token so the function doesn't know the difference
|
||||||
|
|
@ -61,7 +51,8 @@ class SessionManager:
|
||||||
try:
|
try:
|
||||||
session = self.get(request)
|
session = self.get(request)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
session = Session.from_request(session_manager=self, request=request, user=None)
|
session = Session(request, user=None)
|
||||||
|
self.add(session)
|
||||||
else:
|
else:
|
||||||
session.maintain()
|
session.maintain()
|
||||||
|
|
||||||
|
|
@ -87,7 +78,6 @@ class SessionManager:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def add(self, session):
|
def add(self, session):
|
||||||
session.session_manager = self
|
|
||||||
self.sessions[session.token] = session
|
self.sessions[session.token] = session
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
|
@ -96,7 +86,11 @@ class SessionManager:
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
token = _normalize_token(request)
|
token = _normalize_token(request)
|
||||||
session = self.sessions[token]
|
session = self.sessions[token]
|
||||||
if session.expired():
|
invalid = (
|
||||||
|
request.remote_addr != session.ip_address or
|
||||||
|
session.expired()
|
||||||
|
)
|
||||||
|
if invalid:
|
||||||
self.remove(token)
|
self.remove(token)
|
||||||
raise KeyError(token)
|
raise KeyError(token)
|
||||||
return session
|
return session
|
||||||
|
|
@ -116,13 +110,6 @@ class SessionManager:
|
||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
def maintain(self):
|
|
||||||
now = timetools.now()
|
|
||||||
self.last_activity = now
|
|
||||||
state_age = now - self.last_save_state
|
|
||||||
if state_age.seconds > SAVE_STATE_INTERVAL:
|
|
||||||
self.save_state()
|
|
||||||
|
|
||||||
def remove(self, token):
|
def remove(self, token):
|
||||||
token = _normalize_token(token)
|
token = _normalize_token(token)
|
||||||
try:
|
try:
|
||||||
|
|
@ -130,45 +117,13 @@ class SessionManager:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def save_state(self):
|
|
||||||
log.debug('Saving sessions state.')
|
|
||||||
j = [session.jsonify() for session in self.sessions.values() if not session.expired()]
|
|
||||||
j = json.dumps(j)
|
|
||||||
self.state_file.write('w', j)
|
|
||||||
self.last_save_state = timetools.now()
|
|
||||||
|
|
||||||
class Session:
|
class Session:
|
||||||
def __init__(
|
def __init__(self, request, user):
|
||||||
self,
|
self.token = _normalize_token(request)
|
||||||
*,
|
|
||||||
session_manager,
|
|
||||||
token,
|
|
||||||
ip_address,
|
|
||||||
user_agent,
|
|
||||||
user,
|
|
||||||
last_activity=None,
|
|
||||||
):
|
|
||||||
self.session_manager = session_manager
|
|
||||||
self.token = token
|
|
||||||
self.user = user
|
self.user = user
|
||||||
self.ip_address = ip_address
|
self.ip_address = request.remote_addr
|
||||||
self.user_agent = user_agent
|
self.user_agent = request.headers.get('User-Agent', '')
|
||||||
if last_activity is None:
|
|
||||||
self.last_activity = timetools.now()
|
self.last_activity = timetools.now()
|
||||||
else:
|
|
||||||
self.last_activity = last_activity
|
|
||||||
self.session_manager.add(self)
|
|
||||||
self.session_manager.maintain()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_request(cls, *, session_manager, request, user):
|
|
||||||
return cls(
|
|
||||||
session_manager=session_manager,
|
|
||||||
token=_normalize_token(request),
|
|
||||||
user=user,
|
|
||||||
ip_address=request.remote_addr,
|
|
||||||
user_agent=request.headers.get('User-Agent', ''),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.user:
|
if self.user:
|
||||||
|
|
@ -181,15 +136,5 @@ class Session:
|
||||||
age = now - self.last_activity
|
age = now - self.last_activity
|
||||||
return age.seconds > SESSION_MAX_AGE
|
return age.seconds > SESSION_MAX_AGE
|
||||||
|
|
||||||
def jsonify(self):
|
|
||||||
return {
|
|
||||||
'userid': (self.user.id) if self.user else None,
|
|
||||||
'token': self.token,
|
|
||||||
'ip_address': self.ip_address,
|
|
||||||
'user_agent': self.user_agent,
|
|
||||||
'last_activity': self.last_activity.timestamp(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def maintain(self):
|
def maintain(self):
|
||||||
self.last_activity = timetools.now()
|
self.last_activity = timetools.now()
|
||||||
self.session_manager.maintain()
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ vlogging.earlybird_config()
|
||||||
|
|
||||||
import gevent.monkey; gevent.monkey.patch_all()
|
import gevent.monkey; gevent.monkey.patch_all()
|
||||||
import werkzeug.middleware.proxy_fix
|
import werkzeug.middleware.proxy_fix
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import gevent.pywsgi
|
import gevent.pywsgi
|
||||||
import os
|
import os
|
||||||
|
|
@ -23,16 +24,10 @@ import backend
|
||||||
|
|
||||||
site = backend.site
|
site = backend.site
|
||||||
site.wsgi_app = werkzeug.middleware.proxy_fix.ProxyFix(site.wsgi_app)
|
site.wsgi_app = werkzeug.middleware.proxy_fix.ProxyFix(site.wsgi_app)
|
||||||
site.debug = False
|
site.debug = True
|
||||||
|
|
||||||
HTTPS_DIR = pathclass.Path(__file__).parent.with_child('https')
|
HTTPS_DIR = pathclass.Path(__file__).parent.with_child('https')
|
||||||
|
|
||||||
def fix_proxied_logging(method):
|
|
||||||
def format_request(self, *a, **k):
|
|
||||||
self.client_address = self.headers.get('X-Forwarded-For', self.client_address)
|
|
||||||
return method(self, *a, **k)
|
|
||||||
return format_request
|
|
||||||
|
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
|
|
||||||
def etiquette_flask_launch(
|
def etiquette_flask_launch(
|
||||||
|
|
@ -57,8 +52,6 @@ def etiquette_flask_launch(
|
||||||
application=site,
|
application=site,
|
||||||
)
|
)
|
||||||
|
|
||||||
http.handler_class.format_request = fix_proxied_logging(http.handler_class.format_request)
|
|
||||||
|
|
||||||
if localhost_only:
|
if localhost_only:
|
||||||
log.info('Setting localhost_only=True')
|
log.info('Setting localhost_only=True')
|
||||||
site.localhost_only = True
|
site.localhost_only = True
|
||||||
|
|
|
||||||
|
|
@ -105,8 +105,6 @@ textarea::placeholder
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#content_body
|
#content_body
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ p:last-child
|
||||||
{
|
{
|
||||||
/*position: absolute;*/
|
/*position: absolute;*/
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
font-size: 7pt;
|
font-size: 8pt;
|
||||||
|
|
||||||
min-width: 18px;
|
min-width: 18px;
|
||||||
min-height: 14px;
|
min-height: 14px;
|
||||||
|
|
|
||||||
|
|
@ -21,16 +21,6 @@ function reload_config(callback)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
api.admin.remove_session =
|
|
||||||
function remove_session(token, callback)
|
|
||||||
{
|
|
||||||
return http.post({
|
|
||||||
url: "/admin/remove_session",
|
|
||||||
data: {"token": token},
|
|
||||||
callback: callback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.admin.uncache =
|
api.admin.uncache =
|
||||||
function uncache(callback)
|
function uncache(callback)
|
||||||
{
|
{
|
||||||
|
|
@ -250,26 +240,6 @@ function batch_add_tag(photo_ids, tagname, callback)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
api.photos.batch_soft_delete =
|
|
||||||
function batch_soft_delete(photo_ids, callback)
|
|
||||||
{
|
|
||||||
return http.post({
|
|
||||||
url: "/batch/photos/soft_delete",
|
|
||||||
data: {"photo_ids": photo_ids.join(",")},
|
|
||||||
callback: callback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.photos.batch_hard_delete =
|
|
||||||
function batch_hard_delete(photo_ids, callback)
|
|
||||||
{
|
|
||||||
return http.post({
|
|
||||||
url: "/batch/photos/hard_delete",
|
|
||||||
data: {"photo_ids": photo_ids.join(",")},
|
|
||||||
callback: callback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.photos.batch_generate_thumbnail =
|
api.photos.batch_generate_thumbnail =
|
||||||
function batch_generate_thumbnail(photo_ids, callback)
|
function batch_generate_thumbnail(photo_ids, callback)
|
||||||
{
|
{
|
||||||
|
|
@ -607,29 +577,3 @@ function register(username, display_name, password_1, password_2, callback)
|
||||||
callback: callback,
|
callback: callback,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
api.users.set_password =
|
|
||||||
function set_password(username, current_password, password_1, password_2, callback)
|
|
||||||
{
|
|
||||||
const data = {
|
|
||||||
"username": username,
|
|
||||||
"current_password": current_password,
|
|
||||||
"password_1": password_1,
|
|
||||||
"password_2": password_2,
|
|
||||||
};
|
|
||||||
return http.post({
|
|
||||||
url: `/user/${username}/set_password`,
|
|
||||||
data: data,
|
|
||||||
callback: callback,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.users.set_permission =
|
|
||||||
function set_permission(username, permission_string, value, callback)
|
|
||||||
{
|
|
||||||
return http.post({
|
|
||||||
url: `/user/${username}/set_permission`,
|
|
||||||
data: {"permission": permission_string, "value": value},
|
|
||||||
callback: callback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,17 @@ th, td
|
||||||
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body">
|
<div id="content_body">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h1>Admin tools</h1>
|
<h1>Admin tools</h1>
|
||||||
|
<p><button id="reload_config_button" class="green_button" onclick="return reload_config_form();">Reload config file</button></p>
|
||||||
|
<p><button id="uncache_button" class="green_button" onclick="return uncache_form();">Uncache objects</button></p>
|
||||||
|
<p><button id="clear_sessions_button" class="green_button" onclick="return clear_sessions_form();">Clear login sessions</button></p>
|
||||||
<p><a href="/admin/dbdownload">Download database file</a></p>
|
<p><a href="/admin/dbdownload">Download database file</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Statistics</h2>
|
<h2>Statistics</h2>
|
||||||
<p><button id="uncache_button" class="red_button" onclick="return uncache_form();">Uncache objects</button></p>
|
|
||||||
<table>
|
<table>
|
||||||
<tr><th></th><th>Stored</th><th>Cached</th></tr>
|
<tr><th></th><th>Stored</th><th>Cached</th></tr>
|
||||||
<tr><td>Albums</td><td>{{counts.albums}}</td><td>{{cached.albums}}</td></tr>
|
<tr><td>Albums</td><td>{{counts.albums}}</td><td>{{cached.albums}}</td></tr>
|
||||||
|
|
@ -45,60 +47,6 @@ th, td
|
||||||
<tr><td>Users</td><td>{{counts.users}}</td><td>{{cached.users}}</td></tr>
|
<tr><td>Users</td><td>{{counts.users}}</td><td>{{cached.users}}</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
|
||||||
<h2>Users</h2>
|
|
||||||
<table id="users_table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td>ID</td>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>Username</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{% for user in users %}
|
|
||||||
<tr>
|
|
||||||
<td><code>{{user.id}}</code></td>
|
|
||||||
<td><a href="/userid/{{user.id}}">{{user.display_name}}</a></td>
|
|
||||||
<td>{{user.username}}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel">
|
|
||||||
<h2>Config</h2>
|
|
||||||
<p><button id="reload_config_button" class="green_button" onclick="return reload_config_form();">Reload config file</button></p>
|
|
||||||
<pre style="white-space:break-spaces;">{{etq_config}}</pre>
|
|
||||||
<pre style="white-space:break-spaces;">{{server_config}}</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel">
|
|
||||||
<h2>Sessions</h2>
|
|
||||||
<p><button id="clear_sessions_button" class="red_button" onclick="return clear_sessions_form();">Clear login sessions</button></p>
|
|
||||||
<table id="sessions_table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td>Token</td>
|
|
||||||
<td>IP</td>
|
|
||||||
<td>User</td>
|
|
||||||
<td>Last activity</td>
|
|
||||||
<td>Remove</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for (token, session) in sessions %}
|
|
||||||
<tr>
|
|
||||||
<td class="session_token"><code>{{token}}</code></td>
|
|
||||||
<td class="session_ip"><code>{{session.ip_address}}</code></td>
|
|
||||||
<td>{% if session.user %}{{session.user.display_name}}{% else %}<i>anonymous</i>{%endif%}</td>
|
|
||||||
<td><time>{{session.last_activity|timestamp_to_8601}}</time></td>
|
|
||||||
<td><button class="red_button" onclick="return remove_session_form(event);">x</button></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
@ -115,7 +63,6 @@ function clear_sessions_form()
|
||||||
{
|
{
|
||||||
alert(JSON.stringify(response));
|
alert(JSON.stringify(response));
|
||||||
}
|
}
|
||||||
common.delete_all_children(document.querySelector("#sessions_table tbody"));
|
|
||||||
}
|
}
|
||||||
return api.admin.clear_sessions(callback);
|
return api.admin.clear_sessions(callback);
|
||||||
}
|
}
|
||||||
|
|
@ -135,22 +82,6 @@ function reload_config_form()
|
||||||
return api.admin.reload_config(callback);
|
return api.admin.reload_config(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove_session_form(event)
|
|
||||||
{
|
|
||||||
const button = event.target;
|
|
||||||
const row = button.closest("tr");
|
|
||||||
const token = row.querySelector(".session_token").innerText;
|
|
||||||
function callback(response)
|
|
||||||
{
|
|
||||||
if (response.meta.status !== 200)
|
|
||||||
{
|
|
||||||
alert(JSON.stringify(response));
|
|
||||||
}
|
|
||||||
row.parentElement.removeChild(row);
|
|
||||||
}
|
|
||||||
return api.admin.remove_session(token, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function uncache_form()
|
function uncache_form()
|
||||||
{
|
{
|
||||||
const uncache_button = document.getElementById("uncache_button");
|
const uncache_button = document.getElementById("uncache_button");
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body" class="sticky_side_right sticky_bottom_right">
|
<div id="content_body" class="sticky_side_right sticky_bottom_right">
|
||||||
<div id="left">
|
<div id="left">
|
||||||
<div id="album_list" class="panel">
|
<div id="album_list" class="panel">
|
||||||
|
|
@ -160,7 +160,7 @@ const ALBUM_ID = undefined;
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body" class="sticky_side_right sticky_bottom_right">
|
<div id="content_body" class="sticky_side_right sticky_bottom_right">
|
||||||
<div id="right" class="panel">
|
<div id="right" class="panel">
|
||||||
{% if view != "list" %}
|
{% if view != "list" %}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body">
|
<div id="content_body">
|
||||||
<div id="bookmark_panel" class="panel">
|
<div id="bookmark_panel" class="panel">
|
||||||
<h1><span class="dynamic_bookmark_count">{{bookmarks|length}}</span> Bookmarks</h1>
|
<h1><span class="dynamic_bookmark_count">{{bookmarks|length}}</span> Bookmarks</h1>
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body" class="sticky_side_right sticky_bottom_right">
|
<div id="content_body" class="sticky_side_right sticky_bottom_right">
|
||||||
<div id="left">
|
<div id="left">
|
||||||
|
|
||||||
|
|
@ -133,13 +133,6 @@
|
||||||
<div id="download_zip_area">
|
<div id="download_zip_area">
|
||||||
<button class="yellow_button" id="download_zip_button" onclick="return download_zip_form();">Download .zip</button>
|
<button class="yellow_button" id="download_zip_button" onclick="return download_zip_form();">Download .zip</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if request.is_localhost %}
|
|
||||||
<div id="delete_photo_area">
|
|
||||||
<button class="red_button" id="soft_delete_photos_button" data-spinner-delay="500" onclick="return soft_delete_form();">Soft delete
|
|
||||||
<button class="red_button" id="hard_delete_photos_button" data-spinner-delay="500" onclick="return hard_delete_form();">Hard delete
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="message_area">
|
<div id="message_area">
|
||||||
|
|
@ -454,54 +447,6 @@ function unset_searchhidden_form()
|
||||||
api.photos.batch_unset_searchhidden(photo_ids, set_unset_searchhidden_callback);
|
api.photos.batch_unset_searchhidden(photo_ids, set_unset_searchhidden_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
function delete_callback(response)
|
|
||||||
{
|
|
||||||
if (! response.meta.json_ok)
|
|
||||||
{
|
|
||||||
alert(JSON.stringify(response));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (response.meta.status !== 200)
|
|
||||||
{
|
|
||||||
alert(JSON.stringify(response));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
photo_clipboard.clear_clipboard();
|
|
||||||
common.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function soft_delete_form()
|
|
||||||
{
|
|
||||||
const photo_ids = Array.from(photo_clipboard.clipboard);
|
|
||||||
if (photo_ids.length === 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const message = "You are about to REMOVE photos from the database, keeping the files on disk. Type 'REMOVE' to confirm.";
|
|
||||||
if (window.prompt(message) === "REMOVE")
|
|
||||||
{
|
|
||||||
api.photos.batch_hard_delete(photo_ids, delete_callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hard_delete_form()
|
|
||||||
{
|
|
||||||
const photo_ids = Array.from(photo_clipboard.clipboard);
|
|
||||||
if (photo_ids.length === 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const message = "You are about to DELETE photos from disk. This is irreversible. Type 'DELETE' to confirm.";
|
|
||||||
if (window.prompt(message) === "DELETE")
|
|
||||||
{
|
|
||||||
api.photos.batch_hard_delete(photo_ids, delete_callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
function on_pageload()
|
function on_pageload()
|
||||||
{
|
{
|
||||||
photo_clipboard.register_hotkeys();
|
photo_clipboard.register_hotkeys();
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
{% macro make_header(request) %}
|
{% macro make_header(session) %}
|
||||||
<nav id="header">
|
<nav id="header">
|
||||||
<a class="header_element navigation_link" href="/">Etiquette</a>
|
<a class="header_element navigation_link" href="/">Etiquette</a>
|
||||||
<a class="header_element navigation_link" href="/search">Search</a>
|
<a class="header_element navigation_link" href="/search">Search</a>
|
||||||
<a class="header_element navigation_link" href="/tags">Tags</a>
|
<a class="header_element navigation_link" href="/tags">Tags</a>
|
||||||
{% if request.session.user %}
|
{% if session.user %}
|
||||||
<a class="header_element navigation_link dynamic_user_display_name" href="/userid/{{request.session.user.id}}">{{request.session.user.display_name}}{% if request.is_localhost %}<i> localhost</i>{% endif %}</a>
|
<a class="header_element navigation_link dynamic_user_display_name" href="/userid/{{session.user.id}}">{{session.user.display_name}}</a>
|
||||||
<button id="logout_button" class="header_element" onclick="return api.users.logout(common.go_to_root);" style="flex:0">Logout</button>
|
<button id="logout_button" class="header_element" onclick="return api.users.logout(common.go_to_root);" style="flex:0">Logout</button>
|
||||||
{% elif request.is_localhost %}
|
|
||||||
<a class="header_element navigation_link" href="/login"><i>localhost</i></a>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="header_element navigation_link" href="/login">Log in</a>
|
<a class="header_element navigation_link" href="/login">Log in</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ form h2
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body">
|
<div id="content_body">
|
||||||
<form id="login_form" class="panel" action="/login" method="post">
|
<form id="login_form" class="panel" action="/login" method="post">
|
||||||
<h2>Log in</h2>
|
<h2>Log in</h2>
|
||||||
|
|
@ -79,16 +79,18 @@ form h2
|
||||||
<input type="password" id="login_input_password" name="password" placeholder="password">
|
<input type="password" id="login_input_password" name="password" placeholder="password">
|
||||||
<button type="submit" id="login_submit_button" class="green_button" onclick="return login_form(event);">Log in</button>
|
<button type="submit" id="login_submit_button" class="green_button" onclick="return login_form(event);">Log in</button>
|
||||||
</form>
|
</form>
|
||||||
{% if registration_enabled %}
|
|
||||||
<form id="register_form" class="panel" action="/register" method="post">
|
<form id="register_form" class="panel" action="/register" method="post">
|
||||||
<h2>Register</h2>
|
<h2>Register</h2>
|
||||||
|
{% if registration_enabled %}
|
||||||
<input type="text" id="register_input_username" name="username" placeholder="username (at least {{min_username_length}})">
|
<input type="text" id="register_input_username" name="username" placeholder="username (at least {{min_username_length}})">
|
||||||
<input type="text" id="register_input_display_name" name="display_name" placeholder="display name (optional)">
|
<input type="text" id="register_input_display_name" name="display_name" placeholder="display name (optional)">
|
||||||
<input type="password" id="register_input_password_1" name="password_1" placeholder="password (at least {{min_password_length}})">
|
<input type="password" id="register_input_password_1" name="password_1" placeholder="password (at least {{min_password_length}})">
|
||||||
<input type="password" id="register_input_password_2" name="password_2" placeholder="password again">
|
<input type="password" id="register_input_password_2" name="password_2" placeholder="password again">
|
||||||
<button type="submit" id="register_input_button" class="green_button" onclick="return register_form(event);">Register</button>
|
<button type="submit" id="register_input_button" class="green_button" onclick="return register_form(event);">Register</button>
|
||||||
</form>
|
{% else %}
|
||||||
|
<span>Registrations are disabled.</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</form>
|
||||||
<div id="message_area" class="panel">
|
<div id="message_area" class="panel">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body">
|
<div id="content_body">
|
||||||
<div id="left" class="panel">
|
<div id="left" class="panel">
|
||||||
<div id="editor_area">
|
<div id="editor_area">
|
||||||
|
|
@ -184,8 +184,8 @@
|
||||||
{% if photo.author is not none %}
|
{% if photo.author is not none %}
|
||||||
<li>Author: <a href="/userid/{{photo.author.id}}">{{photo.author.display_name}}</a></li>
|
<li>Author: <a href="/userid/{{photo.author.id}}">{{photo.author.display_name}}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if photo.width and photo.height and photo.area %}
|
{% if photo.width and photo.height %}
|
||||||
<li title="{{((photo.area) / 1000000)|round(2)}} mpx">Dimensions: <a href="/search?width={{photo.width}}..{{photo.width}}&height={{photo.height}}..{{photo.height}}">{{photo.width}}×{{photo.height}}</a> px</li>
|
<li title="{{(photo.area / 1000000)|round(2)}} mpx">Dimensions: <a href="/search?width={{photo.width}}..{{photo.width}}&height={{photo.height}}..{{photo.height}}">{{photo.width}}×{{photo.height}}</a> px</li>
|
||||||
{% set aspectratio = photo.aspectratio|round(2) %}
|
{% set aspectratio = photo.aspectratio|round(2) %}
|
||||||
<li>Aspect ratio: <a href="/search?aspectratio={{aspectratio-0.01}}..{{aspectratio+0.01}}">{{aspectratio}}</a></li>
|
<li>Aspect ratio: <a href="/search?aspectratio={{aspectratio-0.01}}..{{aspectratio+0.01}}">{{aspectratio}}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -194,10 +194,8 @@
|
||||||
<li>Duration: <a href="/search?duration={{photo.duration|round(method='floor')|int}}..{{photo.duration|round(method='ceil')|int}}">{{photo.duration_string}}</a></li>
|
<li>Duration: <a href="/search?duration={{photo.duration|round(method='floor')|int}}..{{photo.duration|round(method='ceil')|int}}">{{photo.duration_string}}</a></li>
|
||||||
<li>Overall bitrate: <a href="/search?bitrate={{photo.bitrate|round(method='floor')|int}}..{{photo.bitrate|round(method='ceil')|int}}">{{photo.bitrate|int}}</a> kbps</li>
|
<li>Overall bitrate: <a href="/search?bitrate={{photo.bitrate|round(method='floor')|int}}..{{photo.bitrate|round(method='ceil')|int}}">{{photo.bitrate|int}}</a> kbps</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>Created <a href="/search?created={{photo.created.timestamp()|int-43200}}..{{photo.created.timestamp()|int+43200}}" title="{{photo.created.isoformat()}}">{{photo.created|timestamp_to_naturaldate}}</a></li>
|
<li>Created <a href="/search?created={{photo.created.timestamp()|int-43200}}..{{photo.created.timestamp()|int+43200}}">{{photo.created|timestamp_to_naturaldate}}</a></li>
|
||||||
{% if photo.sha256 %}
|
|
||||||
<li>SHA256: <a href="/search?sha256={{photo.sha256}}"><code>{{photo.sha256[:16]}}</code></a></li>
|
<li>SHA256: <a href="/search?sha256={{photo.sha256}}"><code>{{photo.sha256[:16]}}</code></a></li>
|
||||||
{% endif %}
|
|
||||||
<li><button id="refresh_metadata_button" class="green_button button_with_spinner" onclick="return refresh_metadata_form();">Refresh metadata</button></li>
|
<li><button id="refresh_metadata_button" class="green_button button_with_spinner" onclick="return refresh_metadata_form();">Refresh metadata</button></li>
|
||||||
{% if request.is_localhost %}
|
{% if request.is_localhost %}
|
||||||
<li><button id="show_in_folder_button" onclick="return show_in_folder_form();">Show in folder</button></li>
|
<li><button id="show_in_folder_button" onclick="return show_in_folder_form();">Show in folder</button></li>
|
||||||
|
|
@ -273,7 +271,6 @@
|
||||||
<button id="generate_thumbnail_button" class="green_button button_with_spinner" onclick="return generate_thumbnail_for_video_form();">Capture thumbnail</button>
|
<button id="generate_thumbnail_button" class="green_button button_with_spinner" onclick="return generate_thumbnail_for_video_form();">Capture thumbnail</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if request.is_localhost or (request.session.user and request.session.user.has_object_permission(photo, 'edit')) %}
|
|
||||||
<button
|
<button
|
||||||
class="green_button button_with_confirm"
|
class="green_button button_with_confirm"
|
||||||
data-holder-id="copy_other_photo_tags_holder"
|
data-holder-id="copy_other_photo_tags_holder"
|
||||||
|
|
@ -284,27 +281,24 @@
|
||||||
>
|
>
|
||||||
Copy tags from other photo
|
Copy tags from other photo
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if request.is_localhost or (request.session.user and request.session.user.has_object_permission(photo, 'delete')) %}
|
|
||||||
<button
|
<button
|
||||||
class="red_button button_with_confirm"
|
class="red_button button_with_confirm"
|
||||||
data-onclick="return soft_delete_form();"
|
data-onclick="return delete_photo_form();"
|
||||||
data-prompt="Delete photo, keep file?"
|
data-prompt="Delete photo, keep file?"
|
||||||
data-cancel-class="gray_button"
|
data-cancel-class="gray_button"
|
||||||
>
|
>
|
||||||
Soft delete
|
Remove
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="red_button button_with_confirm"
|
class="red_button button_with_confirm"
|
||||||
data-onclick="return hard_delete_form();"
|
data-onclick="return delete_photo_from_disk_form();"
|
||||||
data-prompt="Delete file on disk?"
|
data-prompt="Delete file on disk?"
|
||||||
data-cancel-class="gray_button"
|
data-cancel-class="gray_button"
|
||||||
>
|
>
|
||||||
Hard delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -428,32 +422,14 @@ function add_remove_photo_tag_callback(response)
|
||||||
return abort;
|
return abort;
|
||||||
}
|
}
|
||||||
|
|
||||||
function soft_delete_form()
|
function delete_photo_form()
|
||||||
{
|
{
|
||||||
function callback(response)
|
api.photos.delete(PHOTO_ID, false, common.go_to_root);
|
||||||
{
|
|
||||||
if (response.meta.status !== 200)
|
|
||||||
{
|
|
||||||
alert(JSON.stringify(response));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
common.go_to_root();
|
|
||||||
}
|
|
||||||
api.photos.delete(PHOTO_ID, false, callback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hard_delete_form()
|
function delete_photo_from_disk_form()
|
||||||
{
|
{
|
||||||
function callback(response)
|
api.photos.delete(PHOTO_ID, true, common.go_to_root);
|
||||||
{
|
|
||||||
if (response.meta.status !== 200)
|
|
||||||
{
|
|
||||||
alert(JSON.stringify(response));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
common.go_to_root();
|
|
||||||
}
|
|
||||||
api.photos.delete(PHOTO_ID, true, callback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function generate_thumbnail_for_video_form()
|
function generate_thumbnail_for_video_form()
|
||||||
|
|
|
||||||
|
|
@ -64,19 +64,16 @@ body > .nice_link
|
||||||
{{motd[1]}}
|
{{motd[1]}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</p>
|
</p>
|
||||||
{% if anonymous_read or request.session.user or request.is_localhost %}
|
|
||||||
<a class="nice_link navigation_link" href="/search">Search</a>
|
<a class="nice_link navigation_link" href="/search">Search</a>
|
||||||
<a class="nice_link navigation_link" href="/tags">Tags</a>
|
<a class="nice_link navigation_link" href="/tags">Browse tags</a>
|
||||||
<a class="nice_link navigation_link" href="/albums">Albums</a>
|
<a class="nice_link navigation_link" href="/albums">Browse albums</a>
|
||||||
<a class="nice_link navigation_link" href="/bookmarks">Bookmarks</a>
|
<a class="nice_link navigation_link" href="/bookmarks">Bookmarks</a>
|
||||||
<a class="nice_link navigation_link" href="/upload">Upload</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.session.user %}
|
{% if request.session.user %}
|
||||||
<a class="nice_link navigation_link" href="/userid/{{request.session.user.id}}">{{request.session.user.display_name}}</a>
|
<a class="nice_link navigation_link" href="/userid/{{request.session.user.id}}">{{request.session.user.display_name}}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="nice_link navigation_link" href="/login">Log in</a>
|
<a class="nice_link navigation_link" href="/login">Log in</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.is_localhost or (request.session.user and request.session.user.has_permission('admin')) %}
|
{% if request.is_localhost %}
|
||||||
<a class="nice_link navigation_link" href="/admin">Admin</a>
|
<a class="nice_link navigation_link" href="/admin">Admin</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="link_group">
|
<div class="link_group">
|
||||||
|
|
@ -84,6 +81,11 @@ body > .nice_link
|
||||||
<a class="nice_link navigation_link" href="/?theme=slate">Slate</a>
|
<a class="nice_link navigation_link" href="/?theme=slate">Slate</a>
|
||||||
<a class="nice_link navigation_link" href="/?theme=pearl">Pearl</a>
|
<a class="nice_link navigation_link" href="/?theme=pearl">Pearl</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="link_group">
|
||||||
|
<a class="nice_link navigation_link" href="https://www.github.com/voussoir/etiquette">GitHub</a>
|
||||||
|
<a class="nice_link navigation_link" href="https://www.gitlab.com/voussoir/etiquette">GitLab</a>
|
||||||
|
<a class="nice_link navigation_link" href="https://www.codeberg.org/voussoir/etiquette">Codeberg</a>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body">
|
<div id="content_body">
|
||||||
<div id="left" class="panel">
|
<div id="left" class="panel">
|
||||||
{% for tagtype in ["musts", "mays", "forbids"] %}
|
{% for tagtype in ["musts", "mays", "forbids"] %}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body">
|
<div id="content_body">
|
||||||
<div id="right" class="panel">
|
<div id="right" class="panel">
|
||||||
<a id="name_tag" target="_blank">Swipe!</a>
|
<a id="name_tag" target="_blank">Swipe!</a>
|
||||||
|
|
@ -446,7 +446,7 @@ function show_current_photo()
|
||||||
name_tag.href = "/photo/" + current_photo.id;
|
name_tag.href = "/photo/" + current_photo.id;
|
||||||
if (current_photo.has_thumbnail)
|
if (current_photo.has_thumbnail)
|
||||||
{
|
{
|
||||||
photo_viewer_img.src = "/photo/" + current_photo.id + "/thumbnail";
|
photo_viewer_img.src = "/thumbnail/" + current_photo.id + ".jpg";
|
||||||
waiting_for_img = true;
|
waiting_for_img = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -459,7 +459,7 @@ function show_current_photo()
|
||||||
upcoming_photo = photo_queue[index];
|
upcoming_photo = photo_queue[index];
|
||||||
if (upcoming_photo !== undefined && upcoming_photo.has_thumbnail)
|
if (upcoming_photo !== undefined && upcoming_photo.has_thumbnail)
|
||||||
{
|
{
|
||||||
upcoming_imgs[index].src = "/photo/" + upcoming_photo.id + "/thumbnail";
|
upcoming_imgs[index].src = "/thumbnail/" + upcoming_photo.id + ".jpg";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@
|
||||||
{
|
{
|
||||||
font-family: initial;
|
font-family: initial;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
#tag_list
|
#tag_list
|
||||||
{
|
{
|
||||||
|
|
@ -87,7 +86,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body" class="sticky_side_right sticky_bottom_right">
|
<div id="content_body" class="sticky_side_right sticky_bottom_right">
|
||||||
<div id="right" class="panel">
|
<div id="right" class="panel">
|
||||||
<div id="editor_area">
|
<div id="editor_area">
|
||||||
|
|
@ -389,7 +388,7 @@ function delete_specific_tag_form(event)
|
||||||
function delete_tag_form(event)
|
function delete_tag_form(event)
|
||||||
{
|
{
|
||||||
const delete_button = event.target;
|
const delete_button = event.target;
|
||||||
const li = delete_button.closest("div");
|
const li = delete_button.closest("li");
|
||||||
const tag_card = tag_card_from_li(li);
|
const tag_card = tag_card_from_li(li);
|
||||||
const tag_name = tag_card.innerText.split(".").pop();
|
const tag_name = tag_card.innerText.split(".").pop();
|
||||||
return api.tags.delete(tag_name, tag_action_callback);
|
return api.tags.delete(tag_name, tag_action_callback);
|
||||||
|
|
@ -398,7 +397,7 @@ function delete_tag_form(event)
|
||||||
function remove_child_form(event)
|
function remove_child_form(event)
|
||||||
{
|
{
|
||||||
const delete_button = event.target;
|
const delete_button = event.target;
|
||||||
const li = delete_button.closest("div");
|
const li = delete_button.closest("li");
|
||||||
const tag_card = tag_card_from_li(li);
|
const tag_card = tag_card_from_li(li);
|
||||||
const qual_name = tag_card.innerText;
|
const qual_name = tag_card.innerText;
|
||||||
let tag_name;
|
let tag_name;
|
||||||
|
|
@ -420,7 +419,7 @@ function remove_child_form(event)
|
||||||
function remove_synonym_form(event)
|
function remove_synonym_form(event)
|
||||||
{
|
{
|
||||||
const delete_button = event.target;
|
const delete_button = event.target;
|
||||||
const li = delete_button.closest("div");
|
const li = delete_button.closest("li");
|
||||||
const tag_card = tag_card_from_li(li);
|
const tag_card = tag_card_from_li(li);
|
||||||
const qual_name = tag_card.innerText;
|
const qual_name = tag_card.innerText;
|
||||||
let tag_name;
|
let tag_name;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body">
|
<div id="content_body">
|
||||||
<p>test</p>
|
<p>test</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html class="theme_{{theme}}">
|
|
||||||
<head>
|
|
||||||
{% import "header.html" as header %}
|
|
||||||
<title>Upload</title>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<link rel="icon" href="/favicon.png" type="image/png"/>
|
|
||||||
<link rel="stylesheet" href="/static/css/common.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/etiquette.css">
|
|
||||||
<script src="/static/js/common.js"></script>
|
|
||||||
<script src="/static/js/api.js"></script>
|
|
||||||
<script src="/static/js/http.js"></script>
|
|
||||||
<script src="/static/js/spinners.js"></script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
button
|
|
||||||
{
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content_body
|
|
||||||
{
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
form
|
|
||||||
{
|
|
||||||
display: grid;
|
|
||||||
grid-auto-rows: max-content;
|
|
||||||
grid-row-gap: 8px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
form h2
|
|
||||||
{
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
progress
|
|
||||||
{
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#login_form { grid-area: login_form; }
|
|
||||||
#register_form { grid-area: register_form; }
|
|
||||||
#message_area
|
|
||||||
{
|
|
||||||
grid-area: message_area;
|
|
||||||
}
|
|
||||||
|
|
||||||
#upload_submit_button
|
|
||||||
{
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 800px)
|
|
||||||
{
|
|
||||||
#content_body
|
|
||||||
{
|
|
||||||
grid-template:
|
|
||||||
"upload_files_form" auto
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 800px)
|
|
||||||
{
|
|
||||||
#content_body
|
|
||||||
{
|
|
||||||
grid-template:
|
|
||||||
"login_form" auto
|
|
||||||
"register_form" auto
|
|
||||||
"message_area" 150px
|
|
||||||
/ 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{header.make_header(request=request)}}
|
|
||||||
<div id="content_body">
|
|
||||||
<form id="upload_form" class="panel" action="/photo/upload" method="post" enctype="multipart/form-data">
|
|
||||||
<div id="div_single_files">
|
|
||||||
<h2>Upload single files</h2>
|
|
||||||
<input id="files_input" type="file" name="file[]" multiple onchange="return file_input_onchange(event);"/>
|
|
||||||
</div>
|
|
||||||
<div id="div_folder">
|
|
||||||
<h2>Upload a folder</h2>
|
|
||||||
<input id="folder_input" type="file" name="folder[]" directory webkitdirectory onchange="return folder_input_onchange(event);"/>
|
|
||||||
</div>
|
|
||||||
<!-- <h2>Options</h2> -->
|
|
||||||
<!-- <label><input id="make_album_checkbox" type="checkbox" onchange="return make_album_checkbox_onchange(event);"> Make an album</label> -->
|
|
||||||
<!-- <input id="make_album_name_input" type="text" class="hidden" placeholder="Album name"/> -->
|
|
||||||
<button type="submit" id="upload_submit_button" class="green_button" onclick="return upload_form(event);">Upload</button>
|
|
||||||
</form>
|
|
||||||
<div class="panel">
|
|
||||||
<p id="progresstext">Progress</p>
|
|
||||||
<progress id="progressbar" max="100" value="0"></progress>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
const message_area = document.getElementById("message_area");
|
|
||||||
|
|
||||||
function make_album_checkbox_onchange(event)
|
|
||||||
{
|
|
||||||
const make_album_name = document.getElementById("make_album_name_input");
|
|
||||||
if (document.getElementById("make_album_checkbox").checked)
|
|
||||||
{
|
|
||||||
make_album_name.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
make_album_name.classList.add("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function file_input_onchange(event)
|
|
||||||
{
|
|
||||||
const files_input = document.getElementById("files_input");
|
|
||||||
if (files_input.files.length > 0)
|
|
||||||
{
|
|
||||||
const folder_input = document.getElementById("div_folder");
|
|
||||||
folder_input.classList.add("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function folder_input_onchange(event)
|
|
||||||
{
|
|
||||||
const folder_input = document.getElementById("folder_input");
|
|
||||||
if (folder_input.files.length > 0)
|
|
||||||
{
|
|
||||||
const files_input = document.getElementById("div_single_files");
|
|
||||||
files_input.classList.add("hidden");
|
|
||||||
// document.getElementById("make_album_checkbox").checked = true;
|
|
||||||
// const make_album_name = document.getElementById("make_album_name_input");
|
|
||||||
// make_album_name.classList.remove("hidden");
|
|
||||||
// const albumname = folder_input.files[0].webkitRelativePath.split("/")[0];
|
|
||||||
// make_album_name.value = albumname;
|
|
||||||
}
|
|
||||||
const progress = document.getElementById("progressbar");
|
|
||||||
progress.max = Array.from(folder_input.files).reduce((accumulator, file) => accumulator + file.size, 0);
|
|
||||||
}
|
|
||||||
function upload_form(event)
|
|
||||||
{
|
|
||||||
event.preventDefault();
|
|
||||||
console.log(event);
|
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
function xhr_event(e)
|
|
||||||
{
|
|
||||||
if (e.loaded === e.total)
|
|
||||||
{
|
|
||||||
progresstext.innerText = 'Processing...';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
progresstext.textContent = `${e.loaded} / ${e.total}`;
|
|
||||||
}
|
|
||||||
progressbar.value = e.total;
|
|
||||||
progressbar.value = e.loaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
function xhr_loadend(event)
|
|
||||||
{
|
|
||||||
window.location.href = "/search"
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.onreadystatechange = function()
|
|
||||||
{
|
|
||||||
// window.alert("readystate: " + xhr.readyState + " " + xhr.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.addEventListener("loadstart", xhr_event);
|
|
||||||
xhr.addEventListener("load", xhr_event);
|
|
||||||
xhr.addEventListener("loadend", xhr_loadend);
|
|
||||||
xhr.addEventListener("progress", xhr_event);
|
|
||||||
xhr.addEventListener("error", xhr_event);
|
|
||||||
xhr.addEventListener("abort", xhr_event);
|
|
||||||
|
|
||||||
xhr.upload.addEventListener("progress", xhr_event);
|
|
||||||
xhr.open("POST", "/photo/upload");
|
|
||||||
|
|
||||||
const files_input = document.getElementById("files_input");
|
|
||||||
const folder_input = document.getElementById("folder_input");
|
|
||||||
let files;
|
|
||||||
if (files_input.files.length > 0)
|
|
||||||
{
|
|
||||||
files = files_input.files;
|
|
||||||
}
|
|
||||||
else if (folder_input.files.length > 0)
|
|
||||||
{
|
|
||||||
files = folder_input.files;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
console.log(files);
|
|
||||||
const formdata = new FormData();
|
|
||||||
for (const file of files)
|
|
||||||
{
|
|
||||||
formdata.append("file", file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (document.getElementById("make_album_checkbox").checked)
|
|
||||||
// {
|
|
||||||
// formdata.append("album_name", document.getElementById("make_album_name_input").value);
|
|
||||||
// }
|
|
||||||
|
|
||||||
const progresstext = document.getElementById("progresstext");
|
|
||||||
const progressbar = document.getElementById("progressbar");
|
|
||||||
|
|
||||||
xhr.send(formdata);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</html>
|
|
||||||
|
|
@ -38,10 +38,10 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(request=request)}}
|
{{header.make_header(session=request.session)}}
|
||||||
<div id="content_body">
|
<div id="content_body">
|
||||||
<div id="hierarchy_self" class="panel">
|
<div id="hierarchy_self" class="panel">
|
||||||
<h1 id="display_name" class="dynamic_user_display_name">{{user.display_name}}</h1>
|
<h1 id="display_name">{{user.display_name}}</h1>
|
||||||
{% if user.display_name != user.username %}
|
{% if user.display_name != user.username %}
|
||||||
<p>Username: {{user.username}}</p>
|
<p>Username: {{user.username}}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -73,43 +73,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user.id == request.session.user.id %}
|
|
||||||
<div id="hierarchy_settings" class="panel">
|
|
||||||
<h2>Settings</h2>
|
|
||||||
<div>
|
|
||||||
<input type="text" id="set_display_name_input" value="{{user.display_name}}" placeholder="display name"/>
|
|
||||||
<button id="set_display_name_button" onclick="return set_display_name_form(event);">Set display name</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<input type="password" id="current_password_input" placeholder="current password"/>
|
|
||||||
<input type="password" id="password_1_input" placeholder="new password"/>
|
|
||||||
<input type="password" id="password_2_input" placeholder="new password again"/>
|
|
||||||
<button id="set_password_button" onclick="return set_password_form(event);">Set password</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="message_area">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if request.is_localhost %}
|
|
||||||
<div id="hierarchy_permissions" class="panel">
|
|
||||||
<h2>Permissions</h2>
|
|
||||||
<ul>
|
|
||||||
{% for permission in constants_all_permissions|sort %}
|
|
||||||
<li><label><input type="checkbox" name="{{permission}}" onchange="return permission_checkbox_onchange(event);" {% if permission in user_permissions %}checked{%endif%}/>{{permission}}</label></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const USER_ID = "{{user.id}}";
|
|
||||||
const USERNAME = "{{user.username}}";
|
|
||||||
|
|
||||||
const PHOTOS = [
|
const PHOTOS = [
|
||||||
{% for photo in user.get_photos(direction='desc')|islice(0, 15) %}
|
{% for photo in user.get_photos(direction='desc')|islice(0, 15) %}
|
||||||
{{photo.jsonify(include_albums=False)|tojson|safe}},
|
{{photo.jsonify(include_albums=False)|tojson|safe}},
|
||||||
|
|
@ -134,14 +101,6 @@ const BOOKMARKS = [
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
|
|
||||||
function permission_checkbox_onchange(event)
|
|
||||||
{
|
|
||||||
function callback(response)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
api.users.set_permission(USERNAME, event.target.name, event.target.checked, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function on_pageload()
|
function on_pageload()
|
||||||
{
|
{
|
||||||
for (const photo of PHOTOS)
|
for (const photo of PHOTOS)
|
||||||
|
|
@ -173,12 +132,15 @@ function on_pageload()
|
||||||
document.addEventListener("DOMContentLoaded", on_pageload);
|
document.addEventListener("DOMContentLoaded", on_pageload);
|
||||||
|
|
||||||
{% if user.id == request.session.user.id %}
|
{% if user.id == request.session.user.id %}
|
||||||
function set_display_name_form(event)
|
const USERNAME = "{{user.username}}";
|
||||||
|
profile_ed_on_open = undefined;
|
||||||
|
|
||||||
|
function profile_ed_on_save(ed)
|
||||||
{
|
{
|
||||||
const input = document.getElementById("set_display_name_input");
|
|
||||||
function callback(response)
|
function callback(response)
|
||||||
{
|
{
|
||||||
console.log("HI");
|
ed.hide_spinner();
|
||||||
|
|
||||||
if (! response.meta.json_ok)
|
if (! response.meta.json_ok)
|
||||||
{
|
{
|
||||||
alert(JSON.stringify(response));
|
alert(JSON.stringify(response));
|
||||||
|
|
@ -186,7 +148,7 @@ function set_display_name_form(event)
|
||||||
}
|
}
|
||||||
if ("error_type" in response.data)
|
if ("error_type" in response.data)
|
||||||
{
|
{
|
||||||
common.create_message_button(message_area, "message_negative", response.data.error_message);
|
ed.show_error(`${response.data.error_type} ${response.data.error_message}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,44 +156,32 @@ function set_display_name_form(event)
|
||||||
const new_display_name = response.data.display_name;
|
const new_display_name = response.data.display_name;
|
||||||
common.update_dynamic_elements("dynamic_user_display_name", new_display_name);
|
common.update_dynamic_elements("dynamic_user_display_name", new_display_name);
|
||||||
|
|
||||||
input.value = new_display_name;
|
ed.elements["display_name"].edit.value = new_display_name;
|
||||||
|
|
||||||
|
ed.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
api.users.edit(USERNAME, input.value, callback);
|
ed.show_spinner();
|
||||||
|
api.users.edit(USERNAME, ed.elements["display_name"].edit.value, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function set_password_form(event)
|
const profile_ed_on_cancel = undefined;
|
||||||
{
|
|
||||||
const current_password = document.getElementById("current_password_input");
|
|
||||||
const password_1 = document.getElementById("password_1_input");
|
|
||||||
const password_2 = document.getElementById("password_2_input");
|
|
||||||
const message_area = document.getElementById("message_area");
|
|
||||||
|
|
||||||
function callback(response)
|
const profile_ed_elements = [
|
||||||
{
|
{
|
||||||
if (! response.meta.json_ok)
|
"id": "display_name",
|
||||||
{
|
"element": document.getElementById("display_name"),
|
||||||
alert(JSON.stringify(response));
|
"placeholder": "Display name",
|
||||||
return;
|
"empty_text": USERNAME,
|
||||||
}
|
"autofocus": true,
|
||||||
if ("error_type" in response.data)
|
},
|
||||||
{
|
];
|
||||||
common.create_message_bubble(message_area, "message_negative", response.data.error_message);
|
const profile_ed = new editor.Editor(
|
||||||
return;
|
profile_ed_elements,
|
||||||
}
|
profile_ed_on_open,
|
||||||
common.create_message_bubble(message_area, "message_positive", "Password has been changed.");
|
profile_ed_on_save,
|
||||||
current_password.value = "";
|
profile_ed_on_cancel,
|
||||||
password_1.value = "";
|
);
|
||||||
password_2.value = "";
|
|
||||||
}
|
|
||||||
api.users.set_password(
|
|
||||||
USERNAME,
|
|
||||||
current_password.value,
|
|
||||||
password_1.value,
|
|
||||||
password_2.value,
|
|
||||||
callback,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1059,23 +1059,6 @@ def upgrade_24_to_25(photodb):
|
||||||
'''
|
'''
|
||||||
m.go()
|
m.go()
|
||||||
|
|
||||||
def upgrade_25_to_26(photodb):
|
|
||||||
'''
|
|
||||||
In this version, the `user_permissions` table was added.
|
|
||||||
'''
|
|
||||||
photodb.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS user_permissions(
|
|
||||||
userid TEXT NOT NULL,
|
|
||||||
permission TEXT NOT NULL,
|
|
||||||
created INT,
|
|
||||||
PRIMARY KEY(userid, permission),
|
|
||||||
FOREIGN KEY(userid) REFERENCES users(id)
|
|
||||||
);
|
|
||||||
''')
|
|
||||||
photodb.execute('''
|
|
||||||
CREATE INDEX IF NOT EXISTS index_user_permissions_userid on user_permissions(userid);
|
|
||||||
''')
|
|
||||||
|
|
||||||
def upgrade_all(data_directory):
|
def upgrade_all(data_directory):
|
||||||
'''
|
'''
|
||||||
Given the directory containing a phototagger database, apply all of the
|
Given the directory containing a phototagger database, apply all of the
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue