Add more detailed permission system, photo uploads by users.
This commit is contained in:
parent
8656b02403
commit
ebe5847afc
32 changed files with 1219 additions and 240 deletions
|
|
@ -41,7 +41,7 @@ ffmpeg = _load_ffmpeg()
|
||||||
|
|
||||||
# Database #########################################################################################
|
# Database #########################################################################################
|
||||||
|
|
||||||
DATABASE_VERSION = 25
|
DATABASE_VERSION = 26
|
||||||
|
|
||||||
DB_INIT = '''
|
DB_INIT = '''
|
||||||
CREATE TABLE IF NOT EXISTS albums(
|
CREATE TABLE IF NOT EXISTS albums(
|
||||||
|
|
@ -204,6 +204,15 @@ 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)
|
||||||
|
|
@ -309,6 +318,7 @@ 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': {
|
||||||
|
|
@ -322,25 +332,30 @@ 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,
|
||||||
},
|
},
|
||||||
|
|
@ -383,3 +398,74 @@ 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,12 +157,14 @@ 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 Unauthorized(EtiquetteException):
|
|
||||||
error_message = 'You\'re not allowed to do that.'
|
|
||||||
|
|
||||||
class WrongLogin(EtiquetteException):
|
class WrongLogin(EtiquetteException):
|
||||||
error_message = 'Wrong username-password combination.'
|
error_message = 'Wrong username-password combination.'
|
||||||
|
|
||||||
|
# PERMISSION ERRORS ################################################################################
|
||||||
|
|
||||||
|
class Unauthorized(EtiquetteException):
|
||||||
|
error_message = 'You\'re not allowed to do that.'
|
||||||
|
|
||||||
# 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
|
duration = probe.video.duration or probe.format.duration
|
||||||
|
|
||||||
if 'timestamp' in special:
|
if 'timestamp' in special:
|
||||||
timestamp_choices = [special['timestamp']]
|
timestamp_choices = [float(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=%d', this_time)
|
log.debug('Attempting video thumbnail at t=%s', 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)
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,10 @@ 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)
|
||||||
|
|
@ -468,7 +472,7 @@ class Album(ObjectBase, GroupableMixin):
|
||||||
|
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
@decorators.required_feature('album.edit')
|
@decorators.required_feature('album.delete')
|
||||||
@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)
|
||||||
|
|
@ -775,6 +779,10 @@ 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)
|
||||||
|
|
@ -854,7 +862,7 @@ class Bookmark(ObjectBase):
|
||||||
|
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
@decorators.required_feature('bookmark.edit')
|
@decorators.required_feature('bookmark.delete')
|
||||||
@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})
|
||||||
|
|
@ -915,6 +923,10 @@ 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)
|
||||||
|
|
@ -1002,7 +1014,12 @@ 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):
|
||||||
tag = self.photodb.get_tag(name=tag)
|
if isinstance(tag, PhotoTagRel):
|
||||||
|
tag = tag.tag
|
||||||
|
elif isinstance(tag, Tag):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
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)
|
||||||
if existing:
|
if existing:
|
||||||
|
|
@ -1075,9 +1092,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)
|
self.add_tag(tag, timestamp=tag.timestamp)
|
||||||
|
|
||||||
@decorators.required_feature('photo.edit')
|
@decorators.required_feature('photo.delete')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def delete(self, *, delete_file=False) -> None:
|
def delete(self, *, delete_file=False) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -1113,11 +1130,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:
|
||||||
For images, you can provide `max_width` and/or `max_height` to
|
You can provide `max_width` and/or `max_height` to override the
|
||||||
override the config file.
|
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
|
||||||
|
|
@ -1140,9 +1157,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=self.photodb.config['thumbnail_width'],
|
width=special.get('max_width', self.photodb.config['thumbnail_width']),
|
||||||
height=self.photodb.config['thumbnail_height'],
|
height=special.get('max_height', self.photodb.config['thumbnail_height']),
|
||||||
**special
|
special=special,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning(traceback.format_exc())
|
log.warning(traceback.format_exc())
|
||||||
|
|
@ -1375,6 +1392,7 @@ 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
|
||||||
|
|
@ -2098,6 +2116,10 @@ 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)
|
||||||
|
|
@ -2291,7 +2313,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.edit')
|
@decorators.required_feature('tag.delete')
|
||||||
@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)
|
||||||
|
|
@ -2500,6 +2522,15 @@ 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):
|
||||||
|
|
@ -2510,7 +2541,7 @@ class User(ObjectBase):
|
||||||
raise exceptions.WrongLogin()
|
raise exceptions.WrongLogin()
|
||||||
return success
|
return success
|
||||||
|
|
||||||
@decorators.required_feature('user.edit')
|
@decorators.required_feature('user.delete')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def delete(self, *, disown_authored_things) -> None:
|
def delete(self, *, disown_authored_things) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -2574,6 +2605,13 @@ 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.
|
||||||
|
|
@ -2618,6 +2656,41 @@ 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',
|
||||||
|
|
@ -2631,6 +2704,14 @@ 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,6 +315,7 @@ 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,
|
||||||
|
|
@ -356,7 +357,7 @@ class PDBPhotoMixin:
|
||||||
data = {
|
data = {
|
||||||
'id': photo_id,
|
'id': photo_id,
|
||||||
'filepath': filepath.absolute_path,
|
'filepath': filepath.absolute_path,
|
||||||
'override_filename': None,
|
'override_filename': override_filename,
|
||||||
'created': timetools.now().timestamp(),
|
'created': timetools.now().timestamp(),
|
||||||
'tagged_at': None,
|
'tagged_at': None,
|
||||||
'author_id': author_id,
|
'author_id': author_id,
|
||||||
|
|
@ -682,7 +683,14 @@ class PDBUserMixin:
|
||||||
|
|
||||||
@decorators.required_feature('user.new')
|
@decorators.required_feature('user.new')
|
||||||
@worms.atomic
|
@worms.atomic
|
||||||
def new_user(self, username, password, *, display_name=None) -> objects.User:
|
def new_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)
|
||||||
|
|
@ -703,15 +711,24 @@ 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': timetools.now().timestamp(),
|
'created': now,
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
|
|
@ -1138,6 +1155,9 @@ 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()
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ log = vlogging.getLogger(__name__)
|
||||||
# Constants ########################################################################################
|
# Constants ########################################################################################
|
||||||
|
|
||||||
DEFAULT_SERVER_CONFIG = {
|
DEFAULT_SERVER_CONFIG = {
|
||||||
'anonymous_read': True,
|
'anonymous_read': False,
|
||||||
'anonymous_write': True,
|
'registration_enabled': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
BROWSER_CACHE_DURATION = 180
|
BROWSER_CACHE_DURATION = 180
|
||||||
|
|
@ -84,7 +84,9 @@ 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.NoSuch):
|
if isinstance(exc, etiquette.exceptions.Unauthorized):
|
||||||
|
status = 403
|
||||||
|
elif isinstance(exc, etiquette.exceptions.NoSuch):
|
||||||
status = 404
|
status = 404
|
||||||
else:
|
else:
|
||||||
status = 400
|
status = 400
|
||||||
|
|
@ -98,7 +100,9 @@ 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)
|
||||||
|
|
||||||
|
|
@ -145,7 +149,9 @@ 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.NoSuch):
|
if isinstance(exc, etiquette.exceptions.Unauthorized):
|
||||||
|
status = 403
|
||||||
|
elif isinstance(exc, etiquette.exceptions.NoSuch):
|
||||||
status = 404
|
status = 404
|
||||||
else:
|
else:
|
||||||
status = 400
|
status = 400
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
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
|
||||||
|
|
@ -15,7 +16,7 @@ session_manager = common.session_manager
|
||||||
|
|
||||||
@site.route('/admin')
|
@site.route('/admin')
|
||||||
def get_admin():
|
def get_admin():
|
||||||
common.permission_manager.admin()
|
common.permission_manager.admin_only()
|
||||||
|
|
||||||
counts = dotdict.DotDict({
|
counts = dotdict.DotDict({
|
||||||
'albums': common.P.get_album_count(),
|
'albums': common.P.get_album_count(),
|
||||||
|
|
@ -31,11 +32,20 @@ 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(request, 'admin.html', cached=cached, counts=counts)
|
return common.render_template(
|
||||||
|
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()
|
common.permission_manager.admin_only()
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
binary = common.P.database_filepath.read('rb')
|
binary = common.P.database_filepath.read('rb')
|
||||||
|
|
@ -50,14 +60,24 @@ 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()
|
common.permission_manager.admin_only()
|
||||||
|
|
||||||
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()
|
common.permission_manager.admin_only()
|
||||||
|
|
||||||
common.P.load_config()
|
common.P.load_config()
|
||||||
common.load_config()
|
common.load_config()
|
||||||
|
|
@ -66,7 +86,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()
|
common.permission_manager.admin_only()
|
||||||
|
|
||||||
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.basic()
|
common.permission_manager.read()
|
||||||
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.basic()
|
common.permission_manager.read()
|
||||||
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.basic()
|
common.permission_manager.read()
|
||||||
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)
|
||||||
|
|
@ -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):
|
||||||
common.permission_manager.basic()
|
album = common.P_album(album_id, response_type='json')
|
||||||
|
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):
|
||||||
common.permission_manager.basic()
|
album = common.P_album(album_id, response_type='json')
|
||||||
|
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,17 +86,18 @@ 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):
|
||||||
common.permission_manager.basic()
|
album = common.P_album(album_id, response_type='json')
|
||||||
|
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):
|
||||||
common.permission_manager.basic()
|
album = common.P_album(album_id, response_type='json')
|
||||||
|
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
|
||||||
|
|
@ -107,9 +108,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):
|
||||||
common.permission_manager.basic()
|
album = common.P_album(album_id, response_type='json')
|
||||||
|
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())
|
||||||
|
|
@ -122,10 +123,10 @@ def post_album_add_photo(album_id):
|
||||||
'''
|
'''
|
||||||
Add a photo or photos to this album.
|
Add a photo or photos to this album.
|
||||||
'''
|
'''
|
||||||
common.permission_manager.basic()
|
album = common.P_album(album_id, response_type='json')
|
||||||
|
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()
|
||||||
|
|
@ -137,10 +138,10 @@ def post_album_remove_photo(album_id):
|
||||||
'''
|
'''
|
||||||
Remove a photo or photos from this album.
|
Remove a photo or photos from this album.
|
||||||
'''
|
'''
|
||||||
common.permission_manager.basic()
|
album = common.P_album(album_id, response_type='json')
|
||||||
|
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()
|
||||||
|
|
@ -153,7 +154,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.basic()
|
common.permission_manager.admin_only()
|
||||||
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')
|
||||||
|
|
@ -178,12 +179,12 @@ def post_album_edit(album_id):
|
||||||
'''
|
'''
|
||||||
Edit the title / description.
|
Edit the title / description.
|
||||||
'''
|
'''
|
||||||
common.permission_manager.basic()
|
album = common.P_album(album_id, response_type='json')
|
||||||
|
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(
|
||||||
|
|
@ -195,9 +196,7 @@ 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.basic()
|
common.permission_manager.localhost_only()
|
||||||
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()
|
||||||
|
|
@ -215,7 +214,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.basic_decorator
|
@common.permission_manager.read_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()}
|
||||||
|
|
@ -224,7 +223,7 @@ def get_all_album_names():
|
||||||
|
|
||||||
@site.route('/albums')
|
@site.route('/albums')
|
||||||
def get_albums_html():
|
def get_albums_html():
|
||||||
common.permission_manager.basic()
|
common.permission_manager.read()
|
||||||
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(
|
||||||
|
|
@ -237,7 +236,7 @@ def get_albums_html():
|
||||||
|
|
||||||
@site.route('/albums.json')
|
@site.route('/albums.json')
|
||||||
def get_albums_json():
|
def get_albums_json():
|
||||||
common.permission_manager.basic()
|
common.permission_manager.read()
|
||||||
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]
|
||||||
|
|
@ -247,7 +246,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.basic()
|
common.permission_manager.permission_string(etiquette.constants.PERMISSION_ALBUM_CREATE)
|
||||||
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)
|
||||||
|
|
@ -270,8 +269,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):
|
||||||
common.permission_manager.basic()
|
album = common.P_album(album_id, response_type='json')
|
||||||
|
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,7 +12,12 @@ 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(request, 'root.html', motd=motd)
|
return common.render_template(
|
||||||
|
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.basic()
|
common.permission_manager.read()
|
||||||
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):
|
||||||
common.permission_manager.basic()
|
bookmark = common.P_bookmark(bookmark_id, response_type='json')
|
||||||
|
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.basic()
|
common.permission_manager.read()
|
||||||
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.basic()
|
common.permission_manager.read()
|
||||||
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.basic()
|
common.permission_manager.read()
|
||||||
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.basic()
|
common.permission_manager.permission_string(etiquette.constants.PERMISSION_BOOKMARK_CREATE)
|
||||||
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):
|
||||||
common.permission_manager.basic()
|
bookmark = common.P_bookmark(bookmark_id, response_type='json')
|
||||||
|
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,5 +1,8 @@
|
||||||
|
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
|
||||||
|
|
@ -8,6 +11,7 @@ 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__)
|
||||||
|
|
@ -17,6 +21,8 @@ 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)
|
||||||
|
|
@ -25,13 +31,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.basic()
|
common.permission_manager.read()
|
||||||
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.basic()
|
common.permission_manager.read()
|
||||||
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)
|
||||||
|
|
@ -40,7 +46,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.basic()
|
common.permission_manager.read()
|
||||||
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)
|
||||||
|
|
||||||
|
|
@ -66,7 +72,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.basic_decorator
|
@common.permission_manager.read_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]
|
||||||
|
|
@ -89,11 +95,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):
|
||||||
common.permission_manager.basic()
|
photo = common.P_photo(photo_id, response_type='json')
|
||||||
|
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({})
|
||||||
|
|
||||||
|
|
@ -106,6 +112,9 @@ 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:
|
||||||
|
|
@ -123,8 +132,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:
|
||||||
|
|
@ -138,9 +147,9 @@ def post_photo_copy_tags(photo_id):
|
||||||
'''
|
'''
|
||||||
Copy the tags from another photo.
|
Copy the tags from another photo.
|
||||||
'''
|
'''
|
||||||
common.permission_manager.basic()
|
photo = common.P_photo(photo_id, response_type='json')
|
||||||
|
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()])
|
||||||
|
|
@ -151,7 +160,7 @@ def post_photo_remove_tag(photo_id):
|
||||||
'''
|
'''
|
||||||
Remove a tag from this photo.
|
Remove a tag from this photo.
|
||||||
'''
|
'''
|
||||||
common.permission_manager.basic()
|
common.permission_manager.edit_thing(photo)
|
||||||
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'],
|
||||||
|
|
@ -164,16 +173,15 @@ def post_photo_tag_rel_delete(photo_tag_rel_id):
|
||||||
'''
|
'''
|
||||||
Remove a tag from a photo.
|
Remove a tag from a photo.
|
||||||
'''
|
'''
|
||||||
common.permission_manager.basic()
|
photo_tag = common.P.get_object_by_id(etiquette.objects.PhotoTagRel, photo_tag_rel_id)
|
||||||
|
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'],
|
||||||
|
|
@ -184,7 +192,6 @@ 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'],
|
||||||
|
|
@ -198,14 +205,15 @@ 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)
|
||||||
|
|
||||||
with common.P.transaction:
|
photos = list(common.P_photos(photo_ids, response_type='json'))
|
||||||
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:
|
for photo in photos:
|
||||||
photo._uncache()
|
photo._uncache()
|
||||||
photo = common.P_photo(photo.id, response_type='json')
|
|
||||||
try:
|
try:
|
||||||
photo.generate_thumbnail()
|
photo.generate_thumbnail(special=special)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning(traceback.format_exc())
|
log.warning(traceback.format_exc())
|
||||||
|
|
||||||
|
|
@ -213,14 +221,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.basic()
|
common.permission_manager.early_read()
|
||||||
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.basic()
|
common.permission_manager.early_read()
|
||||||
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
|
||||||
|
|
@ -229,14 +237,18 @@ 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)
|
||||||
|
|
||||||
|
|
@ -250,50 +262,36 @@ 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.basic()
|
common.permission_manager.early_read()
|
||||||
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.basic()
|
common.permission_manager.early_read()
|
||||||
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):
|
||||||
common.permission_manager.basic()
|
photo = common.P_photo(photo_id, response_type='json')
|
||||||
|
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):
|
||||||
common.permission_manager.basic()
|
photo = common.P_photo(photo_id, response_type='json')
|
||||||
|
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.basic()
|
common.permission_manager.localhost_only()
|
||||||
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':
|
||||||
|
|
@ -307,10 +305,25 @@ 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.basic()
|
common.permission_manager.early_read()
|
||||||
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
|
||||||
|
|
@ -318,16 +331,46 @@ 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.basic()
|
common.permission_manager.early_read()
|
||||||
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.basic()
|
common.permission_manager.read()
|
||||||
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'])
|
||||||
|
|
@ -336,7 +379,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.basic()
|
common.permission_manager.read()
|
||||||
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)
|
||||||
|
|
@ -349,7 +392,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.basic()
|
common.permission_manager.read()
|
||||||
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)
|
||||||
|
|
@ -381,7 +424,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.basic()
|
common.permission_manager.read()
|
||||||
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]
|
||||||
|
|
@ -411,7 +454,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.basic()
|
common.permission_manager.read()
|
||||||
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)
|
||||||
|
|
||||||
|
|
@ -486,7 +529,7 @@ def get_search_core():
|
||||||
|
|
||||||
@site.route('/search_embed')
|
@site.route('/search_embed')
|
||||||
def get_search_embed():
|
def get_search_embed():
|
||||||
common.permission_manager.basic()
|
common.permission_manager.read()
|
||||||
search = get_search_core()
|
search = get_search_core()
|
||||||
response = common.render_template(
|
response = common.render_template(
|
||||||
request,
|
request,
|
||||||
|
|
@ -498,7 +541,7 @@ def get_search_embed():
|
||||||
|
|
||||||
@site.route('/search')
|
@site.route('/search')
|
||||||
def get_search_html():
|
def get_search_html():
|
||||||
common.permission_manager.basic()
|
common.permission_manager.read()
|
||||||
|
|
||||||
search = get_search_core()
|
search = get_search_core()
|
||||||
search.kwargs.view = request.args.get('view', 'grid')
|
search.kwargs.view = request.args.get('view', 'grid')
|
||||||
|
|
@ -549,7 +592,7 @@ def get_search_html():
|
||||||
|
|
||||||
@site.route('/search.atom')
|
@site.route('/search.atom')
|
||||||
def get_search_atom():
|
def get_search_atom():
|
||||||
common.permission_manager.basic()
|
common.permission_manager.read()
|
||||||
search = get_search_core()
|
search = get_search_core()
|
||||||
soup = etiquette.helpers.make_atom_feed(
|
soup = etiquette.helpers.make_atom_feed(
|
||||||
search.results,
|
search.results,
|
||||||
|
|
@ -562,7 +605,7 @@ def get_search_atom():
|
||||||
|
|
||||||
@site.route('/search.json')
|
@site.route('/search.json')
|
||||||
def get_search_json():
|
def get_search_json():
|
||||||
common.permission_manager.basic()
|
common.permission_manager.read()
|
||||||
search = get_search_core()
|
search = get_search_core()
|
||||||
response = search.jsonify()
|
response = search.jsonify()
|
||||||
return flasktools.json_response(response)
|
return flasktools.json_response(response)
|
||||||
|
|
@ -571,6 +614,92 @@ def get_search_json():
|
||||||
|
|
||||||
@site.route('/swipe')
|
@site.route('/swipe')
|
||||||
def get_swipe():
|
def get_swipe():
|
||||||
common.permission_manager.basic()
|
common.permission_manager.read()
|
||||||
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.basic()
|
common.permission_manager.read()
|
||||||
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.basic()
|
common.permission_manager.read()
|
||||||
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.basic()
|
common.permission_manager.read()
|
||||||
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):
|
||||||
common.permission_manager.basic()
|
tag = common.P_tag(tagname, response_type='json')
|
||||||
|
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,10 +63,13 @@ 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):
|
||||||
common.permission_manager.basic()
|
parent = common.P_tag(tagname, response_type='json')
|
||||||
|
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)
|
||||||
|
|
@ -74,11 +77,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):
|
||||||
common.permission_manager.basic()
|
master_tag = common.P_tag(tagname, response_type='json')
|
||||||
|
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}
|
||||||
|
|
@ -87,10 +90,13 @@ 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):
|
||||||
common.permission_manager.basic()
|
parent = common.P_tag(tagname, response_type='json')
|
||||||
|
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)
|
||||||
|
|
@ -98,11 +104,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):
|
||||||
common.permission_manager.basic()
|
master_tag = common.P_tag(tagname, response_type='json')
|
||||||
|
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}
|
||||||
|
|
@ -111,7 +117,7 @@ def post_tag_remove_synonym(tagname):
|
||||||
# Tag listings #####################################################################################
|
# Tag listings #####################################################################################
|
||||||
|
|
||||||
@site.route('/all_tags.json')
|
@site.route('/all_tags.json')
|
||||||
@common.permission_manager.basic_decorator
|
@common.permission_manager.read_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()))
|
||||||
|
|
@ -122,7 +128,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.basic()
|
common.permission_manager.read()
|
||||||
if specific_tag_name is None:
|
if specific_tag_name is None:
|
||||||
specific_tag = None
|
specific_tag = None
|
||||||
else:
|
else:
|
||||||
|
|
@ -161,7 +167,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.basic()
|
common.permission_manager.read()
|
||||||
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)
|
||||||
|
|
||||||
|
|
@ -175,7 +181,8 @@ 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.basic()
|
common.permission_manager.permission_string(etiquette.constants.PERMISSION_TAG_CREATE)
|
||||||
|
|
||||||
name = request.form['name']
|
name = request.form['name']
|
||||||
description = request.form.get('description', None)
|
description = request.form.get('description', None)
|
||||||
|
|
||||||
|
|
@ -187,7 +194,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.basic()
|
common.permission_manager.admin_only()
|
||||||
easybake_string = request.form['easybake_string']
|
easybake_string = request.form['easybake_string']
|
||||||
|
|
||||||
with common.P.transaction:
|
with common.P.transaction:
|
||||||
|
|
@ -197,9 +204,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):
|
||||||
common.permission_manager.basic()
|
tag = common.P_tag(tagname, response_type='json')
|
||||||
|
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,6 +1,7 @@
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -14,13 +15,19 @@ 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.basic()
|
common.permission_manager.read()
|
||||||
user = common.P_user(username, response_type='html')
|
user = common.P_user(username, response_type='html')
|
||||||
return common.render_template(request, 'user.html', user=user)
|
return common.render_template(
|
||||||
|
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.basic()
|
common.permission_manager.read()
|
||||||
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)
|
||||||
|
|
@ -28,7 +35,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.basic()
|
common.permission_manager.read()
|
||||||
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:
|
||||||
|
|
@ -40,12 +47,8 @@ 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')
|
||||||
if request.session.user != user:
|
common.permission_manager.logged_in(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:
|
||||||
|
|
@ -54,6 +57,54 @@ 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'])
|
||||||
|
|
@ -64,7 +115,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.P.config['enable_feature']['user']['new'],
|
registration_enabled=common.site.server_config['registration_enabled'],
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
@ -96,13 +147,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(request, user)
|
request.session = sessions.Session.from_request(session_manager=session_manager, request=request, user=user)
|
||||||
session_manager.add(request.session)
|
session_manager.save_state()
|
||||||
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.basic()
|
common.permission_manager.logged_in()
|
||||||
session_manager.remove(request)
|
session_manager.remove(request)
|
||||||
response = flasktools.json_response({})
|
response = flasktools.json_response({})
|
||||||
return response
|
return response
|
||||||
|
|
@ -138,6 +189,5 @@ 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(request, user)
|
request.session = sessions.Session.from_request(session_manager=session_manager, request=request, user=user)
|
||||||
session_manager.add(request.session)
|
|
||||||
return flasktools.json_response({})
|
return flasktools.json_response({})
|
||||||
|
|
|
||||||
|
|
@ -3,41 +3,124 @@ 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(self):
|
def admin_only(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:
|
||||||
return flask.abort(403)
|
raise etiquette.exceptions.Unauthorized()
|
||||||
|
|
||||||
def basic(self):
|
def logged_in(self, user=None):
|
||||||
if request.method not in {'GET', 'POST'}:
|
'''
|
||||||
return flask.abort(405)
|
Require that the visitor be logged in as any user, or as one
|
||||||
elif request.is_localhost:
|
specific user.
|
||||||
|
'''
|
||||||
|
if request.session and request.session.user and user is None:
|
||||||
request.checked_permissions = True
|
request.checked_permissions = True
|
||||||
return True
|
return True
|
||||||
elif request.method == 'GET' and self.site.server_config['anonymous_read'] or request.session.user:
|
if request.session and request.session.user and request.session.user == 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:
|
||||||
return flask.abort(403)
|
raise etiquette.exceptions.Unauthorized()
|
||||||
|
|
||||||
def basic_decorator(self, endpoint):
|
def early_read(self):
|
||||||
|
'''
|
||||||
|
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.basic()
|
self.read()
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,8 @@ 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: 8pt;
|
font-size: 7pt;
|
||||||
|
|
||||||
min-width: 18px;
|
min-width: 18px;
|
||||||
min-height: 14px;
|
min-height: 14px;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,16 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
@ -240,6 +250,26 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
@ -577,3 +607,29 @@ 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,17 +27,15 @@ th, td
|
||||||
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<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>
|
||||||
|
|
@ -47,6 +45,60 @@ 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>
|
||||||
|
|
||||||
|
|
@ -63,6 +115,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +135,22 @@ 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(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<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(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<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(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<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(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<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,6 +133,13 @@
|
||||||
<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">
|
||||||
|
|
@ -447,6 +454,54 @@ 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,11 +1,13 @@
|
||||||
{% macro make_header(session) %}
|
{% macro make_header(request) %}
|
||||||
<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 session.user %}
|
{% if request.session.user %}
|
||||||
<a class="header_element navigation_link dynamic_user_display_name" href="/userid/{{session.user.id}}">{{session.user.display_name}}</a>
|
<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>
|
||||||
<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(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<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,18 +79,16 @@ 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>
|
||||||
{% else %}
|
|
||||||
<span>Registrations are disabled.</span>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
<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(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<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 %}
|
{% if photo.width and photo.height and photo.area %}
|
||||||
<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 %}
|
||||||
|
|
@ -273,6 +273,7 @@
|
||||||
<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"
|
||||||
|
|
@ -283,24 +284,27 @@
|
||||||
>
|
>
|
||||||
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 delete_photo_form();"
|
data-onclick="return soft_delete_form();"
|
||||||
data-prompt="Delete photo, keep file?"
|
data-prompt="Delete photo, keep file?"
|
||||||
data-cancel-class="gray_button"
|
data-cancel-class="gray_button"
|
||||||
>
|
>
|
||||||
Remove
|
Soft delete
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="red_button button_with_confirm"
|
class="red_button button_with_confirm"
|
||||||
data-onclick="return delete_photo_from_disk_form();"
|
data-onclick="return hard_delete_form();"
|
||||||
data-prompt="Delete file on disk?"
|
data-prompt="Delete file on disk?"
|
||||||
data-cancel-class="gray_button"
|
data-cancel-class="gray_button"
|
||||||
>
|
>
|
||||||
Delete
|
Hard delete
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -424,14 +428,32 @@ function add_remove_photo_tag_callback(response)
|
||||||
return abort;
|
return abort;
|
||||||
}
|
}
|
||||||
|
|
||||||
function delete_photo_form()
|
function soft_delete_form()
|
||||||
{
|
{
|
||||||
api.photos.delete(PHOTO_ID, false, common.go_to_root);
|
function callback(response)
|
||||||
|
{
|
||||||
|
if (response.meta.status !== 200)
|
||||||
|
{
|
||||||
|
alert(JSON.stringify(response));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
common.go_to_root();
|
||||||
|
}
|
||||||
|
api.photos.delete(PHOTO_ID, false, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function delete_photo_from_disk_form()
|
function hard_delete_form()
|
||||||
{
|
{
|
||||||
api.photos.delete(PHOTO_ID, true, common.go_to_root);
|
function callback(response)
|
||||||
|
{
|
||||||
|
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,16 +64,19 @@ 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">Tags</a>
|
||||||
<a class="nice_link navigation_link" href="/albums">Albums</a>
|
<a class="nice_link navigation_link" href="/albums">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 %}
|
{% if request.is_localhost or (request.session.user and request.session.user.has_permission('admin')) %}
|
||||||
<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">
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<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(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
{
|
{
|
||||||
font-family: initial;
|
font-family: initial;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin:0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
#tag_list
|
#tag_list
|
||||||
{
|
{
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<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">
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{header.make_header(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<div id="content_body">
|
<div id="content_body">
|
||||||
<p>test</p>
|
<p>test</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
217
frontends/etiquette_flask/templates/upload.html
Normal file
217
frontends/etiquette_flask/templates/upload.html
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
<!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(session=request.session)}}
|
{{header.make_header(request=request)}}
|
||||||
<div id="content_body">
|
<div id="content_body">
|
||||||
<div id="hierarchy_self" class="panel">
|
<div id="hierarchy_self" class="panel">
|
||||||
<h1 id="display_name">{{user.display_name}}</h1>
|
<h1 id="display_name" class="dynamic_user_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,10 +73,43 @@
|
||||||
</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}},
|
||||||
|
|
@ -101,6 +134,14 @@ 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)
|
||||||
|
|
@ -132,15 +173,12 @@ 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 %}
|
||||||
const USERNAME = "{{user.username}}";
|
function set_display_name_form(event)
|
||||||
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)
|
||||||
{
|
{
|
||||||
ed.hide_spinner();
|
console.log("HI");
|
||||||
|
|
||||||
if (! response.meta.json_ok)
|
if (! response.meta.json_ok)
|
||||||
{
|
{
|
||||||
alert(JSON.stringify(response));
|
alert(JSON.stringify(response));
|
||||||
|
|
@ -148,7 +186,7 @@ function profile_ed_on_save(ed)
|
||||||
}
|
}
|
||||||
if ("error_type" in response.data)
|
if ("error_type" in response.data)
|
||||||
{
|
{
|
||||||
ed.show_error(`${response.data.error_type} ${response.data.error_message}`);
|
common.create_message_button(message_area, "message_negative", response.data.error_message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,32 +194,44 @@ function profile_ed_on_save(ed)
|
||||||
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);
|
||||||
|
|
||||||
ed.elements["display_name"].edit.value = new_display_name;
|
input.value = new_display_name;
|
||||||
|
|
||||||
ed.save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ed.show_spinner();
|
api.users.edit(USERNAME, input.value, callback);
|
||||||
api.users.edit(USERNAME, ed.elements["display_name"].edit.value, callback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile_ed_on_cancel = undefined;
|
function set_password_form(event)
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
|
||||||
const profile_ed_elements = [
|
function callback(response)
|
||||||
{
|
{
|
||||||
"id": "display_name",
|
if (! response.meta.json_ok)
|
||||||
"element": document.getElementById("display_name"),
|
{
|
||||||
"placeholder": "Display name",
|
alert(JSON.stringify(response));
|
||||||
"empty_text": USERNAME,
|
return;
|
||||||
"autofocus": true,
|
}
|
||||||
},
|
if ("error_type" in response.data)
|
||||||
];
|
{
|
||||||
const profile_ed = new editor.Editor(
|
common.create_message_bubble(message_area, "message_negative", response.data.error_message);
|
||||||
profile_ed_elements,
|
return;
|
||||||
profile_ed_on_open,
|
}
|
||||||
profile_ed_on_save,
|
common.create_message_bubble(message_area, "message_positive", "Password has been changed.");
|
||||||
profile_ed_on_cancel,
|
current_password.value = "";
|
||||||
);
|
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,6 +1059,23 @@ 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