diff options
| author | Hunternif <hunternif@gmail.com> | 2021-08-04 01:56:00 +0100 |
|---|---|---|
| committer | Hunternif <hunternif@gmail.com> | 2021-08-04 01:56:00 +0100 |
| commit | ca861cdc44ca476cec2949f237bbc7503862ad58 (patch) | |
| tree | 520f918a9a77432bf283b46b4296812eb4332ffe /server | |
| parent | dd03540398fcab11bfc2c3cebed2fb7a78592c16 (diff) | |
| parent | 59452711668b9f7a7aeea3c57301b22882987c2b (diff) | |
Merge remote-tracking branch 'origin/master' into hunternif
# Conflicts:
# client/css/post-content-control.styl
# client/css/post-list-view.styl
# client/html/post_edit_sidebar.tpl
# client/js/controllers/post_list_controller.js
# client/js/controllers/post_main_controller.js
# client/js/controllers/post_upload_controller.js
# client/js/controllers/tag_controller.js
# client/js/controllers/user_list_controller.js
# client/js/controls/expander_control.js
# client/js/controls/post_content_control.js
# client/js/controls/post_edit_sidebar_control.js
# client/js/controls/post_readonly_sidebar_control.js
# client/js/controls/tag_input_control.js
# client/js/main.js
# client/js/models/abstract_list.js
# client/js/models/post.js
# client/js/models/post_list.js
# client/js/models/settings.js
# client/js/models/tag.js
# client/js/models/tag_list.js
# client/js/tags.js
# client/js/util/search.js
# client/js/util/touch.js
# client/js/util/uri.js
# client/js/util/views.js
# client/js/views/post_main_view.js
# client/js/views/post_upload_view.js
# client/js/views/posts_header_view.js
# client/js/views/posts_page_view.js
# client/js/views/settings_view.js
# client/js/views/tag_view.js
# client/package-lock.json
# client/package.json
# server/config.yaml.dist
# server/szurubooru/api/__init__.py
# server/szurubooru/api/post_api.py
# server/szurubooru/api/tag_api.py
# server/szurubooru/func/posts.py
# server/szurubooru/func/tags.py
# server/szurubooru/model/__init__.py
# server/szurubooru/model/post.py
# server/szurubooru/model/tag.py
# server/szurubooru/search/configs/__init__.py
# server/szurubooru/search/configs/post_search_config.py
# server/szurubooru/search/executor.py
# server/szurubooru/tests/api/test_post_retrieving.py
# server/szurubooru/tests/api/test_post_updating.py
# server/szurubooru/tests/api/test_tag_updating.py
# server/szurubooru/tests/conftest.py
# server/szurubooru/tests/func/test_posts.py
# server/szurubooru/tests/func/test_tags.py
# server/szurubooru/tests/search/configs/test_post_search_config.py
Diffstat (limited to 'server')
198 files changed, 12610 insertions, 7523 deletions
diff --git a/server/.dockerignore b/server/.dockerignore index a20ffa9..cdfeafc 100644 --- a/server/.dockerignore +++ b/server/.dockerignore @@ -1,8 +1,15 @@ -szurubooru/tests/* -setup.cfg -.pylintrc -mypi.ini +# Linter configs +pyproject.toml +.flake8 +# Python requirements files +requirements.txt +dev-requirements.txt + +# Docker build files Dockerfile .dockerignore -**/.gitignore +hooks/ + +# User configured config file +config.yaml diff --git a/server/.flake8 b/server/.flake8 new file mode 100644 index 0000000..023ae12 --- /dev/null +++ b/server/.flake8 @@ -0,0 +1,5 @@ +[flake8] +filename = szurubooru/ +exclude = __pycache__ +ignore = F401, W503, W504, E203, E231 +max-line-length = 79 diff --git a/server/.pylintrc b/server/.pylintrc deleted file mode 100644 index 846bac6..0000000 --- a/server/.pylintrc +++ /dev/null @@ -1,37 +0,0 @@ -[basic] -function-rgx=^_?[a-z_][a-z0-9_]{2,}$|^test_ -method-rgx=^[a-z_][a-z0-9_]{2,}$|^test_ -const-rgx=^[A-Z_]+$|^_[a-zA-Z_]*$ -good-names=ex,_,logger,i - -[variables] -dummy-variables-rgx=_|dummy - -[format] -max-line-length=79 - -[messages control] -reports=no -disable= - # we're not java - missing-docstring, - broad-except, - - # covered better by pycodestyle - bad-continuation, - - # we're adults - redefined-builtin, - duplicate-code, - too-many-return-statements, - too-many-arguments, - - # plain stupid - no-self-use, - too-few-public-methods - -[typecheck] -generated-members=add|add_all - -[similarities] -min-similarity-lines=5 diff --git a/server/Dockerfile b/server/Dockerfile index dd5bd5a..9a597a1 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,46 +1,90 @@ -FROM scratch as approot +ARG ALPINE_VERSION=3.12 + + +FROM alpine:$ALPINE_VERSION as prereqs WORKDIR /opt/app -COPY alembic.ini wait-for-es generate-thumb ./ -COPY szurubooru/ ./szurubooru/ -COPY config.yaml.dist ./ +RUN apk --no-cache add \ + python3 \ + ffmpeg \ + py3-pip \ + # from requirements.txt: + py3-yaml \ + py3-psycopg2 \ + py3-sqlalchemy \ + py3-certifi \ + py3-numpy \ + py3-pillow \ + py3-pynacl \ + py3-tz \ + py3-pyrfc3339 \ + && pip3 install --no-cache-dir --disable-pip-version-check \ + alembic \ + "coloredlogs==5.0" \ + youtube-dl \ + && apk --no-cache del py3-pip + +COPY ./ /opt/app/ +RUN rm -rf /opt/app/szurubooru/tests + + +FROM prereqs as testing +WORKDIR /opt/app + +RUN apk --no-cache add \ + py3-pip \ + py3-pytest \ + py3-pytest-cov \ + postgresql \ + && pip3 install --no-cache-dir --disable-pip-version-check \ + pytest-pgsql \ + freezegun \ + && apk --no-cache del py3-pip \ + && addgroup app \ + && adduser -SDH -h /opt/app -g '' -G app app \ + && chown app:app /opt/app + +COPY --chown=app:app ./szurubooru/tests /opt/app/szurubooru/tests/ + +ENV TEST_ENVIRONMENT="true" +USER app +ENTRYPOINT ["pytest", "--tb=short"] +CMD ["szurubooru/"] -FROM python:3.6-slim +FROM prereqs as release WORKDIR /opt/app ARG PUID=1000 ARG PGID=1000 -ARG PORT=6666 -RUN \ - # Set users - mkdir -p /opt/app /data && \ - groupadd -g ${PGID} app && \ - useradd -d /opt/app -M -c '' -g app -r -u ${PUID} app && \ - chown -R app:app /opt/app /data && \ - # Create init file - echo "#!/bin/sh" >> /init && \ - echo "set -e" >> /init && \ - echo "cd /opt/app" >> /init && \ - echo "./wait-for-es" >> /init && \ - echo "alembic upgrade head" >> /init && \ - echo "exec waitress-serve --port ${PORT} szurubooru.facade:app" \ - >> /init && \ - chmod a+x /init && \ - # Install ffmpeg - apt-get -yqq update && \ - apt-get -yq install --no-install-recommends ffmpeg && \ - rm -rf /var/lib/apt/lists/* && \ - # Install waitress - pip3 install --no-cache-dir waitress -COPY --chown=app:app requirements.txt ./requirements.txt -RUN pip3 install --no-cache-dir -r ./requirements.txt +RUN apk --no-cache add \ + dumb-init \ + py3-setuptools \ + py3-waitress \ + && mkdir -p /opt/app /data \ + && addgroup -g ${PGID} app \ + && adduser -SDH -h /opt/app -g '' -G app -u ${PUID} app \ + && chown -R app:app /opt/app /data -# done to minimize number of layers in final image -COPY --chown=app:app --from=approot / / +USER app +CMD ["/opt/app/docker-start.sh"] -VOLUME ["/data/"] +ARG PORT=6666 +ENV PORT=${PORT} EXPOSE ${PORT} -USER app -CMD ["/init"] + +VOLUME ["/data/"] + +ARG DOCKER_REPO +ARG BUILD_DATE +ARG SOURCE_COMMIT +LABEL \ + maintainer="" \ + org.opencontainers.image.title="${DOCKER_REPO}" \ + org.opencontainers.image.url="https://github.com/rr-/szurubooru" \ + org.opencontainers.image.documentation="https://github.com/rr-/szurubooru/blob/${SOURCE_COMMIT}/doc/INSTALL.md" \ + org.opencontainers.image.created="${BUILD_DATE}" \ + org.opencontainers.image.source="https://github.com/rr-/szurubooru" \ + org.opencontainers.image.revision="${SOURCE_COMMIT}" \ + org.opencontainers.image.licenses="GPL-3.0" diff --git a/server/config.yaml.dist b/server/config.yaml.dist index 7440777..3de5568 100644 --- a/server/config.yaml.dist +++ b/server/config.yaml.dist @@ -3,12 +3,10 @@ # shown in the website title and on the front page name: szurubooru -# user agent name used to download files from the web on behalf of the api users -user_agent: -# used to salt the users' password hashes +# full url to the homepage of this szurubooru site, with no trailing slash +domain: # example: http://example.com +# used to salt the users' password hashes and generate filenames for static content secret: change -# required for running the test suite -test_database: 'sqlite:///:memory:' # Delete thumbnails and source files on post delete # Original functionality is no, to mitigate the impacts of admins going @@ -21,18 +19,29 @@ thumbnails: post_width: 300 post_height: 300 +# settings used to download files from the web on behalf of the api users +user_agent: +max_dl_filesize: 25.0E+6 # maximum filesize limit in bytes + +# automatically convert animated GIF uploads to video formats convert: gif: to_webm: false to_mp4: false +# allow posts to be uploaded even if some image processing errors occur +allow_broken_uploads: false + # used to send password reset e-mails smtp: host: # example: localhost port: # example: 25 user: # example: bot pass: # example: groovy123 - # host can be left empty, in which case it is recommended to fill contactEmail. + from: # example: noreply@example.com + # if host is left empty the password reset feature will be disabled, + # in which case it is recommended to fill contactEmail so that users + # know who to contact when they want to reset their password contact_email: # example: bob@example.com. Meant for manual password reset procedures @@ -41,12 +50,21 @@ enable_safety: yes tag_name_regex: ^\S+$ tag_category_name_regex: ^[^\s%+#/]+$ +pool_name_regex: ^\S+$ +pool_category_name_regex: ^[^\s%+#/]+$ + # don't make these more restrictive unless you want to annoy people; if you do # customize them, make sure to update the instructions in the registration form # template as well. password_regex: '^.{5,}$' user_name_regex: '^[a-zA-Z0-9_-]{1,32}$' +# webhooks to call when events occur (such as post/tag/user/etc. changes) +# the listed urls will be called with a HTTP POST request with a payload +# containing a snapshot resource as JSON. See doc/API.md for details +webhooks: + # - https://api.example.com/webhooks/ + default_rank: regular privileges: @@ -112,6 +130,7 @@ privileges: 'tag_categories:create': moderator 'tag_categories:edit:name': moderator 'tag_categories:edit:color': moderator + 'tag_categories:edit:order': moderator 'tag_categories:list': anonymous 'tag_categories:view': anonymous 'tag_categories:delete': moderator @@ -123,6 +142,24 @@ privileges: 'metrics:list': regular 'metrics:delete': moderator + 'pools:create': regular + 'pools:edit:names': power + 'pools:edit:category': power + 'pools:edit:description': power + 'pools:edit:posts': power + 'pools:list': regular + 'pools:view': anonymous + 'pools:merge': moderator + 'pools:delete': moderator + + 'pool_categories:create': moderator + 'pool_categories:edit:name': moderator + 'pool_categories:edit:color': moderator + 'pool_categories:list': anonymous + 'pool_categories:view': anonymous + 'pool_categories:delete': moderator + 'pool_categories:set_default': moderator + 'comments:create': regular 'comments:delete:any': moderator 'comments:delete:own': regular @@ -135,6 +172,7 @@ privileges: 'snapshots:list': power 'uploads:create': regular + 'uploads:use_downloader': power ## ONLY SET THESE IF DEPLOYING OUTSIDE OF DOCKER #debug: 0 # generate server logs? @@ -144,7 +182,3 @@ privileges: ## usage: schema://user:password@host:port/database_name ## example: postgres://szuru:dog@localhost:5432/szuru_test #database: -#elasticsearch: # used for reverse image search -# host: localhost -# port: 9200 -# index: szurubooru diff --git a/server/dev-requirements.txt b/server/dev-requirements.txt index c9dc234..04788a3 100644 --- a/server/dev-requirements.txt +++ b/server/dev-requirements.txt @@ -1,4 +1,5 @@ pytest>=2.9.1 pytest-cov>=2.2.1 +pytest-pgsql>=1.1.1 freezegun>=0.3.6 pycodestyle>=2.0.0 diff --git a/server/docker-start.sh b/server/docker-start.sh new file mode 100755 index 0000000..34a0e49 --- /dev/null +++ b/server/docker-start.sh @@ -0,0 +1,8 @@ +#!/usr/bin/dumb-init /bin/sh +set -e +cd /opt/app + +alembic upgrade head + +echo "Starting szurubooru API on port ${PORT}" +exec waitress-serve-3 --port ${PORT} szurubooru.facade:app diff --git a/server/generate-thumb b/server/generate-thumb deleted file mode 100755 index 87d7e02..0000000 --- a/server/generate-thumb +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -''' -Generates thumbnails for posts from CLI. Useful for testing changes to -thumbnail generators, and for weird inputs. -''' - -import argparse -import os.path -import sys -from szurubooru.func import posts - -def main(): - parser = argparse.ArgumentParser('Starts szurubooru using waitress.') - parser.add_argument('post_id', metavar='POST', help='post to generate thumbnail for') - args = parser.parse_args() - - try: - post = posts.get_post_by_id(args.post_id) - posts.generate_post_thumbnail(post) - except posts.PostNotFoundError: - pass - except: - raise - -if __name__ == '__main__': - main() diff --git a/server/hooks/build b/server/hooks/build new file mode 100755 index 0000000..b5e914b --- /dev/null +++ b/server/hooks/build @@ -0,0 +1,7 @@ +#!/bin/sh + +docker build \ + --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ + --build-arg SOURCE_COMMIT \ + --build-arg DOCKER_REPO \ + -f $DOCKERFILE_PATH -t $IMAGE_NAME . diff --git a/server/hooks/post_push b/server/hooks/post_push new file mode 100755 index 0000000..1b1e0ad --- /dev/null +++ b/server/hooks/post_push @@ -0,0 +1,19 @@ +#!/bin/sh + +add_tag() { + echo "Also tagging image as ${DOCKER_REPO}:${1}" + docker tag $IMAGE_NAME $DOCKER_REPO:$1 + docker push $DOCKER_REPO:$1 +} + +CLOSEST_VER=$(git describe --tags --abbrev=0) +CLOSEST_MAJOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f1) +CLOSEST_MINOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f2) + +add_tag "${CLOSEST_MAJOR_VER}-edge" +add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}-edge" + +if git describe --exact-match --abbrev=0 2> /dev/null; then + add_tag "${CLOSEST_MAJOR_VER}" + add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}" +fi diff --git a/server/hooks/test b/server/hooks/test new file mode 100755 index 0000000..b325186 --- /dev/null +++ b/server/hooks/test @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +docker run --rm \ + -t $(docker build --target testing -q .) \ + --color=no szurubooru/ + +exit $? diff --git a/server/mypy.ini b/server/mypy.ini deleted file mode 100644 index a0300b7..0000000 --- a/server/mypy.ini +++ /dev/null @@ -1,14 +0,0 @@ -[mypy] -ignore_missing_imports = True -follow_imports = skip -disallow_untyped_calls = True -disallow_untyped_defs = True -check_untyped_defs = True -disallow_subclassing_any = False -warn_redundant_casts = True -warn_unused_ignores = True -strict_optional = True -strict_boolean = False - -[mypy-szurubooru.tests.*] -ignore_errors=True diff --git a/server/pyproject.toml b/server/pyproject.toml new file mode 100644 index 0000000..ccf47fc --- /dev/null +++ b/server/pyproject.toml @@ -0,0 +1,10 @@ +[tool.black] +line-length = 79 + +[tool.isort] +known_first_party = ["szurubooru"] +known_third_party = ["PIL", "alembic", "coloredlogs", "freezegun", "nacl", "numpy", "pyrfc3339", "pytest", "pytz", "sqlalchemy", "yaml", "youtube_dl"] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true diff --git a/server/requirements.txt b/server/requirements.txt index a142493..d80ec06 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -3,10 +3,10 @@ pyyaml>=3.11 psycopg2-binary>=2.6.1 SQLAlchemy>=1.0.12 coloredlogs==5.0 -elasticsearch>=5.0.0,<7.0.0 -elasticsearch-dsl>=5.0.0,<7.0.0 +certifi>=2017.11.5 numpy>=1.8.2 pillow>=4.3.0 -pynacl==1.2.1 +pynacl>=1.2.1 pytz>=2018.3 pyRFC3339>=1.0 +youtube-dl diff --git a/server/setup.cfg b/server/setup.cfg deleted file mode 100644 index 7e835b4..0000000 --- a/server/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[tool:pytest] -testpaths=szurubooru -addopts=--cov-report=term-missing --cov=szurubooru --tb=short diff --git a/server/szuru-admin b/server/szuru-admin new file mode 100755 index 0000000..004a751 --- /dev/null +++ b/server/szuru-admin @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 + +""" +Collection of CLI commands for an administrator to use +""" + +import logging +import os +import re +import time +from argparse import ArgumentParser +from getpass import getpass +from sys import stderr + +from szurubooru import config, db, errors, model +from szurubooru.func import files, images +from szurubooru.func import posts as postfuncs +from szurubooru.func import users as userfuncs + + +def reset_password(username: str) -> None: + user = userfuncs.get_user_by_name_or_email(username) + + new_password = getpass("Enter new password for '%s': " % user.name) + check_password = getpass("Re-enter password: ") + + if check_password != new_password: + raise errors.ValidationError("Passwords do not match") + + userfuncs.update_user_password(user, new_password) + db.get_session().commit() + print("Sucessfully changed password for '%s'" % user.name) + + +def check_audio() -> None: + post_list = ( + db.session.query(model.Post) + .filter(model.Post.type == model.Post.TYPE_VIDEO) + .order_by(model.Post.post_id) + .all() + ) + + for post in post_list: + print("Checking post %d ..." % post.post_id, end="\r", file=stderr) + content = files.get(postfuncs.get_post_content_path(post)) + + has_existing_flag = model.Post.FLAG_SOUND in post.flags + try: + has_sound_data = images.Image(content).check_for_sound() + except errors.ProcessingError: + print( + "Post %d caused an error when checking for sound" + % post.post_id + ) + + if has_sound_data and not has_existing_flag: + print("Post %d has sound data but is not flagged" % post.post_id) + if not has_sound_data and has_existing_flag: + print("Post %d has no sound data but is flagged" % post.post_id) + + +def reset_filenames() -> None: + regex = re.compile(r"(\d+)_[0-9a-f]{16}\.(\S+)") + + def convert_to_new_filename(old_name: str) -> str: + matches = regex.match(old_name) + if not matches: + return None + post_id = int(matches.group(1)) + post_ext = matches.group(2) + return "%d_%s.%s" % ( + post_id, + postfuncs.get_post_security_hash(post_id), + post_ext, + ) + + def rename_in_dir(dir: str) -> None: + for old_path in os.listdir(config.config["data_dir"] + dir): + new_path = convert_to_new_filename(old_path) + if not new_path: + continue + if old_path != new_path: + print("%s -> %s" % (dir + old_path, dir + new_path)) + os.rename( + config.config["data_dir"] + dir + old_path, + config.config["data_dir"] + dir + new_path, + ) + + rename_in_dir("posts/") + rename_in_dir("generated-thumbnails/") + rename_in_dir("posts/custom-thumbnails/") + + +def main() -> None: + parser_top = ArgumentParser( + description="Collection of CLI commands for an administrator to use", + epilog="Look at README.md for more info", + ) + parser = parser_top.add_mutually_exclusive_group(required=True) + parser.add_argument( + "--change-password", + metavar="<username>", + help="change the password of specified user", + ) + parser.add_argument( + "--check-all-audio", + action="store_true", + help="check the audio flags of all posts, " + "noting discrepancies, without modifying posts", + ) + parser.add_argument( + "--reset-filenames", + action="store_true", + help="reset and rename the content and thumbnail " + "filenames in case of a lost/changed secret key", + ) + command = parser_top.parse_args() + + try: + if command.change_password: + reset_password(command.change_password) + elif command.check_all_audio: + check_audio() + elif command.reset_filenames: + reset_filenames() + except errors.BaseError as e: + print(e, file=stderr) + + +if __name__ == "__main__": + main() diff --git a/server/szurubooru/api/__init__.py b/server/szurubooru/api/__init__.py index a888a19..99c9524 100644 --- a/server/szurubooru/api/__init__.py +++ b/server/szurubooru/api/__init__.py @@ -1,11 +1,13 @@ +import szurubooru.api.comment_api import szurubooru.api.info_api -import szurubooru.api.user_api -import szurubooru.api.user_token_api +import szurubooru.api.metric_api +import szurubooru.api.password_reset_api +import szurubooru.api.pool_api +import szurubooru.api.pool_category_api import szurubooru.api.post_api +import szurubooru.api.snapshot_api import szurubooru.api.tag_api import szurubooru.api.tag_category_api -import szurubooru.api.comment_api -import szurubooru.api.password_reset_api -import szurubooru.api.snapshot_api import szurubooru.api.upload_api -import szurubooru.api.metric_api +import szurubooru.api.user_api +import szurubooru.api.user_token_api diff --git a/server/szurubooru/api/comment_api.py b/server/szurubooru/api/comment_api.py index cc8350a..d60d23e 100644 --- a/server/szurubooru/api/comment_api.py +++ b/server/szurubooru/api/comment_api.py @@ -1,44 +1,52 @@ -from typing import Dict from datetime import datetime -from szurubooru import search, rest, model -from szurubooru.func import ( - auth, comments, posts, scores, versions, serialization) +from typing import Dict +from szurubooru import model, rest, search +from szurubooru.func import ( + auth, + comments, + posts, + scores, + serialization, + versions, +) _search_executor = search.Executor(search.configs.CommentSearchConfig()) def _get_comment(params: Dict[str, str]) -> model.Comment: try: - comment_id = int(params['comment_id']) + comment_id = int(params["comment_id"]) except TypeError: raise comments.InvalidCommentIdError( - 'Invalid comment ID: %r.' % params['comment_id']) + "Invalid comment ID: %r." % params["comment_id"] + ) return comments.get_comment_by_id(comment_id) -def _serialize( - ctx: rest.Context, comment: model.Comment) -> rest.Response: +def _serialize(ctx: rest.Context, comment: model.Comment) -> rest.Response: return comments.serialize_comment( - comment, - ctx.user, - options=serialization.get_serialization_options(ctx)) + comment, ctx.user, options=serialization.get_serialization_options(ctx) + ) -@rest.routes.get('/comments/?') +@rest.routes.get("/comments/?") def get_comments( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'comments:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "comments:list") return _search_executor.execute_and_serialize( - ctx, lambda comment: _serialize(ctx, comment)) + ctx, lambda comment: _serialize(ctx, comment) + ) -@rest.routes.post('/comments/?') +@rest.routes.post("/comments/?") def create_comment( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'comments:create') - text = ctx.get_param_as_string('text') - post_id = ctx.get_param_as_int('postId') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "comments:create") + text = ctx.get_param_as_string("text") + post_id = ctx.get_param_as_int("postId") post = posts.get_post_by_id(post_id) comment = comments.create_comment(ctx.user, post, text) ctx.session.add(comment) @@ -46,53 +54,55 @@ def create_comment( return _serialize(ctx, comment) -@rest.routes.get('/comment/(?P<comment_id>[^/]+)/?') +@rest.routes.get("/comment/(?P<comment_id>[^/]+)/?") def get_comment(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'comments:view') + auth.verify_privilege(ctx.user, "comments:view") comment = _get_comment(params) return _serialize(ctx, comment) -@rest.routes.put('/comment/(?P<comment_id>[^/]+)/?') +@rest.routes.put("/comment/(?P<comment_id>[^/]+)/?") def update_comment(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: comment = _get_comment(params) versions.verify_version(comment, ctx) versions.bump_version(comment) - infix = 'own' if ctx.user.user_id == comment.user_id else 'any' - text = ctx.get_param_as_string('text') - auth.verify_privilege(ctx.user, 'comments:edit:%s' % infix) + infix = "own" if ctx.user.user_id == comment.user_id else "any" + text = ctx.get_param_as_string("text") + auth.verify_privilege(ctx.user, "comments:edit:%s" % infix) comments.update_comment_text(comment, text) comment.last_edit_time = datetime.utcnow() ctx.session.commit() return _serialize(ctx, comment) -@rest.routes.delete('/comment/(?P<comment_id>[^/]+)/?') +@rest.routes.delete("/comment/(?P<comment_id>[^/]+)/?") def delete_comment(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: comment = _get_comment(params) versions.verify_version(comment, ctx) - infix = 'own' if ctx.user.user_id == comment.user_id else 'any' - auth.verify_privilege(ctx.user, 'comments:delete:%s' % infix) + infix = "own" if ctx.user.user_id == comment.user_id else "any" + auth.verify_privilege(ctx.user, "comments:delete:%s" % infix) ctx.session.delete(comment) ctx.session.commit() return {} -@rest.routes.put('/comment/(?P<comment_id>[^/]+)/score/?') +@rest.routes.put("/comment/(?P<comment_id>[^/]+)/score/?") def set_comment_score( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'comments:score') - score = ctx.get_param_as_int('score') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "comments:score") + score = ctx.get_param_as_int("score") comment = _get_comment(params) scores.set_score(comment, ctx.user, score) ctx.session.commit() return _serialize(ctx, comment) -@rest.routes.delete('/comment/(?P<comment_id>[^/]+)/score/?') +@rest.routes.delete("/comment/(?P<comment_id>[^/]+)/score/?") def delete_comment_score( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'comments:score') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "comments:score") comment = _get_comment(params) scores.delete_score(comment, ctx.user) ctx.session.commit() diff --git a/server/szurubooru/api/info_api.py b/server/szurubooru/api/info_api.py index 19b2be7..757b09c 100644 --- a/server/szurubooru/api/info_api.py +++ b/server/szurubooru/api/info_api.py @@ -1,60 +1,66 @@ import os -from typing import Optional, Dict from datetime import datetime, timedelta +from typing import Dict, Optional + from szurubooru import config, rest from szurubooru.func import auth, posts, users, util - _cache_time = None # type: Optional[datetime] _cache_result = None # type: Optional[int] def _get_disk_usage() -> int: - global _cache_time, _cache_result # pylint: disable=global-statement + global _cache_time, _cache_result threshold = timedelta(hours=48) now = datetime.utcnow() if _cache_time and _cache_time > now - threshold: assert _cache_result is not None return _cache_result total_size = 0 - for dir_path, _, file_names in os.walk(config.config['data_dir']): + for dir_path, _, file_names in os.walk(config.config["data_dir"]): for file_name in file_names: file_path = os.path.join(dir_path, file_name) - total_size += os.path.getsize(file_path) + try: + total_size += os.path.getsize(file_path) + except FileNotFoundError: + pass _cache_time = now _cache_result = total_size return total_size -@rest.routes.get('/info/?') -def get_info( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: +@rest.routes.get("/info/?") +def get_info(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: post_feature = posts.try_get_current_post_feature() ret = { - 'postCount': posts.get_post_count(), - 'diskUsage': _get_disk_usage(), - 'serverTime': datetime.utcnow(), - 'config': { - 'name': config.config['name'], - 'userNameRegex': config.config['user_name_regex'], - 'passwordRegex': config.config['password_regex'], - 'tagNameRegex': config.config['tag_name_regex'], - 'tagCategoryNameRegex': config.config['tag_category_name_regex'], - 'defaultUserRank': config.config['default_rank'], - 'enableSafety': config.config['enable_safety'], - 'contactEmail': config.config['contact_email'], - 'canSendMails': bool(config.config['smtp']['host']), - 'privileges': - util.snake_case_to_lower_camel_case_keys( - config.config['privileges']), + "postCount": posts.get_post_count(), + "diskUsage": _get_disk_usage(), + "serverTime": datetime.utcnow(), + "config": { + "name": config.config["name"], + "userNameRegex": config.config["user_name_regex"], + "passwordRegex": config.config["password_regex"], + "tagNameRegex": config.config["tag_name_regex"], + "tagCategoryNameRegex": config.config["tag_category_name_regex"], + "defaultUserRank": config.config["default_rank"], + "enableSafety": config.config["enable_safety"], + "contactEmail": config.config["contact_email"], + "canSendMails": bool(config.config["smtp"]["host"]), + "privileges": util.snake_case_to_lower_camel_case_keys( + config.config["privileges"] + ), }, } - if auth.has_privilege(ctx.user, 'posts:view:featured'): - ret['featuredPost'] = ( + if auth.has_privilege(ctx.user, "posts:view:featured"): + ret["featuredPost"] = ( posts.serialize_post(post_feature.post, ctx.user) - if post_feature else None) - ret['featuringUser'] = ( + if post_feature + else None + ) + ret["featuringUser"] = ( users.serialize_user(post_feature.user, ctx.user) - if post_feature else None) - ret['featuringTime'] = post_feature.time if post_feature else None + if post_feature + else None + ) + ret["featuringTime"] = post_feature.time if post_feature else None return ret diff --git a/server/szurubooru/api/password_reset_api.py b/server/szurubooru/api/password_reset_api.py index 887d2f0..e0e31b7 100644 --- a/server/szurubooru/api/password_reset_api.py +++ b/server/szurubooru/api/password_reset_api.py @@ -1,56 +1,65 @@ +from hashlib import md5 from typing import Dict + from szurubooru import config, errors, rest from szurubooru.func import auth, mailer, users, versions -from hashlib import md5 - -MAIL_SUBJECT = 'Password reset for {name}' +MAIL_SUBJECT = "Password reset for {name}" MAIL_BODY = ( - 'You (or someone else) requested to reset your password on {name}.\n' - 'If you wish to proceed, click this link: {url}\n' - 'Otherwise, please ignore this email.') + "You (or someone else) requested to reset your password on {name}.\n" + "If you wish to proceed, click this link: {url}\n" + "Otherwise, please ignore this email." +) -@rest.routes.get('/password-reset/(?P<user_name>[^/]+)/?') +@rest.routes.get("/password-reset/(?P<user_name>[^/]+)/?") def start_password_reset( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user_name = params['user_name'] + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + user_name = params["user_name"] user = users.get_user_by_name_or_email(user_name) if not user.email: raise errors.ValidationError( - 'User %r hasn\'t supplied email. Cannot reset password.' % ( - user_name)) + "User %r hasn't supplied email. Cannot reset password." + % (user_name) + ) token = auth.generate_authentication_token(user) - if 'HTTP_ORIGIN' in ctx.env: - url = ctx.env['HTTP_ORIGIN'].rstrip('/') + if config.config["domain"]: + url = config.config["domain"] + elif "HTTP_ORIGIN" in ctx.env: + url = ctx.env["HTTP_ORIGIN"].rstrip("/") + elif "HTTP_REFERER" in ctx.env: + url = ctx.env["HTTP_REFERER"].rstrip("/") else: - url = '' - url += '/password-reset/%s:%s' % (user.name, token) + url = "" + url += "/password-reset/%s:%s" % (user.name, token) mailer.send_mail( - 'noreply@%s' % config.config['name'], + config.config["smtp"]["from"], user.email, - MAIL_SUBJECT.format(name=config.config['name']), - MAIL_BODY.format(name=config.config['name'], url=url)) + MAIL_SUBJECT.format(name=config.config["name"]), + MAIL_BODY.format(name=config.config["name"], url=url), + ) return {} def _hash(token: str) -> str: - return md5(token.encode('utf-8')).hexdigest() + return md5(token.encode("utf-8")).hexdigest() -@rest.routes.post('/password-reset/(?P<user_name>[^/]+)/?') +@rest.routes.post("/password-reset/(?P<user_name>[^/]+)/?") def finish_password_reset( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user_name = params['user_name'] + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + user_name = params["user_name"] user = users.get_user_by_name_or_email(user_name) good_token = auth.generate_authentication_token(user) - token = ctx.get_param_as_string('token') + token = ctx.get_param_as_string("token") if _hash(token) != _hash(good_token): - raise errors.ValidationError('Invalid password reset token.') + raise errors.ValidationError("Invalid password reset token.") new_password = users.reset_user_password(user) versions.bump_version(user) ctx.session.commit() - return {'password': new_password} + return {"password": new_password} diff --git a/server/szurubooru/api/pool_api.py b/server/szurubooru/api/pool_api.py new file mode 100644 index 0000000..a2fb716 --- /dev/null +++ b/server/szurubooru/api/pool_api.py @@ -0,0 +1,113 @@ +from datetime import datetime +from typing import Dict, List, Optional + +from szurubooru import db, model, rest, search +from szurubooru.func import auth, pools, serialization, snapshots, versions + +_search_executor = search.Executor(search.configs.PoolSearchConfig()) + + +def _serialize(ctx: rest.Context, pool: model.Pool) -> rest.Response: + return pools.serialize_pool( + pool, options=serialization.get_serialization_options(ctx) + ) + + +def _get_pool(params: Dict[str, str]) -> model.Pool: + return pools.get_pool_by_id(params["pool_id"]) + + +@rest.routes.get("/pools/?") +def get_pools( + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "pools:list") + return _search_executor.execute_and_serialize( + ctx, lambda pool: _serialize(ctx, pool) + ) + + +@rest.routes.post("/pool/?") +def create_pool( + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "pools:create") + + names = ctx.get_param_as_string_list("names") + category = ctx.get_param_as_string("category") + description = ctx.get_param_as_string("description", default="") + posts = ctx.get_param_as_int_list("posts", default=[]) + + pool = pools.create_pool(names, category, posts) + pool.last_edit_time = datetime.utcnow() + pools.update_pool_description(pool, description) + ctx.session.add(pool) + ctx.session.flush() + snapshots.create(pool, ctx.user) + ctx.session.commit() + return _serialize(ctx, pool) + + +@rest.routes.get("/pool/(?P<pool_id>[^/]+)/?") +def get_pool(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + auth.verify_privilege(ctx.user, "pools:view") + pool = _get_pool(params) + return _serialize(ctx, pool) + + +@rest.routes.put("/pool/(?P<pool_id>[^/]+)/?") +def update_pool(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + pool = _get_pool(params) + versions.verify_version(pool, ctx) + versions.bump_version(pool) + if ctx.has_param("names"): + auth.verify_privilege(ctx.user, "pools:edit:names") + pools.update_pool_names(pool, ctx.get_param_as_string_list("names")) + if ctx.has_param("category"): + auth.verify_privilege(ctx.user, "pools:edit:category") + pools.update_pool_category_name( + pool, ctx.get_param_as_string("category") + ) + if ctx.has_param("description"): + auth.verify_privilege(ctx.user, "pools:edit:description") + pools.update_pool_description( + pool, ctx.get_param_as_string("description") + ) + if ctx.has_param("posts"): + auth.verify_privilege(ctx.user, "pools:edit:posts") + posts = ctx.get_param_as_int_list("posts") + pools.update_pool_posts(pool, posts) + pool.last_edit_time = datetime.utcnow() + ctx.session.flush() + snapshots.modify(pool, ctx.user) + ctx.session.commit() + return _serialize(ctx, pool) + + +@rest.routes.delete("/pool/(?P<pool_id>[^/]+)/?") +def delete_pool(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + pool = _get_pool(params) + versions.verify_version(pool, ctx) + auth.verify_privilege(ctx.user, "pools:delete") + snapshots.delete(pool, ctx.user) + pools.delete(pool) + ctx.session.commit() + return {} + + +@rest.routes.post("/pool-merge/?") +def merge_pools( + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + source_pool_id = ctx.get_param_as_string("remove") + target_pool_id = ctx.get_param_as_string("mergeTo") + source_pool = pools.get_pool_by_id(source_pool_id) + target_pool = pools.get_pool_by_id(target_pool_id) + versions.verify_version(source_pool, ctx, "removeVersion") + versions.verify_version(target_pool, ctx, "mergeToVersion") + versions.bump_version(target_pool) + auth.verify_privilege(ctx.user, "pools:merge") + pools.merge_pools(source_pool, target_pool) + snapshots.merge(source_pool, target_pool, ctx.user) + ctx.session.commit() + return _serialize(ctx, target_pool) diff --git a/server/szurubooru/api/pool_category_api.py b/server/szurubooru/api/pool_category_api.py new file mode 100644 index 0000000..9af41d4 --- /dev/null +++ b/server/szurubooru/api/pool_category_api.py @@ -0,0 +1,109 @@ +from typing import Dict + +from szurubooru import model, rest +from szurubooru.func import ( + auth, + pool_categories, + pools, + serialization, + snapshots, + versions, +) + + +def _serialize( + ctx: rest.Context, category: model.PoolCategory +) -> rest.Response: + return pool_categories.serialize_category( + category, options=serialization.get_serialization_options(ctx) + ) + + +@rest.routes.get("/pool-categories/?") +def get_pool_categories( + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "pool_categories:list") + categories = pool_categories.get_all_categories() + return { + "results": [_serialize(ctx, category) for category in categories], + } + + +@rest.routes.post("/pool-categories/?") +def create_pool_category( + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "pool_categories:create") + name = ctx.get_param_as_string("name") + color = ctx.get_param_as_string("color") + category = pool_categories.create_category(name, color) + ctx.session.add(category) + ctx.session.flush() + snapshots.create(category, ctx.user) + ctx.session.commit() + return _serialize(ctx, category) + + +@rest.routes.get("/pool-category/(?P<category_name>[^/]+)/?") +def get_pool_category( + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "pool_categories:view") + category = pool_categories.get_category_by_name(params["category_name"]) + return _serialize(ctx, category) + + +@rest.routes.put("/pool-category/(?P<category_name>[^/]+)/?") +def update_pool_category( + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + category = pool_categories.get_category_by_name( + params["category_name"], lock=True + ) + versions.verify_version(category, ctx) + versions.bump_version(category) + if ctx.has_param("name"): + auth.verify_privilege(ctx.user, "pool_categories:edit:name") + pool_categories.update_category_name( + category, ctx.get_param_as_string("name") + ) + if ctx.has_param("color"): + auth.verify_privilege(ctx.user, "pool_categories:edit:color") + pool_categories.update_category_color( + category, ctx.get_param_as_string("color") + ) + ctx.session.flush() + snapshots.modify(category, ctx.user) + ctx.session.commit() + return _serialize(ctx, category) + + +@rest.routes.delete("/pool-category/(?P<category_name>[^/]+)/?") +def delete_pool_category( + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + category = pool_categories.get_category_by_name( + params["category_name"], lock=True + ) + versions.verify_version(category, ctx) + auth.verify_privilege(ctx.user, "pool_categories:delete") + pool_categories.delete_category(category) + snapshots.delete(category, ctx.user) + ctx.session.commit() + return {} + + +@rest.routes.put("/pool-category/(?P<category_name>[^/]+)/default/?") +def set_pool_category_as_default( + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "pool_categories:set_default") + category = pool_categories.get_category_by_name( + params["category_name"], lock=True + ) + pool_categories.set_default_category(category) + ctx.session.flush() + snapshots.modify(category, ctx.user) + ctx.session.commit() + return _serialize(ctx, category) diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py index ffbf34d..a83ccc0 100644 --- a/server/szurubooru/api/post_api.py +++ b/server/szurubooru/api/post_api.py @@ -1,10 +1,20 @@ from math import ceil -from typing import Optional, Dict, List from datetime import datetime -from szurubooru import db, model, errors, rest, search -from szurubooru.func import ( - auth, tags, posts, snapshots, favorites, scores, serialization, versions, metrics) +from typing import Dict, List, Optional +from szurubooru import db, errors, model, rest, search +from szurubooru.func import ( + auth, + favorites, + metrics, + mime, + posts, + scores, + serialization, + snapshots, + tags, + versions, +) _search_executor_config = search.configs.PostSearchConfig() _search_executor = search.Executor(_search_executor_config) @@ -12,10 +22,11 @@ _search_executor = search.Executor(_search_executor_config) def _get_post_id(params: Dict[str, str]) -> int: try: - return int(params['post_id']) + return int(params["post_id"]) except TypeError: raise posts.InvalidPostIdError( - 'Invalid post ID: %r.' % params['post_id']) + "Invalid post ID: %r." % params["post_id"] + ) def _get_post(params: Dict[str, str]) -> model.Post: @@ -23,52 +34,62 @@ def _get_post(params: Dict[str, str]) -> model.Post: def _serialize_post( - ctx: rest.Context, post: Optional[model.Post]) -> rest.Response: + ctx: rest.Context, post: Optional[model.Post] +) -> rest.Response: return posts.serialize_post( - post, - ctx.user, - options=serialization.get_serialization_options(ctx)) + post, ctx.user, options=serialization.get_serialization_options(ctx) + ) -@rest.routes.get('/posts/?') +@rest.routes.get("/posts/?") def get_posts( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:list") _search_executor_config.user = ctx.user return _search_executor.execute_and_serialize( - ctx, lambda post: _serialize_post(ctx, post)) + ctx, lambda post: _serialize_post(ctx, post) + ) -@rest.routes.post('/posts/?') +@rest.routes.post("/posts/?") def create_post( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - anonymous = ctx.get_param_as_bool('anonymous', default=False) + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + anonymous = ctx.get_param_as_bool("anonymous", default=False) if anonymous: - auth.verify_privilege(ctx.user, 'posts:create:anonymous') + auth.verify_privilege(ctx.user, "posts:create:anonymous") else: - auth.verify_privilege(ctx.user, 'posts:create:identified') - content = ctx.get_file('content') - tag_names = ctx.get_param_as_string_list('tags', default=[]) - safety = ctx.get_param_as_string('safety') - source = ctx.get_param_as_string('source', default='') - if ctx.has_param('contentUrl') and not source: - source = ctx.get_param_as_string('contentUrl', default='') - relations = ctx.get_param_as_int_list('relations', default=[]) - notes = ctx.get_param_as_list('notes', default=[]) - flags = ctx.get_param_as_string_list('flags', default=[]) + auth.verify_privilege(ctx.user, "posts:create:identified") + content = ctx.get_file( + "content", + use_video_downloader=auth.has_privilege( + ctx.user, "uploads:use_downloader" + ), + ) + tag_names = ctx.get_param_as_string_list("tags", default=[]) + safety = ctx.get_param_as_string("safety") + source = ctx.get_param_as_string("source", default="") + if ctx.has_param("contentUrl") and not source: + source = ctx.get_param_as_string("contentUrl", default="") + relations = ctx.get_param_as_int_list("relations", default=[]) + notes = ctx.get_param_as_list("notes", default=[]) + flags = ctx.get_param_as_string_list( + "flags", default=posts.get_default_flags(content) + ) post, new_tags = posts.create_post( - content, tag_names, None if anonymous else ctx.user) + content, tag_names, None if anonymous else ctx.user + ) if len(new_tags): - auth.verify_privilege(ctx.user, 'tags:create') + auth.verify_privilege(ctx.user, "tags:create") posts.update_post_safety(post, safety) posts.update_post_source(post, source) posts.update_post_relations(post, relations) posts.update_post_notes(post, notes) posts.update_post_flags(post, flags) - posts.test_sound(post, content) - if ctx.has_file('thumbnail'): - posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) + if ctx.has_file("thumbnail"): + posts.update_post_thumbnail(post, ctx.get_file("thumbnail")) ctx.session.add(post) ctx.session.flush() create_snapshots_for_post(post, new_tags, None if anonymous else ctx.user) @@ -77,74 +98,83 @@ def create_post( create_snapshots_for_post( alternate_post, alternate_post_new_tags, - None if anonymous else ctx.user) + None if anonymous else ctx.user, + ) ctx.session.commit() return _serialize_post(ctx, post) def create_snapshots_for_post( - post: model.Post, - new_tags: List[model.Tag], - user: Optional[model.User]): + post: model.Post, new_tags: List[model.Tag], user: Optional[model.User] +): snapshots.create(post, user) for tag in new_tags: snapshots.create(tag, user) -@rest.routes.get('/post/(?P<post_id>[^/]+)/?') +@rest.routes.get("/post/(?P<post_id>[^/]+)/?") def get_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:view') + auth.verify_privilege(ctx.user, "posts:view") post = _get_post(params) return _serialize_post(ctx, post) -@rest.routes.put('/post/(?P<post_id>[^/]+)/?') +@rest.routes.put("/post/(?P<post_id>[^/]+)/?") def update_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: post = _get_post(params) versions.verify_version(post, ctx) versions.bump_version(post) - if ctx.has_file('content'): - auth.verify_privilege(ctx.user, 'posts:edit:content') - posts.update_post_content(post, ctx.get_file('content')) - if ctx.has_param('tags'): - auth.verify_privilege(ctx.user, 'posts:edit:tags') + if ctx.has_file("content"): + auth.verify_privilege(ctx.user, "posts:edit:content") + posts.update_post_content( + post, + ctx.get_file( + "content", + use_video_downloader=auth.has_privilege( + ctx.user, "uploads:use_downloader" + ), + ), + ) + if ctx.has_param("tags"): + auth.verify_privilege(ctx.user, "posts:edit:tags") new_tags = posts.update_post_tags( - post, ctx.get_param_as_string_list('tags')) + post, ctx.get_param_as_string_list("tags") + ) if len(new_tags): - auth.verify_privilege(ctx.user, 'tags:create') + auth.verify_privilege(ctx.user, "tags:create") db.session.flush() for tag in new_tags: snapshots.create(tag, ctx.user) - if ctx.has_param('safety'): - auth.verify_privilege(ctx.user, 'posts:edit:safety') - posts.update_post_safety(post, ctx.get_param_as_string('safety')) - if ctx.has_param('source'): - auth.verify_privilege(ctx.user, 'posts:edit:source') - posts.update_post_source(post, ctx.get_param_as_string('source')) - elif ctx.has_param('contentUrl'): - posts.update_post_source(post, ctx.get_param_as_string('contentUrl')) - if ctx.has_param('relations'): - auth.verify_privilege(ctx.user, 'posts:edit:relations') + if ctx.has_param("safety"): + auth.verify_privilege(ctx.user, "posts:edit:safety") + posts.update_post_safety(post, ctx.get_param_as_string("safety")) + if ctx.has_param("source"): + auth.verify_privilege(ctx.user, "posts:edit:source") + posts.update_post_source(post, ctx.get_param_as_string("source")) + elif ctx.has_param("contentUrl"): + posts.update_post_source(post, ctx.get_param_as_string("contentUrl")) + if ctx.has_param("relations"): + auth.verify_privilege(ctx.user, "posts:edit:relations") posts.update_post_relations( - post, ctx.get_param_as_int_list('relations')) - if ctx.has_param('notes'): - auth.verify_privilege(ctx.user, 'posts:edit:notes') - posts.update_post_notes(post, ctx.get_param_as_list('notes')) - if ctx.has_param('flags'): - auth.verify_privilege(ctx.user, 'posts:edit:flags') - posts.update_post_flags(post, ctx.get_param_as_string_list('flags')) - if ctx.has_file('thumbnail'): - auth.verify_privilege(ctx.user, 'posts:edit:thumbnail') - posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) - if ctx.has_param('metrics'): - auth.verify_privilege(ctx.user, 'metrics:edit:posts') + post, ctx.get_param_as_int_list("relations") + ) + if ctx.has_param("notes"): + auth.verify_privilege(ctx.user, "posts:edit:notes") + posts.update_post_notes(post, ctx.get_param_as_list("notes")) + if ctx.has_param("flags"): + auth.verify_privilege(ctx.user, "posts:edit:flags") + posts.update_post_flags(post, ctx.get_param_as_string_list("flags")) + if ctx.has_file("thumbnail"): + auth.verify_privilege(ctx.user, "posts:edit:thumbnail") + posts.update_post_thumbnail(post, ctx.get_file("thumbnail")) + if ctx.has_param("metrics"): + auth.verify_privilege(ctx.user, "metrics:edit:posts") metrics.update_or_create_post_metrics( - post, ctx.get_param_as_list('metrics')) - if ctx.has_param('metricRanges'): - auth.verify_privilege(ctx.user, 'metrics:edit:posts') + post, ctx.get_param_as_list("metrics")) + if ctx.has_param("metricRanges"): + auth.verify_privilege(ctx.user, "metrics:edit:posts") metrics.update_or_create_post_metric_ranges( - post, ctx.get_param_as_list('metricRanges')) - + post, ctx.get_param_as_list("metricRanges")) post.last_edit_time = datetime.utcnow() ctx.session.flush() snapshots.modify(post, ctx.user) @@ -152,9 +182,9 @@ def update_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: return _serialize_post(ctx, post) -@rest.routes.delete('/post/(?P<post_id>[^/]+)/?') +@rest.routes.delete("/post/(?P<post_id>[^/]+)/?") def delete_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:delete') + auth.verify_privilege(ctx.user, "posts:delete") post = _get_post(params) versions.verify_version(post, ctx) snapshots.delete(post, ctx.user) @@ -163,103 +193,113 @@ def delete_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: return {} -@rest.routes.post('/post-merge/?') +@rest.routes.post("/post-merge/?") def merge_posts( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - source_post_id = ctx.get_param_as_int('remove') - target_post_id = ctx.get_param_as_int('mergeTo') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + source_post_id = ctx.get_param_as_int("remove") + target_post_id = ctx.get_param_as_int("mergeTo") source_post = posts.get_post_by_id(source_post_id) target_post = posts.get_post_by_id(target_post_id) - replace_content = ctx.get_param_as_bool('replaceContent') - versions.verify_version(source_post, ctx, 'removeVersion') - versions.verify_version(target_post, ctx, 'mergeToVersion') + replace_content = ctx.get_param_as_bool("replaceContent") + versions.verify_version(source_post, ctx, "removeVersion") + versions.verify_version(target_post, ctx, "mergeToVersion") versions.bump_version(target_post) - auth.verify_privilege(ctx.user, 'posts:merge') + auth.verify_privilege(ctx.user, "posts:merge") posts.merge_posts(source_post, target_post, replace_content) snapshots.merge(source_post, target_post, ctx.user) ctx.session.commit() return _serialize_post(ctx, target_post) -@rest.routes.get('/featured-post/?') +@rest.routes.get("/featured-post/?") def get_featured_post( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:view:featured') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:view:featured") post = posts.try_get_featured_post() return _serialize_post(ctx, post) -@rest.routes.post('/featured-post/?') +@rest.routes.post("/featured-post/?") def set_featured_post( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:feature') - post_id = ctx.get_param_as_int('id') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:feature") + post_id = ctx.get_param_as_int("id") post = posts.get_post_by_id(post_id) featured_post = posts.try_get_featured_post() if featured_post and featured_post.post_id == post.post_id: raise posts.PostAlreadyFeaturedError( - 'Post %r is already featured.' % post_id) + "Post %r is already featured." % post_id + ) posts.feature_post(post, ctx.user) snapshots.modify(post, ctx.user) ctx.session.commit() return _serialize_post(ctx, post) -@rest.routes.put('/post/(?P<post_id>[^/]+)/score/?') +@rest.routes.put("/post/(?P<post_id>[^/]+)/score/?") def set_post_score(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:score') + auth.verify_privilege(ctx.user, "posts:score") post = _get_post(params) - score = ctx.get_param_as_int('score') + score = ctx.get_param_as_int("score") scores.set_score(post, ctx.user, score) ctx.session.commit() return _serialize_post(ctx, post) -@rest.routes.delete('/post/(?P<post_id>[^/]+)/score/?') +@rest.routes.delete("/post/(?P<post_id>[^/]+)/score/?") def delete_post_score( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:score') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:score") post = _get_post(params) scores.delete_score(post, ctx.user) ctx.session.commit() return _serialize_post(ctx, post) -@rest.routes.post('/post/(?P<post_id>[^/]+)/favorite/?') +@rest.routes.post("/post/(?P<post_id>[^/]+)/favorite/?") def add_post_to_favorites( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:favorite') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:favorite") post = _get_post(params) favorites.set_favorite(post, ctx.user) ctx.session.commit() return _serialize_post(ctx, post) -@rest.routes.delete('/post/(?P<post_id>[^/]+)/favorite/?') +@rest.routes.delete("/post/(?P<post_id>[^/]+)/favorite/?") def delete_post_from_favorites( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:favorite') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:favorite") post = _get_post(params) favorites.unset_favorite(post, ctx.user) ctx.session.commit() return _serialize_post(ctx, post) -@rest.routes.get('/post/(?P<post_id>[^/]+)/around/?') +@rest.routes.get("/post/(?P<post_id>[^/]+)/around/?") def get_posts_around( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:list') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:list") _search_executor_config.user = ctx.user post_id = _get_post_id(params) return _search_executor.get_around_and_serialize( - ctx, post_id, lambda post: _serialize_post(ctx, post)) + ctx, post_id, lambda post: _serialize_post(ctx, post) + ) -@rest.routes.post('/posts/reverse-search/?') +@rest.routes.post("/posts/reverse-search/?") def get_posts_by_image( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:reverse_search') - content = ctx.get_file('content') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "posts:reverse_search") + content = ctx.get_file("content") try: lookalikes = posts.search_by_image(content) @@ -267,32 +307,32 @@ def get_posts_by_image( lookalikes = [] return { - 'exactPost': - _serialize_post(ctx, posts.search_by_image_exact(content)), - 'similarPosts': - [ - { - 'distance': lookalike.distance, - 'post': _serialize_post(ctx, lookalike.post), - } - for lookalike in lookalikes - ], + "exactPost": _serialize_post( + ctx, posts.search_by_image_exact(content) + ), + "similarPosts": [ + { + "distance": distance, + "post": _serialize_post(ctx, post), + } + for distance, post in lookalikes + ], } -@rest.routes.get('/posts/median/?') +@rest.routes.get("/posts/median/?") def get_posts_median( ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'posts:list') + auth.verify_privilege(ctx.user, "posts:list") _search_executor_config.user = ctx.user - query_text = ctx.get_param_as_string('query', default='') + query_text = ctx.get_param_as_string("query", default="") total_count = _search_executor.count(query_text) offset = ceil(total_count / 2) - 1 _, results = _search_executor.execute(query_text, offset, 1) return { - 'query': query_text, - 'offset': offset, - 'limit': 1, - 'total': len(results), - 'results': list([_serialize_post(ctx, post) for post in results]) + "query": query_text, + "offset": offset, + "limit": 1, + "total": len(results), + "results": list([_serialize_post(ctx, post) for post in results]) } diff --git a/server/szurubooru/api/snapshot_api.py b/server/szurubooru/api/snapshot_api.py index 469be7f..87012a2 100644 --- a/server/szurubooru/api/snapshot_api.py +++ b/server/szurubooru/api/snapshot_api.py @@ -1,14 +1,16 @@ from typing import Dict -from szurubooru import search, rest -from szurubooru.func import auth, snapshots +from szurubooru import rest, search +from szurubooru.func import auth, snapshots _search_executor = search.Executor(search.configs.SnapshotSearchConfig()) -@rest.routes.get('/snapshots/?') +@rest.routes.get("/snapshots/?") def get_snapshots( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'snapshots:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "snapshots:list") return _search_executor.execute_and_serialize( - ctx, lambda snapshot: snapshots.serialize_snapshot(snapshot, ctx.user)) + ctx, lambda snapshot: snapshots.serialize_snapshot(snapshot, ctx.user) + ) diff --git a/server/szurubooru/api/tag_api.py b/server/szurubooru/api/tag_api.py index 6b370af..5f54324 100644 --- a/server/szurubooru/api/tag_api.py +++ b/server/szurubooru/api/tag_api.py @@ -1,19 +1,27 @@ -from typing import Optional, List, Dict from datetime import datetime -from szurubooru import db, model, search, rest -from szurubooru.func import auth, tags, metrics, snapshots, serialization, versions +from typing import Dict, List, Optional +from szurubooru import db, model, rest, search +from szurubooru.func import ( + auth, + metrics, + serialization, + snapshots, + tags, + versions +) _search_executor = search.Executor(search.configs.TagSearchConfig()) def _serialize(ctx: rest.Context, tag: model.Tag) -> rest.Response: return tags.serialize_tag( - tag, options=serialization.get_serialization_options(ctx)) + tag, options=serialization.get_serialization_options(ctx) + ) def _get_tag(params: Dict[str, str]) -> model.Tag: - return tags.get_tag_by_name(params['tag_name']) + return tags.get_tag_by_name(params["tag_name"]) def _create_if_needed(tag_names: List[str], user: model.User) -> None: @@ -21,29 +29,31 @@ def _create_if_needed(tag_names: List[str], user: model.User) -> None: return _existing_tags, new_tags = tags.get_or_create_tags_by_names(tag_names) if len(new_tags): - auth.verify_privilege(user, 'tags:create') + auth.verify_privilege(user, "tags:create") db.session.flush() for tag in new_tags: snapshots.create(tag, user) -@rest.routes.get('/tags/?') +@rest.routes.get("/tags/?") def get_tags(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'tags:list') + auth.verify_privilege(ctx.user, "tags:list") return _search_executor.execute_and_serialize( - ctx, lambda tag: _serialize(ctx, tag)) + ctx, lambda tag: _serialize(ctx, tag) + ) -@rest.routes.post('/tags/?') +@rest.routes.post("/tags/?") def create_tag( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'tags:create') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "tags:create") - names = ctx.get_param_as_string_list('names') - category = ctx.get_param_as_string('category') - description = ctx.get_param_as_string('description', default='') - suggestions = ctx.get_param_as_string_list('suggestions', default=[]) - implications = ctx.get_param_as_string_list('implications', default=[]) + names = ctx.get_param_as_string_list("names") + category = ctx.get_param_as_string("category") + description = ctx.get_param_as_string("description", default="") + suggestions = ctx.get_param_as_string_list("suggestions", default=[]) + implications = ctx.get_param_as_string_list("implications", default=[]) _create_if_needed(suggestions, ctx.user) _create_if_needed(implications, ctx.user) @@ -57,44 +67,44 @@ def create_tag( return _serialize(ctx, tag) -@rest.routes.get('/tag/(?P<tag_name>.+)') +@rest.routes.get("/tag/(?P<tag_name>.+)") def get_tag(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'tags:view') + auth.verify_privilege(ctx.user, "tags:view") tag = _get_tag(params) return _serialize(ctx, tag) -@rest.routes.put('/tag/(?P<tag_name>.+)') +@rest.routes.put("/tag/(?P<tag_name>.+)") def update_tag(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: tag = _get_tag(params) versions.verify_version(tag, ctx) versions.bump_version(tag) - if ctx.has_param('names'): - auth.verify_privilege(ctx.user, 'tags:edit:names') - tags.update_tag_names(tag, ctx.get_param_as_string_list('names')) - if ctx.has_param('category'): - auth.verify_privilege(ctx.user, 'tags:edit:category') - tags.update_tag_category_name( - tag, ctx.get_param_as_string('category')) - if ctx.has_param('description'): - auth.verify_privilege(ctx.user, 'tags:edit:description') + if ctx.has_param("names"): + auth.verify_privilege(ctx.user, "tags:edit:names") + tags.update_tag_names(tag, ctx.get_param_as_string_list("names")) + if ctx.has_param("category"): + auth.verify_privilege(ctx.user, "tags:edit:category") + tags.update_tag_category_name(tag, ctx.get_param_as_string("category")) + if ctx.has_param("description"): + auth.verify_privilege(ctx.user, "tags:edit:description") tags.update_tag_description( - tag, ctx.get_param_as_string('description')) - if ctx.has_param('suggestions'): - auth.verify_privilege(ctx.user, 'tags:edit:suggestions') - suggestions = ctx.get_param_as_string_list('suggestions') + tag, ctx.get_param_as_string("description") + ) + if ctx.has_param("suggestions"): + auth.verify_privilege(ctx.user, "tags:edit:suggestions") + suggestions = ctx.get_param_as_string_list("suggestions") _create_if_needed(suggestions, ctx.user) tags.update_tag_suggestions(tag, suggestions) - if ctx.has_param('implications'): - auth.verify_privilege(ctx.user, 'tags:edit:implications') - implications = ctx.get_param_as_string_list('implications') + if ctx.has_param("implications"): + auth.verify_privilege(ctx.user, "tags:edit:implications") + implications = ctx.get_param_as_string_list("implications") _create_if_needed(implications, ctx.user) tags.update_tag_implications(tag, implications) - if ctx.has_param('metric'): - auth.verify_privilege(ctx.user, 'metrics:edit:bounds') - new_metric = metrics.update_or_create_metric(tag, ctx.get_param('metric')) + if ctx.has_param("metric"): + auth.verify_privilege(ctx.user, "metrics:edit:bounds") + new_metric = metrics.update_or_create_metric(tag, ctx.get_param("metric")) if new_metric is not None: - auth.verify_privilege(ctx.user, 'metrics:create') + auth.verify_privilege(ctx.user, "metrics:create") db.session.flush() # snapshots.create(new_metric, ctx.user) @@ -105,44 +115,45 @@ def update_tag(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: return _serialize(ctx, tag) -@rest.routes.delete('/tag/(?P<tag_name>.+)') +@rest.routes.delete("/tag/(?P<tag_name>.+)") def delete_tag(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: tag = _get_tag(params) versions.verify_version(tag, ctx) - auth.verify_privilege(ctx.user, 'tags:delete') + auth.verify_privilege(ctx.user, "tags:delete") snapshots.delete(tag, ctx.user) tags.delete(tag) ctx.session.commit() return {} -@rest.routes.post('/tag-merge/?') +@rest.routes.post("/tag-merge/?") def merge_tags( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - source_tag_name = ctx.get_param_as_string('remove') - target_tag_name = ctx.get_param_as_string('mergeTo') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + source_tag_name = ctx.get_param_as_string("remove") + target_tag_name = ctx.get_param_as_string("mergeTo") source_tag = tags.get_tag_by_name(source_tag_name) target_tag = tags.get_tag_by_name(target_tag_name) - versions.verify_version(source_tag, ctx, 'removeVersion') - versions.verify_version(target_tag, ctx, 'mergeToVersion') + versions.verify_version(source_tag, ctx, "removeVersion") + versions.verify_version(target_tag, ctx, "mergeToVersion") versions.bump_version(target_tag) - auth.verify_privilege(ctx.user, 'tags:merge') + auth.verify_privilege(ctx.user, "tags:merge") tags.merge_tags(source_tag, target_tag) snapshots.merge(source_tag, target_tag, ctx.user) ctx.session.commit() return _serialize(ctx, target_tag) -@rest.routes.get('/tag-siblings/(?P<tag_name>.+)') +@rest.routes.get("/tag-siblings/(?P<tag_name>.+)") def get_tag_siblings( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'tags:view') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "tags:view") tag = _get_tag(params) result = tags.get_tag_siblings(tag) serialized_siblings = [] for sibling, occurrences in result: - serialized_siblings.append({ - 'tag': _serialize(ctx, sibling), - 'occurrences': occurrences - }) - return {'results': serialized_siblings} + serialized_siblings.append( + {"tag": _serialize(ctx, sibling), "occurrences": occurrences} + ) + return {"results": serialized_siblings} diff --git a/server/szurubooru/api/tag_category_api.py b/server/szurubooru/api/tag_category_api.py index 07da999..95d0ed8 100644 --- a/server/szurubooru/api/tag_category_api.py +++ b/server/szurubooru/api/tag_category_api.py @@ -1,32 +1,44 @@ from typing import Dict + from szurubooru import model, rest from szurubooru.func import ( - auth, tags, tag_categories, snapshots, serialization, versions) + auth, + serialization, + snapshots, + tag_categories, + tags, + versions, +) def _serialize( - ctx: rest.Context, category: model.TagCategory) -> rest.Response: + ctx: rest.Context, category: model.TagCategory +) -> rest.Response: return tag_categories.serialize_category( - category, options=serialization.get_serialization_options(ctx)) + category, options=serialization.get_serialization_options(ctx) + ) -@rest.routes.get('/tag-categories/?') +@rest.routes.get("/tag-categories/?") def get_tag_categories( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'tag_categories:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "tag_categories:list") categories = tag_categories.get_all_categories() return { - 'results': [_serialize(ctx, category) for category in categories], + "results": [_serialize(ctx, category) for category in categories], } -@rest.routes.post('/tag-categories/?') +@rest.routes.post("/tag-categories/?") def create_tag_category( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'tag_categories:create') - name = ctx.get_param_as_string('name') - color = ctx.get_param_as_string('color') - category = tag_categories.create_category(name, color) + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "tag_categories:create") + name = ctx.get_param_as_string("name") + color = ctx.get_param_as_string("color") + order = ctx.get_param_as_int("order") + category = tag_categories.create_category(name, color, order) ctx.session.add(category) ctx.session.flush() snapshots.create(category, ctx.user) @@ -34,54 +46,68 @@ def create_tag_category( return _serialize(ctx, category) -@rest.routes.get('/tag-category/(?P<category_name>[^/]+)/?') +@rest.routes.get("/tag-category/(?P<category_name>[^/]+)/?") def get_tag_category( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'tag_categories:view') - category = tag_categories.get_category_by_name(params['category_name']) + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "tag_categories:view") + category = tag_categories.get_category_by_name(params["category_name"]) return _serialize(ctx, category) -@rest.routes.put('/tag-category/(?P<category_name>[^/]+)/?') +@rest.routes.put("/tag-category/(?P<category_name>[^/]+)/?") def update_tag_category( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: category = tag_categories.get_category_by_name( - params['category_name'], lock=True) + params["category_name"], lock=True + ) versions.verify_version(category, ctx) versions.bump_version(category) - if ctx.has_param('name'): - auth.verify_privilege(ctx.user, 'tag_categories:edit:name') + if ctx.has_param("name"): + auth.verify_privilege(ctx.user, "tag_categories:edit:name") tag_categories.update_category_name( - category, ctx.get_param_as_string('name')) - if ctx.has_param('color'): - auth.verify_privilege(ctx.user, 'tag_categories:edit:color') + category, ctx.get_param_as_string("name") + ) + if ctx.has_param("color"): + auth.verify_privilege(ctx.user, "tag_categories:edit:color") tag_categories.update_category_color( - category, ctx.get_param_as_string('color')) + category, ctx.get_param_as_string("color") + ) + if ctx.has_param("order"): + auth.verify_privilege(ctx.user, "tag_categories:edit:order") + tag_categories.update_category_order( + category, ctx.get_param_as_int("order") + ) ctx.session.flush() snapshots.modify(category, ctx.user) ctx.session.commit() return _serialize(ctx, category) -@rest.routes.delete('/tag-category/(?P<category_name>[^/]+)/?') +@rest.routes.delete("/tag-category/(?P<category_name>[^/]+)/?") def delete_tag_category( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: category = tag_categories.get_category_by_name( - params['category_name'], lock=True) + params["category_name"], lock=True + ) versions.verify_version(category, ctx) - auth.verify_privilege(ctx.user, 'tag_categories:delete') + auth.verify_privilege(ctx.user, "tag_categories:delete") tag_categories.delete_category(category) snapshots.delete(category, ctx.user) ctx.session.commit() return {} -@rest.routes.put('/tag-category/(?P<category_name>[^/]+)/default/?') +@rest.routes.put("/tag-category/(?P<category_name>[^/]+)/default/?") def set_tag_category_as_default( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - auth.verify_privilege(ctx.user, 'tag_categories:set_default') + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + auth.verify_privilege(ctx.user, "tag_categories:set_default") category = tag_categories.get_category_by_name( - params['category_name'], lock=True) + params["category_name"], lock=True + ) tag_categories.set_default_category(category) ctx.session.flush() snapshots.modify(category, ctx.user) diff --git a/server/szurubooru/api/upload_api.py b/server/szurubooru/api/upload_api.py index 6a6ecfe..3b7bca8 100644 --- a/server/szurubooru/api/upload_api.py +++ b/server/szurubooru/api/upload_api.py @@ -1,12 +1,20 @@ from typing import Dict + from szurubooru import rest from szurubooru.func import auth, file_uploads -@rest.routes.post('/uploads/?') +@rest.routes.post("/uploads/?") def create_temporary_file( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'uploads:create') - content = ctx.get_file('content', allow_tokens=False) + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "uploads:create") + content = ctx.get_file( + "content", + allow_tokens=False, + use_video_downloader=auth.has_privilege( + ctx.user, "uploads:use_downloader" + ), + ) token = file_uploads.save(content) - return {'token': token} + return {"token": token} diff --git a/server/szurubooru/api/user_api.py b/server/szurubooru/api/user_api.py index 5e14fab..a6196cb 100644 --- a/server/szurubooru/api/user_api.py +++ b/server/szurubooru/api/user_api.py @@ -1,97 +1,102 @@ from typing import Any, Dict -from szurubooru import model, search, rest -from szurubooru.func import auth, users, serialization, versions +from szurubooru import model, rest, search +from szurubooru.func import auth, serialization, users, versions _search_executor = search.Executor(search.configs.UserSearchConfig()) def _serialize( - ctx: rest.Context, user: model.User, **kwargs: Any) -> rest.Response: + ctx: rest.Context, user: model.User, **kwargs: Any +) -> rest.Response: return users.serialize_user( user, ctx.user, options=serialization.get_serialization_options(ctx), - **kwargs) + **kwargs + ) -@rest.routes.get('/users/?') +@rest.routes.get("/users/?") def get_users( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: - auth.verify_privilege(ctx.user, 'users:list') + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: + auth.verify_privilege(ctx.user, "users:list") return _search_executor.execute_and_serialize( - ctx, lambda user: _serialize(ctx, user)) + ctx, lambda user: _serialize(ctx, user) + ) -@rest.routes.post('/users/?') +@rest.routes.post("/users/?") def create_user( - ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response: + ctx: rest.Context, _params: Dict[str, str] = {} +) -> rest.Response: if ctx.user.user_id is None: - auth.verify_privilege(ctx.user, 'users:create:self') + auth.verify_privilege(ctx.user, "users:create:self") else: - auth.verify_privilege(ctx.user, 'users:create:any') + auth.verify_privilege(ctx.user, "users:create:any") - name = ctx.get_param_as_string('name') - password = ctx.get_param_as_string('password') - email = ctx.get_param_as_string('email', default='') + name = ctx.get_param_as_string("name") + password = ctx.get_param_as_string("password") + email = ctx.get_param_as_string("email", default="") user = users.create_user(name, password, email) - if ctx.has_param('rank'): - users.update_user_rank(user, ctx.get_param_as_string('rank'), ctx.user) - if ctx.has_param('avatarStyle'): + if ctx.has_param("rank"): + users.update_user_rank(user, ctx.get_param_as_string("rank"), ctx.user) + if ctx.has_param("avatarStyle"): users.update_user_avatar( user, - ctx.get_param_as_string('avatarStyle'), - ctx.get_file('avatar', default=b'')) + ctx.get_param_as_string("avatarStyle"), + ctx.get_file("avatar", default=b""), + ) ctx.session.add(user) ctx.session.commit() return _serialize(ctx, user, force_show_email=True) -@rest.routes.get('/user/(?P<user_name>[^/]+)/?') +@rest.routes.get("/user/(?P<user_name>[^/]+)/?") def get_user(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user = users.get_user_by_name(params['user_name']) + user = users.get_user_by_name(params["user_name"]) if ctx.user.user_id != user.user_id: - auth.verify_privilege(ctx.user, 'users:view') + auth.verify_privilege(ctx.user, "users:view") return _serialize(ctx, user) -@rest.routes.put('/user/(?P<user_name>[^/]+)/?') +@rest.routes.put("/user/(?P<user_name>[^/]+)/?") def update_user(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user = users.get_user_by_name(params['user_name']) + user = users.get_user_by_name(params["user_name"]) versions.verify_version(user, ctx) versions.bump_version(user) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - if ctx.has_param('name'): - auth.verify_privilege(ctx.user, 'users:edit:%s:name' % infix) - users.update_user_name(user, ctx.get_param_as_string('name')) - if ctx.has_param('password'): - auth.verify_privilege(ctx.user, 'users:edit:%s:pass' % infix) - users.update_user_password( - user, ctx.get_param_as_string('password')) - if ctx.has_param('email'): - auth.verify_privilege(ctx.user, 'users:edit:%s:email' % infix) - users.update_user_email(user, ctx.get_param_as_string('email')) - if ctx.has_param('rank'): - auth.verify_privilege(ctx.user, 'users:edit:%s:rank' % infix) - users.update_user_rank( - user, ctx.get_param_as_string('rank'), ctx.user) - if ctx.has_param('avatarStyle'): - auth.verify_privilege(ctx.user, 'users:edit:%s:avatar' % infix) + infix = "self" if ctx.user.user_id == user.user_id else "any" + if ctx.has_param("name"): + auth.verify_privilege(ctx.user, "users:edit:%s:name" % infix) + users.update_user_name(user, ctx.get_param_as_string("name")) + if ctx.has_param("password"): + auth.verify_privilege(ctx.user, "users:edit:%s:pass" % infix) + users.update_user_password(user, ctx.get_param_as_string("password")) + if ctx.has_param("email"): + auth.verify_privilege(ctx.user, "users:edit:%s:email" % infix) + users.update_user_email(user, ctx.get_param_as_string("email")) + if ctx.has_param("rank"): + auth.verify_privilege(ctx.user, "users:edit:%s:rank" % infix) + users.update_user_rank(user, ctx.get_param_as_string("rank"), ctx.user) + if ctx.has_param("avatarStyle"): + auth.verify_privilege(ctx.user, "users:edit:%s:avatar" % infix) users.update_user_avatar( user, - ctx.get_param_as_string('avatarStyle'), - ctx.get_file('avatar', default=b'')) + ctx.get_param_as_string("avatarStyle"), + ctx.get_file("avatar", default=b""), + ) ctx.session.commit() return _serialize(ctx, user) -@rest.routes.delete('/user/(?P<user_name>[^/]+)/?') +@rest.routes.delete("/user/(?P<user_name>[^/]+)/?") def delete_user(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user = users.get_user_by_name(params['user_name']) + user = users.get_user_by_name(params["user_name"]) versions.verify_version(user, ctx) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - auth.verify_privilege(ctx.user, 'users:delete:%s' % infix) + infix = "self" if ctx.user.user_id == user.user_id else "any" + auth.verify_privilege(ctx.user, "users:delete:%s" % infix) ctx.session.delete(user) ctx.session.commit() return {} diff --git a/server/szurubooru/api/user_token_api.py b/server/szurubooru/api/user_token_api.py index 7739823..772f97a 100644 --- a/server/szurubooru/api/user_token_api.py +++ b/server/szurubooru/api/user_token_api.py @@ -1,82 +1,90 @@ from typing import Dict + from szurubooru import model, rest -from szurubooru.func import auth, users, user_tokens, serialization, versions +from szurubooru.func import auth, serialization, user_tokens, users, versions def _serialize( - ctx: rest.Context, user_token: model.UserToken) -> rest.Response: + ctx: rest.Context, user_token: model.UserToken +) -> rest.Response: return user_tokens.serialize_user_token( user_token, ctx.user, - options=serialization.get_serialization_options(ctx)) + options=serialization.get_serialization_options(ctx), + ) -@rest.routes.get('/user-tokens/(?P<user_name>[^/]+)/?') +@rest.routes.get("/user-tokens/(?P<user_name>[^/]+)/?") def get_user_tokens( - ctx: rest.Context, params: Dict[str, str] = {}) -> rest.Response: - user = users.get_user_by_name(params['user_name']) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - auth.verify_privilege(ctx.user, 'user_tokens:list:%s' % infix) + ctx: rest.Context, params: Dict[str, str] = {} +) -> rest.Response: + user = users.get_user_by_name(params["user_name"]) + infix = "self" if ctx.user.user_id == user.user_id else "any" + auth.verify_privilege(ctx.user, "user_tokens:list:%s" % infix) user_token_list = user_tokens.get_user_tokens(user) - return { - 'results': [_serialize(ctx, token) for token in user_token_list] - } + return {"results": [_serialize(ctx, token) for token in user_token_list]} -@rest.routes.post('/user-token/(?P<user_name>[^/]+)/?') +@rest.routes.post("/user-token/(?P<user_name>[^/]+)/?") def create_user_token( - ctx: rest.Context, params: Dict[str, str] = {}) -> rest.Response: - user = users.get_user_by_name(params['user_name']) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - auth.verify_privilege(ctx.user, 'user_tokens:create:%s' % infix) - enabled = ctx.get_param_as_bool('enabled', True) + ctx: rest.Context, params: Dict[str, str] = {} +) -> rest.Response: + user = users.get_user_by_name(params["user_name"]) + infix = "self" if ctx.user.user_id == user.user_id else "any" + auth.verify_privilege(ctx.user, "user_tokens:create:%s" % infix) + enabled = ctx.get_param_as_bool("enabled", True) user_token = user_tokens.create_user_token(user, enabled) - if ctx.has_param('note'): - note = ctx.get_param_as_string('note') + if ctx.has_param("note"): + note = ctx.get_param_as_string("note") user_tokens.update_user_token_note(user_token, note) - if ctx.has_param('expirationTime'): - expiration_time = ctx.get_param_as_string('expirationTime') + if ctx.has_param("expirationTime"): + expiration_time = ctx.get_param_as_string("expirationTime") user_tokens.update_user_token_expiration_time( - user_token, expiration_time) + user_token, expiration_time + ) ctx.session.add(user_token) ctx.session.commit() return _serialize(ctx, user_token) -@rest.routes.put('/user-token/(?P<user_name>[^/]+)/(?P<user_token>[^/]+)/?') +@rest.routes.put("/user-token/(?P<user_name>[^/]+)/(?P<user_token>[^/]+)/?") def update_user_token( - ctx: rest.Context, params: Dict[str, str] = {}) -> rest.Response: - user = users.get_user_by_name(params['user_name']) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix) - user_token = user_tokens.get_by_user_and_token(user, params['user_token']) + ctx: rest.Context, params: Dict[str, str] = {} +) -> rest.Response: + user = users.get_user_by_name(params["user_name"]) + infix = "self" if ctx.user.user_id == user.user_id else "any" + auth.verify_privilege(ctx.user, "user_tokens:edit:%s" % infix) + user_token = user_tokens.get_by_user_and_token(user, params["user_token"]) versions.verify_version(user_token, ctx) versions.bump_version(user_token) - if ctx.has_param('enabled'): - auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix) + if ctx.has_param("enabled"): + auth.verify_privilege(ctx.user, "user_tokens:edit:%s" % infix) user_tokens.update_user_token_enabled( - user_token, ctx.get_param_as_bool('enabled')) - if ctx.has_param('note'): - auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix) - note = ctx.get_param_as_string('note') + user_token, ctx.get_param_as_bool("enabled") + ) + if ctx.has_param("note"): + auth.verify_privilege(ctx.user, "user_tokens:edit:%s" % infix) + note = ctx.get_param_as_string("note") user_tokens.update_user_token_note(user_token, note) - if ctx.has_param('expirationTime'): - auth.verify_privilege(ctx.user, 'user_tokens:edit:%s' % infix) - expiration_time = ctx.get_param_as_string('expirationTime') + if ctx.has_param("expirationTime"): + auth.verify_privilege(ctx.user, "user_tokens:edit:%s" % infix) + expiration_time = ctx.get_param_as_string("expirationTime") user_tokens.update_user_token_expiration_time( - user_token, expiration_time) + user_token, expiration_time + ) user_tokens.update_user_token_edit_time(user_token) ctx.session.commit() return _serialize(ctx, user_token) -@rest.routes.delete('/user-token/(?P<user_name>[^/]+)/(?P<user_token>[^/]+)/?') +@rest.routes.delete("/user-token/(?P<user_name>[^/]+)/(?P<user_token>[^/]+)/?") def delete_user_token( - ctx: rest.Context, params: Dict[str, str]) -> rest.Response: - user = users.get_user_by_name(params['user_name']) - infix = 'self' if ctx.user.user_id == user.user_id else 'any' - auth.verify_privilege(ctx.user, 'user_tokens:delete:%s' % infix) - user_token = user_tokens.get_by_user_and_token(user, params['user_token']) + ctx: rest.Context, params: Dict[str, str] +) -> rest.Response: + user = users.get_user_by_name(params["user_name"]) + infix = "self" if ctx.user.user_id == user.user_id else "any" + auth.verify_privilege(ctx.user, "user_tokens:delete:%s" % infix) + user_token = user_tokens.get_by_user_and_token(user, params["user_token"]) if user_token is not None: ctx.session.delete(user_token) ctx.session.commit() diff --git a/server/szurubooru/config.py b/server/szurubooru/config.py index f9c745c..1515a54 100644 --- a/server/szurubooru/config.py +++ b/server/szurubooru/config.py @@ -1,14 +1,19 @@ -from typing import Dict +import logging import os +from typing import Dict + import yaml + from szurubooru import errors +logger = logging.getLogger(__name__) -def merge(left: Dict, right: Dict) -> Dict: + +def _merge(left: Dict, right: Dict) -> Dict: for key in right: if key in left: if isinstance(left[key], dict) and isinstance(right[key], dict): - merge(left[key], right[key]) + _merge(left[key], right[key]) elif left[key] != right[key]: left[key] = right[key] else: @@ -16,44 +21,45 @@ def merge(left: Dict, right: Dict) -> Dict: return left -def docker_config() -> Dict: - for key in [ - 'POSTGRES_USER', - 'POSTGRES_PASSWORD', - 'POSTGRES_HOST', - 'ESEARCH_HOST' - ]: - if not os.getenv(key, False): - raise errors.ConfigError(f'Environment variable "{key}" not set') +def _docker_config() -> Dict: + if "TEST_ENVIRONMENT" not in os.environ: + for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]: + if key not in os.environ: + raise errors.ConfigError( + f'Environment variable "{key}" not set' + ) return { - 'debug': True, - 'show_sql': int(os.getenv('LOG_SQL', 0)), - 'data_url': os.getenv('DATA_URL', 'data/'), - 'data_dir': '/data/', - 'database': 'postgres://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s' % { - 'user': os.getenv('POSTGRES_USER'), - 'pass': os.getenv('POSTGRES_PASSWORD'), - 'host': os.getenv('POSTGRES_HOST'), - 'port': int(os.getenv('POSTGRES_PORT', 5432)), - 'db': os.getenv('POSTGRES_DB', os.getenv('POSTGRES_USER')) + "debug": True, + "show_sql": int(os.getenv("LOG_SQL", 0)), + "data_url": os.getenv("DATA_URL", "data/"), + "data_dir": "/data/", + "database": "postgres://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s" + % { + "user": os.getenv("POSTGRES_USER"), + "pass": os.getenv("POSTGRES_PASSWORD"), + "host": os.getenv("POSTGRES_HOST"), + "port": int(os.getenv("POSTGRES_PORT", 5432)), + "db": os.getenv("POSTGRES_DB", os.getenv("POSTGRES_USER")), }, - 'elasticsearch': { - 'host': os.getenv('ESEARCH_HOST'), - 'port': int(os.getenv('ESEARCH_PORT', 9200)), - 'index': os.getenv('ESEARCH_INDEX', 'szurubooru') - } } -def read_config() -> Dict: - with open('config.yaml.dist') as handle: - ret = yaml.load(handle.read()) - if os.path.exists('config.yaml'): - with open('config.yaml') as handle: - ret = merge(ret, yaml.load(handle.read())) - if os.path.exists('/.dockerenv'): - ret = merge(ret, docker_config()) - return ret +def _file_config(filename: str) -> Dict: + with open(filename) as handle: + return yaml.load(handle.read(), Loader=yaml.SafeLoader) or {} + + +def _read_config() -> Dict: + ret = _file_config("config.yaml.dist") + if os.path.isfile("config.yaml"): + ret = _merge(ret, _file_config("config.yaml")) + elif os.path.isdir("config.yaml"): + logger.warning( + "'config.yaml' should be a file, not a directory, skipping" + ) + if os.path.exists("/.dockerenv"): + ret = _merge(ret, _docker_config()) + return ret -config = read_config() # pylint: disable=invalid-name +config = _read_config() diff --git a/server/szurubooru/db.py b/server/szurubooru/db.py index f90bfaf..ed59a70 100644 --- a/server/szurubooru/db.py +++ b/server/szurubooru/db.py @@ -1,14 +1,15 @@ -from typing import Any import threading +from typing import Any + import sqlalchemy as sa import sqlalchemy.orm + from szurubooru import config -# pylint: disable=invalid-name _data = threading.local() -_engine = sa.create_engine(config.config['database']) # type: Any -sessionmaker = sa.orm.sessionmaker(bind=_engine, autoflush=False) # type: Any -session = sa.orm.scoped_session(sessionmaker) # type: Any +_engine = sa.create_engine(config.config["database"]) # type: Any +_sessionmaker = sa.orm.sessionmaker(bind=_engine, autoflush=False) # type: Any +session = sa.orm.scoped_session(_sessionmaker) # type: Any def get_session() -> Any: @@ -30,7 +31,7 @@ def get_query_count() -> int: def _bump_query_count() -> None: - _data.query_count = getattr(_data, 'query_count', 0) + 1 + _data.query_count = getattr(_data, "query_count", 0) + 1 -sa.event.listen(_engine, 'after_execute', lambda *args: _bump_query_count()) +sa.event.listen(_engine, "after_execute", lambda *args: _bump_query_count()) diff --git a/server/szurubooru/errors.py b/server/szurubooru/errors.py index beeb469..9106f04 100644 --- a/server/szurubooru/errors.py +++ b/server/szurubooru/errors.py @@ -3,9 +3,10 @@ from typing import Dict class BaseError(RuntimeError): def __init__( - self, - message: str = 'Unknown error', - extra_fields: Dict[str, str] = None) -> None: + self, + message: str = "Unknown error", + extra_fields: Dict[str, str] = None, + ) -> None: super().__init__(message) self.extra_fields = extra_fields diff --git a/server/szurubooru/facade.py b/server/szurubooru/facade.py index 90709f9..ecf34c7 100644 --- a/server/szurubooru/facade.py +++ b/server/szurubooru/facade.py @@ -1,125 +1,148 @@ -import os -import time import logging +import os import threading -from typing import Callable, Any, Type +import time +from typing import Any, Callable, Type import coloredlogs import sqlalchemy as sa import sqlalchemy.orm.exc -from szurubooru import config, db, errors, rest -from szurubooru.func import posts, file_uploads -# pylint: disable=unused-import -from szurubooru import api, middleware + +from szurubooru import api, config, db, errors, middleware, rest +from szurubooru.func.file_uploads import purge_old_uploads +from szurubooru.func.posts import update_all_post_signatures def _map_error( - ex: Exception, - target_class: Type[rest.errors.BaseHttpError], - title: str) -> rest.errors.BaseHttpError: + ex: Exception, target_class: Type[rest.errors.BaseHttpError], title: str +) -> rest.errors.BaseHttpError: return target_class( name=type(ex).__name__, title=title, description=str(ex), - extra_fields=getattr(ex, 'extra_fields', {})) + extra_fields=getattr(ex, "extra_fields", {}), + ) def _on_auth_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpForbidden, 'Authentication error') + raise _map_error(ex, rest.errors.HttpForbidden, "Authentication error") def _on_validation_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpBadRequest, 'Validation error') + raise _map_error(ex, rest.errors.HttpBadRequest, "Validation error") def _on_search_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpBadRequest, 'Search error') + raise _map_error(ex, rest.errors.HttpBadRequest, "Search error") def _on_integrity_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpConflict, 'Integrity violation') + raise _map_error(ex, rest.errors.HttpConflict, "Integrity violation") def _on_not_found_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpNotFound, 'Not found') + raise _map_error(ex, rest.errors.HttpNotFound, "Not found") def _on_processing_error(ex: Exception) -> None: - raise _map_error(ex, rest.errors.HttpBadRequest, 'Processing error') + raise _map_error(ex, rest.errors.HttpBadRequest, "Processing error") def _on_third_party_error(ex: Exception) -> None: raise _map_error( - ex, - rest.errors.HttpInternalServerError, - 'Server configuration error') + ex, rest.errors.HttpInternalServerError, "Server configuration error" + ) def _on_stale_data_error(_ex: Exception) -> None: raise rest.errors.HttpConflict( - name='IntegrityError', - title='Integrity violation', + name="IntegrityError", + title="Integrity violation", description=( - 'Someone else modified this in the meantime. ' - 'Please try again.')) + "Someone else modified this in the meantime. " "Please try again." + ), + ) def validate_config() -> None: - ''' + """ Check whether config doesn't contain errors that might prove lethal at runtime. - ''' + """ from szurubooru.func.auth import RANK_MAP - for privilege, rank in config.config['privileges'].items(): + + for privilege, rank in config.config["privileges"].items(): if rank not in RANK_MAP.values(): raise errors.ConfigError( - 'Rank %r for privilege %r is missing' % (rank, privilege)) - if config.config['default_rank'] not in RANK_MAP.values(): + "Rank %r for privilege %r is missing" % (rank, privilege) + ) + if config.config["default_rank"] not in RANK_MAP.values(): raise errors.ConfigError( - 'Default rank %r is not on the list of known ranks' % ( - config.config['default_rank'])) + "Default rank %r is not on the list of known ranks" + % (config.config["default_rank"]) + ) - for key in ['data_url', 'data_dir']: + for key in ["data_url", "data_dir"]: if not config.config[key]: raise errors.ConfigError( - 'Service is not configured: %r is missing' % key) + "Service is not configured: %r is missing" % key + ) - if not os.path.isabs(config.config['data_dir']): - raise errors.ConfigError( - 'data_dir must be an absolute path') + if not os.path.isabs(config.config["data_dir"]): + raise errors.ConfigError("data_dir must be an absolute path") - if not config.config['database']: - raise errors.ConfigError('Database is not configured') + if not config.config["database"]: + raise errors.ConfigError("Database is not configured") + if config.config["webhooks"] and not isinstance( + config.config["webhooks"], list + ): + raise errors.ConfigError("Webhooks must be provided as a list of URLs") -def purge_old_uploads() -> None: + if config.config["smtp"]["host"]: + if not config.config["smtp"]["port"]: + raise errors.ConfigError("SMTP host is set but port is not set") + if not config.config["smtp"]["user"]: + raise errors.ConfigError( + "SMTP host is set but username is not set" + ) + if not config.config["smtp"]["pass"]: + raise errors.ConfigError( + "SMTP host is set but password is not set" + ) + if not config.config["smtp"]["from"]: + raise errors.ConfigError( + "From address must be set to use mail-based password reset" + ) + + +def purge_old_uploads_daemon() -> None: while True: try: - file_uploads.purge_old_uploads() + purge_old_uploads() except Exception as ex: logging.exception(ex) time.sleep(60 * 5) def create_app() -> Callable[[Any, Any], Any]: - ''' Create a WSGI compatible App object. ''' + """ Create a WSGI compatible App object. """ validate_config() - coloredlogs.install(fmt='[%(asctime)-15s] %(name)s %(message)s') - logging.getLogger('elasticsearch').disabled = True - if config.config['debug']: - logging.getLogger('szurubooru').setLevel(logging.INFO) - if config.config['show_sql']: - logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) + coloredlogs.install(fmt="[%(asctime)-15s] %(name)s %(message)s") + if config.config["debug"]: + logging.getLogger("szurubooru").setLevel(logging.INFO) + if config.config["show_sql"]: + logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) - purge_thread = threading.Thread(target=purge_old_uploads) + purge_thread = threading.Thread(target=purge_old_uploads_daemon) purge_thread.daemon = True purge_thread.start() - try: - posts.populate_reverse_search() - db.session.commit() - except errors.ThirdPartyError: - pass + hashing_thread = threading.Thread(target=update_all_post_signatures) + hashing_thread.daemon = False + hashing_thread.start() + + db.session.commit() rest.errors.handle(errors.AuthError, _on_auth_error) rest.errors.handle(errors.ValidationError, _on_validation_error) @@ -133,4 +156,4 @@ def create_app() -> Callable[[Any, Any], Any]: return rest.application -app = create_app() # pylint: disable=invalid-name +app = create_app() diff --git a/server/szurubooru/func/auth.py b/server/szurubooru/func/auth.py index 65be79a..d013775 100644 --- a/server/szurubooru/func/auth.py +++ b/server/szurubooru/func/auth.py @@ -1,60 +1,67 @@ -from typing import Tuple, Optional import hashlib import random import uuid from collections import OrderedDict from datetime import datetime +from typing import Optional, Tuple + from nacl import pwhash from nacl.exceptions import InvalidkeyError -from szurubooru import config, db, model, errors -from szurubooru.func import util +from szurubooru import config, db, errors, model +from szurubooru.func import util -RANK_MAP = OrderedDict([ - (model.User.RANK_ANONYMOUS, 'anonymous'), - (model.User.RANK_RESTRICTED, 'restricted'), - (model.User.RANK_REGULAR, 'regular'), - (model.User.RANK_POWER, 'power'), - (model.User.RANK_MODERATOR, 'moderator'), - (model.User.RANK_ADMINISTRATOR, 'administrator'), - (model.User.RANK_NOBODY, 'nobody'), -]) +RANK_MAP = OrderedDict( + [ + (model.User.RANK_ANONYMOUS, "anonymous"), + (model.User.RANK_RESTRICTED, "restricted"), + (model.User.RANK_REGULAR, "regular"), + (model.User.RANK_POWER, "power"), + (model.User.RANK_MODERATOR, "moderator"), + (model.User.RANK_ADMINISTRATOR, "administrator"), + (model.User.RANK_NOBODY, "nobody"), + ] +) def get_password_hash(salt: str, password: str) -> Tuple[str, int]: - ''' Retrieve argon2id password hash. ''' - return pwhash.argon2id.str( - (config.config['secret'] + salt + password).encode('utf8') - ).decode('utf8'), 3 + """ Retrieve argon2id password hash. """ + return ( + pwhash.argon2id.str( + (config.config["secret"] + salt + password).encode("utf8") + ).decode("utf8"), + 3, + ) def get_sha256_legacy_password_hash( - salt: str, password: str) -> Tuple[str, int]: - ''' Retrieve old-style sha256 password hash. ''' + salt: str, password: str +) -> Tuple[str, int]: + """ Retrieve old-style sha256 password hash. """ digest = hashlib.sha256() - digest.update(config.config['secret'].encode('utf8')) - digest.update(salt.encode('utf8')) - digest.update(password.encode('utf8')) + digest.update(config.config["secret"].encode("utf8")) + digest.update(salt.encode("utf8")) + digest.update(password.encode("utf8")) return digest.hexdigest(), 2 def get_sha1_legacy_password_hash(salt: str, password: str) -> Tuple[str, int]: - ''' Retrieve old-style sha1 password hash. ''' + """ Retrieve old-style sha1 password hash. """ digest = hashlib.sha1() - digest.update(b'1A2/$_4xVa') - digest.update(salt.encode('utf8')) - digest.update(password.encode('utf8')) + digest.update(b"1A2/$_4xVa") + digest.update(salt.encode("utf8")) + digest.update(password.encode("utf8")) return digest.hexdigest(), 1 def create_password() -> str: alphabet = { - 'c': list('bcdfghijklmnpqrstvwxyz'), - 'v': list('aeiou'), - 'n': list('0123456789'), + "c": list("bcdfghijklmnpqrstvwxyz"), + "v": list("aeiou"), + "n": list("0123456789"), } - pattern = 'cvcvnncvcv' - return ''.join(random.choice(alphabet[l]) for l in list(pattern)) + pattern = "cvcvnncvcv" + return "".join(random.choice(alphabet[type]) for type in list(pattern)) def is_valid_password(user: model.User, password: str) -> bool: @@ -63,12 +70,13 @@ def is_valid_password(user: model.User, password: str) -> bool: try: return pwhash.verify( - user.password_hash.encode('utf8'), - (config.config['secret'] + salt + password).encode('utf8')) + user.password_hash.encode("utf8"), + (config.config["secret"] + salt + password).encode("utf8"), + ) except InvalidkeyError: possible_hashes = [ get_sha256_legacy_password_hash(salt, password)[0], - get_sha1_legacy_password_hash(salt, password)[0] + get_sha1_legacy_password_hash(salt, password)[0], ] if valid_hash in possible_hashes: # Convert the user password hash to the new hash @@ -82,16 +90,18 @@ def is_valid_password(user: model.User, password: str) -> bool: def is_valid_token(user_token: Optional[model.UserToken]) -> bool: - ''' + """ Token must be enabled and if it has an expiration, it must be greater than now. - ''' + """ if user_token is None: return False if not user_token.enabled: return False - if (user_token.expiration_time is not None - and user_token.expiration_time < datetime.utcnow()): + if ( + user_token.expiration_time is not None + and user_token.expiration_time < datetime.utcnow() + ): return False return True @@ -99,26 +109,27 @@ def is_valid_token(user_token: Optional[model.UserToken]) -> bool: def has_privilege(user: model.User, privilege_name: str) -> bool: assert user all_ranks = list(RANK_MAP.keys()) - assert privilege_name in config.config['privileges'] + assert privilege_name in config.config["privileges"] assert user.rank in all_ranks minimal_rank = util.flip(RANK_MAP)[ - config.config['privileges'][privilege_name]] - good_ranks = all_ranks[all_ranks.index(minimal_rank):] + config.config["privileges"][privilege_name] + ] + good_ranks = all_ranks[all_ranks.index(minimal_rank) :] return user.rank in good_ranks def verify_privilege(user: model.User, privilege_name: str) -> None: assert user if not has_privilege(user, privilege_name): - raise errors.AuthError('Insufficient privileges to do this.') + raise errors.AuthError("Insufficient privileges to do this.") def generate_authentication_token(user: model.User) -> str: - ''' Generate nonguessable challenge (e.g. links in password reminder). ''' + """ Generate nonguessable challenge (e.g. links in password reminder). """ assert user digest = hashlib.md5() - digest.update(config.config['secret'].encode('utf8')) - digest.update(user.password_salt.encode('utf8')) + digest.update(config.config["secret"].encode("utf8")) + digest.update(user.password_salt.encode("utf8")) return digest.hexdigest() diff --git a/server/szurubooru/func/cache.py b/server/szurubooru/func/cache.py index 01e4659..65e99e1 100644 --- a/server/szurubooru/func/cache.py +++ b/server/szurubooru/func/cache.py @@ -1,5 +1,5 @@ -from typing import Any, List, Dict from datetime import datetime +from typing import Any, Dict, List class LruCacheItem: @@ -18,12 +18,11 @@ class LruCache: def insert_item(self, item: LruCacheItem) -> None: if item.key in self.hash: item_index = next( - i - for i, v in enumerate(self.item_list) - if v.key == item.key) + i for i, v in enumerate(self.item_list) if v.key == item.key + ) self.item_list[:] = ( - self.item_list[:item_index] + - self.item_list[item_index + 1:]) + self.item_list[:item_index] + self.item_list[item_index + 1 :] + ) self.item_list.insert(0, item) else: if len(self.item_list) > self.length: diff --git a/server/szurubooru/func/comments.py b/server/szurubooru/func/comments.py index 9f88283..5eb7c8e 100644 --- a/server/szurubooru/func/comments.py +++ b/server/szurubooru/func/comments.py @@ -1,7 +1,8 @@ from datetime import datetime -from typing import Any, Optional, List, Dict, Callable -from szurubooru import db, model, errors, rest -from szurubooru.func import users, scores, serialization +from typing import Any, Callable, Dict, List, Optional + +from szurubooru import db, errors, model, rest +from szurubooru.func import scores, serialization, users class InvalidCommentIdError(errors.ValidationError): @@ -23,15 +24,15 @@ class CommentSerializer(serialization.BaseSerializer): def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'id': self.serialize_id, - 'user': self.serialize_user, - 'postId': self.serialize_post_id, - 'version': self.serialize_version, - 'text': self.serialize_text, - 'creationTime': self.serialize_creation_time, - 'lastEditTime': self.serialize_last_edit_time, - 'score': self.serialize_score, - 'ownScore': self.serialize_own_score, + "id": self.serialize_id, + "user": self.serialize_user, + "postId": self.serialize_post_id, + "version": self.serialize_version, + "text": self.serialize_text, + "creationTime": self.serialize_creation_time, + "lastEditTime": self.serialize_last_edit_time, + "score": self.serialize_score, + "ownScore": self.serialize_own_score, } def serialize_id(self) -> Any: @@ -63,9 +64,8 @@ class CommentSerializer(serialization.BaseSerializer): def serialize_comment( - comment: model.Comment, - auth_user: model.User, - options: List[str] = []) -> rest.Response: + comment: model.Comment, auth_user: model.User, options: List[str] = [] +) -> rest.Response: if comment is None: return None return CommentSerializer(comment, auth_user).serialize(options) @@ -74,21 +74,22 @@ def serialize_comment( def try_get_comment_by_id(comment_id: int) -> Optional[model.Comment]: comment_id = int(comment_id) return ( - db.session - .query(model.Comment) + db.session.query(model.Comment) .filter(model.Comment.comment_id == comment_id) - .one_or_none()) + .one_or_none() + ) def get_comment_by_id(comment_id: int) -> model.Comment: comment = try_get_comment_by_id(comment_id) if comment: return comment - raise CommentNotFoundError('Comment %r not found.' % comment_id) + raise CommentNotFoundError("Comment %r not found." % comment_id) def create_comment( - user: model.User, post: model.Post, text: str) -> model.Comment: + user: model.User, post: model.Post, text: str +) -> model.Comment: comment = model.Comment() comment.user = user comment.post = post @@ -100,5 +101,5 @@ def create_comment( def update_comment_text(comment: model.Comment, text: str) -> None: assert comment if not text: - raise EmptyCommentTextError('Comment text cannot be empty.') + raise EmptyCommentTextError("Comment text cannot be empty.") comment.text = text diff --git a/server/szurubooru/func/diff.py b/server/szurubooru/func/diff.py index 90014f7..3282ebb 100644 --- a/server/szurubooru/func/diff.py +++ b/server/szurubooru/func/diff.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Any +from typing import Any, Dict, List def get_list_diff(old: List[Any], new: List[Any]) -> Any: @@ -16,8 +16,11 @@ def get_list_diff(old: List[Any], new: List[Any]) -> Any: equal = False added.append(item) - return None if equal else { - 'type': 'list change', 'added': added, 'removed': removed} + return ( + None + if equal + else {"type": "list change", "added": added, "removed": removed} + ) def get_dict_diff(old: Dict[str, Any], new: Dict[str, Any]) -> Any: @@ -40,23 +43,20 @@ def get_dict_diff(old: Dict[str, Any], new: Dict[str, Any]) -> Any: else: equal = False value[key] = { - 'type': 'primitive change', - 'old-value': old[key], - 'new-value': new[key], + "type": "primitive change", + "old-value": old[key], + "new-value": new[key], } else: equal = False - value[key] = { - 'type': 'deleted property', - 'value': old[key] - } + value[key] = {"type": "deleted property", "value": old[key]} for key in new.keys(): if key not in old: equal = False value[key] = { - 'type': 'added property', - 'value': new[key], + "type": "added property", + "value": new[key], } - return None if equal else {'type': 'object change', 'value': value} + return None if equal else {"type": "object change", "value": value} diff --git a/server/szurubooru/func/favorites.py b/server/szurubooru/func/favorites.py index f567bfa..a012537 100644 --- a/server/szurubooru/func/favorites.py +++ b/server/szurubooru/func/favorites.py @@ -1,6 +1,7 @@ -from typing import Any, Optional, Callable, Tuple from datetime import datetime -from szurubooru import db, model, errors +from typing import Any, Callable, Optional, Tuple + +from szurubooru import db, errors, model class InvalidFavoriteTargetError(errors.ValidationError): @@ -8,10 +9,11 @@ class InvalidFavoriteTargetError(errors.ValidationError): def _get_table_info( - entity: model.Base) -> Tuple[model.Base, Callable[[model.Base], Any]]: + entity: model.Base, +) -> Tuple[model.Base, Callable[[model.Base], Any]]: assert entity resource_type, _, _ = model.util.get_resource_info(entity) - if resource_type == 'post': + if resource_type == "post": return model.PostFavorite, lambda table: table.post_id raise InvalidFavoriteTargetError() @@ -38,6 +40,7 @@ def unset_favorite(entity: model.Base, user: Optional[model.User]) -> None: def set_favorite(entity: model.Base, user: Optional[model.User]) -> None: from szurubooru.func import scores + assert entity assert user try: diff --git a/server/szurubooru/func/file_uploads.py b/server/szurubooru/func/file_uploads.py index e7f93d8..800397d 100644 --- a/server/szurubooru/func/file_uploads.py +++ b/server/szurubooru/func/file_uploads.py @@ -1,25 +1,25 @@ -from typing import Optional from datetime import datetime, timedelta -from szurubooru.func import files, util +from typing import Optional +from szurubooru.func import files, util MAX_MINUTES = 60 def _get_path(checksum: str) -> str: - return 'temporary-uploads/%s.dat' % checksum + return "temporary-uploads/%s.dat" % checksum def purge_old_uploads() -> None: now = datetime.now() - for file in files.scan('temporary-uploads'): + for file in files.scan("temporary-uploads"): file_time = datetime.fromtimestamp(file.stat().st_ctime) if now - file_time > timedelta(minutes=MAX_MINUTES): - files.delete('temporary-uploads/%s' % file.name) + files.delete("temporary-uploads/%s" % file.name) def get(checksum: str) -> Optional[bytes]: - return files.get('temporary-uploads/%s.dat' % checksum) + return files.get("temporary-uploads/%s.dat" % checksum) def save(content: bytes) -> str: diff --git a/server/szurubooru/func/files.py b/server/szurubooru/func/files.py index fa9f36f..6a89826 100644 --- a/server/szurubooru/func/files.py +++ b/server/szurubooru/func/files.py @@ -1,10 +1,11 @@ -from typing import Any, Optional, List import os +from typing import Any, List, Optional + from szurubooru import config def _get_full_path(path: str) -> str: - return os.path.join(config.config['data_dir'], path) + return os.path.join(config.config["data_dir"], path) def delete(path: str) -> None: @@ -31,12 +32,12 @@ def get(path: str) -> Optional[bytes]: full_path = _get_full_path(path) if not os.path.exists(full_path): return None - with open(full_path, 'rb') as handle: + with open(full_path, "rb") as handle: return handle.read() def save(path: str, content: bytes) -> None: full_path = _get_full_path(path) os.makedirs(os.path.dirname(full_path), exist_ok=True) - with open(full_path, 'wb') as handle: + with open(full_path, "wb") as handle: handle.write(content) diff --git a/server/szurubooru/func/image_hash.py b/server/szurubooru/func/image_hash.py index b89b218..fc7d141 100644 --- a/server/szurubooru/func/image_hash.py +++ b/server/szurubooru/func/image_hash.py @@ -1,14 +1,14 @@ import logging -from io import BytesIO +import math from datetime import datetime -from typing import Any, Optional, Tuple, Set, List, Callable -import elasticsearch -import elasticsearch_dsl +from io import BytesIO +from typing import Any, Callable, List, Optional, Set, Tuple + import numpy as np from PIL import Image + from szurubooru import config, errors -# pylint: disable=invalid-name logger = logging.getLogger(__name__) # Math based on paper from H. Chi Wong, Marshall Bern and David Goldberg @@ -17,87 +17,96 @@ logger = logging.getLogger(__name__) LOWER_PERCENTILE = 5 UPPER_PERCENTILE = 95 -IDENTICAL_TOLERANCE = 2 / 255. +IDENTICAL_TOLERANCE = 2 / 255.0 DISTANCE_CUTOFF = 0.45 N_LEVELS = 2 N = 9 P = None SAMPLE_WORDS = 16 MAX_WORDS = 63 -ES_DOC_TYPE = 'image' -ES_MAX_RESULTS = 100 - -Window = Tuple[Tuple[float, float], Tuple[float, float]] -NpMatrix = Any +SIG_CHUNK_BITS = 32 +SIG_NUMS = 8 * N * N +SIG_BASE = 2 * N_LEVELS + 2 +SIG_CHUNK_WIDTH = int(SIG_CHUNK_BITS / math.log2(SIG_BASE)) +SIG_CHUNK_NUMS = SIG_NUMS / SIG_CHUNK_WIDTH +assert SIG_NUMS % SIG_CHUNK_WIDTH == 0 -def _get_session() -> elasticsearch.Elasticsearch: - return elasticsearch.Elasticsearch([{ - 'host': config.config['elasticsearch']['host'], - 'port': config.config['elasticsearch']['port'], - }]) +Window = Tuple[Tuple[float, float], Tuple[float, float]] +NpMatrix = np.ndarray def _preprocess_image(content: bytes) -> NpMatrix: - img = Image.open(BytesIO(content)) - return np.asarray(img.convert('L'), dtype=np.uint8) + try: + img = Image.open(BytesIO(content)) + return np.asarray(img.convert("L"), dtype=np.uint8) + except IOError: + raise errors.ProcessingError( + "Unable to generate a signature hash " "for this image." + ) def _crop_image( - image: NpMatrix, - lower_percentile: float, - upper_percentile: float) -> Window: + image: NpMatrix, lower_percentile: float, upper_percentile: float +) -> Window: rw = np.cumsum(np.sum(np.abs(np.diff(image, axis=1)), axis=1)) cw = np.cumsum(np.sum(np.abs(np.diff(image, axis=0)), axis=0)) upper_column_limit = np.searchsorted( - cw, np.percentile(cw, upper_percentile), side='left') + cw, np.percentile(cw, upper_percentile), side="left" + ) lower_column_limit = np.searchsorted( - cw, np.percentile(cw, lower_percentile), side='right') + cw, np.percentile(cw, lower_percentile), side="right" + ) upper_row_limit = np.searchsorted( - rw, np.percentile(rw, upper_percentile), side='left') + rw, np.percentile(rw, upper_percentile), side="left" + ) lower_row_limit = np.searchsorted( - rw, np.percentile(rw, lower_percentile), side='right') + rw, np.percentile(rw, lower_percentile), side="right" + ) if lower_row_limit > upper_row_limit: - lower_row_limit = int(lower_percentile / 100. * image.shape[0]) - upper_row_limit = int(upper_percentile / 100. * image.shape[0]) + lower_row_limit = int(lower_percentile / 100.0 * image.shape[0]) + upper_row_limit = int(upper_percentile / 100.0 * image.shape[0]) if lower_column_limit > upper_column_limit: - lower_column_limit = int(lower_percentile / 100. * image.shape[1]) - upper_column_limit = int(upper_percentile / 100. * image.shape[1]) + lower_column_limit = int(lower_percentile / 100.0 * image.shape[1]) + upper_column_limit = int(upper_percentile / 100.0 * image.shape[1]) return ( (lower_row_limit, upper_row_limit), - (lower_column_limit, upper_column_limit)) + (lower_column_limit, upper_column_limit), + ) def _normalize_and_threshold( - diff_array: NpMatrix, - identical_tolerance: float, - n_levels: int) -> None: + diff_array: NpMatrix, identical_tolerance: float, n_levels: int +) -> None: mask = np.abs(diff_array) < identical_tolerance - diff_array[mask] = 0. + diff_array[mask] = 0.0 if np.all(mask): return positive_cutoffs = np.percentile( - diff_array[diff_array > 0.], np.linspace(0, 100, n_levels + 1)) + diff_array[diff_array > 0.0], np.linspace(0, 100, n_levels + 1) + ) negative_cutoffs = np.percentile( - diff_array[diff_array < 0.], np.linspace(100, 0, n_levels + 1)) + diff_array[diff_array < 0.0], np.linspace(100, 0, n_levels + 1) + ) for level, interval in enumerate( - positive_cutoffs[i:i + 2] - for i in range(positive_cutoffs.shape[0] - 1)): + positive_cutoffs[i : i + 2] + for i in range(positive_cutoffs.shape[0] - 1) + ): diff_array[ - (diff_array >= interval[0]) & (diff_array <= interval[1])] = \ - level + 1 + (diff_array >= interval[0]) & (diff_array <= interval[1]) + ] = (level + 1) for level, interval in enumerate( - negative_cutoffs[i:i + 2] - for i in range(negative_cutoffs.shape[0] - 1)): + negative_cutoffs[i : i + 2] + for i in range(negative_cutoffs.shape[0] - 1) + ): diff_array[ - (diff_array <= interval[0]) & (diff_array >= interval[1])] = \ - -(level + 1) + (diff_array <= interval[0]) & (diff_array >= interval[1]) + ] = -(level + 1) def _compute_grid_points( - image: NpMatrix, - n: float, - window: Window = None) -> Tuple[NpMatrix, NpMatrix]: + image: NpMatrix, n: float, window: Window = None +) -> Tuple[NpMatrix, NpMatrix]: if window is None: window = ((0, image.shape[0]), (0, image.shape[1])) x_coords = np.linspace(window[0][0], window[0][1], n + 2, dtype=int)[1:-1] @@ -106,12 +115,10 @@ def _compute_grid_points( def _compute_mean_level( - image: NpMatrix, - x_coords: NpMatrix, - y_coords: NpMatrix, - p: Optional[float]) -> NpMatrix: + image: NpMatrix, x_coords: NpMatrix, y_coords: NpMatrix, p: Optional[float] +) -> NpMatrix: if p is None: - p = max([2.0, int(0.5 + min(image.shape) / 20.)]) + p = max([2.0, int(0.5 + min(image.shape) / 20.0)]) avg_grey = np.zeros((x_coords.shape[0], y_coords.shape[0])) for i, x in enumerate(x_coords): lower_x_lim = int(max([x - p / 2, 0])) @@ -120,7 +127,8 @@ def _compute_mean_level( lower_y_lim = int(max([y - p / 2, 0])) upper_y_lim = int(min([lower_y_lim + p, image.shape[1]])) avg_grey[i, j] = np.mean( - image[lower_x_lim:upper_x_lim, lower_y_lim:upper_y_lim]) + image[lower_x_lim:upper_x_lim, lower_y_lim:upper_y_lim] + ) return avg_grey @@ -130,95 +138,117 @@ def _compute_differentials(grey_level_matrix: NpMatrix) -> NpMatrix: ( np.diff(grey_level_matrix), ( - np.zeros(grey_level_matrix.shape[0]) - .reshape((grey_level_matrix.shape[0], 1)) - ) - ), axis=1) + np.zeros(grey_level_matrix.shape[0]).reshape( + (grey_level_matrix.shape[0], 1) + ) + ), + ), + axis=1, + ) down_neighbors = -np.concatenate( ( np.diff(grey_level_matrix, axis=0), ( - np.zeros(grey_level_matrix.shape[1]) - .reshape((1, grey_level_matrix.shape[1])) - ) - )) + np.zeros(grey_level_matrix.shape[1]).reshape( + (1, grey_level_matrix.shape[1]) + ) + ), + ) + ) left_neighbors = -np.concatenate( - (right_neighbors[:, -1:], right_neighbors[:, :-1]), axis=1) + (right_neighbors[:, -1:], right_neighbors[:, :-1]), axis=1 + ) up_neighbors = -np.concatenate((down_neighbors[-1:], down_neighbors[:-1])) diagonals = np.arange( - -grey_level_matrix.shape[0] + 1, grey_level_matrix.shape[0]) - upper_left_neighbors = sum([ - np.diagflat(np.insert(np.diff(np.diag(grey_level_matrix, i)), 0, 0), i) - for i in diagonals]) - upper_right_neighbors = sum([ - np.diagflat(np.insert(np.diff(np.diag(flipped, i)), 0, 0), i) - for i in diagonals]) + -grey_level_matrix.shape[0] + 1, grey_level_matrix.shape[0] + ) + upper_left_neighbors = sum( + [ + np.diagflat( + np.insert(np.diff(np.diag(grey_level_matrix, i)), 0, 0), i + ) + for i in diagonals + ] + ) + upper_right_neighbors = sum( + [ + np.diagflat(np.insert(np.diff(np.diag(flipped, i)), 0, 0), i) + for i in diagonals + ] + ) lower_right_neighbors = -np.pad( - upper_left_neighbors[1:, 1:], (0, 1), mode='constant') + upper_left_neighbors[1:, 1:], (0, 1), mode="constant" + ) lower_left_neighbors = -np.pad( - upper_right_neighbors[1:, 1:], (0, 1), mode='constant') - return np.dstack(np.array([ - upper_left_neighbors, - up_neighbors, - np.fliplr(upper_right_neighbors), - left_neighbors, - right_neighbors, - np.fliplr(lower_left_neighbors), - down_neighbors, - lower_right_neighbors])) + upper_right_neighbors[1:, 1:], (0, 1), mode="constant" + ) + return np.dstack( + np.array( + [ + upper_left_neighbors, + up_neighbors, + np.fliplr(upper_right_neighbors), + left_neighbors, + right_neighbors, + np.fliplr(lower_left_neighbors), + down_neighbors, + lower_right_neighbors, + ] + ) + ) -def _generate_signature(content: bytes) -> NpMatrix: - im_array = _preprocess_image(content) - image_limits = _crop_image( - im_array, - lower_percentile=LOWER_PERCENTILE, - upper_percentile=UPPER_PERCENTILE) - x_coords, y_coords = _compute_grid_points( - im_array, n=N, window=image_limits) - avg_grey = _compute_mean_level(im_array, x_coords, y_coords, p=P) - diff_matrix = _compute_differentials(avg_grey) - _normalize_and_threshold( - diff_matrix, - identical_tolerance=IDENTICAL_TOLERANCE, - n_levels=N_LEVELS) - return np.ravel(diff_matrix).astype('int8') +def _words_to_int(word_array: NpMatrix) -> List[int]: + width = word_array.shape[1] + coding_vector = 3 ** np.arange(width) + return np.dot(word_array + 1, coding_vector).astype(int).tolist() def _get_words(array: NpMatrix, k: int, n: int) -> NpMatrix: - word_positions = np.linspace( - 0, array.shape[0], n, endpoint=False).astype('int') + word_positions = np.linspace(0, array.shape[0], n, endpoint=False).astype( + "int" + ) assert k <= array.shape[0] assert word_positions.shape[0] <= array.shape[0] - words = np.zeros((n, k)).astype('int8') + words = np.zeros((n, k)).astype("int8") for i, pos in enumerate(word_positions): if pos + k <= array.shape[0]: - words[i] = array[pos:pos + k] + words[i] = array[pos : pos + k] else: temp = array[pos:].copy() - temp.resize(k) + temp.resize(k, refcheck=False) words[i] = temp - _max_contrast(words) - words = _words_to_int(words) + words[words > 0] = 1 + words[words < 0] = -1 return words -def _words_to_int(word_array: NpMatrix) -> NpMatrix: - width = word_array.shape[1] - coding_vector = 3**np.arange(width) - return np.dot(word_array + 1, coding_vector) +def generate_signature(content: bytes) -> NpMatrix: + im_array = _preprocess_image(content) + image_limits = _crop_image( + im_array, + lower_percentile=LOWER_PERCENTILE, + upper_percentile=UPPER_PERCENTILE, + ) + x_coords, y_coords = _compute_grid_points( + im_array, n=N, window=image_limits + ) + avg_grey = _compute_mean_level(im_array, x_coords, y_coords, p=P) + diff_matrix = _compute_differentials(avg_grey) + _normalize_and_threshold( + diff_matrix, identical_tolerance=IDENTICAL_TOLERANCE, n_levels=N_LEVELS + ) + return np.ravel(diff_matrix).astype("int8") -def _max_contrast(array: NpMatrix) -> None: - array[array > 0] = 1 - array[array < 0] = -1 +def generate_words(signature: NpMatrix) -> List[int]: + return _words_to_int(_get_words(signature, k=SAMPLE_WORDS, n=MAX_WORDS)) -def _normalized_distance( - target_array: NpMatrix, - vec: NpMatrix, - nan_value: float = 1.0) -> List[float]: - target_array = target_array.astype(int) +def normalized_distance( + target_array: Any, vec: NpMatrix, nan_value: float = 1.0 +) -> List[float]: + target_array = np.array(target_array).astype(int) vec = vec.astype(int) topvec = np.linalg.norm(vec - target_array, axis=1) norm1 = np.linalg.norm(vec, axis=0) @@ -228,124 +258,50 @@ def _normalized_distance( return finvec -def _safety_blanket(default_param_factory: Callable[[], Any]) -> Callable: - def wrapper_outer(target_function: Callable) -> Callable: - def wrapper_inner(*args: Any, **kwargs: Any) -> Any: - try: - return target_function(*args, **kwargs) - except elasticsearch.exceptions.NotFoundError: - # index not yet created, will be created dynamically by - # add_image() - return default_param_factory() - except elasticsearch.exceptions.ElasticsearchException as ex: - logger.warning('Problem with elastic search: %s', ex) - raise errors.ThirdPartyError( - 'Error connecting to elastic search.') - except IOError: - raise errors.ProcessingError('Not an image.') - except Exception as ex: - raise errors.ThirdPartyError('Unknown error (%s).' % ex) - return wrapper_inner - return wrapper_outer - - -class Lookalike: - def __init__(self, score: int, distance: float, path: Any) -> None: - self.score = score - self.distance = distance - self.path = path - +def pack_signature(signature: NpMatrix) -> bytes: + """ + Serializes the signature vector for efficient storage in a database. -@_safety_blanket(lambda: None) -def add_image(path: str, image_content: bytes) -> None: - assert path - assert image_content - signature = _generate_signature(image_content) - words = _get_words(signature, k=SAMPLE_WORDS, n=MAX_WORDS) + Shifts the range of the signature vector from [-N_LEVELS,+N_LEVELS] + to [0, base] - record = { - 'signature': signature.tolist(), - 'path': path, - 'timestamp': datetime.now(), - } - for i in range(MAX_WORDS): - record['simple_word_' + str(i)] = words[i].tolist() - - _get_session().index( - index=config.config['elasticsearch']['index'], - doc_type=ES_DOC_TYPE, - body=record, - refresh=True) - - -@_safety_blanket(lambda: None) -def delete_image(path: str) -> None: - assert path - _get_session().delete_by_query( - index=config.config['elasticsearch']['index'], - doc_type=ES_DOC_TYPE, - body={'query': {'term': {'path': path}}}) - - -@_safety_blanket(lambda: []) -def search_by_image(image_content: bytes) -> List[Lookalike]: - signature = _generate_signature(image_content) - words = _get_words(signature, k=SAMPLE_WORDS, n=MAX_WORDS) - - res = _get_session().search( - index=config.config['elasticsearch']['index'], - doc_type=ES_DOC_TYPE, - body={ - 'query': - { - 'bool': - { - 'should': - [ - {'term': {'simple_word_%d' % i: word.tolist()}} - for i, word in enumerate(words) - ] - } - }, - '_source': {'excludes': ['simple_word_*']}}, - size=ES_MAX_RESULTS, - timeout='10s')['hits']['hits'] - - if len(res) == 0: - return [] - - sigs = np.array([x['_source']['signature'] for x in res]) - dists = _normalized_distance(sigs, np.array(signature)) - - ids = set() # type: Set[int] - ret = [] - for item, dist in zip(res, dists): - id = item['_id'] - score = item['_score'] - path = item['_source']['path'] - if id in ids: - continue - ids.add(id) - if dist < DISTANCE_CUTOFF: - ret.append(Lookalike(score=score, distance=dist, path=path)) - return ret + The vector can then be broken up into chunks, with each chunk + consisting of SIG_CHUNK_WIDTH digits of radix `base`. + This is then converted into a more packed array consisting of + uint32 elements (for SIG_CHUNK_BITS = 32). + """ + coding_vector = np.flipud(SIG_BASE ** np.arange(SIG_CHUNK_WIDTH)) + return ( + np.array( + [ + np.dot(x, coding_vector) + for x in np.reshape( + signature + N_LEVELS, (-1, SIG_CHUNK_WIDTH) + ) + ] + ) + .astype(f"uint{SIG_CHUNK_BITS}") + .tobytes() + ) -@_safety_blanket(lambda: None) -def purge() -> None: - _get_session().delete_by_query( - index=config.config['elasticsearch']['index'], - doc_type=ES_DOC_TYPE, - body={'query': {'match_all': {}}}, - refresh=True) +def unpack_signature(packed: bytes) -> NpMatrix: + """ + Deserializes the signature vector once recieved from the database. -@_safety_blanket(lambda: set()) -def get_all_paths() -> Set[str]: - search = ( - elasticsearch_dsl.Search( - using=_get_session(), - index=config.config['elasticsearch']['index'], - doc_type=ES_DOC_TYPE) - .source(['path'])) - return set(h.path for h in search.scan()) + Functions as an inverse transformation of pack_signature() + """ + return np.ravel( + np.array( + [ + [ + int(digit) - N_LEVELS + for digit in np.base_repr(e, base=SIG_BASE).zfill( + SIG_CHUNK_WIDTH + ) + ] + for e in np.frombuffer(packed, dtype=f"uint{SIG_CHUNK_BITS}") + ] + ).astype("int8") + ) diff --git a/server/szurubooru/func/images.py b/server/szurubooru/func/images.py index 6e88ab4..6413ac8 100644 --- a/server/szurubooru/func/images.py +++ b/server/szurubooru/func/images.py @@ -1,14 +1,14 @@ -from typing import List -import logging import json -import shlex -import subprocess +import logging import math import re +import shlex +import subprocess +from typing import List + from szurubooru import errors from szurubooru.func import mime, util - logger = logging.getLogger(__name__) @@ -19,97 +19,142 @@ class Image: @property def width(self) -> int: - return self.info['streams'][0]['width'] + return self.info["streams"][0]["width"] @property def height(self) -> int: - return self.info['streams'][0]['height'] + return self.info["streams"][0]["height"] @property def frames(self) -> int: - return self.info['streams'][0]['nb_read_frames'] + return self.info["streams"][0]["nb_read_frames"] def resize_fill(self, width: int, height: int) -> None: width_greater = self.width > self.height width, height = (-1, height) if width_greater else (width, -1) cli = [ - '-i', '{path}', - '-f', 'image2', - '-filter:v', "scale='{width}:{height}'".format( - width=width, height=height), - '-map', '0:v:0', - '-vframes', '1', - '-vcodec', 'png', - '-', + "-i", + "{path}", + "-f", + "image2", + "-filter:v", + "scale='{width}:{height}'".format(width=width, height=height), + "-map", + "0:v:0", + "-vframes", + "1", + "-vcodec", + "png", + "-", ] - if 'duration' in self.info['format'] \ - and self.info['format']['format_name'] != 'swf': - duration = float(self.info['format']['duration']) + if ( + "duration" in self.info["format"] + and self.info["format"]["format_name"] != "swf" + ): + duration = float(self.info["format"]["duration"]) if duration > 3: cli = [ - '-ss', - '%d' % math.floor(duration * 0.3), + "-ss", + "%d" % math.floor(duration * 0.3), ] + cli content = self._execute(cli, ignore_error_if_data=True) if not content: - raise errors.ProcessingError('Error while resizing image.') + raise errors.ProcessingError("Error while resizing image.") self.content = content self._reload_info() def to_png(self) -> bytes: - return self._execute([ - '-i', '{path}', - '-f', 'image2', - '-map', '0:v:0', - '-vframes', '1', - '-vcodec', 'png', - '-', - ]) + return self._execute( + [ + "-i", + "{path}", + "-f", + "image2", + "-map", + "0:v:0", + "-vframes", + "1", + "-vcodec", + "png", + "-", + ] + ) def to_jpeg(self) -> bytes: - return self._execute([ - '-f', 'lavfi', - '-i', 'color=white:s=%dx%d' % (self.width, self.height), - '-i', '{path}', - '-f', 'image2', - '-filter_complex', 'overlay', - '-map', '0:v:0', - '-vframes', '1', - '-vcodec', 'mjpeg', - '-', - ]) + return self._execute( + [ + "-f", + "lavfi", + "-i", + "color=white:s=%dx%d" % (self.width, self.height), + "-i", + "{path}", + "-f", + "image2", + "-filter_complex", + "overlay", + "-map", + "0:v:0", + "-vframes", + "1", + "-vcodec", + "mjpeg", + "-", + ] + ) def to_webm(self) -> bytes: - with util.create_temp_file_path(suffix='.log') as phase_log_path: + with util.create_temp_file_path(suffix=".log") as phase_log_path: # Pass 1 - self._execute([ - '-i', '{path}', - '-pass', '1', - '-passlogfile', phase_log_path, - '-vcodec', 'libvpx-vp9', - '-crf', '4', - '-b:v', '2500K', - '-acodec', 'libvorbis', - '-f', 'webm', - '-y', '/dev/null' - ]) + self._execute( + [ + "-i", + "{path}", + "-pass", + "1", + "-passlogfile", + phase_log_path, + "-vcodec", + "libvpx-vp9", + "-crf", + "4", + "-b:v", + "2500K", + "-acodec", + "libvorbis", + "-f", + "webm", + "-y", + "/dev/null", + ] + ) # Pass 2 - return self._execute([ - '-i', '{path}', - '-pass', '2', - '-passlogfile', phase_log_path, - '-vcodec', 'libvpx-vp9', - '-crf', '4', - '-b:v', '2500K', - '-acodec', 'libvorbis', - '-f', 'webm', - '-' - ]) + return self._execute( + [ + "-i", + "{path}", + "-pass", + "2", + "-passlogfile", + phase_log_path, + "-vcodec", + "libvpx-vp9", + "-crf", + "4", + "-b:v", + "2500K", + "-acodec", + "libvorbis", + "-f", + "webm", + "-", + ] + ) def to_mp4(self) -> bytes: - with util.create_temp_file_path(suffix='.dat') as mp4_temp_path: + with util.create_temp_file_path(suffix=".dat") as mp4_temp_path: width = self.width height = self.height altered_dimensions = False @@ -123,96 +168,138 @@ class Image: altered_dimensions = True args = [ - '-i', '{path}', - '-vcodec', 'libx264', - '-preset', 'slow', - '-crf', '22', - '-b:v', '200K', - '-profile:v', 'main', - '-pix_fmt', 'yuv420p', - '-acodec', 'aac', - '-f', 'mp4' + "-i", + "{path}", + "-vcodec", + "libx264", + "-preset", + "slow", + "-crf", + "22", + "-b:v", + "200K", + "-profile:v", + "main", + "-pix_fmt", + "yuv420p", + "-acodec", + "aac", + "-f", + "mp4", ] if altered_dimensions: - args += ['-filter:v', 'scale=\'%d:%d\'' % (width, height)] + args += ["-filter:v", "scale='%d:%d'" % (width, height)] - self._execute(args + ['-y', mp4_temp_path]) + self._execute(args + ["-y", mp4_temp_path]) - with open(mp4_temp_path, 'rb') as mp4_temp: + with open(mp4_temp_path, "rb") as mp4_temp: return mp4_temp.read() def check_for_sound(self) -> bool: - audioinfo = json.loads(self._execute([ - '-i', '{path}', - '-of', 'json', - '-select_streams', 'a', - '-show_streams', - ], program='ffprobe').decode('utf-8')) - assert 'streams' in audioinfo - if len(audioinfo['streams']) < 1: + audioinfo = json.loads( + self._execute( + [ + "-i", + "{path}", + "-of", + "json", + "-select_streams", + "a", + "-show_streams", + ], + program="ffprobe", + ).decode("utf-8") + ) + assert "streams" in audioinfo + if len(audioinfo["streams"]) < 1: return False - log = self._execute([ - '-hide_banner', - '-progress', '-', - '-i', '{path}', - '-af', 'volumedetect', - '-max_muxing_queue_size', '99999', - '-vn', '-sn', - '-f', 'null', - '-y', '/dev/null', - ], get_logs=True).decode('utf-8', errors='replace') - log_match = re.search(r'.*volumedetect.*mean_volume: (.*) dB', log) - assert log_match - assert log_match.groups() + log = self._execute( + [ + "-hide_banner", + "-progress", + "-", + "-i", + "{path}", + "-af", + "volumedetect", + "-max_muxing_queue_size", + "99999", + "-vn", + "-sn", + "-f", + "null", + "-y", + "/dev/null", + ], + get_logs=True, + ).decode("utf-8", errors="replace") + log_match = re.search(r".*volumedetect.*mean_volume: (.*) dB", log) + if not log_match or not log_match.groups(): + raise errors.ProcessingError( + "A problem occured when trying to check for audio" + ) meanvol = float(log_match.groups()[0]) # -91.0 dB is the minimum for 16-bit audio, assume sound if > -80.0 dB return meanvol > -80.0 def _execute( - self, - cli: List[str], - program: str = 'ffmpeg', - ignore_error_if_data: bool = False, - get_logs: bool = False) -> bytes: + self, + cli: List[str], + program: str = "ffmpeg", + ignore_error_if_data: bool = False, + get_logs: bool = False, + ) -> bytes: extension = mime.get_extension(mime.get_mime_type(self.content)) assert extension - with util.create_temp_file(suffix='.' + extension) as handle: + with util.create_temp_file(suffix="." + extension) as handle: handle.write(self.content) handle.flush() - cli = [program, '-loglevel', '32' if get_logs else '24'] + cli + cli = [program, "-loglevel", "32" if get_logs else "24"] + cli cli = [part.format(path=handle.name) for part in cli] proc = subprocess.Popen( cli, stdout=subprocess.PIPE, stdin=subprocess.PIPE, - stderr=subprocess.PIPE) + stderr=subprocess.PIPE, + ) out, err = proc.communicate(input=self.content) if proc.returncode != 0: logger.warning( - 'Failed to execute ffmpeg command (cli=%r, err=%r)', - ' '.join(shlex.quote(arg) for arg in cli), - err) - if ((len(out) > 0 and not ignore_error_if_data) - or len(out) == 0): + "Failed to execute ffmpeg command (cli=%r, err=%r)", + " ".join(shlex.quote(arg) for arg in cli), + err, + ) + if (len(out) > 0 and not ignore_error_if_data) or len( + out + ) == 0: raise errors.ProcessingError( - 'Error while processing image.\n' - + err.decode('utf-8')) + "Error while processing image.\n" + err.decode("utf-8") + ) return err if get_logs else out def _reload_info(self) -> None: - self.info = json.loads(self._execute([ - '-i', '{path}', - '-of', 'json', - '-select_streams', 'v', - '-show_format', - '-show_streams', - ], program='ffprobe').decode('utf-8')) - assert 'format' in self.info - assert 'streams' in self.info - if len(self.info['streams']) < 1: - logger.warning('The video contains no video streams.') + self.info = json.loads( + self._execute( + [ + "-i", + "{path}", + "-of", + "json", + "-select_streams", + "v", + "-show_format", + "-show_streams", + ], + program="ffprobe", + ).decode("utf-8") + ) + assert "format" in self.info + assert "streams" in self.info + if len(self.info["streams"]) < 1: + logger.warning("The video contains no video streams.") raise errors.ProcessingError( - 'The video contains no video streams.') + "The video contains no video streams." + ) diff --git a/server/szurubooru/func/mailer.py b/server/szurubooru/func/mailer.py index 76682f1..c4cf9db 100644 --- a/server/szurubooru/func/mailer.py +++ b/server/szurubooru/func/mailer.py @@ -1,16 +1,18 @@ -import smtplib import email.mime.text +import smtplib + from szurubooru import config def send_mail(sender: str, recipient: str, subject: str, body: str) -> None: msg = email.mime.text.MIMEText(body) - msg['Subject'] = subject - msg['From'] = sender - msg['To'] = recipient + msg["Subject"] = subject + msg["From"] = sender + msg["To"] = recipient smtp = smtplib.SMTP( - config.config['smtp']['host'], int(config.config['smtp']['port'])) - smtp.login(config.config['smtp']['user'], config.config['smtp']['pass']) + config.config["smtp"]["host"], int(config.config["smtp"]["port"]) + ) + smtp.login(config.config["smtp"]["user"], config.config["smtp"]["pass"]) smtp.send_message(msg) smtp.quit() diff --git a/server/szurubooru/func/mime.py b/server/szurubooru/func/mime.py index c83f744..5f6279b 100644 --- a/server/szurubooru/func/mime.py +++ b/server/szurubooru/func/mime.py @@ -4,55 +4,66 @@ from typing import Optional def get_mime_type(content: bytes) -> str: if not content: - return 'application/octet-stream' + return "application/octet-stream" - if content[0:3] in (b'CWS', b'FWS', b'ZWS'): - return 'application/x-shockwave-flash' + if content[0:3] in (b"CWS", b"FWS", b"ZWS"): + return "application/x-shockwave-flash" - if content[0:3] == b'\xFF\xD8\xFF': - return 'image/jpeg' + if content[0:3] == b"\xFF\xD8\xFF": + return "image/jpeg" - if content[0:6] == b'\x89PNG\x0D\x0A': - return 'image/png' + if content[0:6] == b"\x89PNG\x0D\x0A": + return "image/png" - if content[0:6] in (b'GIF87a', b'GIF89a'): - return 'image/gif' + if content[0:6] in (b"GIF87a", b"GIF89a"): + return "image/gif" - if content[0:4] == b'\x1A\x45\xDF\xA3': - return 'video/webm' + if content[8:12] == b"WEBP": + return "image/webp" - if content[4:12] in (b'ftypisom', b'ftypmp42'): - return 'video/mp4' + if content[0:4] == b"\x1A\x45\xDF\xA3": + return "video/webm" - return 'application/octet-stream' + if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypmp42"): + return "video/mp4" + + return "application/octet-stream" def get_extension(mime_type: str) -> Optional[str]: extension_map = { - 'application/x-shockwave-flash': 'swf', - 'image/gif': 'gif', - 'image/jpeg': 'jpg', - 'image/png': 'png', - 'video/mp4': 'mp4', - 'video/webm': 'webm', - 'application/octet-stream': 'dat', + "application/x-shockwave-flash": "swf", + "image/gif": "gif", + "image/jpeg": "jpg", + "image/png": "png", + "image/webp": "webp", + "video/mp4": "mp4", + "video/webm": "webm", + "application/octet-stream": "dat", } - return extension_map.get((mime_type or '').strip().lower(), None) + return extension_map.get((mime_type or "").strip().lower(), None) def is_flash(mime_type: str) -> bool: - return mime_type.lower() == 'application/x-shockwave-flash' + return mime_type.lower() == "application/x-shockwave-flash" def is_video(mime_type: str) -> bool: - return mime_type.lower() in ('application/ogg', 'video/mp4', 'video/webm') + return mime_type.lower() in ("application/ogg", "video/mp4", "video/webm") def is_image(mime_type: str) -> bool: - return mime_type.lower() in ('image/jpeg', 'image/png', 'image/gif') + return mime_type.lower() in ( + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + ) def is_animated_gif(content: bytes) -> bool: - pattern = b'\x21\xF9\x04[\x00-\xFF]{4}\x00[\x2C\x21]' - return get_mime_type(content) == 'image/gif' \ + pattern = b"\x21\xF9\x04[\x00-\xFF]{4}\x00[\x2C\x21]" + return ( + get_mime_type(content) == "image/gif" and len(re.findall(pattern, content)) > 1 + ) diff --git a/server/szurubooru/func/net.py b/server/szurubooru/func/net.py index e6326c0..4e4c222 100644 --- a/server/szurubooru/func/net.py +++ b/server/szurubooru/func/net.py @@ -1,16 +1,94 @@ +import json +import logging +import os +import urllib.error import urllib.request -from szurubooru import config -from szurubooru import errors +from tempfile import NamedTemporaryFile +from threading import Thread +from typing import Any, Dict, List +from youtube_dl import YoutubeDL +from youtube_dl.utils import YoutubeDLError -def download(url: str) -> bytes: +from szurubooru import config, errors +from szurubooru.func import mime, util + +logger = logging.getLogger(__name__) + + +def download(url: str, use_video_downloader: bool = False) -> bytes: assert url request = urllib.request.Request(url) - if config.config['user_agent']: - request.add_header('User-Agent', config.config['user_agent']) - request.add_header('Referer', url) + if config.config["user_agent"]: + request.add_header("User-Agent", config.config["user_agent"]) + request.add_header("Referer", url) try: with urllib.request.urlopen(request) as handle: - return handle.read() + content = handle.read() except Exception as ex: - raise errors.ProcessingError('Error downloading %s (%s)' % (url, ex)) + raise errors.ProcessingError("Error downloading %s (%s)" % (url, ex)) + if ( + use_video_downloader + and mime.get_mime_type(content) == "application/octet-stream" + ): + return _youtube_dl_wrapper(url) + return content + + +def _youtube_dl_wrapper(url: str) -> bytes: + outpath = os.path.join( + config.config["data_dir"], + "temporary-uploads", + "youtubedl-" + util.get_sha1(url)[0:8] + ".dat", + ) + options = { + "ignoreerrors": False, + "format": "best[ext=webm]/best[ext=mp4]/best[ext=flv]", + "logger": logger, + "max_filesize": config.config["max_dl_filesize"], + "max_downloads": 1, + "outtmpl": outpath, + } + try: + with YoutubeDL(options) as ydl: + ydl.extract_info(url, download=True) + with open(outpath, "rb") as f: + return f.read() + except YoutubeDLError as ex: + raise errors.ThirdPartyError( + "Error downloading video %s (%s)" % (url, ex) + ) + except FileNotFoundError: + raise errors.ThirdPartyError( + "Error downloading video %s (file could not be saved)" % (url) + ) + + +def post_to_webhooks(payload: Dict[str, Any]) -> List[Thread]: + threads = [ + Thread(target=_post_to_webhook, args=(webhook, payload)) + for webhook in (config.config["webhooks"] or []) + ] + for thread in threads: + thread.daemon = False + thread.start() + return threads + + +def _post_to_webhook(webhook: str, payload: Dict[str, Any]) -> None: + req = urllib.request.Request(webhook) + req.data = json.dumps( + payload, + default=lambda x: x.isoformat("T") + "Z", + ).encode("utf-8") + req.add_header("Content-Type", "application/json") + try: + res = urllib.request.urlopen(req) + if not 200 <= res.status <= 299: + logger.warning( + f"Webhook {webhook} returned {res.status} {res.reason}" + ) + return res.status + except urllib.error.URLError as e: + logger.warning(f"Unable to call webhook {webhook}: {str(e)}") + return 400 diff --git a/server/szurubooru/func/pool_categories.py b/server/szurubooru/func/pool_categories.py new file mode 100644 index 0000000..c13857c --- /dev/null +++ b/server/szurubooru/func/pool_categories.py @@ -0,0 +1,207 @@ +import re +from typing import Any, Callable, Dict, List, Optional + +import sqlalchemy as sa + +from szurubooru import config, db, errors, model, rest +from szurubooru.func import cache, serialization, util + +DEFAULT_CATEGORY_NAME_CACHE_KEY = "default-pool-category" + + +class PoolCategoryNotFoundError(errors.NotFoundError): + pass + + +class PoolCategoryAlreadyExistsError(errors.ValidationError): + pass + + +class PoolCategoryIsInUseError(errors.ValidationError): + pass + + +class InvalidPoolCategoryNameError(errors.ValidationError): + pass + + +class InvalidPoolCategoryColorError(errors.ValidationError): + pass + + +def _verify_name_validity(name: str) -> None: + name_regex = config.config["pool_category_name_regex"] + if not re.match(name_regex, name): + raise InvalidPoolCategoryNameError( + "Name must satisfy regex %r." % name_regex + ) + + +class PoolCategorySerializer(serialization.BaseSerializer): + def __init__(self, category: model.PoolCategory) -> None: + self.category = category + + def _serializers(self) -> Dict[str, Callable[[], Any]]: + return { + "name": self.serialize_name, + "version": self.serialize_version, + "color": self.serialize_color, + "usages": self.serialize_usages, + "default": self.serialize_default, + } + + def serialize_name(self) -> Any: + return self.category.name + + def serialize_version(self) -> Any: + return self.category.version + + def serialize_color(self) -> Any: + return self.category.color + + def serialize_usages(self) -> Any: + return self.category.pool_count + + def serialize_default(self) -> Any: + return self.category.default + + +def serialize_category( + category: Optional[model.PoolCategory], options: List[str] = [] +) -> Optional[rest.Response]: + if not category: + return None + return PoolCategorySerializer(category).serialize(options) + + +def create_category(name: str, color: str) -> model.PoolCategory: + category = model.PoolCategory() + update_category_name(category, name) + update_category_color(category, color) + if not get_all_categories(): + category.default = True + return category + + +def update_category_name(category: model.PoolCategory, name: str) -> None: + assert category + if not name: + raise InvalidPoolCategoryNameError("Name cannot be empty.") + expr = sa.func.lower(model.PoolCategory.name) == name.lower() + if category.pool_category_id: + expr = expr & ( + model.PoolCategory.pool_category_id != category.pool_category_id + ) + already_exists = ( + db.session.query(model.PoolCategory).filter(expr).count() > 0 + ) + if already_exists: + raise PoolCategoryAlreadyExistsError( + "A category with this name already exists." + ) + if util.value_exceeds_column_size(name, model.PoolCategory.name): + raise InvalidPoolCategoryNameError("Name is too long.") + _verify_name_validity(name) + category.name = name + cache.remove(DEFAULT_CATEGORY_NAME_CACHE_KEY) + + +def update_category_color(category: model.PoolCategory, color: str) -> None: + assert category + if not color: + raise InvalidPoolCategoryColorError("Color cannot be empty.") + if not re.match(r"^#?[0-9a-z]+$", color): + raise InvalidPoolCategoryColorError("Invalid color.") + if util.value_exceeds_column_size(color, model.PoolCategory.color): + raise InvalidPoolCategoryColorError("Color is too long.") + category.color = color + + +def try_get_category_by_name( + name: str, lock: bool = False +) -> Optional[model.PoolCategory]: + query = db.session.query(model.PoolCategory).filter( + sa.func.lower(model.PoolCategory.name) == name.lower() + ) + if lock: + query = query.with_for_update() + return query.one_or_none() + + +def get_category_by_name(name: str, lock: bool = False) -> model.PoolCategory: + category = try_get_category_by_name(name, lock) + if not category: + raise PoolCategoryNotFoundError("Pool category %r not found." % name) + return category + + +def get_all_category_names() -> List[str]: + return [cat.name for cat in get_all_categories()] + + +def get_all_categories() -> List[model.PoolCategory]: + return ( + db.session.query(model.PoolCategory) + .order_by(model.PoolCategory.name.asc()) + .all() + ) + + +def try_get_default_category( + lock: bool = False, +) -> Optional[model.PoolCategory]: + query = db.session.query(model.PoolCategory).filter( + model.PoolCategory.default + ) + if lock: + query = query.with_for_update() + category = query.first() + # if for some reason (e.g. as a result of migration) there's no default + # category, get the first record available. + if not category: + query = db.session.query(model.PoolCategory).order_by( + model.PoolCategory.pool_category_id.asc() + ) + if lock: + query = query.with_for_update() + category = query.first() + return category + + +def get_default_category(lock: bool = False) -> model.PoolCategory: + category = try_get_default_category(lock) + if not category: + raise PoolCategoryNotFoundError("No pool category created yet.") + return category + + +def get_default_category_name() -> str: + if cache.has(DEFAULT_CATEGORY_NAME_CACHE_KEY): + return cache.get(DEFAULT_CATEGORY_NAME_CACHE_KEY) + default_category = get_default_category() + default_category_name = default_category.name + cache.put(DEFAULT_CATEGORY_NAME_CACHE_KEY, default_category_name) + return default_category_name + + +def set_default_category(category: model.PoolCategory) -> None: + assert category + old_category = try_get_default_category(lock=True) + if old_category: + db.session.refresh(old_category) + old_category.default = False + db.session.refresh(category) + category.default = True + cache.remove(DEFAULT_CATEGORY_NAME_CACHE_KEY) + + +def delete_category(category: model.PoolCategory) -> None: + assert category + if len(get_all_category_names()) == 1: + raise PoolCategoryIsInUseError("Cannot delete the last category.") + if (category.pool_count or 0) > 0: + raise PoolCategoryIsInUseError( + "Pool category has some usages and cannot be deleted. " + + "Please remove this category from relevant pools first." + ) + db.session.delete(category) diff --git a/server/szurubooru/func/pools.py b/server/szurubooru/func/pools.py new file mode 100644 index 0000000..c3ea9f0 --- /dev/null +++ b/server/szurubooru/func/pools.py @@ -0,0 +1,337 @@ +import re +from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Tuple + +import sqlalchemy as sa + +from szurubooru import config, db, errors, model, rest +from szurubooru.func import pool_categories, posts, serialization, util + + +class PoolNotFoundError(errors.NotFoundError): + pass + + +class PoolAlreadyExistsError(errors.ValidationError): + pass + + +class PoolIsInUseError(errors.ValidationError): + pass + + +class InvalidPoolNameError(errors.ValidationError): + pass + + +class InvalidPoolDuplicateError(errors.ValidationError): + pass + + +class InvalidPoolCategoryError(errors.ValidationError): + pass + + +class InvalidPoolDescriptionError(errors.ValidationError): + pass + + +class InvalidPoolRelationError(errors.ValidationError): + pass + + +class InvalidPoolNonexistentPostError(errors.ValidationError): + pass + + +def _verify_name_validity(name: str) -> None: + if util.value_exceeds_column_size(name, model.PoolName.name): + raise InvalidPoolNameError("Name is too long.") + name_regex = config.config["pool_name_regex"] + if not re.match(name_regex, name): + raise InvalidPoolNameError("Name must satisfy regex %r." % name_regex) + + +def _get_names(pool: model.Pool) -> List[str]: + assert pool + return [pool_name.name for pool_name in pool.names] + + +def _lower_list(names: List[str]) -> List[str]: + return [name.lower() for name in names] + + +def _check_name_intersection( + names1: List[str], names2: List[str], case_sensitive: bool +) -> bool: + if not case_sensitive: + names1 = _lower_list(names1) + names2 = _lower_list(names2) + return len(set(names1).intersection(names2)) > 0 + + +def _duplicates(a: List[int]) -> List[int]: + seen = set() + dupes = [] + for x in a: + if x not in seen: + seen.add(x) + else: + dupes.append(x) + return dupes + + +def sort_pools(pools: List[model.Pool]) -> List[model.Pool]: + default_category_name = pool_categories.get_default_category_name() + return sorted( + pools, + key=lambda pool: ( + default_category_name == pool.category.name, + pool.category.name, + pool.names[0].name, + ), + ) + + +class PoolSerializer(serialization.BaseSerializer): + def __init__(self, pool: model.Pool) -> None: + self.pool = pool + + def _serializers(self) -> Dict[str, Callable[[], Any]]: + return { + "id": self.serialize_id, + "names": self.serialize_names, + "category": self.serialize_category, + "version": self.serialize_version, + "description": self.serialize_description, + "creationTime": self.serialize_creation_time, + "lastEditTime": self.serialize_last_edit_time, + "postCount": self.serialize_post_count, + "posts": self.serialize_posts, + } + + def serialize_id(self) -> Any: + return self.pool.pool_id + + def serialize_names(self) -> Any: + return [pool_name.name for pool_name in self.pool.names] + + def serialize_category(self) -> Any: + return self.pool.category.name + + def serialize_version(self) -> Any: + return self.pool.version + + def serialize_description(self) -> Any: + return self.pool.description + + def serialize_creation_time(self) -> Any: + return self.pool.creation_time + + def serialize_last_edit_time(self) -> Any: + return self.pool.last_edit_time + + def serialize_post_count(self) -> Any: + return self.pool.post_count + + def serialize_posts(self) -> Any: + return [ + post + for post in [ + posts.serialize_micro_post(rel, None) + for rel in self.pool.posts + ] + ] + + +def serialize_pool( + pool: model.Pool, options: List[str] = [] +) -> Optional[rest.Response]: + if not pool: + return None + return PoolSerializer(pool).serialize(options) + + +def serialize_micro_pool(pool: model.Pool) -> Optional[rest.Response]: + return serialize_pool( + pool, options=["id", "names", "category", "description", "postCount"] + ) + + +def try_get_pool_by_id(pool_id: int) -> Optional[model.Pool]: + return ( + db.session.query(model.Pool) + .filter(model.Pool.pool_id == pool_id) + .one_or_none() + ) + + +def get_pool_by_id(pool_id: int) -> model.Pool: + pool = try_get_pool_by_id(pool_id) + if not pool: + raise PoolNotFoundError("Pool %r not found." % pool_id) + return pool + + +def try_get_pool_by_name(name: str) -> Optional[model.Pool]: + return ( + db.session.query(model.Pool) + .join(model.PoolName) + .filter(sa.func.lower(model.PoolName.name) == name.lower()) + .one_or_none() + ) + + +def get_pool_by_name(name: str) -> model.Pool: + pool = try_get_pool_by_name(name) + if not pool: + raise PoolNotFoundError("Pool %r not found." % name) + return pool + + +def get_pools_by_names(names: List[str]) -> List[model.Pool]: + names = util.icase_unique(names) + if len(names) == 0: + return [] + return ( + db.session.query(model.Pool) + .join(model.PoolName) + .filter( + sa.sql.or_( + sa.func.lower(model.PoolName.name) == name.lower() + for name in names + ) + ) + .all() + ) + + +def get_or_create_pools_by_names( + names: List[str], +) -> Tuple[List[model.Pool], List[model.Pool]]: + names = util.icase_unique(names) + existing_pools = get_pools_by_names(names) + new_pools = [] + pool_category_name = pool_categories.get_default_category_name() + for name in names: + found = False + for existing_pool in existing_pools: + if _check_name_intersection( + _get_names(existing_pool), [name], False + ): + found = True + break + if not found: + new_pool = create_pool( + names=[name], category_name=pool_category_name, post_ids=[] + ) + db.session.add(new_pool) + new_pools.append(new_pool) + return existing_pools, new_pools + + +def delete(source_pool: model.Pool) -> None: + assert source_pool + db.session.delete(source_pool) + + +def merge_pools(source_pool: model.Pool, target_pool: model.Pool) -> None: + assert source_pool + assert target_pool + if source_pool.pool_id == target_pool.pool_id: + raise InvalidPoolRelationError("Cannot merge pool with itself.") + + def merge_pool_posts(source_pool_id: int, target_pool_id: int) -> None: + alias1 = model.PoolPost + alias2 = sa.orm.util.aliased(model.PoolPost) + update_stmt = sa.sql.expression.update(alias1).where( + alias1.pool_id == source_pool_id + ) + update_stmt = update_stmt.where( + ~sa.exists() + .where(alias1.post_id == alias2.post_id) + .where(alias2.pool_id == target_pool_id) + ) + update_stmt = update_stmt.values(pool_id=target_pool_id) + db.session.execute(update_stmt) + + merge_pool_posts(source_pool.pool_id, target_pool.pool_id) + delete(source_pool) + + +def create_pool( + names: List[str], category_name: str, post_ids: List[int] +) -> model.Pool: + pool = model.Pool() + pool.creation_time = datetime.utcnow() + update_pool_names(pool, names) + update_pool_category_name(pool, category_name) + update_pool_posts(pool, post_ids) + return pool + + +def update_pool_category_name(pool: model.Pool, category_name: str) -> None: + assert pool + pool.category = pool_categories.get_category_by_name(category_name) + + +def update_pool_names(pool: model.Pool, names: List[str]) -> None: + # sanitize + assert pool + names = util.icase_unique([name for name in names if name]) + if not len(names): + raise InvalidPoolNameError("At least one name must be specified.") + for name in names: + _verify_name_validity(name) + + # check for existing pools + expr = sa.sql.false() + for name in names: + expr = expr | (sa.func.lower(model.PoolName.name) == name.lower()) + if pool.pool_id: + expr = expr & (model.PoolName.pool_id != pool.pool_id) + existing_pools = db.session.query(model.PoolName).filter(expr).all() + if len(existing_pools): + raise PoolAlreadyExistsError( + "One of names is already used by another pool." + ) + + # remove unwanted items + for pool_name in pool.names[:]: + if not _check_name_intersection([pool_name.name], names, True): + pool.names.remove(pool_name) + # add wanted items + for name in names: + if not _check_name_intersection(_get_names(pool), [name], True): + pool.names.append(model.PoolName(name, -1)) + + # set alias order to match the request + for i, name in enumerate(names): + for pool_name in pool.names: + if pool_name.name.lower() == name.lower(): + pool_name.order = i + + +def update_pool_description(pool: model.Pool, description: str) -> None: + assert pool + if util.value_exceeds_column_size(description, model.Pool.description): + raise InvalidPoolDescriptionError("Description is too long.") + pool.description = description or None + + +def update_pool_posts(pool: model.Pool, post_ids: List[int]) -> None: + assert pool + dupes = _duplicates(post_ids) + if len(dupes) > 0: + dupes = ", ".join(list(str(x) for x in dupes)) + raise InvalidPoolDuplicateError("Duplicate post(s) in pool: " + dupes) + ret = posts.get_posts_by_ids(post_ids) + if len(post_ids) != len(ret): + missing = set(post_ids) - set(post.post_id for post in ret) + missing = ", ".join(list(str(x) for x in missing)) + raise InvalidPoolNonexistentPostError( + "The following posts do not exist: " + missing + ) + pool.posts.clear() + for post in ret: + pool.posts.append(post) diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index b6816b0..ee7c31a 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -1,17 +1,35 @@ import hmac -from typing import Any, Optional, Tuple, List, Dict, Callable +import logging from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Tuple + import sqlalchemy as sa -from szurubooru import config, db, model, errors, rest + +from szurubooru import config, db, errors, model, rest from szurubooru.func import ( - users, scores, comments, tags, metrics, util, - mime, images, files, image_hash, serialization, snapshots) + comments, + files, + image_hash, + images, + metrics, + mime, + pools, + scores, + serialization, + snapshots, + tags, + users, + util, +) + +logger = logging.getLogger(__name__) EMPTY_PIXEL = ( - b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00' - b'\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00' - b'\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b') + b"\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00" + b"\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00" + b"\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b" +) class PostNotFoundError(errors.NotFoundError): @@ -25,11 +43,12 @@ class PostAlreadyFeaturedError(errors.ValidationError): class PostAlreadyUploadedError(errors.ValidationError): def __init__(self, other_post: model.Post) -> None: super().__init__( - 'Post already uploaded (%d)' % other_post.post_id, + "Post already uploaded (%d)" % other_post.post_id, { - 'otherPostUrl': get_post_content_url(other_post), - 'otherPostId': other_post.post_id, - }) + "otherPostUrl": get_post_content_url(other_post), + "otherPostId": other_post.post_id, + }, + ) class InvalidPostIdError(errors.ValidationError): @@ -60,81 +79,83 @@ class InvalidPostFlagError(errors.ValidationError): pass -class PostLookalike(image_hash.Lookalike): - def __init__(self, score: int, distance: float, post: model.Post) -> None: - super().__init__(score, distance, post.post_id) - self.post = post - - SAFETY_MAP = { - model.Post.SAFETY_SAFE: 'safe', - model.Post.SAFETY_SKETCHY: 'sketchy', - model.Post.SAFETY_UNSAFE: 'unsafe', + model.Post.SAFETY_SAFE: "safe", + model.Post.SAFETY_SKETCHY: "sketchy", + model.Post.SAFETY_UNSAFE: "unsafe", } TYPE_MAP = { - model.Post.TYPE_IMAGE: 'image', - model.Post.TYPE_ANIMATION: 'animation', - model.Post.TYPE_VIDEO: 'video', - model.Post.TYPE_FLASH: 'flash', + model.Post.TYPE_IMAGE: "image", + model.Post.TYPE_ANIMATION: "animation", + model.Post.TYPE_VIDEO: "video", + model.Post.TYPE_FLASH: "flash", } FLAG_MAP = { - model.Post.FLAG_LOOP: 'loop', - model.Post.FLAG_SOUND: 'sound', + model.Post.FLAG_LOOP: "loop", + model.Post.FLAG_SOUND: "sound", } def get_post_security_hash(id: int) -> str: return hmac.new( - config.config['secret'].encode('utf8'), - str(id).encode('utf-8')).hexdigest()[0:16] + config.config["secret"].encode("utf8"), + msg=str(id).encode("utf-8"), + digestmod="md5", + ).hexdigest()[0:16] def get_post_content_url(post: model.Post) -> str: assert post - return '%s/posts/%d_%s.%s' % ( - config.config['data_url'].rstrip('/'), + return "%s/posts/%d_%s.%s" % ( + config.config["data_url"].rstrip("/"), post.post_id, get_post_security_hash(post.post_id), - mime.get_extension(post.mime_type) or 'dat') + mime.get_extension(post.mime_type) or "dat", + ) def get_post_thumbnail_url(post: model.Post) -> str: assert post - return '%s/generated-thumbnails/%d_%s.jpg' % ( - config.config['data_url'].rstrip('/'), + return "%s/generated-thumbnails/%d_%s.jpg" % ( + config.config["data_url"].rstrip("/"), post.post_id, - get_post_security_hash(post.post_id)) + get_post_security_hash(post.post_id), + ) def get_post_content_path(post: model.Post) -> str: assert post assert post.post_id - return 'posts/%d_%s.%s' % ( + return "posts/%d_%s.%s" % ( post.post_id, get_post_security_hash(post.post_id), - mime.get_extension(post.mime_type) or 'dat') + mime.get_extension(post.mime_type) or "dat", + ) def get_post_thumbnail_path(post: model.Post) -> str: assert post - return 'generated-thumbnails/%d_%s.jpg' % ( + return "generated-thumbnails/%d_%s.jpg" % ( post.post_id, - get_post_security_hash(post.post_id)) + get_post_security_hash(post.post_id), + ) def get_post_thumbnail_backup_path(post: model.Post) -> str: assert post - return 'posts/custom-thumbnails/%d_%s.dat' % ( - post.post_id, get_post_security_hash(post.post_id)) + return "posts/custom-thumbnails/%d_%s.dat" % ( + post.post_id, + get_post_security_hash(post.post_id), + ) def serialize_note(note: model.PostNote) -> rest.Response: assert note return { - 'polygon': note.polygon, - 'text': note.text, + "polygon": note.polygon, + "text": note.text, } @@ -145,40 +166,41 @@ class PostSerializer(serialization.BaseSerializer): def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'id': self.serialize_id, - 'version': self.serialize_version, - 'creationTime': self.serialize_creation_time, - 'lastEditTime': self.serialize_last_edit_time, - 'safety': self.serialize_safety, - 'source': self.serialize_source, - 'type': self.serialize_type, - 'mimeType': self.serialize_mime, - 'checksum': self.serialize_checksum, - 'fileSize': self.serialize_file_size, - 'canvasWidth': self.serialize_canvas_width, - 'canvasHeight': self.serialize_canvas_height, - 'contentUrl': self.serialize_content_url, - 'thumbnailUrl': self.serialize_thumbnail_url, - 'flags': self.serialize_flags, - 'tags': self.serialize_tags, - 'relations': self.serialize_relations, - 'user': self.serialize_user, - 'score': self.serialize_score, - 'ownScore': self.serialize_own_score, - 'ownFavorite': self.serialize_own_favorite, - 'tagCount': self.serialize_tag_count, - 'favoriteCount': self.serialize_favorite_count, - 'commentCount': self.serialize_comment_count, - 'noteCount': self.serialize_note_count, - 'relationCount': self.serialize_relation_count, - 'featureCount': self.serialize_feature_count, - 'lastFeatureTime': self.serialize_last_feature_time, - 'favoritedBy': self.serialize_favorited_by, - 'hasCustomThumbnail': self.serialize_has_custom_thumbnail, - 'notes': self.serialize_notes, - 'comments': self.serialize_comments, - 'metrics': self.serialize_metrics, - 'metricRanges': self.serialize_metric_ranges, + "id": self.serialize_id, + "version": self.serialize_version, + "creationTime": self.serialize_creation_time, + "lastEditTime": self.serialize_last_edit_time, + "safety": self.serialize_safety, + "source": self.serialize_source, + "type": self.serialize_type, + "mimeType": self.serialize_mime, + "checksum": self.serialize_checksum, + "fileSize": self.serialize_file_size, + "canvasWidth": self.serialize_canvas_width, + "canvasHeight": self.serialize_canvas_height, + "contentUrl": self.serialize_content_url, + "thumbnailUrl": self.serialize_thumbnail_url, + "flags": self.serialize_flags, + "tags": self.serialize_tags, + "relations": self.serialize_relations, + "user": self.serialize_user, + "score": self.serialize_score, + "ownScore": self.serialize_own_score, + "ownFavorite": self.serialize_own_favorite, + "tagCount": self.serialize_tag_count, + "favoriteCount": self.serialize_favorite_count, + "commentCount": self.serialize_comment_count, + "noteCount": self.serialize_note_count, + "relationCount": self.serialize_relation_count, + "featureCount": self.serialize_feature_count, + "lastFeatureTime": self.serialize_last_feature_time, + "favoritedBy": self.serialize_favorited_by, + "hasCustomThumbnail": self.serialize_has_custom_thumbnail, + "notes": self.serialize_notes, + "comments": self.serialize_comments, + "metrics": self.serialize_metrics, + "metricRanges": self.serialize_metric_ranges, + "pools": self.serialize_pools, } def serialize_id(self) -> Any: @@ -229,25 +251,28 @@ class PostSerializer(serialization.BaseSerializer): def serialize_tags(self) -> Any: return [ { - 'names': [name.name for name in tag.names], - 'category': tag.category.name, - 'usages': tag.post_count, - 'metric': { - 'min': tag.metric.min, - 'max': tag.metric.max + "names": [name.name for name in tag.names], + "category": tag.category.name, + "usages": tag.post_count, + "metric": { + "min": tag.metric.min, + "max": tag.metric.max } if tag.metric else None, } - for tag in tags.sort_tags(self.post.tags)] + for tag in tags.sort_tags(self.post.tags) + ] def serialize_relations(self) -> Any: return sorted( { - post['id']: post + post["id"]: post for post in [ serialize_micro_post(rel, self.auth_user) - for rel in self.post.relations] + for rel in self.post.relations + ] }.values(), - key=lambda post: post['id']) + key=lambda post: post["id"], + ) def serialize_user(self) -> Any: return users.serialize_micro_user(self.post.user, self.auth_user) @@ -259,10 +284,16 @@ class PostSerializer(serialization.BaseSerializer): return scores.get_score(self.post, self.auth_user) def serialize_own_favorite(self) -> Any: - return len([ - user for user in self.post.favorited_by - if user.user_id == self.auth_user.user_id] - ) > 0 + return ( + len( + [ + user + for user in self.post.favorited_by + if user.user_id == self.auth_user.user_id + ] + ) + > 0 + ) def serialize_tag_count(self) -> Any: return self.post.tag_count @@ -297,43 +328,58 @@ class PostSerializer(serialization.BaseSerializer): def serialize_notes(self) -> Any: return sorted( [serialize_note(note) for note in self.post.notes], - key=lambda x: x['polygon']) + key=lambda x: x["polygon"], + ) def serialize_comments(self) -> Any: return [ comments.serialize_comment(comment, self.auth_user) for comment in sorted( - self.post.comments, - key=lambda comment: comment.creation_time)] + self.post.comments, key=lambda comment: comment.creation_time + ) + ] + + def serialize_pools(self) -> List[Any]: + return [ + pools.serialize_micro_pool(pool) + for pool in sorted( + self.post.pools, key=lambda pool: pool.creation_time + ) + ] def serialize_metrics(self) -> Any: return [ metrics.serialize_post_metric(metric) for metric in sorted( self.post.metrics, - key=lambda metric: metric.metric.tag_name)] + key=lambda metric: metric.metric.tag_name + ) + ] def serialize_metric_ranges(self) -> Any: return [ metrics.serialize_post_metric_range(metric_range) for metric_range in sorted( self.post.metric_ranges, - key=lambda metric_range: metric_range.metric.tag_name)] + key=lambda metric_range: metric_range.metric.tag_name + ) + ] def serialize_post( - post: Optional[model.Post], - auth_user: model.User, - options: List[str] = []) -> Optional[rest.Response]: + post: Optional[model.Post], auth_user: model.User, options: List[str] = [] +) -> Optional[rest.Response]: if not post: return None return PostSerializer(post, auth_user).serialize(options) def serialize_micro_post( - post: model.Post, auth_user: model.User) -> Optional[rest.Response]: + post: model.Post, auth_user: model.User +) -> Optional[rest.Response]: return serialize_post( - post, auth_user=auth_user, options=['id', 'thumbnailUrl']) + post, auth_user=auth_user, options=["id", "thumbnailUrl"] + ) def get_post_count() -> int: @@ -342,25 +388,37 @@ def get_post_count() -> int: def try_get_post_by_id(post_id: int) -> Optional[model.Post]: return ( - db.session - .query(model.Post) + db.session.query(model.Post) .filter(model.Post.post_id == post_id) - .one_or_none()) + .one_or_none() + ) def get_post_by_id(post_id: int) -> model.Post: post = try_get_post_by_id(post_id) if not post: - raise PostNotFoundError('Post %r not found.' % post_id) + raise PostNotFoundError("Post %r not found." % post_id) return post +def get_posts_by_ids(ids: List[int]) -> List[model.Post]: + if len(ids) == 0: + return [] + posts = ( + db.session.query(model.Post) + .filter(sa.sql.or_(model.Post.post_id == post_id for post_id in ids)) + .all() + ) + id_order = {v: k for k, v in enumerate(ids)} + return sorted(posts, key=lambda post: id_order.get(post.post_id)) + + def try_get_current_post_feature() -> Optional[model.PostFeature]: return ( - db.session - .query(model.PostFeature) + db.session.query(model.PostFeature) .order_by(model.PostFeature.time.desc()) - .first()) + .first() + ) def try_get_featured_post() -> Optional[model.Post]: @@ -369,22 +427,22 @@ def try_get_featured_post() -> Optional[model.Post]: def create_post( - content: bytes, - tag_names: List[str], - user: Optional[model.User]) -> Tuple[model.Post, List[model.Tag]]: + content: bytes, tag_names: List[str], user: Optional[model.User] +) -> Tuple[model.Post, List[model.Tag]]: post = model.Post() post.safety = model.Post.SAFETY_SAFE post.user = user post.creation_time = datetime.utcnow() post.flags = [] - post.type = '' - post.checksum = '' - post.mime_type = '' - db.session.add(post) + post.type = "" + post.checksum = "" + post.mime_type = "" update_post_content(post, content) new_tags = update_post_tags(post, tag_names) + + db.session.add(post) return post, new_tags @@ -393,35 +451,38 @@ def update_post_safety(post: model.Post, safety: str) -> None: safety = util.flip(SAFETY_MAP).get(safety, None) if not safety: raise InvalidPostSafetyError( - 'Safety can be either of %r.' % list(SAFETY_MAP.values())) + "Safety can be either of %r." % list(SAFETY_MAP.values()) + ) post.safety = safety def update_post_source(post: model.Post, source: Optional[str]) -> None: assert post if util.value_exceeds_column_size(source, model.Post.source): - raise InvalidPostSourceError('Source is too long.') + raise InvalidPostSourceError("Source is too long.") post.source = source or None -@sa.events.event.listens_for(model.Post, 'after_insert') +@sa.events.event.listens_for(model.Post, "after_insert") def _after_post_insert( - _mapper: Any, _connection: Any, post: model.Post) -> None: + _mapper: Any, _connection: Any, post: model.Post +) -> None: _sync_post_content(post) -@sa.events.event.listens_for(model.Post, 'after_update') +@sa.events.event.listens_for(model.Post, "after_update") def _after_post_update( - _mapper: Any, _connection: Any, post: model.Post) -> None: + _mapper: Any, _connection: Any, post: model.Post +) -> None: _sync_post_content(post) -@sa.events.event.listens_for(model.Post, 'before_delete') +@sa.events.event.listens_for(model.Post, "before_delete") def _before_post_delete( - _mapper: Any, _connection: Any, post: model.Post) -> None: + _mapper: Any, _connection: Any, post: model.Post +) -> None: if post.post_id: - image_hash.delete_image(post.post_id) - if config.config['delete_source_files']: + if config.config["delete_source_files"]: files.delete(get_post_content_path(post)) files.delete(get_post_thumbnail_path(post)) @@ -429,54 +490,50 @@ def _before_post_delete( def _sync_post_content(post: model.Post) -> None: regenerate_thumb = False - if hasattr(post, '__content'): - content = getattr(post, '__content') + if hasattr(post, "__content"): + content = getattr(post, "__content") files.save(get_post_content_path(post), content) - delattr(post, '__content') + delattr(post, "__content") regenerate_thumb = True - if post.post_id and post.type in ( - model.Post.TYPE_IMAGE, model.Post.TYPE_ANIMATION): - image_hash.delete_image(post.post_id) - image_hash.add_image(post.post_id, content) - if hasattr(post, '__thumbnail'): - if getattr(post, '__thumbnail'): + if hasattr(post, "__thumbnail"): + if getattr(post, "__thumbnail"): files.save( get_post_thumbnail_backup_path(post), - getattr(post, '__thumbnail')) + getattr(post, "__thumbnail"), + ) else: files.delete(get_post_thumbnail_backup_path(post)) - delattr(post, '__thumbnail') + delattr(post, "__thumbnail") regenerate_thumb = True if regenerate_thumb: generate_post_thumbnail(post) -def generate_alternate_formats(post: model.Post, content: bytes) \ - -> List[Tuple[model.Post, List[model.Tag]]]: +def generate_alternate_formats( + post: model.Post, content: bytes +) -> List[Tuple[model.Post, List[model.Tag]]]: assert post assert content new_posts = [] if mime.is_animated_gif(content): tag_names = [tag.first_name for tag in post.tags] - if config.config['convert']['gif']['to_mp4']: + if config.config["convert"]["gif"]["to_mp4"]: mp4_post, new_tags = create_post( - images.Image(content).to_mp4(), - tag_names, - post.user) - update_post_flags(mp4_post, ['loop']) + images.Image(content).to_mp4(), tag_names, post.user + ) + update_post_flags(mp4_post, ["loop"]) update_post_safety(mp4_post, post.safety) update_post_source(mp4_post, post.source) new_posts += [(mp4_post, new_tags)] - if config.config['convert']['gif']['to_webm']: + if config.config["convert"]["gif"]["to_webm"]: webm_post, new_tags = create_post( - images.Image(content).to_webm(), - tag_names, - post.user) - update_post_flags(webm_post, ['loop']) + images.Image(content).to_webm(), tag_names, post.user + ) + update_post_flags(webm_post, ["loop"]) update_post_safety(webm_post, post.safety) update_post_source(webm_post, post.source) new_posts += [(webm_post, new_tags)] @@ -492,25 +549,75 @@ def generate_alternate_formats(post: model.Post, content: bytes) \ return new_posts -def test_sound(post: model.Post, content: bytes) -> None: - assert post +def get_default_flags(content: bytes) -> List[str]: assert content + ret = [] if mime.is_video(mime.get_mime_type(content)): + ret.append(model.Post.FLAG_LOOP) if images.Image(content).check_for_sound(): - flags = post.flags - if model.Post.FLAG_SOUND not in flags: - flags.append(model.Post.FLAG_SOUND) - update_post_flags(post, flags) + ret.append(model.Post.FLAG_SOUND) + return ret + + +def purge_post_signature(post: model.Post) -> None: + ( + db.session.query(model.PostSignature) + .filter(model.PostSignature.post_id == post.post_id) + .delete() + ) + + +def generate_post_signature(post: model.Post, content: bytes) -> None: + try: + unpacked_signature = image_hash.generate_signature(content) + packed_signature = image_hash.pack_signature(unpacked_signature) + words = image_hash.generate_words(unpacked_signature) + + db.session.add( + model.PostSignature( + post=post, signature=packed_signature, words=words + ) + ) + except errors.ProcessingError: + if not config.config["allow_broken_uploads"]: + raise InvalidPostContentError( + "Unable to generate image hash data." + ) + + +def update_all_post_signatures() -> None: + posts_to_hash = ( + db.session.query(model.Post) + .filter( + (model.Post.type == model.Post.TYPE_IMAGE) + | (model.Post.type == model.Post.TYPE_ANIMATION) + ) + .filter(model.Post.signature == None) # noqa: E711 + .order_by(model.Post.post_id.asc()) + .all() + ) + for post in posts_to_hash: + try: + generate_post_signature( + post, files.get(get_post_content_path(post)) + ) + db.session.commit() + logger.info("Hashed Post %d", post.post_id) + except Exception as ex: + logger.exception(ex) def update_post_content(post: model.Post, content: Optional[bytes]) -> None: assert post if not content: - raise InvalidPostContentError('Post content missing.') + raise InvalidPostContentError("Post content missing.") + + update_signature = False post.mime_type = mime.get_mime_type(content) if mime.is_flash(post.mime_type): post.type = model.Post.TYPE_FLASH elif mime.is_image(post.mime_type): + update_signature = True if mime.is_animated_gif(content): post.type = model.Post.TYPE_ANIMATION else: @@ -519,39 +626,56 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None: post.type = model.Post.TYPE_VIDEO else: raise InvalidPostContentError( - 'Unhandled file type: %r' % post.mime_type) + "Unhandled file type: %r" % post.mime_type + ) post.checksum = util.get_sha1(content) other_post = ( - db.session - .query(model.Post) + db.session.query(model.Post) .filter(model.Post.checksum == post.checksum) .filter(model.Post.post_id != post.post_id) - .one_or_none()) - if other_post \ - and other_post.post_id \ - and other_post.post_id != post.post_id: + .one_or_none() + ) + if ( + other_post + and other_post.post_id + and other_post.post_id != post.post_id + ): raise PostAlreadyUploadedError(other_post) + if update_signature: + purge_post_signature(post) + post.signature = generate_post_signature(post, content) + post.file_size = len(content) try: image = images.Image(content) post.canvas_width = image.width post.canvas_height = image.height except errors.ProcessingError: - post.canvas_width = None - post.canvas_height = None - if (post.canvas_width is not None and post.canvas_width <= 0) \ - or (post.canvas_height is not None and post.canvas_height <= 0): - post.canvas_width = None - post.canvas_height = None - setattr(post, '__content', content) + if not config.config["allow_broken_uploads"]: + raise InvalidPostContentError("Unable to process image metadata") + else: + post.canvas_width = None + post.canvas_height = None + if (post.canvas_width is not None and post.canvas_width <= 0) or ( + post.canvas_height is not None and post.canvas_height <= 0 + ): + if not config.config["allow_broken_uploads"]: + raise InvalidPostContentError( + "Invalid image dimensions returned during processing" + ) + else: + post.canvas_width = None + post.canvas_height = None + setattr(post, "__content", content) def update_post_thumbnail( - post: model.Post, content: Optional[bytes] = None) -> None: + post: model.Post, content: Optional[bytes] = None +) -> None: assert post - setattr(post, '__thumbnail', content) + setattr(post, "__thumbnail", content) def generate_post_thumbnail(post: model.Post) -> None: @@ -564,15 +688,17 @@ def generate_post_thumbnail(post: model.Post) -> None: assert content image = images.Image(content) image.resize_fill( - int(config.config['thumbnails']['post_width']), - int(config.config['thumbnails']['post_height'])) + int(config.config["thumbnails"]["post_width"]), + int(config.config["thumbnails"]["post_height"]), + ) files.save(get_post_thumbnail_path(post), image.to_jpeg()) except errors.ProcessingError: files.save(get_post_thumbnail_path(post), EMPTY_PIXEL) def update_post_tags( - post: model.Post, tag_names: List[str]) -> List[model.Tag]: + post: model.Post, tag_names: List[str] +) -> List[model.Tag]: assert post existing_tags, new_tags = tags.get_or_create_tags_by_names(tag_names) post.tags = existing_tags + new_tags @@ -584,22 +710,21 @@ def update_post_relations(post: model.Post, new_post_ids: List[int]) -> None: try: new_post_ids = [int(id) for id in new_post_ids] except ValueError: - raise InvalidPostRelationError( - 'A relation must be numeric post ID.') + raise InvalidPostRelationError("A relation must be numeric post ID.") old_posts = post.relations old_post_ids = [int(p.post_id) for p in old_posts] if new_post_ids: new_posts = ( - db.session - .query(model.Post) + db.session.query(model.Post) .filter(model.Post.post_id.in_(new_post_ids)) - .all()) + .all() + ) else: new_posts = [] if len(new_posts) != len(new_post_ids): - raise InvalidPostRelationError('One of relations does not exist.') + raise InvalidPostRelationError("One of relations does not exist.") if post.post_id in new_post_ids: - raise InvalidPostRelationError('Post cannot relate to itself.') + raise InvalidPostRelationError("Post cannot relate to itself.") relations_to_del = [p for p in old_posts if p.post_id not in new_post_ids] relations_to_add = [p for p in new_posts if p.post_id not in old_post_ids] @@ -615,37 +740,44 @@ def update_post_notes(post: model.Post, notes: Any) -> None: assert post post.notes = [] for note in notes: - for field in ('polygon', 'text'): + for field in ("polygon", "text"): if field not in note: - raise InvalidPostNoteError('Note is missing %r field.' % field) - if not note['text']: - raise InvalidPostNoteError('A note\'s text cannot be empty.') - if not isinstance(note['polygon'], (list, tuple)): + raise InvalidPostNoteError("Note is missing %r field." % field) + if not note["text"]: + raise InvalidPostNoteError("A note's text cannot be empty.") + if not isinstance(note["polygon"], (list, tuple)): raise InvalidPostNoteError( - 'A note\'s polygon must be a list of points.') - if len(note['polygon']) < 3: + "A note's polygon must be a list of points." + ) + if len(note["polygon"]) < 3: raise InvalidPostNoteError( - 'A note\'s polygon must have at least 3 points.') - for point in note['polygon']: + "A note's polygon must have at least 3 points." + ) + for point in note["polygon"]: if not isinstance(point, (list, tuple)): raise InvalidPostNoteError( - 'A note\'s polygon point must be a list of length 2.') + "A note's polygon point must be a list of length 2." + ) if len(point) != 2: raise InvalidPostNoteError( - 'A point in note\'s polygon must have two coordinates.') + "A point in note's polygon must have two coordinates." + ) try: pos_x = float(point[0]) pos_y = float(point[1]) if not 0 <= pos_x <= 1 or not 0 <= pos_y <= 1: raise InvalidPostNoteError( - 'All points must fit in the image (0..1 range).') + "All points must fit in the image (0..1 range)." + ) except ValueError: raise InvalidPostNoteError( - 'A point in note\'s polygon must be numeric.') - if util.value_exceeds_column_size(note['text'], model.PostNote.text): - raise InvalidPostNoteError('Note text is too long.') + "A point in note's polygon must be numeric." + ) + if util.value_exceeds_column_size(note["text"], model.PostNote.text): + raise InvalidPostNoteError("Note text is too long.") post.notes.append( - model.PostNote(polygon=note['polygon'], text=str(note['text']))) + model.PostNote(polygon=note["polygon"], text=str(note["text"])) + ) def update_post_flags(post: model.Post, flags: List[str]) -> None: @@ -655,7 +787,8 @@ def update_post_flags(post: model.Post, flags: List[str]) -> None: flag = util.flip(FLAG_MAP).get(flag, None) if not flag: raise InvalidPostFlagError( - 'Flag must be one of %r.' % list(FLAG_MAP.values())) + "Flag must be one of %r." % list(FLAG_MAP.values()) + ) target_flags.append(flag) post.flags = target_flags @@ -675,32 +808,31 @@ def delete(post: model.Post) -> None: def merge_posts( - source_post: model.Post, - target_post: model.Post, - replace_content: bool) -> None: + source_post: model.Post, target_post: model.Post, replace_content: bool +) -> None: assert source_post assert target_post if source_post.post_id == target_post.post_id: - raise InvalidPostRelationError('Cannot merge post with itself.') + raise InvalidPostRelationError("Cannot merge post with itself.") def merge_tables( - table: model.Base, - anti_dup_func: Optional[Callable[[model.Base, model.Base], bool]], - source_post_id: int, - target_post_id: int) -> None: + table: model.Base, + anti_dup_func: Optional[Callable[[model.Base, model.Base], bool]], + source_post_id: int, + target_post_id: int, + ) -> None: alias1 = table alias2 = sa.orm.util.aliased(table) - update_stmt = ( - sa.sql.expression.update(alias1) - .where(alias1.post_id == source_post_id)) + update_stmt = sa.sql.expression.update(alias1).where( + alias1.post_id == source_post_id + ) if anti_dup_func is not None: - update_stmt = ( - update_stmt - .where( - ~sa.exists() - .where(anti_dup_func(alias1, alias2)) - .where(alias2.post_id == target_post_id))) + update_stmt = update_stmt.where( + ~sa.exists() + .where(anti_dup_func(alias1, alias2)) + .where(alias2.post_id == target_post_id) + ) update_stmt = update_stmt.values(post_id=target_post_id) db.session.execute(update_stmt) @@ -710,21 +842,24 @@ def merge_posts( model.PostTag, lambda alias1, alias2: alias1.tag_id == alias2.tag_id, source_post_id, - target_post_id) + target_post_id, + ) def merge_scores(source_post_id: int, target_post_id: int) -> None: merge_tables( model.PostScore, lambda alias1, alias2: alias1.user_id == alias2.user_id, source_post_id, - target_post_id) + target_post_id, + ) def merge_favorites(source_post_id: int, target_post_id: int) -> None: merge_tables( model.PostFavorite, lambda alias1, alias2: alias1.user_id == alias2.user_id, source_post_id, - target_post_id) + target_post_id, + ) def merge_comments(source_post_id: int, target_post_id: int) -> None: merge_tables(model.Comment, None, source_post_id, target_post_id) @@ -739,8 +874,10 @@ def merge_posts( .where( ~sa.exists() .where(alias2.child_id == alias1.child_id) - .where(alias2.parent_id == target_post_id)) - .values(parent_id=target_post_id)) + .where(alias2.parent_id == target_post_id) + ) + .values(parent_id=target_post_id) + ) db.session.execute(update_stmt) update_stmt = ( @@ -750,26 +887,31 @@ def merge_posts( .where( ~sa.exists() .where(alias2.parent_id == alias1.parent_id) - .where(alias2.child_id == target_post_id)) - .values(child_id=target_post_id)) + .where(alias2.child_id == target_post_id) + ) + .values(child_id=target_post_id) + ) db.session.execute(update_stmt) - def transfer_flags(source_post_id: int, target_post_id: int) -> None: - target = get_post_by_id(target_post_id) - source = get_post_by_id(source_post_id) - target.flags = source.flags - merge_tags(source_post.post_id, target_post.post_id) merge_comments(source_post.post_id, target_post.post_id) merge_scores(source_post.post_id, target_post.post_id) merge_favorites(source_post.post_id, target_post.post_id) merge_relations(source_post.post_id, target_post.post_id) + def transfer_flags(source_post_id: int, target_post_id: int) -> None: + target = get_post_by_id(target_post_id) + source = get_post_by_id(source_post_id) + target.flags = source.flags + db.session.flush() + content = None if replace_content: content = files.get(get_post_content_path(source_post)) transfer_flags(source_post.post_id, target_post.post_id) + # fixes unknown issue with SA's cascade deletions + purge_post_signature(source_post) delete(source_post) db.session.flush() @@ -780,44 +922,50 @@ def merge_posts( def search_by_image_exact(image_content: bytes) -> Optional[model.Post]: checksum = util.get_sha1(image_content) return ( - db.session - .query(model.Post) + db.session.query(model.Post) .filter(model.Post.checksum == checksum) - .one_or_none()) + .one_or_none() + ) -def search_by_image(image_content: bytes) -> List[PostLookalike]: - ret = [] - for result in image_hash.search_by_image(image_content): - post = try_get_post_by_id(result.path) - if post: - ret.append(PostLookalike( - score=result.score, - distance=result.distance, - post=post)) - return ret +def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]: + query_signature = image_hash.generate_signature(image_content) + query_words = image_hash.generate_words(query_signature) + """ + The unnest function is used here to expand one row containing the 'words' + array into multiple rows each containing a singular word. -def populate_reverse_search() -> None: - excluded_post_ids = image_hash.get_all_paths() + Documentation of the unnest function can be found here: + https://www.postgresql.org/docs/9.2/functions-array.html + """ - post_ids_to_hash = ( - db.session - .query(model.Post.post_id) - .filter( - (model.Post.type == model.Post.TYPE_IMAGE) | - (model.Post.type == model.Post.TYPE_ANIMATION)) - .filter(~model.Post.post_id.in_(excluded_post_ids)) - .order_by(model.Post.post_id.asc()) - .all()) + dbquery = """ + SELECT s.post_id, s.signature, count(a.query) AS score + FROM post_signature AS s, unnest(s.words, :q) AS a(word, query) + WHERE a.word = a.query + GROUP BY s.post_id + ORDER BY score DESC LIMIT 100; + """ - for post_ids_chunk in util.chunks(post_ids_to_hash, 100): - posts_chunk = ( - db.session - .query(model.Post) - .filter(model.Post.post_id.in_(post_ids_chunk)) - .all()) - for post in posts_chunk: - content_path = get_post_content_path(post) - if files.has(content_path): - image_hash.add_image(post.post_id, files.get(content_path)) + candidates = db.session.execute(dbquery, {"q": query_words}) + data = tuple( + zip( + *[ + (post_id, image_hash.unpack_signature(packedsig)) + for post_id, packedsig, score in candidates + ] + ) + ) + if data: + candidate_post_ids, sigarray = data + distances = image_hash.normalized_distance(sigarray, query_signature) + return [ + (distance, try_get_post_by_id(candidate_post_id)) + for candidate_post_id, distance in zip( + candidate_post_ids, distances + ) + if distance < image_hash.DISTANCE_CUTOFF + ] + else: + return [] diff --git a/server/szurubooru/func/scores.py b/server/szurubooru/func/scores.py index 615fd98..b095f48 100644 --- a/server/szurubooru/func/scores.py +++ b/server/szurubooru/func/scores.py @@ -1,6 +1,7 @@ import datetime -from typing import Any, Tuple, Callable -from szurubooru import db, model, errors +from typing import Any, Callable, Tuple + +from szurubooru import db, errors, model class InvalidScoreTargetError(errors.ValidationError): @@ -12,12 +13,13 @@ class InvalidScoreValueError(errors.ValidationError): def _get_table_info( - entity: model.Base) -> Tuple[model.Base, Callable[[model.Base], Any]]: + entity: model.Base, +) -> Tuple[model.Base, Callable[[model.Base], Any]]: assert entity resource_type, _, _ = model.util.get_resource_info(entity) - if resource_type == 'post': + if resource_type == "post": return model.PostScore, lambda table: table.post_id - elif resource_type == 'comment': + elif resource_type == "comment": return model.CommentScore, lambda table: table.comment_id raise InvalidScoreTargetError() @@ -40,16 +42,17 @@ def get_score(entity: model.Base, user: model.User) -> int: assert user table, get_column = _get_table_info(entity) row = ( - db.session - .query(table.score) + db.session.query(table.score) .filter(get_column(table) == get_column(entity)) .filter(table.user_id == user.user_id) - .one_or_none()) + .one_or_none() + ) return row[0] if row else 0 def set_score(entity: model.Base, user: model.User, score: int) -> None: from szurubooru.func import favorites + assert entity assert user if not score: @@ -61,7 +64,8 @@ def set_score(entity: model.Base, user: model.User, score: int) -> None: return if score not in (-1, 1): raise InvalidScoreValueError( - 'Score %r is invalid. Valid scores: %r.' % (score, (-1, 1))) + "Score %r is invalid. Valid scores: %r." % (score, (-1, 1)) + ) score_entity = _get_score_entity(entity, user) if score_entity: score_entity.score = score diff --git a/server/szurubooru/func/serialization.py b/server/szurubooru/func/serialization.py index 699fb47..d2fadc0 100644 --- a/server/szurubooru/func/serialization.py +++ b/server/szurubooru/func/serialization.py @@ -1,9 +1,10 @@ -from typing import Any, List, Dict, Callable -from szurubooru import model, rest, errors +from typing import Any, Callable, Dict, List + +from szurubooru import errors, model, rest def get_serialization_options(ctx: rest.Context) -> List[str]: - return ctx.get_param_as_list('fields', default=[]) + return ctx.get_param_as_list("fields", default=[]) class BaseSerializer: @@ -17,8 +18,9 @@ class BaseSerializer: for key in options: if key not in field_factories: raise errors.ValidationError( - 'Invalid key: %r. Valid keys: %r.' % ( - key, list(sorted(field_factories.keys())))) + "Invalid key: %r. Valid keys: %r." + % (key, list(sorted(field_factories.keys()))) + ) factory = field_factories[key] ret[key] = factory() return ret diff --git a/server/szurubooru/func/snapshots.py b/server/szurubooru/func/snapshots.py index 240c3bc..afb26ea 100644 --- a/server/szurubooru/func/snapshots.py +++ b/server/szurubooru/func/snapshots.py @@ -1,73 +1,111 @@ -from typing import Any, Optional, Dict, Callable from datetime import datetime +from typing import Any, Callable, Dict, Optional + +import sqlalchemy as sa + from szurubooru import db, model -from szurubooru.func import diff, users +from szurubooru.func import diff, net, users def get_tag_category_snapshot(category: model.TagCategory) -> Dict[str, Any]: assert category return { - 'name': category.name, - 'color': category.color, - 'default': True if category.default else False, + "name": category.name, + "color": category.color, + "default": True if category.default else False, } def get_tag_snapshot(tag: model.Tag) -> Dict[str, Any]: assert tag return { - 'names': [tag_name.name for tag_name in tag.names], - 'category': tag.category.name, - 'suggestions': sorted(rel.first_name for rel in tag.suggestions), - 'implications': sorted(rel.first_name for rel in tag.implications), + "names": [tag_name.name for tag_name in tag.names], + "category": tag.category.name, + "suggestions": sorted(rel.first_name for rel in tag.suggestions), + "implications": sorted(rel.first_name for rel in tag.implications), + } + + +def get_pool_category_snapshot(category: model.PoolCategory) -> Dict[str, Any]: + assert category + return { + "name": category.name, + "color": category.color, + "default": True if category.default else False, + } + + +def get_pool_snapshot(pool: model.Pool) -> Dict[str, Any]: + assert pool + return { + "names": [pool_name.name for pool_name in pool.names], + "category": pool.category.name, + "posts": [post.post_id for post in pool.posts], } def get_post_snapshot(post: model.Post) -> Dict[str, Any]: assert post return { - 'source': post.source, - 'safety': post.safety, - 'checksum': post.checksum, - 'flags': post.flags, - 'featured': post.is_featured, - 'tags': sorted([tag.first_name for tag in post.tags]), - 'relations': sorted([rel.post_id for rel in post.relations]), - 'notes': sorted([{ - 'polygon': [[point[0], point[1]] for point in note.polygon], - 'text': note.text, - } for note in post.notes], key=lambda x: x['polygon']), + "source": post.source, + "safety": post.safety, + "checksum": post.checksum, + "flags": post.flags, + "featured": post.is_featured, + "tags": sorted([tag.first_name for tag in post.tags]), + "relations": sorted([rel.post_id for rel in post.relations]), + "notes": sorted( + [ + { + "polygon": [ + [point[0], point[1]] for point in note.polygon + ], + "text": note.text, + } + for note in post.notes + ], + key=lambda x: x["polygon"], + ), } _snapshot_factories = { # lambdas allow mocking target functions in the tests - # pylint: disable=unnecessary-lambda - 'tag_category': lambda entity: get_tag_category_snapshot(entity), - 'tag': lambda entity: get_tag_snapshot(entity), - 'post': lambda entity: get_post_snapshot(entity), + "tag_category": lambda entity: get_tag_category_snapshot(entity), + "tag": lambda entity: get_tag_snapshot(entity), + "post": lambda entity: get_post_snapshot(entity), + "pool_category": lambda entity: get_pool_category_snapshot(entity), + "pool": lambda entity: get_pool_snapshot(entity), } # type: Dict[model.Base, Callable[[model.Base], Dict[str ,Any]]] def serialize_snapshot( - snapshot: model.Snapshot, auth_user: model.User) -> Dict[str, Any]: + snapshot: model.Snapshot, auth_user: model.User +) -> Dict[str, Any]: assert snapshot return { - 'operation': snapshot.operation, - 'type': snapshot.resource_type, - 'id': snapshot.resource_name, - 'user': users.serialize_micro_user(snapshot.user, auth_user), - 'data': snapshot.data, - 'time': snapshot.creation_time, + "operation": snapshot.operation, + "type": snapshot.resource_type, + "id": snapshot.resource_name, + "user": users.serialize_micro_user(snapshot.user, auth_user), + "data": snapshot.data, + "time": snapshot.creation_time, } +def _post_to_webhooks(snapshot: model.Snapshot) -> None: + webhook_user = model.User() + webhook_user.name = None + webhook_user.rank = "anonymous" + net.post_to_webhooks(serialize_snapshot(snapshot, webhook_user)) + + def _create( - operation: str, - entity: model.Base, - auth_user: Optional[model.User]) -> model.Snapshot: - resource_type, resource_pkey, resource_name = ( - model.util.get_resource_info(entity)) + operation: str, entity: model.Base, auth_user: Optional[model.User] +) -> model.Snapshot: + resource_type, resource_pkey, resource_name = model.util.get_resource_info( + entity + ) snapshot = model.Snapshot() snapshot.creation_time = datetime.utcnow() @@ -85,9 +123,9 @@ def create(entity: model.Base, auth_user: Optional[model.User]) -> None: snapshot_factory = _snapshot_factories[snapshot.resource_type] snapshot.data = snapshot_factory(entity) db.session.add(snapshot) + _post_to_webhooks(snapshot) -# pylint: disable=protected-access def modify(entity: model.Base, auth_user: Optional[model.User]) -> None: assert entity @@ -95,18 +133,19 @@ def modify(entity: model.Base, auth_user: Optional[model.User]) -> None: ( cls for cls in model.Base._decl_class_registry.values() - if hasattr(cls, '__table__') + if hasattr(cls, "__table__") and cls.__table__.fullname == entity.__table__.fullname ), - None) + None, + ) assert table snapshot = _create(model.Snapshot.OPERATION_MODIFIED, entity, auth_user) snapshot_factory = _snapshot_factories[snapshot.resource_type] - detached_session = db.sessionmaker() + detached_session = sa.orm.sessionmaker(bind=db.session.get_bind())() detached_entity = detached_session.query(table).get(snapshot.resource_pkey) - assert detached_entity, 'Entity not found in DB, have you committed it?' + assert detached_entity, "Entity not found in DB, have you committed it?" detached_snapshot = snapshot_factory(detached_entity) detached_session.close() @@ -116,6 +155,7 @@ def modify(entity: model.Base, auth_user: Optional[model.User]) -> None: if not snapshot.data: return db.session.add(snapshot) + _post_to_webhooks(snapshot) def delete(entity: model.Base, auth_user: Optional[model.User]) -> None: @@ -124,17 +164,24 @@ def delete(entity: model.Base, auth_user: Optional[model.User]) -> None: snapshot_factory = _snapshot_factories[snapshot.resource_type] snapshot.data = snapshot_factory(entity) db.session.add(snapshot) + _post_to_webhooks(snapshot) def merge( - source_entity: model.Base, - target_entity: model.Base, - auth_user: Optional[model.User]) -> None: + source_entity: model.Base, + target_entity: model.Base, + auth_user: Optional[model.User], +) -> None: assert source_entity assert target_entity snapshot = _create( - model.Snapshot.OPERATION_MERGED, source_entity, auth_user) - resource_type, _resource_pkey, resource_name = ( - model.util.get_resource_info(target_entity)) + model.Snapshot.OPERATION_MERGED, source_entity, auth_user + ) + ( + resource_type, + _resource_pkey, + resource_name, + ) = model.util.get_resource_info(target_entity) snapshot.data = [resource_type, resource_name] db.session.add(snapshot) + _post_to_webhooks(snapshot) diff --git a/server/szurubooru/func/tag_categories.py b/server/szurubooru/func/tag_categories.py index bec2f0d..d2c6021 100644 --- a/server/szurubooru/func/tag_categories.py +++ b/server/szurubooru/func/tag_categories.py @@ -1,11 +1,12 @@ import re -from typing import Any, Optional, Dict, List, Callable +from typing import Any, Callable, Dict, List, Optional + import sqlalchemy as sa -from szurubooru import config, db, model, errors, rest -from szurubooru.func import util, serialization, cache +from szurubooru import config, db, errors, model, rest +from szurubooru.func import cache, serialization, util -DEFAULT_CATEGORY_NAME_CACHE_KEY = 'default-tag-category' +DEFAULT_CATEGORY_NAME_CACHE_KEY = "default-tag-category" class TagCategoryNotFoundError(errors.NotFoundError): @@ -29,10 +30,11 @@ class InvalidTagCategoryColorError(errors.ValidationError): def _verify_name_validity(name: str) -> None: - name_regex = config.config['tag_category_name_regex'] + name_regex = config.config["tag_category_name_regex"] if not re.match(name_regex, name): raise InvalidTagCategoryNameError( - 'Name must satisfy regex %r.' % name_regex) + "Name must satisfy regex %r." % name_regex + ) class TagCategorySerializer(serialization.BaseSerializer): @@ -41,11 +43,12 @@ class TagCategorySerializer(serialization.BaseSerializer): def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'name': self.serialize_name, - 'version': self.serialize_version, - 'color': self.serialize_color, - 'usages': self.serialize_usages, - 'default': self.serialize_default, + "name": self.serialize_name, + "version": self.serialize_version, + "color": self.serialize_color, + "usages": self.serialize_usages, + "default": self.serialize_default, + "order": self.serialize_order, } def serialize_name(self) -> Any: @@ -63,19 +66,23 @@ class TagCategorySerializer(serialization.BaseSerializer): def serialize_default(self) -> Any: return self.category.default + def serialize_order(self) -> Any: + return self.category.order + def serialize_category( - category: Optional[model.TagCategory], - options: List[str] = []) -> Optional[rest.Response]: + category: Optional[model.TagCategory], options: List[str] = [] +) -> Optional[rest.Response]: if not category: return None return TagCategorySerializer(category).serialize(options) -def create_category(name: str, color: str) -> model.TagCategory: +def create_category(name: str, color: str, order: int) -> model.TagCategory: category = model.TagCategory() update_category_name(category, name) update_category_color(category, color) + update_category_order(category, order) if not get_all_categories(): category.default = True return category @@ -84,18 +91,21 @@ def create_category(name: str, color: str) -> model.TagCategory: def update_category_name(category: model.TagCategory, name: str) -> None: assert category if not name: - raise InvalidTagCategoryNameError('Name cannot be empty.') + raise InvalidTagCategoryNameError("Name cannot be empty.") expr = sa.func.lower(model.TagCategory.name) == name.lower() if category.tag_category_id: expr = expr & ( - model.TagCategory.tag_category_id != category.tag_category_id) + model.TagCategory.tag_category_id != category.tag_category_id + ) already_exists = ( - db.session.query(model.TagCategory).filter(expr).count() > 0) + db.session.query(model.TagCategory).filter(expr).count() > 0 + ) if already_exists: raise TagCategoryAlreadyExistsError( - 'A category with this name already exists.') + "A category with this name already exists." + ) if util.value_exceeds_column_size(name, model.TagCategory.name): - raise InvalidTagCategoryNameError('Name is too long.') + raise InvalidTagCategoryNameError("Name is too long.") _verify_name_validity(name) category.name = name cache.remove(DEFAULT_CATEGORY_NAME_CACHE_KEY) @@ -104,58 +114,66 @@ def update_category_name(category: model.TagCategory, name: str) -> None: def update_category_color(category: model.TagCategory, color: str) -> None: assert category if not color: - raise InvalidTagCategoryColorError('Color cannot be empty.') - if not re.match(r'^#?[0-9a-z]+$', color): - raise InvalidTagCategoryColorError('Invalid color.') + raise InvalidTagCategoryColorError("Color cannot be empty.") + if not re.match(r"^#?[0-9a-z]+$", color): + raise InvalidTagCategoryColorError("Invalid color.") if util.value_exceeds_column_size(color, model.TagCategory.color): - raise InvalidTagCategoryColorError('Color is too long.') + raise InvalidTagCategoryColorError("Color is too long.") category.color = color +def update_category_order(category: model.TagCategory, order: int) -> None: + assert category + category.order = order + + def try_get_category_by_name( - name: str, lock: bool = False) -> Optional[model.TagCategory]: - query = ( - db.session - .query(model.TagCategory) - .filter(sa.func.lower(model.TagCategory.name) == name.lower())) + name: str, lock: bool = False +) -> Optional[model.TagCategory]: + query = db.session.query(model.TagCategory).filter( + sa.func.lower(model.TagCategory.name) == name.lower() + ) if lock: - query = query.with_lockmode('update') + query = query.with_for_update() return query.one_or_none() def get_category_by_name(name: str, lock: bool = False) -> model.TagCategory: category = try_get_category_by_name(name, lock) if not category: - raise TagCategoryNotFoundError('Tag category %r not found.' % name) + raise TagCategoryNotFoundError("Tag category %r not found." % name) return category def get_all_category_names() -> List[str]: - return [row[0] for row in db.session.query(model.TagCategory.name).all()] + return [cat.name for cat in get_all_categories()] def get_all_categories() -> List[model.TagCategory]: - return db.session.query(model.TagCategory).all() + return ( + db.session.query(model.TagCategory) + .order_by(model.TagCategory.order.asc(), model.TagCategory.name.asc()) + .all() + ) def try_get_default_category( - lock: bool = False) -> Optional[model.TagCategory]: - query = ( - db.session - .query(model.TagCategory) - .filter(model.TagCategory.default)) + lock: bool = False, +) -> Optional[model.TagCategory]: + query = db.session.query(model.TagCategory).filter( + model.TagCategory.default + ) if lock: - query = query.with_lockmode('update') + query = query.with_for_update() category = query.first() # if for some reason (e.g. as a result of migration) there's no default # category, get the first record available. if not category: - query = ( - db.session - .query(model.TagCategory) - .order_by(model.TagCategory.tag_category_id.asc())) + query = db.session.query(model.TagCategory).order_by( + model.TagCategory.tag_category_id.asc() + ) if lock: - query = query.with_lockmode('update') + query = query.with_for_update() category = query.first() return category @@ -163,7 +181,7 @@ def try_get_default_category( def get_default_category(lock: bool = False) -> model.TagCategory: category = try_get_default_category(lock) if not category: - raise TagCategoryNotFoundError('No tag category created yet.') + raise TagCategoryNotFoundError("No tag category created yet.") return category @@ -190,9 +208,10 @@ def set_default_category(category: model.TagCategory) -> None: def delete_category(category: model.TagCategory) -> None: assert category if len(get_all_category_names()) == 1: - raise TagCategoryIsInUseError('Cannot delete the last category.') + raise TagCategoryIsInUseError("Cannot delete the last category.") if (category.tag_count or 0) > 0: raise TagCategoryIsInUseError( - 'Tag category has some usages and cannot be deleted. ' + - 'Please remove this category from relevant tags first..') + "Tag category has some usages and cannot be deleted. " + + "Please remove this category from relevant tags first.." + ) db.session.delete(category) diff --git a/server/szurubooru/func/tags.py b/server/szurubooru/func/tags.py index 3384b4e..4981c5d 100644 --- a/server/szurubooru/func/tags.py +++ b/server/szurubooru/func/tags.py @@ -1,11 +1,11 @@ -import json -import os import re -from typing import Any, Optional, Tuple, List, Dict, Callable from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Tuple + import sqlalchemy as sa -from szurubooru import config, db, model, errors, rest -from szurubooru.func import util, tag_categories, serialization + +from szurubooru import config, db, errors, model, rest +from szurubooru.func import serialization, tag_categories, util class TagNotFoundError(errors.NotFoundError): @@ -38,10 +38,10 @@ class InvalidTagDescriptionError(errors.ValidationError): def _verify_name_validity(name: str) -> None: if util.value_exceeds_column_size(name, model.TagName.name): - raise InvalidTagNameError('Name is too long.') - name_regex = config.config['tag_name_regex'] + raise InvalidTagNameError("Name is too long.") + name_regex = config.config["tag_name_regex"] if not re.match(name_regex, name): - raise InvalidTagNameError('Name must satisfy regex %r.' % name_regex) + raise InvalidTagNameError("Name must satisfy regex %r." % name_regex) def _get_names(tag: model.Tag) -> List[str]: @@ -54,7 +54,8 @@ def _lower_list(names: List[str]) -> List[str]: def _check_name_intersection( - names1: List[str], names2: List[str], case_sensitive: bool) -> bool: + names1: List[str], names2: List[str], case_sensitive: bool +) -> bool: if not case_sensitive: names1 = _lower_list(names1) names2 = _lower_list(names2) @@ -66,17 +67,19 @@ def sort_tags(tags: List[model.Tag]) -> List[model.Tag]: return sorted( tags, key=lambda tag: ( + tag.category.order, default_category_name == tag.category.name, tag.category.name, - tag.names[0].name) + tag.names[0].name, + ), ) def serialize_relation(tag): return { - 'names': [tag_name.name for tag_name in tag.names], - 'category': tag.category.name, - 'usages': tag.post_count, + "names": [tag_name.name for tag_name in tag.names], + "category": tag.category.name, + "usages": tag.post_count, } @@ -86,16 +89,16 @@ class TagSerializer(serialization.BaseSerializer): def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'names': self.serialize_names, - 'category': self.serialize_category, - 'version': self.serialize_version, - 'description': self.serialize_description, - 'creationTime': self.serialize_creation_time, - 'lastEditTime': self.serialize_last_edit_time, - 'usages': self.serialize_usages, - 'suggestions': self.serialize_suggestions, - 'implications': self.serialize_implications, - 'metric': self.serialize_metric, + "names": self.serialize_names, + "category": self.serialize_category, + "version": self.serialize_version, + "description": self.serialize_description, + "creationTime": self.serialize_creation_time, + "lastEditTime": self.serialize_last_edit_time, + "usages": self.serialize_usages, + "suggestions": self.serialize_suggestions, + "implications": self.serialize_implications, + "metric": self.serialize_metric, } def serialize_names(self) -> Any: @@ -122,26 +125,29 @@ class TagSerializer(serialization.BaseSerializer): def serialize_suggestions(self) -> Any: return [ serialize_relation(relation) - for relation in sort_tags(self.tag.suggestions)] + for relation in sort_tags(self.tag.suggestions) + ] def serialize_implications(self) -> Any: return [ serialize_relation(relation) - for relation in sort_tags(self.tag.implications)] + for relation in sort_tags(self.tag.implications) + ] def serialize_metric(self) -> Any: if not self.tag.metric: return None else: return { - 'version': self.tag.metric.version, - 'min': self.tag.metric.min, - 'max': self.tag.metric.max, + "version": self.tag.metric.version, + "min": self.tag.metric.min, + "max": self.tag.metric.max, } def serialize_tag( - tag: model.Tag, options: List[str] = []) -> Optional[rest.Response]: + tag: model.Tag, options: List[str] = [] +) -> Optional[rest.Response]: if not tag: return None return TagSerializer(tag).serialize(options) @@ -149,17 +155,17 @@ def serialize_tag( def try_get_tag_by_name(name: str) -> Optional[model.Tag]: return ( - db.session - .query(model.Tag) + db.session.query(model.Tag) .join(model.TagName) .filter(sa.func.lower(model.TagName.name) == name.lower()) - .one_or_none()) + .one_or_none() + ) def get_tag_by_name(name: str) -> model.Tag: tag = try_get_tag_by_name(name) if not tag: - raise TagNotFoundError('Tag %r not found.' % name) + raise TagNotFoundError("Tag %r not found." % name) return tag @@ -173,12 +179,16 @@ def get_tags_by_names(names: List[str]) -> List[model.Tag]: .filter( sa.sql.or_( sa.func.lower(model.TagName.name) == name.lower() - for name in names)) - .all()) + for name in names + ) + ) + .all() + ) def get_or_create_tags_by_names( - names: List[str]) -> Tuple[List[model.Tag], List[model.Tag]]: + names: List[str], +) -> Tuple[List[model.Tag], List[model.Tag]]: names = util.icase_unique(names) existing_tags = get_tags_by_names(names) new_tags = [] @@ -187,7 +197,8 @@ def get_or_create_tags_by_names( found = False for existing_tag in existing_tags: if _check_name_intersection( - _get_names(existing_tag), [name], False): + _get_names(existing_tag), [name], False + ): found = True break if not found: @@ -195,7 +206,8 @@ def get_or_create_tags_by_names( names=[name], category_name=tag_category_name, suggestions=[], - implications=[]) + implications=[], + ) db.session.add(new_tag) new_tags.append(new_tag) return existing_tags, new_tags @@ -207,8 +219,7 @@ def get_tag_siblings(tag: model.Tag) -> List[model.Tag]: pt_alias1 = sa.orm.aliased(model.PostTag) pt_alias2 = sa.orm.aliased(model.PostTag) result = ( - db.session - .query(tag_alias, sa.func.count(pt_alias2.post_id)) + db.session.query(tag_alias, sa.func.count(pt_alias2.post_id)) .join(pt_alias1, pt_alias1.tag_id == tag_alias.tag_id) .join(pt_alias2, pt_alias2.post_id == pt_alias1.post_id) .filter(pt_alias2.tag_id == tag.tag_id) @@ -216,18 +227,23 @@ def get_tag_siblings(tag: model.Tag) -> List[model.Tag]: .group_by(tag_alias.tag_id) .order_by(sa.func.count(pt_alias2.post_id).desc()) .order_by(tag_alias.first_name) - .limit(50)) + .limit(50) + ) return result def delete(source_tag: model.Tag) -> None: assert source_tag db.session.execute( - sa.sql.expression.delete(model.TagSuggestion) - .where(model.TagSuggestion.child_id == source_tag.tag_id)) + sa.sql.expression.delete(model.TagSuggestion).where( + model.TagSuggestion.child_id == source_tag.tag_id + ) + ) db.session.execute( - sa.sql.expression.delete(model.TagImplication) - .where(model.TagImplication.child_id == source_tag.tag_id)) + sa.sql.expression.delete(model.TagImplication).where( + model.TagImplication.child_id == source_tag.tag_id + ) + ) db.session.delete(source_tag) @@ -235,27 +251,27 @@ def merge_tags(source_tag: model.Tag, target_tag: model.Tag) -> None: assert source_tag assert target_tag if source_tag.tag_id == target_tag.tag_id: - raise InvalidTagRelationError('Cannot merge tag with itself.') + raise InvalidTagRelationError("Cannot merge tag with itself.") if source_tag.metric or target_tag.metric: - raise InvalidTagRelationError('Cannot merge tags with metrics.') + raise InvalidTagRelationError("Cannot merge tags with metrics.") def merge_posts(source_tag_id: int, target_tag_id: int) -> None: alias1 = model.PostTag alias2 = sa.orm.util.aliased(model.PostTag) - update_stmt = ( - sa.sql.expression.update(alias1) - .where(alias1.tag_id == source_tag_id)) - update_stmt = ( - update_stmt - .where( - ~sa.exists() - .where(alias1.post_id == alias2.post_id) - .where(alias2.tag_id == target_tag_id))) + update_stmt = sa.sql.expression.update(alias1).where( + alias1.tag_id == source_tag_id + ) + update_stmt = update_stmt.where( + ~sa.exists() + .where(alias1.post_id == alias2.post_id) + .where(alias2.tag_id == target_tag_id) + ) update_stmt = update_stmt.values(tag_id=target_tag_id) db.session.execute(update_stmt) def merge_relations( - table: model.Base, source_tag_id: int, target_tag_id: int) -> None: + table: model.Base, source_tag_id: int, target_tag_id: int + ) -> None: alias1 = table alias2 = sa.orm.util.aliased(table) update_stmt = ( @@ -265,8 +281,10 @@ def merge_tags(source_tag: model.Tag, target_tag: model.Tag) -> None: .where( ~sa.exists() .where(alias2.child_id == alias1.child_id) - .where(alias2.parent_id == target_tag_id)) - .values(parent_id=target_tag_id)) + .where(alias2.parent_id == target_tag_id) + ) + .values(parent_id=target_tag_id) + ) db.session.execute(update_stmt) update_stmt = ( @@ -276,8 +294,10 @@ def merge_tags(source_tag: model.Tag, target_tag: model.Tag) -> None: .where( ~sa.exists() .where(alias2.parent_id == alias1.parent_id) - .where(alias2.child_id == target_tag_id)) - .values(child_id=target_tag_id)) + .where(alias2.child_id == target_tag_id) + ) + .values(child_id=target_tag_id) + ) db.session.execute(update_stmt) def merge_suggestions(source_tag_id: int, target_tag_id: int) -> None: @@ -293,10 +313,11 @@ def merge_tags(source_tag: model.Tag, target_tag: model.Tag) -> None: def create_tag( - names: List[str], - category_name: str, - suggestions: List[str], - implications: List[str]) -> model.Tag: + names: List[str], + category_name: str, + suggestions: List[str], + implications: List[str], +) -> model.Tag: tag = model.Tag() tag.creation_time = datetime.utcnow() update_tag_names(tag, names) @@ -316,7 +337,7 @@ def update_tag_names(tag: model.Tag, names: List[str]) -> None: assert tag names = util.icase_unique([name for name in names if name]) if not len(names): - raise InvalidTagNameError('At least one name must be specified.') + raise InvalidTagNameError("At least one name must be specified.") for name in names: _verify_name_validity(name) @@ -329,7 +350,8 @@ def update_tag_names(tag: model.Tag, names: List[str]) -> None: existing_tags = db.session.query(model.TagName).filter(expr).all() if len(existing_tags): raise TagAlreadyExistsError( - 'One of names is already used by another tag.') + "One of names is already used by another tag." + ) # remove unwanted items for tag_name in tag.names[:]: @@ -351,7 +373,7 @@ def update_tag_names(tag: model.Tag, names: List[str]) -> None: def update_tag_implications(tag: model.Tag, relations: List[str]) -> None: assert tag if _check_name_intersection(_get_names(tag), relations, False): - raise InvalidTagRelationError('Tag cannot imply itself.') + raise InvalidTagRelationError("Tag cannot imply itself.") tag.implications = get_tags_by_names(relations) @@ -359,12 +381,12 @@ def update_tag_implications(tag: model.Tag, relations: List[str]) -> None: def update_tag_suggestions(tag: model.Tag, relations: List[str]) -> None: assert tag if _check_name_intersection(_get_names(tag), relations, False): - raise InvalidTagRelationError('Tag cannot suggest itself.') + raise InvalidTagRelationError("Tag cannot suggest itself.") tag.suggestions = get_tags_by_names(relations) def update_tag_description(tag: model.Tag, description: str) -> None: assert tag if util.value_exceeds_column_size(description, model.Tag.description): - raise InvalidTagDescriptionError('Description is too long.') + raise InvalidTagDescriptionError("Description is too long.") tag.description = description or None diff --git a/server/szurubooru/func/user_tokens.py b/server/szurubooru/func/user_tokens.py index c0f4bad..8d977e4 100644 --- a/server/szurubooru/func/user_tokens.py +++ b/server/szurubooru/func/user_tokens.py @@ -1,8 +1,10 @@ from datetime import datetime -from typing import Any, Optional, List, Dict, Callable -from pyrfc3339 import parser as rfc3339_parser +from typing import Any, Callable, Dict, List, Optional + import pytz -from szurubooru import db, model, rest, errors +from pyrfc3339 import parser as rfc3339_parser + +from szurubooru import db, errors, model, rest from szurubooru.func import auth, serialization, users, util @@ -16,23 +18,22 @@ class InvalidNoteError(errors.ValidationError): class UserTokenSerializer(serialization.BaseSerializer): def __init__( - self, - user_token: model.UserToken, - auth_user: model.User) -> None: + self, user_token: model.UserToken, auth_user: model.User + ) -> None: self.user_token = user_token self.auth_user = auth_user def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'user': self.serialize_user, - 'token': self.serialize_token, - 'note': self.serialize_note, - 'enabled': self.serialize_enabled, - 'expirationTime': self.serialize_expiration_time, - 'creationTime': self.serialize_creation_time, - 'lastEditTime': self.serialize_last_edit_time, - 'lastUsageTime': self.serialize_last_usage_time, - 'version': self.serialize_version, + "user": self.serialize_user, + "token": self.serialize_token, + "note": self.serialize_note, + "enabled": self.serialize_enabled, + "expirationTime": self.serialize_expiration_time, + "creationTime": self.serialize_creation_time, + "lastEditTime": self.serialize_last_edit_time, + "lastUsageTime": self.serialize_last_usage_time, + "version": self.serialize_version, } def serialize_user(self) -> Any: @@ -64,31 +65,31 @@ class UserTokenSerializer(serialization.BaseSerializer): def serialize_user_token( - user_token: Optional[model.UserToken], - auth_user: model.User, - options: List[str] = []) -> Optional[rest.Response]: + user_token: Optional[model.UserToken], + auth_user: model.User, + options: List[str] = [], +) -> Optional[rest.Response]: if not user_token: return None return UserTokenSerializer(user_token, auth_user).serialize(options) -def get_by_user_and_token( - user: model.User, token: str) -> model.UserToken: +def get_by_user_and_token(user: model.User, token: str) -> model.UserToken: return ( - db.session - .query(model.UserToken) + db.session.query(model.UserToken) .filter(model.UserToken.user_id == user.user_id) .filter(model.UserToken.token == token) - .one_or_none()) + .one_or_none() + ) def get_user_tokens(user: model.User) -> List[model.UserToken]: assert user return ( - db.session - .query(model.UserToken) + db.session.query(model.UserToken) .filter(model.UserToken.user_id == user.user_id) - .all()) + .all() + ) def create_user_token(user: model.User, enabled: bool) -> model.UserToken: @@ -103,7 +104,8 @@ def create_user_token(user: model.User, enabled: bool) -> model.UserToken: def update_user_token_enabled( - user_token: model.UserToken, enabled: bool) -> None: + user_token: model.UserToken, enabled: bool +) -> None: assert user_token user_token.enabled = enabled update_user_token_edit_time(user_token) @@ -115,28 +117,30 @@ def update_user_token_edit_time(user_token: model.UserToken) -> None: def update_user_token_expiration_time( - user_token: model.UserToken, expiration_time_str: str) -> None: + user_token: model.UserToken, expiration_time_str: str +) -> None: assert user_token try: expiration_time = rfc3339_parser.parse(expiration_time_str, utc=True) expiration_time = expiration_time.astimezone(pytz.UTC) if expiration_time < datetime.utcnow().replace(tzinfo=pytz.UTC): raise InvalidExpirationError( - 'Expiration cannot happen in the past') + "Expiration cannot happen in the past" + ) user_token.expiration_time = expiration_time update_user_token_edit_time(user_token) except ValueError: raise InvalidExpirationError( - 'Expiration is in an invalid format {}'.format( - expiration_time_str)) + "Expiration is in an invalid format {}".format(expiration_time_str) + ) def update_user_token_note(user_token: model.UserToken, note: str) -> None: assert user_token - note = note.strip() if note is not None else '' + note = note.strip() if note is not None else "" note = None if len(note) == 0 else note if util.value_exceeds_column_size(note, model.UserToken.note): - raise InvalidNoteError('Note is too long.') + raise InvalidNoteError("Note is too long.") user_token.note = note update_user_token_edit_time(user_token) diff --git a/server/szurubooru/func/users.py b/server/szurubooru/func/users.py index e5946dc..5cbe3cc 100644 --- a/server/szurubooru/func/users.py +++ b/server/szurubooru/func/users.py @@ -1,9 +1,11 @@ -from datetime import datetime -from typing import Any, Optional, Union, List, Dict, Callable import re +from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Union + import sqlalchemy as sa -from szurubooru import config, db, model, errors, rest -from szurubooru.func import auth, util, serialization, files, images + +from szurubooru import config, db, errors, model, rest +from szurubooru.func import auth, files, images, serialization, util class UserNotFoundError(errors.NotFoundError): @@ -35,36 +37,41 @@ class InvalidAvatarError(errors.ValidationError): def get_avatar_path(user_name: str) -> str: - return 'avatars/' + user_name.lower() + '.png' + return "avatars/" + user_name.lower() + ".png" def get_avatar_url(user: model.User) -> str: assert user if user.avatar_style == user.AVATAR_GRAVATAR: assert user.email or user.name - return 'https://gravatar.com/avatar/%s?d=retro&s=%d' % ( + return "https://gravatar.com/avatar/%s?d=retro&s=%d" % ( util.get_md5((user.email or user.name).lower()), - config.config['thumbnails']['avatar_width']) + config.config["thumbnails"]["avatar_width"], + ) assert user.name - return '%s/avatars/%s.png' % ( - config.config['data_url'].rstrip('/'), user.name.lower()) + return "%s/avatars/%s.png" % ( + config.config["data_url"].rstrip("/"), + user.name.lower(), + ) def get_email( - user: model.User, - auth_user: model.User, - force_show_email: bool) -> Union[bool, str]: + user: model.User, auth_user: model.User, force_show_email: bool +) -> Union[bool, str]: assert user assert auth_user - if not force_show_email \ - and auth_user.user_id != user.user_id \ - and not auth.has_privilege(auth_user, 'users:edit:any:email'): + if ( + not force_show_email + and auth_user.user_id != user.user_id + and not auth.has_privilege(auth_user, "users:edit:any:email") + ): return False return user.email def get_liked_post_count( - user: model.User, auth_user: model.User) -> Union[bool, int]: + user: model.User, auth_user: model.User +) -> Union[bool, int]: assert user assert auth_user if auth_user.user_id != user.user_id: @@ -73,7 +80,8 @@ def get_liked_post_count( def get_disliked_post_count( - user: model.User, auth_user: model.User) -> Union[bool, int]: + user: model.User, auth_user: model.User +) -> Union[bool, int]: assert user assert auth_user if auth_user.user_id != user.user_id: @@ -83,29 +91,30 @@ def get_disliked_post_count( class UserSerializer(serialization.BaseSerializer): def __init__( - self, - user: model.User, - auth_user: model.User, - force_show_email: bool = False) -> None: + self, + user: model.User, + auth_user: model.User, + force_show_email: bool = False, + ) -> None: self.user = user self.auth_user = auth_user self.force_show_email = force_show_email def _serializers(self) -> Dict[str, Callable[[], Any]]: return { - 'name': self.serialize_name, - 'creationTime': self.serialize_creation_time, - 'lastLoginTime': self.serialize_last_login_time, - 'version': self.serialize_version, - 'rank': self.serialize_rank, - 'avatarStyle': self.serialize_avatar_style, - 'avatarUrl': self.serialize_avatar_url, - 'commentCount': self.serialize_comment_count, - 'uploadedPostCount': self.serialize_uploaded_post_count, - 'favoritePostCount': self.serialize_favorite_post_count, - 'likedPostCount': self.serialize_liked_post_count, - 'dislikedPostCount': self.serialize_disliked_post_count, - 'email': self.serialize_email, + "name": self.serialize_name, + "creationTime": self.serialize_creation_time, + "lastLoginTime": self.serialize_last_login_time, + "version": self.serialize_version, + "rank": self.serialize_rank, + "avatarStyle": self.serialize_avatar_style, + "avatarUrl": self.serialize_avatar_url, + "commentCount": self.serialize_comment_count, + "uploadedPostCount": self.serialize_uploaded_post_count, + "favoritePostCount": self.serialize_favorite_post_count, + "likedPostCount": self.serialize_liked_post_count, + "dislikedPostCount": self.serialize_disliked_post_count, + "email": self.serialize_email, } def serialize_name(self) -> Any: @@ -149,20 +158,22 @@ class UserSerializer(serialization.BaseSerializer): def serialize_user( - user: Optional[model.User], - auth_user: model.User, - options: List[str] = [], - force_show_email: bool = False) -> Optional[rest.Response]: + user: Optional[model.User], + auth_user: model.User, + options: List[str] = [], + force_show_email: bool = False, +) -> Optional[rest.Response]: if not user: return None return UserSerializer(user, auth_user, force_show_email).serialize(options) def serialize_micro_user( - user: Optional[model.User], - auth_user: model.User) -> Optional[rest.Response]: + user: Optional[model.User], auth_user: model.User +) -> Optional[rest.Response]: return serialize_user( - user, auth_user=auth_user, options=['name', 'avatarUrl']) + user, auth_user=auth_user, options=["name", "avatarUrl"] + ) def get_user_count() -> int: @@ -171,33 +182,34 @@ def get_user_count() -> int: def try_get_user_by_name(name: str) -> Optional[model.User]: return ( - db.session - .query(model.User) + db.session.query(model.User) .filter(sa.func.lower(model.User.name) == sa.func.lower(name)) - .one_or_none()) + .one_or_none() + ) def get_user_by_name(name: str) -> model.User: user = try_get_user_by_name(name) if not user: - raise UserNotFoundError('User %r not found.' % name) + raise UserNotFoundError("User %r not found." % name) return user def try_get_user_by_name_or_email(name_or_email: str) -> Optional[model.User]: return ( - db.session - .query(model.User) + db.session.query(model.User) .filter( - (sa.func.lower(model.User.name) == sa.func.lower(name_or_email)) | - (sa.func.lower(model.User.email) == sa.func.lower(name_or_email))) - .one_or_none()) + (sa.func.lower(model.User.name) == sa.func.lower(name_or_email)) + | (sa.func.lower(model.User.email) == sa.func.lower(name_or_email)) + ) + .one_or_none() + ) def get_user_by_name_or_email(name_or_email: str) -> model.User: user = try_get_user_by_name_or_email(name_or_email) if not user: - raise UserNotFoundError('User %r not found.' % name_or_email) + raise UserNotFoundError("User %r not found." % name_or_email) return user @@ -207,7 +219,7 @@ def create_user(name: str, password: str, email: str) -> model.User: update_user_password(user, password) update_user_email(user, email) if get_user_count() > 0: - user.rank = util.flip(auth.RANK_MAP)[config.config['default_rank']] + user.rank = util.flip(auth.RANK_MAP)[config.config["default_rank"]] else: user.rank = model.User.RANK_ADMINISTRATOR user.creation_time = datetime.utcnow() @@ -218,17 +230,18 @@ def create_user(name: str, password: str, email: str) -> model.User: def update_user_name(user: model.User, name: str) -> None: assert user if not name: - raise InvalidUserNameError('Name cannot be empty.') + raise InvalidUserNameError("Name cannot be empty.") if util.value_exceeds_column_size(name, model.User.name): - raise InvalidUserNameError('User name is too long.') + raise InvalidUserNameError("User name is too long.") name = name.strip() - name_regex = config.config['user_name_regex'] + name_regex = config.config["user_name_regex"] if not re.match(name_regex, name): raise InvalidUserNameError( - 'User name %r must satisfy regex %r.' % (name, name_regex)) + "User name %r must satisfy regex %r." % (name, name_regex) + ) other_user = try_get_user_by_name(name) if other_user and other_user.user_id != user.user_id: - raise UserAlreadyExistsError('User %r already exists.' % name) + raise UserAlreadyExistsError("User %r already exists." % name) if user.name and files.has(get_avatar_path(user.name)): files.move(get_avatar_path(user.name), get_avatar_path(name)) user.name = name @@ -237,14 +250,16 @@ def update_user_name(user: model.User, name: str) -> None: def update_user_password(user: model.User, password: str) -> None: assert user if not password: - raise InvalidPasswordError('Password cannot be empty.') - password_regex = config.config['password_regex'] + raise InvalidPasswordError("Password cannot be empty.") + password_regex = config.config["password_regex"] if not re.match(password_regex, password): raise InvalidPasswordError( - 'Password must satisfy regex %r.' % password_regex) + "Password must satisfy regex %r." % password_regex + ) user.password_salt = auth.create_password() password_hash, revision = auth.get_password_hash( - user.password_salt, password) + user.password_salt, password + ) user.password_hash = password_hash user.password_revision = revision @@ -253,53 +268,56 @@ def update_user_email(user: model.User, email: str) -> None: assert user email = email.strip() if util.value_exceeds_column_size(email, model.User.email): - raise InvalidEmailError('Email is too long.') + raise InvalidEmailError("Email is too long.") if not util.is_valid_email(email): - raise InvalidEmailError('E-mail is invalid.') + raise InvalidEmailError("E-mail is invalid.") user.email = email or None def update_user_rank( - user: model.User, rank: str, auth_user: model.User) -> None: + user: model.User, rank: str, auth_user: model.User +) -> None: assert user if not rank: - raise InvalidRankError('Rank cannot be empty.') + raise InvalidRankError("Rank cannot be empty.") rank = util.flip(auth.RANK_MAP).get(rank.strip(), None) all_ranks = list(auth.RANK_MAP.values()) if not rank: - raise InvalidRankError( - 'Rank can be either of %r.' % all_ranks) + raise InvalidRankError("Rank can be either of %r." % all_ranks) if rank in (model.User.RANK_ANONYMOUS, model.User.RANK_NOBODY): - raise InvalidRankError('Rank %r cannot be used.' % auth.RANK_MAP[rank]) - if all_ranks.index(auth_user.rank) \ - < all_ranks.index(rank) and get_user_count() > 0: - raise errors.AuthError('Trying to set higher rank than your own.') + raise InvalidRankError("Rank %r cannot be used." % auth.RANK_MAP[rank]) + if ( + all_ranks.index(auth_user.rank) < all_ranks.index(rank) + and get_user_count() > 0 + ): + raise errors.AuthError("Trying to set higher rank than your own.") user.rank = rank def update_user_avatar( - user: model.User, - avatar_style: str, - avatar_content: Optional[bytes] = None) -> None: + user: model.User, avatar_style: str, avatar_content: Optional[bytes] = None +) -> None: assert user - if avatar_style == 'gravatar': + if avatar_style == "gravatar": user.avatar_style = user.AVATAR_GRAVATAR - elif avatar_style == 'manual': + elif avatar_style == "manual": user.avatar_style = user.AVATAR_MANUAL - avatar_path = 'avatars/' + user.name.lower() + '.png' + avatar_path = "avatars/" + user.name.lower() + ".png" if not avatar_content: if files.has(avatar_path): return - raise InvalidAvatarError('Avatar content missing.') + raise InvalidAvatarError("Avatar content missing.") image = images.Image(avatar_content) image.resize_fill( - int(config.config['thumbnails']['avatar_width']), - int(config.config['thumbnails']['avatar_height'])) + int(config.config["thumbnails"]["avatar_width"]), + int(config.config["thumbnails"]["avatar_height"]), + ) files.save(avatar_path, image.to_png()) else: raise InvalidAvatarError( - 'Avatar style %r is invalid. Valid avatar styles: %r.' % ( - avatar_style, ['gravatar', 'manual'])) + "Avatar style %r is invalid. Valid avatar styles: %r." + % (avatar_style, ["gravatar", "manual"]) + ) def bump_user_login_time(user: model.User) -> None: @@ -312,7 +330,8 @@ def reset_user_password(user: model.User) -> str: password = auth.create_password() user.password_salt = auth.create_password() password_hash, revision = auth.get_password_hash( - user.password_salt, password) + user.password_salt, password + ) user.password_hash = password_hash user.password_revision = revision return password diff --git a/server/szurubooru/func/util.py b/server/szurubooru/func/util.py index 4638d4b..eacdc2a 100644 --- a/server/szurubooru/func/util.py +++ b/server/szurubooru/func/util.py @@ -1,29 +1,32 @@ -import os import hashlib +import os import re import tempfile -from typing import Any, Optional, Union, Tuple, List, Dict, Generator, TypeVar -from datetime import datetime, timedelta from contextlib import contextmanager -from szurubooru import errors +from datetime import datetime, timedelta +from typing import Any, Dict, Generator, List, Optional, Tuple, TypeVar, Union +from szurubooru import errors -T = TypeVar('T') +T = TypeVar("T") def snake_case_to_lower_camel_case(text: str) -> str: - components = text.split('_') - return components[0].lower() + \ - ''.join(word[0].upper() + word[1:].lower() for word in components[1:]) + components = text.split("_") + return components[0].lower() + "".join( + word[0].upper() + word[1:].lower() for word in components[1:] + ) def snake_case_to_upper_train_case(text: str) -> str: - return '-'.join( - word[0].upper() + word[1:].lower() for word in text.split('_')) + return "-".join( + word[0].upper() + word[1:].lower() for word in text.split("_") + ) def snake_case_to_lower_camel_case_keys( - source: Dict[str, Any]) -> Dict[str, Any]: + source: Dict[str, Any] +) -> Dict[str, Any]: target = {} for key, value in source.items(): target[snake_case_to_lower_camel_case(key)] = value @@ -35,7 +38,7 @@ def create_temp_file(**kwargs: Any) -> Generator: (descriptor, path) = tempfile.mkstemp(**kwargs) os.close(descriptor) try: - with open(path, 'r+b') as handle: + with open(path, "r+b") as handle: yield handle finally: os.remove(path) @@ -65,7 +68,7 @@ def flatten_list(source: List[List[T]]) -> List[T]: def get_md5(source: Union[str, bytes]) -> str: if not isinstance(source, bytes): - source = source.encode('utf-8') + source = source.encode("utf-8") md5 = hashlib.md5() md5.update(source) return md5.hexdigest() @@ -73,7 +76,7 @@ def get_md5(source: Union[str, bytes]) -> str: def get_sha1(source: Union[str, bytes]) -> str: if not isinstance(source, bytes): - source = source.encode('utf-8') + source = source.encode("utf-8") sha1 = hashlib.sha1() sha1.update(source) return sha1.hexdigest() @@ -84,12 +87,13 @@ def flip(source: Dict[Any, Any]) -> Dict[Any, Any]: def is_valid_email(email: Optional[str]) -> bool: - ''' Return whether given email address is valid or empty. ''' - return not email or re.match(r'^[^@]*@[^@]*\.[^@]*$', email) is not None + """ Return whether given email address is valid or empty. """ + return not email or re.match(r"^[^@]*@[^@]*\.[^@]*$", email) is not None + +class dotdict(dict): + """ dot.notation access to dictionary attributes. """ -class dotdict(dict): # pylint: disable=invalid-name - ''' dot.notation access to dictionary attributes. ''' def __getattr__(self, attr: str) -> Any: return self.get(attr) @@ -98,51 +102,54 @@ class dotdict(dict): # pylint: disable=invalid-name def parse_time_range(value: str) -> Tuple[datetime, datetime]: - ''' Return tuple containing min/max time for given text representation. ''' + """ Return tuple containing min/max time for given text representation. """ one_day = timedelta(days=1) one_second = timedelta(seconds=1) almost_one_day = one_day - one_second value = value.lower() if not value: - raise errors.ValidationError('Empty date format.') + raise errors.ValidationError("Empty date format.") - if value == 'today': + if value == "today": now = datetime.utcnow() return ( datetime(now.year, now.month, now.day, 0, 0, 0), - datetime(now.year, now.month, now.day, 0, 0, 0) + almost_one_day + datetime(now.year, now.month, now.day, 0, 0, 0) + almost_one_day, ) - if value == 'yesterday': + if value == "yesterday": now = datetime.utcnow() return ( datetime(now.year, now.month, now.day, 0, 0, 0) - one_day, - datetime(now.year, now.month, now.day, 0, 0, 0) - one_second) + datetime(now.year, now.month, now.day, 0, 0, 0) - one_second, + ) - match = re.match(r'^(\d{4})$', value) + match = re.match(r"^(\d{4})$", value) if match: year = int(match.group(1)) return (datetime(year, 1, 1), datetime(year + 1, 1, 1) - one_second) - match = re.match(r'^(\d{4})-(\d{1,2})$', value) + match = re.match(r"^(\d{4})-(\d{1,2})$", value) if match: year = int(match.group(1)) month = int(match.group(2)) return ( datetime(year, month, 1), - datetime(year, month + 1, 1) - one_second) + datetime(year, month + 1, 1) - one_second, + ) - match = re.match(r'^(\d{4})-(\d{1,2})-(\d{1,2})$', value) + match = re.match(r"^(\d{4})-(\d{1,2})-(\d{1,2})$", value) if match: year = int(match.group(1)) month = int(match.group(2)) day = int(match.group(3)) return ( datetime(year, month, day), - datetime(year, month, day + 1) - one_second) + datetime(year, month, day + 1) - one_second, + ) - raise errors.ValidationError('Invalid date format: %r.' % value) + raise errors.ValidationError("Invalid date format: %r." % value) def icase_unique(source: List[str]) -> List[str]: @@ -172,4 +179,4 @@ def get_column_size(column: Any) -> Optional[int]: def chunks(source_list: List[Any], part_size: int) -> Generator: for i in range(0, len(source_list), part_size): - yield source_list[i:i + part_size] + yield source_list[i : i + part_size] diff --git a/server/szurubooru/func/versions.py b/server/szurubooru/func/versions.py index 6e5a367..790b05b 100644 --- a/server/szurubooru/func/versions.py +++ b/server/szurubooru/func/versions.py @@ -1,16 +1,16 @@ -from szurubooru import errors, rest, model +from szurubooru import errors, model, rest def verify_version( - entity: model.Base, - context: rest.Context, - field_name: str = 'version') -> None: + entity: model.Base, context: rest.Context, field_name: str = "version" +) -> None: actual_version = context.get_param_as_int(field_name) expected_version = entity.version if actual_version != expected_version: raise errors.IntegrityError( - 'Someone else modified this in the meantime. ' + - 'Please try again.') + "Someone else modified this in the meantime. " + + "Please try again." + ) def bump_version(entity: model.Base) -> None: diff --git a/server/szurubooru/middleware/__init__.py b/server/szurubooru/middleware/__init__.py index c5a90d8..7177564 100644 --- a/server/szurubooru/middleware/__init__.py +++ b/server/szurubooru/middleware/__init__.py @@ -1,4 +1,4 @@ -''' Various hooks that get executed for each request. ''' +""" Various hooks that get executed for each request. """ import szurubooru.middleware.authenticator import szurubooru.middleware.cache_purger diff --git a/server/szurubooru/middleware/authenticator.py b/server/szurubooru/middleware/authenticator.py index 4340ec9..e73b235 100644 --- a/server/szurubooru/middleware/authenticator.py +++ b/server/szurubooru/middleware/authenticator.py @@ -1,55 +1,66 @@ import base64 from typing import Optional, Tuple -from szurubooru import model, errors, rest -from szurubooru.func import auth, users, user_tokens + +from szurubooru import errors, model, rest +from szurubooru.func import auth, user_tokens, users from szurubooru.rest.errors import HttpBadRequest def _authenticate_basic_auth(username: str, password: str) -> model.User: - ''' Try to authenticate user. Throw AuthError for invalid users. ''' + """ Try to authenticate user. Throw AuthError for invalid users. """ user = users.get_user_by_name(username) if not auth.is_valid_password(user, password): - raise errors.AuthError('Invalid password.') + raise errors.AuthError("Invalid password.") return user def _authenticate_token( - username: str, token: str) -> Tuple[model.User, model.UserToken]: - ''' Try to authenticate user. Throw AuthError for invalid users. ''' + username: str, token: str +) -> Tuple[model.User, model.UserToken]: + """ Try to authenticate user. Throw AuthError for invalid users. """ user = users.get_user_by_name(username) user_token = user_tokens.get_by_user_and_token(user, token) if not auth.is_valid_token(user_token): - raise errors.AuthError('Invalid token.') + raise errors.AuthError("Invalid token.") return user, user_token def _get_user(ctx: rest.Context, bump_login: bool) -> Optional[model.User]: - if not ctx.has_header('Authorization'): + if not ctx.has_header("Authorization"): return None auth_token = None try: - auth_type, credentials = ctx.get_header('Authorization').split(' ', 1) - if auth_type.lower() == 'basic': - username, password = base64.decodebytes( - credentials.encode('ascii')).decode('utf8').split(':', 1) + auth_type, credentials = ctx.get_header("Authorization").split(" ", 1) + if auth_type.lower() == "basic": + username, password = ( + base64.decodebytes(credentials.encode("ascii")) + .decode("utf8") + .split(":", 1) + ) auth_user = _authenticate_basic_auth(username, password) - elif auth_type.lower() == 'token': - username, token = base64.decodebytes( - credentials.encode('ascii')).decode('utf8').split(':', 1) + elif auth_type.lower() == "token": + username, token = ( + base64.decodebytes(credentials.encode("ascii")) + .decode("utf8") + .split(":", 1) + ) auth_user, auth_token = _authenticate_token(username, token) else: raise HttpBadRequest( - 'ValidationError', - 'Only basic or token HTTP authentication is supported.') + "ValidationError", + "Only basic or token HTTP authentication is supported.", + ) except ValueError as err: msg = ( - 'Authorization header values are not properly formed. ' - 'Supplied header {0}. Got error: {1}') + "Authorization header values are not properly formed. " + "Supplied header {0}. Got error: {1}" + ) raise HttpBadRequest( - 'ValidationError', - msg.format(ctx.get_header('Authorization'), str(err))) + "ValidationError", + msg.format(ctx.get_header("Authorization"), str(err)), + ) if bump_login and auth_user.user_id: users.bump_user_login_time(auth_user) @@ -61,8 +72,8 @@ def _get_user(ctx: rest.Context, bump_login: bool) -> Optional[model.User]: def process_request(ctx: rest.Context) -> None: - ''' Bind the user to request. Update last login time if needed. ''' - bump_login = ctx.get_param_as_bool('bump-login', default=False) + """ Bind the user to request. Update last login time if needed. """ + bump_login = ctx.get_param_as_bool("bump-login", default=False) auth_user = _get_user(ctx, bump_login) if auth_user: ctx.user = auth_user diff --git a/server/szurubooru/middleware/cache_purger.py b/server/szurubooru/middleware/cache_purger.py index d83fb84..e3d05a7 100644 --- a/server/szurubooru/middleware/cache_purger.py +++ b/server/szurubooru/middleware/cache_purger.py @@ -5,5 +5,5 @@ from szurubooru.rest import middleware @middleware.pre_hook def process_request(ctx: rest.Context) -> None: - if ctx.method != 'GET': + if ctx.method != "GET": cache.purge() diff --git a/server/szurubooru/middleware/request_logger.py b/server/szurubooru/middleware/request_logger.py index 54e40e4..79fffbd 100644 --- a/server/szurubooru/middleware/request_logger.py +++ b/server/szurubooru/middleware/request_logger.py @@ -1,8 +1,8 @@ import logging + from szurubooru import db, rest from szurubooru.rest import middleware - logger = logging.getLogger(__name__) @@ -14,8 +14,9 @@ def process_request(_ctx: rest.Context) -> None: @middleware.post_hook def process_response(ctx: rest.Context) -> None: logger.info( - '%s %s (user=%s, queries=%d)', + "%s %s (user=%s, queries=%d)", ctx.method, ctx.url, ctx.user.name, - db.get_query_count()) + db.get_query_count(), + ) diff --git a/server/szurubooru/migrations/env.py b/server/szurubooru/migrations/env.py index 59d031f..cd4f6ad 100644 --- a/server/szurubooru/migrations/env.py +++ b/server/szurubooru/migrations/env.py @@ -1,28 +1,40 @@ +""" +Alembic setup and configuration script + +isort:skip_file +""" + + +import logging.config import os import sys +from time import sleep import alembic import sqlalchemy as sa -import logging.config + +# fmt: off # make szurubooru module importable dir_to_self = os.path.dirname(os.path.realpath(__file__)) sys.path.append(os.path.join(dir_to_self, *[os.pardir] * 2)) -import szurubooru.model.base -import szurubooru.config +import szurubooru.config # noqa: E402 +import szurubooru.model.base # noqa: E402 +# fmt: on + alembic_config = alembic.context.config logging.config.fileConfig(alembic_config.config_file_name) szuru_config = szurubooru.config.config -alembic_config.set_main_option('sqlalchemy.url', szuru_config['database']) +alembic_config.set_main_option("sqlalchemy.url", szuru_config["database"]) target_metadata = szurubooru.model.Base.metadata def run_migrations_offline(): - ''' + """ Run migrations in 'offline' mode. This configures the context with just a URL @@ -32,35 +44,47 @@ def run_migrations_offline(): Calls to context.execute() here emit the given string to the script output. - ''' - url = alembic_config.get_main_option('sqlalchemy.url') + """ + url = alembic_config.get_main_option("sqlalchemy.url") alembic.context.configure( url=url, target_metadata=target_metadata, literal_binds=True, - compare_type=True) + compare_type=True, + ) with alembic.context.begin_transaction(): alembic.context.run_migrations() def run_migrations_online(): - ''' + """ Run migrations in 'online' mode. In this scenario we need to create an Engine and associate a connection with the context. - ''' + """ connectable = sa.engine_from_config( alembic_config.get_section(alembic_config.config_ini_section), - prefix='sqlalchemy.', - poolclass=sa.pool.NullPool) + prefix="sqlalchemy.", + poolclass=sa.pool.NullPool, + ) + + def connect_with_timeout(connectable, timeout=45): + dt = 5 + for _ in range(int(timeout / dt)): + try: + return connectable.connect() + except sa.exc.OperationalError: + sleep(dt) + return connectable.connect() - with connectable.connect() as connection: + with connect_with_timeout(connectable) as connection: alembic.context.configure( connection=connection, target_metadata=target_metadata, - compare_type=True) + compare_type=True, + ) with alembic.context.begin_transaction(): alembic.context.run_migrations() diff --git a/server/szurubooru/migrations/script.py.mako b/server/szurubooru/migrations/script.py.mako index 13adc51..f065447 100644 --- a/server/szurubooru/migrations/script.py.mako +++ b/server/szurubooru/migrations/script.py.mako @@ -7,6 +7,7 @@ Created at: ${create_date} import sqlalchemy as sa from alembic import op + ${imports if imports else ""} revision = ${repr(up_revision)} diff --git a/server/szurubooru/migrations/versions/00cb3a2734db_create_tag_tables.py b/server/szurubooru/migrations/versions/00cb3a2734db_create_tag_tables.py index 77d7641..a817044 100644 --- a/server/szurubooru/migrations/versions/00cb3a2734db_create_tag_tables.py +++ b/server/szurubooru/migrations/versions/00cb3a2734db_create_tag_tables.py @@ -1,65 +1,70 @@ -''' +""" Create tag tables Revision ID: 00cb3a2734db Created at: 2016-04-15 23:15:36.255429 -''' +""" import sqlalchemy as sa from alembic import op -revision = '00cb3a2734db' -down_revision = 'e5c1216a8503' +revision = "00cb3a2734db" +down_revision = "e5c1216a8503" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'tag_category', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.Unicode(length=32), nullable=False), - sa.Column('color', sa.Unicode(length=32), nullable=False), - sa.PrimaryKeyConstraint('id')) + "tag_category", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=32), nullable=False), + sa.Column("color", sa.Unicode(length=32), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'tag', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('category_id', sa.Integer(), nullable=False), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_edit_time', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['category_id'], ['tag_category.id']), - sa.PrimaryKeyConstraint('id')) + "tag", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("category_id", sa.Integer(), nullable=False), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_edit_time", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(["category_id"], ["tag_category.id"]), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'tag_name', - sa.Column('tag_name_id', sa.Integer(), nullable=False), - sa.Column('tag_id', sa.Integer(), nullable=False), - sa.Column('name', sa.Unicode(length=64), nullable=False), - sa.ForeignKeyConstraint(['tag_id'], ['tag.id']), - sa.PrimaryKeyConstraint('tag_name_id'), - sa.UniqueConstraint('name')) + "tag_name", + sa.Column("tag_name_id", sa.Integer(), nullable=False), + sa.Column("tag_id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=64), nullable=False), + sa.ForeignKeyConstraint(["tag_id"], ["tag.id"]), + sa.PrimaryKeyConstraint("tag_name_id"), + sa.UniqueConstraint("name"), + ) op.create_table( - 'tag_implication', - sa.Column('parent_id', sa.Integer(), nullable=False), - sa.Column('child_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['parent_id'], ['tag.id']), - sa.ForeignKeyConstraint(['child_id'], ['tag.id']), - sa.PrimaryKeyConstraint('parent_id', 'child_id')) + "tag_implication", + sa.Column("parent_id", sa.Integer(), nullable=False), + sa.Column("child_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["parent_id"], ["tag.id"]), + sa.ForeignKeyConstraint(["child_id"], ["tag.id"]), + sa.PrimaryKeyConstraint("parent_id", "child_id"), + ) op.create_table( - 'tag_suggestion', - sa.Column('parent_id', sa.Integer(), nullable=False), - sa.Column('child_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['parent_id'], ['tag.id']), - sa.ForeignKeyConstraint(['child_id'], ['tag.id']), - sa.PrimaryKeyConstraint('parent_id', 'child_id')) + "tag_suggestion", + sa.Column("parent_id", sa.Integer(), nullable=False), + sa.Column("child_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["parent_id"], ["tag.id"]), + sa.ForeignKeyConstraint(["child_id"], ["tag.id"]), + sa.PrimaryKeyConstraint("parent_id", "child_id"), + ) def downgrade(): - op.drop_table('tag_suggestion') - op.drop_table('tag_implication') - op.drop_table('tag_name') - op.drop_table('tag') - op.drop_table('tag_category') + op.drop_table("tag_suggestion") + op.drop_table("tag_implication") + op.drop_table("tag_name") + op.drop_table("tag") + op.drop_table("tag_category") diff --git a/server/szurubooru/migrations/versions/02ef5f73f4ab_add_hashes_to_post_file_names.py b/server/szurubooru/migrations/versions/02ef5f73f4ab_add_hashes_to_post_file_names.py index c7e5d01..fb4f09c 100644 --- a/server/szurubooru/migrations/versions/02ef5f73f4ab_add_hashes_to_post_file_names.py +++ b/server/szurubooru/migrations/versions/02ef5f73f4ab_add_hashes_to_post_file_names.py @@ -1,43 +1,45 @@ -''' +""" Add hashes to post file names Revision ID: 02ef5f73f4ab Created at: 2017-08-24 13:30:46.766928 -''' +""" import os import re + from szurubooru.func import files, posts -revision = '02ef5f73f4ab' -down_revision = '5f00af3004a4' +revision = "02ef5f73f4ab" +down_revision = "5f00af3004a4" branch_labels = None depends_on = None def upgrade(): - for name in ['posts', 'posts/custom-thumbnails', 'generated-thumbnails']: + for name in ["posts", "posts/custom-thumbnails", "generated-thumbnails"]: for entry in list(files.scan(name)): - match = re.match(r'^(?P<name>\d+)\.(?P<ext>\w+)$', entry.name) + match = re.match(r"^(?P<name>\d+)\.(?P<ext>\w+)$", entry.name) if match: - post_id = int(match.group('name')) + post_id = int(match.group("name")) security_hash = posts.get_post_security_hash(post_id) - ext = match.group('ext') - new_name = '%s_%s.%s' % (post_id, security_hash, ext) + ext = match.group("ext") + new_name = "%s_%s.%s" % (post_id, security_hash, ext) new_path = os.path.join(os.path.dirname(entry.path), new_name) os.rename(entry.path, new_path) def downgrade(): - for name in ['posts', 'posts/custom-thumbnails', 'generated-thumbnails']: + for name in ["posts", "posts/custom-thumbnails", "generated-thumbnails"]: for entry in list(files.scan(name)): match = re.match( - r'^(?P<name>\d+)_(?P<hash>[0-9A-Fa-f]+)\.(?P<ext>\w+)$', - entry.name) + r"^(?P<name>\d+)_(?P<hash>[0-9A-Fa-f]+)\.(?P<ext>\w+)$", + entry.name, + ) if match: - post_id = int(match.group('name')) - security_hash = match.group('hash') - ext = match.group('ext') - new_name = '%s.%s' % (post_id, ext) + post_id = int(match.group("name")) + security_hash = match.group("hash") # noqa: F841 + ext = match.group("ext") + new_name = "%s.%s" % (post_id, ext) new_path = os.path.join(os.path.dirname(entry.path), new_name) os.rename(entry.path, new_path) diff --git a/server/szurubooru/migrations/versions/055d0e048fb3_add_default_column_to_tag_categories.py b/server/szurubooru/migrations/versions/055d0e048fb3_add_default_column_to_tag_categories.py index 1ced159..0b4efb8 100644 --- a/server/szurubooru/migrations/versions/055d0e048fb3_add_default_column_to_tag_categories.py +++ b/server/szurubooru/migrations/versions/055d0e048fb3_add_default_column_to_tag_categories.py @@ -1,28 +1,30 @@ -''' +""" Add default column to tag categories Revision ID: 055d0e048fb3 Created at: 2016-05-22 18:12:58.149678 -''' +""" import sqlalchemy as sa from alembic import op -revision = '055d0e048fb3' -down_revision = '49ab4e1139ef' +revision = "055d0e048fb3" +down_revision = "49ab4e1139ef" branch_labels = None depends_on = None def upgrade(): op.add_column( - 'tag_category', sa.Column('default', sa.Boolean(), nullable=True)) + "tag_category", sa.Column("default", sa.Boolean(), nullable=True) + ) op.execute( - sa.table('tag_category', sa.column('default')) + sa.table("tag_category", sa.column("default")) .update() - .values(default=False)) - op.alter_column('tag_category', 'default', nullable=False) + .values(default=False) + ) + op.alter_column("tag_category", "default", nullable=False) def downgrade(): - op.drop_column('tag_category', 'default') + op.drop_column("tag_category", "default") diff --git a/server/szurubooru/migrations/versions/1cd4c7b22846_change_flags_column_to_string.py b/server/szurubooru/migrations/versions/1cd4c7b22846_change_flags_column_to_string.py index b450b1d..ef9c5f3 100644 --- a/server/szurubooru/migrations/versions/1cd4c7b22846_change_flags_column_to_string.py +++ b/server/szurubooru/migrations/versions/1cd4c7b22846_change_flags_column_to_string.py @@ -1,63 +1,54 @@ -''' +""" Change flags column to string Revision ID: 1cd4c7b22846 Created at: 2018-09-21 19:37:27.686568 -''' +""" import sqlalchemy as sa from alembic import op -revision = '1cd4c7b22846' -down_revision = 'a39c7f98a7fa' +revision = "1cd4c7b22846" +down_revision = "a39c7f98a7fa" branch_labels = None depends_on = None def upgrade(): conn = op.get_bind() - op.alter_column('post', 'flags', new_column_name='oldflags') - op.add_column('post', sa.Column( - 'flags', sa.Unicode(200), default='', nullable=True)) + op.alter_column("post", "flags", new_column_name="oldflags") + op.add_column( + "post", sa.Column("flags", sa.Unicode(200), default="", nullable=True) + ) posts = sa.Table( - 'post', + "post", sa.MetaData(), - sa.Column('id', sa.Integer, primary_key=True), - sa.Column('flags', sa.Unicode(200), default='', nullable=True), - sa.Column('oldflags', sa.PickleType(), nullable=True), + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("flags", sa.Unicode(200), default="", nullable=True), + sa.Column("oldflags", sa.PickleType(), nullable=True), ) for row in conn.execute(posts.select()): - newflag = ','.join(row.oldflags) if row.oldflags else '' + newflag = ",".join(row.oldflags) if row.oldflags else "" conn.execute( - # pylint: disable=no-value-for-parameter - posts.update().where( - posts.c.id == row.id - ).values( - flags=newflag - ) + posts.update().where(posts.c.id == row.id).values(flags=newflag) ) - op.drop_column('post', 'oldflags') + op.drop_column("post", "oldflags") def downgrade(): conn = op.get_bind() - op.alter_column('post', 'flags', new_column_name='oldflags') - op.add_column('post', sa.Column('flags', sa.PickleType(), nullable=True)) + op.alter_column("post", "flags", new_column_name="oldflags") + op.add_column("post", sa.Column("flags", sa.PickleType(), nullable=True)) posts = sa.Table( - 'post', + "post", sa.MetaData(), - sa.Column('id', sa.Integer, primary_key=True), - sa.Column('flags', sa.PickleType(), nullable=True), - sa.Column('oldflags', sa.Unicode(200), default='', nullable=True), + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("flags", sa.PickleType(), nullable=True), + sa.Column("oldflags", sa.Unicode(200), default="", nullable=True), ) for row in conn.execute(posts.select()): - newflag = [x for x in row.oldflags.split(',') if x] + newflag = [x for x in row.oldflags.split(",") if x] conn.execute( - # pylint: disable=no-value-for-parameter - posts.update().where( - posts.c.id == row.id - ).values( - flags=newflag - ) + posts.update().where(posts.c.id == row.id).values(flags=newflag) ) - op.drop_column('post', 'oldflags') + op.drop_column("post", "oldflags") diff --git a/server/szurubooru/migrations/versions/1e280b5d5df1_longer_tag_names.py b/server/szurubooru/migrations/versions/1e280b5d5df1_longer_tag_names.py new file mode 100644 index 0000000..5a79103 --- /dev/null +++ b/server/szurubooru/migrations/versions/1e280b5d5df1_longer_tag_names.py @@ -0,0 +1,50 @@ +""" +Longer tag names + +Revision ID: 1e280b5d5df1 +Created at: 2020-03-15 18:57:12.901148 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "1e280b5d5df1" +down_revision = "52d6ea6584b8" +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column( + "tag_name", + "name", + type_=sa.Unicode(128), + existing_type=sa.Unicode(64), + existing_nullable=False, + ) + + op.alter_column( + "snapshot", + "resource_name", + type_=sa.Unicode(128), + existing_type=sa.Unicode(64), + existing_nullable=False, + ) + + +def downgrade(): + op.alter_column( + "tag_name", + "name", + type_=sa.Unicode(64), + existing_type=sa.Unicode(128), + existing_nullable=False, + ) + + op.alter_column( + "snapshot", + "resource_name", + type_=sa.Unicode(64), + existing_type=sa.Unicode(128), + existing_nullable=False, + ) diff --git a/server/szurubooru/migrations/versions/23abaf4a0a4b_add_mime_type_to_posts.py b/server/szurubooru/migrations/versions/23abaf4a0a4b_add_mime_type_to_posts.py index e18119b..c8c4031 100644 --- a/server/szurubooru/migrations/versions/23abaf4a0a4b_add_mime_type_to_posts.py +++ b/server/szurubooru/migrations/versions/23abaf4a0a4b_add_mime_type_to_posts.py @@ -1,23 +1,24 @@ -''' +""" Add mime type to posts Revision ID: 23abaf4a0a4b Created at: 2016-05-02 00:02:33.024885 -''' +""" import sqlalchemy as sa from alembic import op -revision = '23abaf4a0a4b' -down_revision = 'ed6dd16a30f3' +revision = "23abaf4a0a4b" +down_revision = "ed6dd16a30f3" branch_labels = None depends_on = None def upgrade(): op.add_column( - 'post', sa.Column('mime-type', sa.Unicode(length=32), nullable=False)) + "post", sa.Column("mime-type", sa.Unicode(length=32), nullable=False) + ) def downgrade(): - op.drop_column('post', 'mime-type') + op.drop_column("post", "mime-type") diff --git a/server/szurubooru/migrations/versions/336a76ec1338_create_post_tables.py b/server/szurubooru/migrations/versions/336a76ec1338_create_post_tables.py index b767c98..aea6cc7 100644 --- a/server/szurubooru/migrations/versions/336a76ec1338_create_post_tables.py +++ b/server/szurubooru/migrations/versions/336a76ec1338_create_post_tables.py @@ -1,64 +1,67 @@ -''' +""" Create post tables Revision ID: 336a76ec1338 Created at: 2016-04-19 12:06:08.649503 -''' +""" import sqlalchemy as sa from alembic import op -revision = '336a76ec1338' -down_revision = '00cb3a2734db' +revision = "336a76ec1338" +down_revision = "00cb3a2734db" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'post', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_edit_time', sa.DateTime(), nullable=True), - sa.Column('safety', sa.Unicode(length=32), nullable=False), - sa.Column('type', sa.Unicode(length=32), nullable=False), - sa.Column('checksum', sa.Unicode(length=64), nullable=False), - sa.Column('source', sa.Unicode(length=200), nullable=True), - sa.Column('file_size', sa.Integer(), nullable=True), - sa.Column('image_width', sa.Integer(), nullable=True), - sa.Column('image_height', sa.Integer(), nullable=True), - sa.Column('flags', sa.Integer(), nullable=False), - sa.Column('auto_fav_count', sa.Integer(), nullable=False), - sa.Column('auto_score', sa.Integer(), nullable=False), - sa.Column('auto_feature_count', sa.Integer(), nullable=False), - sa.Column('auto_comment_count', sa.Integer(), nullable=False), - sa.Column('auto_note_count', sa.Integer(), nullable=False), - sa.Column('auto_fav_time', sa.Integer(), nullable=False), - sa.Column('auto_feature_time', sa.Integer(), nullable=False), - sa.Column('auto_comment_creation_time', sa.Integer(), nullable=False), - sa.Column('auto_comment_edit_time', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('id')) + "post", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_edit_time", sa.DateTime(), nullable=True), + sa.Column("safety", sa.Unicode(length=32), nullable=False), + sa.Column("type", sa.Unicode(length=32), nullable=False), + sa.Column("checksum", sa.Unicode(length=64), nullable=False), + sa.Column("source", sa.Unicode(length=200), nullable=True), + sa.Column("file_size", sa.Integer(), nullable=True), + sa.Column("image_width", sa.Integer(), nullable=True), + sa.Column("image_height", sa.Integer(), nullable=True), + sa.Column("flags", sa.Integer(), nullable=False), + sa.Column("auto_fav_count", sa.Integer(), nullable=False), + sa.Column("auto_score", sa.Integer(), nullable=False), + sa.Column("auto_feature_count", sa.Integer(), nullable=False), + sa.Column("auto_comment_count", sa.Integer(), nullable=False), + sa.Column("auto_note_count", sa.Integer(), nullable=False), + sa.Column("auto_fav_time", sa.Integer(), nullable=False), + sa.Column("auto_feature_time", sa.Integer(), nullable=False), + sa.Column("auto_comment_creation_time", sa.Integer(), nullable=False), + sa.Column("auto_comment_edit_time", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'post_relation', - sa.Column('parent_id', sa.Integer(), nullable=False), - sa.Column('child_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['child_id'], ['post.id']), - sa.ForeignKeyConstraint(['parent_id'], ['post.id']), - sa.PrimaryKeyConstraint('parent_id', 'child_id')) + "post_relation", + sa.Column("parent_id", sa.Integer(), nullable=False), + sa.Column("child_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["child_id"], ["post.id"]), + sa.ForeignKeyConstraint(["parent_id"], ["post.id"]), + sa.PrimaryKeyConstraint("parent_id", "child_id"), + ) op.create_table( - 'post_tag', - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('tag_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.ForeignKeyConstraint(['tag_id'], ['tag.id']), - sa.PrimaryKeyConstraint('post_id', 'tag_id')) + "post_tag", + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("tag_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.ForeignKeyConstraint(["tag_id"], ["tag.id"]), + sa.PrimaryKeyConstraint("post_id", "tag_id"), + ) def downgrade(): - op.drop_table('post_tag') - op.drop_table('post_relation') - op.drop_table('post') + op.drop_table("post_tag") + op.drop_table("post_relation") + op.drop_table("post") diff --git a/server/szurubooru/migrations/versions/3c1f0316fa7f_resize_post_columns.py b/server/szurubooru/migrations/versions/3c1f0316fa7f_resize_post_columns.py new file mode 100644 index 0000000..17e30d5 --- /dev/null +++ b/server/szurubooru/migrations/versions/3c1f0316fa7f_resize_post_columns.py @@ -0,0 +1,34 @@ +""" +resize post columns + +Revision ID: 3c1f0316fa7f +Created at: 2019-07-27 22:29:33.874837 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "3c1f0316fa7f" +down_revision = "1cd4c7b22846" +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column( + "post", "flags", type_=sa.Unicode(32), existing_type=sa.Unicode(200) + ) + + op.alter_column( + "post", "source", type_=sa.Unicode(2048), existing_type=sa.Unicode(200) + ) + + +def downgrade(): + op.alter_column( + "post", "flags", type_=sa.Unicode(200), existing_type=sa.Unicode(32) + ) + + op.alter_column( + "post", "source", type_=sa.Unicode(200), existing_type=sa.Unicode(2048) + ) diff --git a/server/szurubooru/migrations/versions/46cd5229839b_add_snapshot_resource_repr.py b/server/szurubooru/migrations/versions/46cd5229839b_add_snapshot_resource_repr.py index 0a46fbc..7620e90 100644 --- a/server/szurubooru/migrations/versions/46cd5229839b_add_snapshot_resource_repr.py +++ b/server/szurubooru/migrations/versions/46cd5229839b_add_snapshot_resource_repr.py @@ -1,24 +1,25 @@ -''' +""" Add snapshot resource_repr column Revision ID: 46cd5229839b Created at: 2016-04-21 19:00:48.087069 -''' +""" import sqlalchemy as sa from alembic import op -revision = '46cd5229839b' -down_revision = '565e01e3cf6d' +revision = "46cd5229839b" +down_revision = "565e01e3cf6d" branch_labels = None depends_on = None def upgrade(): op.add_column( - 'snapshot', - sa.Column('resource_repr', sa.Unicode(length=64), nullable=False)) + "snapshot", + sa.Column("resource_repr", sa.Unicode(length=64), nullable=False), + ) def downgrade(): - op.drop_column('snapshot', 'resource_repr') + op.drop_column("snapshot", "resource_repr") diff --git a/server/szurubooru/migrations/versions/46df355634dc_add_comment_tables.py b/server/szurubooru/migrations/versions/46df355634dc_add_comment_tables.py index 49971fe..c2a0092 100644 --- a/server/szurubooru/migrations/versions/46df355634dc_add_comment_tables.py +++ b/server/szurubooru/migrations/versions/46df355634dc_add_comment_tables.py @@ -1,43 +1,45 @@ -''' +""" Add comment tables Revision ID: 46df355634dc Created at: 2016-04-24 09:02:05.008648 -''' +""" import sqlalchemy as sa from alembic import op -revision = '46df355634dc' -down_revision = '84bd402f15f0' +revision = "46df355634dc" +down_revision = "84bd402f15f0" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'comment', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_edit_time', sa.DateTime(), nullable=True), - sa.Column('text', sa.UnicodeText(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.PrimaryKeyConstraint('id')) + "comment", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_edit_time", sa.DateTime(), nullable=True), + sa.Column("text", sa.UnicodeText(), nullable=True), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'comment_score', - sa.Column('comment_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('time', sa.DateTime(), nullable=False), - sa.Column('score', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['comment_id'], ['comment.id']), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('comment_id', 'user_id')) + "comment_score", + sa.Column("comment_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("time", sa.DateTime(), nullable=False), + sa.Column("score", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["comment_id"], ["comment.id"]), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("comment_id", "user_id"), + ) def downgrade(): - op.drop_table('comment_score') - op.drop_table('comment') + op.drop_table("comment_score") + op.drop_table("comment") diff --git a/server/szurubooru/migrations/versions/49ab4e1139ef_create_indexes.py b/server/szurubooru/migrations/versions/49ab4e1139ef_create_indexes.py index b18e410..73a0ad1 100644 --- a/server/szurubooru/migrations/versions/49ab4e1139ef_create_indexes.py +++ b/server/szurubooru/migrations/versions/49ab4e1139ef_create_indexes.py @@ -1,71 +1,74 @@ -''' +""" Create indexes Revision ID: 49ab4e1139ef Created at: 2016-05-09 09:38:28.078936 -''' +""" import sqlalchemy as sa from alembic import op -revision = '49ab4e1139ef' -down_revision = '23abaf4a0a4b' +revision = "49ab4e1139ef" +down_revision = "23abaf4a0a4b" branch_labels = None depends_on = None def upgrade(): for index_name, table_name, column_name in [ - ('ix_comment_post_id', 'comment', 'post_id'), - ('ix_comment_user_id', 'comment', 'user_id'), - ('ix_comment_score_user_id', 'comment_score', 'user_id'), - ('ix_post_user_id', 'post', 'user_id'), - ('ix_post_favorite_post_id', 'post_favorite', 'post_id'), - ('ix_post_favorite_user_id', 'post_favorite', 'user_id'), - ('ix_post_feature_post_id', 'post_feature', 'post_id'), - ('ix_post_feature_user_id', 'post_feature', 'user_id'), - ('ix_post_note_post_id', 'post_note', 'post_id'), - ('ix_post_relation_child_id', 'post_relation', 'child_id'), - ('ix_post_relation_parent_id', 'post_relation', 'parent_id'), - ('ix_post_score_post_id', 'post_score', 'post_id'), - ('ix_post_score_user_id', 'post_score', 'user_id'), - ('ix_post_tag_post_id', 'post_tag', 'post_id'), - ('ix_post_tag_tag_id', 'post_tag', 'tag_id'), - ('ix_snapshot_resource_id', 'snapshot', 'resource_id'), - ('ix_snapshot_resource_type', 'snapshot', 'resource_type'), - ('ix_tag_category_id', 'tag', 'category_id'), - ('ix_tag_implication_child_id', 'tag_implication', 'child_id'), - ('ix_tag_implication_parent_id', 'tag_implication', 'parent_id'), - ('ix_tag_name_tag_id', 'tag_name', 'tag_id'), - ('ix_tag_suggestion_child_id', 'tag_suggestion', 'child_id'), - ('ix_tag_suggestion_parent_id', 'tag_suggestion', 'parent_id')]: + ("ix_comment_post_id", "comment", "post_id"), + ("ix_comment_user_id", "comment", "user_id"), + ("ix_comment_score_user_id", "comment_score", "user_id"), + ("ix_post_user_id", "post", "user_id"), + ("ix_post_favorite_post_id", "post_favorite", "post_id"), + ("ix_post_favorite_user_id", "post_favorite", "user_id"), + ("ix_post_feature_post_id", "post_feature", "post_id"), + ("ix_post_feature_user_id", "post_feature", "user_id"), + ("ix_post_note_post_id", "post_note", "post_id"), + ("ix_post_relation_child_id", "post_relation", "child_id"), + ("ix_post_relation_parent_id", "post_relation", "parent_id"), + ("ix_post_score_post_id", "post_score", "post_id"), + ("ix_post_score_user_id", "post_score", "user_id"), + ("ix_post_tag_post_id", "post_tag", "post_id"), + ("ix_post_tag_tag_id", "post_tag", "tag_id"), + ("ix_snapshot_resource_id", "snapshot", "resource_id"), + ("ix_snapshot_resource_type", "snapshot", "resource_type"), + ("ix_tag_category_id", "tag", "category_id"), + ("ix_tag_implication_child_id", "tag_implication", "child_id"), + ("ix_tag_implication_parent_id", "tag_implication", "parent_id"), + ("ix_tag_name_tag_id", "tag_name", "tag_id"), + ("ix_tag_suggestion_child_id", "tag_suggestion", "child_id"), + ("ix_tag_suggestion_parent_id", "tag_suggestion", "parent_id"), + ]: op.create_index( - op.f(index_name), table_name, [column_name], unique=False) + op.f(index_name), table_name, [column_name], unique=False + ) def downgrade(): for index_name, table_name in [ - ('ix_tag_suggestion_parent_id', 'tag_suggestion'), - ('ix_tag_suggestion_child_id', 'tag_suggestion'), - ('ix_tag_name_tag_id', 'tag_name'), - ('ix_tag_implication_parent_id', 'tag_implication'), - ('ix_tag_implication_child_id', 'tag_implication'), - ('ix_tag_category_id', 'tag'), - ('ix_snapshot_resource_type', 'snapshot'), - ('ix_snapshot_resource_id', 'snapshot'), - ('ix_post_tag_tag_id', 'post_tag'), - ('ix_post_tag_post_id', 'post_tag'), - ('ix_post_score_user_id', 'post_score'), - ('ix_post_score_post_id', 'post_score'), - ('ix_post_relation_parent_id', 'post_relation'), - ('ix_post_relation_child_id', 'post_relation'), - ('ix_post_note_post_id', 'post_note'), - ('ix_post_feature_user_id', 'post_feature'), - ('ix_post_feature_post_id', 'post_feature'), - ('ix_post_favorite_user_id', 'post_favorite'), - ('ix_post_favorite_post_id', 'post_favorite'), - ('ix_post_user_id', 'post'), - ('ix_comment_score_user_id', 'comment_score'), - ('ix_comment_user_id', 'comment'), - ('ix_comment_post_id', 'comment')]: + ("ix_tag_suggestion_parent_id", "tag_suggestion"), + ("ix_tag_suggestion_child_id", "tag_suggestion"), + ("ix_tag_name_tag_id", "tag_name"), + ("ix_tag_implication_parent_id", "tag_implication"), + ("ix_tag_implication_child_id", "tag_implication"), + ("ix_tag_category_id", "tag"), + ("ix_snapshot_resource_type", "snapshot"), + ("ix_snapshot_resource_id", "snapshot"), + ("ix_post_tag_tag_id", "post_tag"), + ("ix_post_tag_post_id", "post_tag"), + ("ix_post_score_user_id", "post_score"), + ("ix_post_score_post_id", "post_score"), + ("ix_post_relation_parent_id", "post_relation"), + ("ix_post_relation_child_id", "post_relation"), + ("ix_post_note_post_id", "post_note"), + ("ix_post_feature_user_id", "post_feature"), + ("ix_post_feature_post_id", "post_feature"), + ("ix_post_favorite_user_id", "post_favorite"), + ("ix_post_favorite_post_id", "post_favorite"), + ("ix_post_user_id", "post"), + ("ix_comment_score_user_id", "comment_score"), + ("ix_comment_user_id", "comment"), + ("ix_comment_post_id", "comment"), + ]: op.drop_index(op.f(index_name), table_name=table_name) diff --git a/server/szurubooru/migrations/versions/4a020f1d271a_rename_snapshot_columns.py b/server/szurubooru/migrations/versions/4a020f1d271a_rename_snapshot_columns.py index e1c98ca..957bede 100644 --- a/server/szurubooru/migrations/versions/4a020f1d271a_rename_snapshot_columns.py +++ b/server/szurubooru/migrations/versions/4a020f1d271a_rename_snapshot_columns.py @@ -1,54 +1,57 @@ -''' +""" Rename snapshot columns Revision ID: 4a020f1d271a Created at: 2016-08-16 09:25:38.350861 -''' +""" import sqlalchemy as sa from alembic import op - -revision = '4a020f1d271a' -down_revision = '840b460c5613' +revision = "4a020f1d271a" +down_revision = "840b460c5613" branch_labels = None depends_on = None def upgrade(): op.add_column( - 'snapshot', - sa.Column('resource_name', sa.Unicode(length=64), nullable=False)) + "snapshot", + sa.Column("resource_name", sa.Unicode(length=64), nullable=False), + ) op.add_column( - 'snapshot', - sa.Column('resource_pkey', sa.Integer(), nullable=False)) + "snapshot", sa.Column("resource_pkey", sa.Integer(), nullable=False) + ) op.create_index( - op.f('ix_snapshot_resource_pkey'), - 'snapshot', - ['resource_pkey'], - unique=False) - op.drop_index('ix_snapshot_resource_id', table_name='snapshot') - op.drop_column('snapshot', 'resource_id') - op.drop_column('snapshot', 'resource_repr') + op.f("ix_snapshot_resource_pkey"), + "snapshot", + ["resource_pkey"], + unique=False, + ) + op.drop_index("ix_snapshot_resource_id", table_name="snapshot") + op.drop_column("snapshot", "resource_id") + op.drop_column("snapshot", "resource_repr") def downgrade(): op.add_column( - 'snapshot', + "snapshot", sa.Column( - 'resource_repr', + "resource_repr", sa.VARCHAR(length=64), autoincrement=False, - nullable=False)) + nullable=False, + ), + ) op.add_column( - 'snapshot', + "snapshot", sa.Column( - 'resource_id', - sa.INTEGER(), - autoincrement=False, - nullable=False)) + "resource_id", sa.INTEGER(), autoincrement=False, nullable=False + ), + ) op.create_index( - 'ix_snapshot_resource_id', 'snapshot', ['resource_id'], unique=False) - op.drop_index(op.f('ix_snapshot_resource_pkey'), table_name='snapshot') - op.drop_column('snapshot', 'resource_pkey') - op.drop_column('snapshot', 'resource_name') + "ix_snapshot_resource_id", "snapshot", ["resource_id"], unique=False + ) + op.drop_index(op.f("ix_snapshot_resource_pkey"), table_name="snapshot") + op.drop_column("snapshot", "resource_pkey") + op.drop_column("snapshot", "resource_name") diff --git a/server/szurubooru/migrations/versions/4c526f869323_add_description_to_tags.py b/server/szurubooru/migrations/versions/4c526f869323_add_description_to_tags.py index f53866f..443bc3c 100644 --- a/server/szurubooru/migrations/versions/4c526f869323_add_description_to_tags.py +++ b/server/szurubooru/migrations/versions/4c526f869323_add_description_to_tags.py @@ -1,23 +1,24 @@ -''' +""" Add description to tags Revision ID: 4c526f869323 Created at: 2016-06-21 17:56:34.979741 -''' +""" import sqlalchemy as sa from alembic import op -revision = '4c526f869323' -down_revision = '055d0e048fb3' +revision = "4c526f869323" +down_revision = "055d0e048fb3" branch_labels = None depends_on = None def upgrade(): op.add_column( - 'tag', sa.Column('description', sa.UnicodeText(), nullable=True)) + "tag", sa.Column("description", sa.UnicodeText(), nullable=True) + ) def downgrade(): - op.drop_column('tag', 'description') + op.drop_column("tag", "description") diff --git a/server/szurubooru/migrations/versions/52d6ea6584b8_generate_post_signature_table.py b/server/szurubooru/migrations/versions/52d6ea6584b8_generate_post_signature_table.py new file mode 100644 index 0000000..e544f80 --- /dev/null +++ b/server/szurubooru/migrations/versions/52d6ea6584b8_generate_post_signature_table.py @@ -0,0 +1,30 @@ +""" +Generate post signature table + +Revision ID: 52d6ea6584b8 +Created at: 2020-03-07 17:03:40.193512 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "52d6ea6584b8" +down_revision = "3c1f0316fa7f" +branch_labels = None +depends_on = None + + +def upgrade(): + ArrayType = sa.dialects.postgresql.ARRAY(sa.Integer, dimensions=1) + op.create_table( + "post_signature", + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("signature", sa.LargeBinary(), nullable=False), + sa.Column("words", ArrayType, nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.PrimaryKeyConstraint("post_id"), + ) + + +def downgrade(): + op.drop_table("post_signature") diff --git a/server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py b/server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py new file mode 100644 index 0000000..73bf342 --- /dev/null +++ b/server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py @@ -0,0 +1,60 @@ +""" +add default pool category + +Revision ID: 54de8acc6cef +Created at: 2020-05-03 14:57:46.825766 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "54de8acc6cef" +down_revision = "6a2f424ec9d2" +branch_labels = None +depends_on = None + + +Base = sa.ext.declarative.declarative_base() + + +class PoolCategory(Base): + __tablename__ = "pool_category" + __table_args__ = {"extend_existing": True} + + pool_category_id = sa.Column("id", sa.Integer, primary_key=True) + version = sa.Column("version", sa.Integer, nullable=False) + name = sa.Column("name", sa.Unicode(32), nullable=False) + color = sa.Column("color", sa.Unicode(32), nullable=False) + default = sa.Column("default", sa.Boolean, nullable=False) + + __mapper_args__ = { + "version_id_col": version, + "version_id_generator": False, + } + + +def upgrade(): + session = sa.orm.session.Session(bind=op.get_bind()) + if session.query(PoolCategory).count() == 0: + category = PoolCategory() + category.name = "default" + category.color = "default" + category.version = 1 + category.default = True + session.add(category) + session.commit() + + +def downgrade(): + session = sa.orm.session.Session(bind=op.get_bind()) + default_category = ( + session.query(PoolCategory) + .filter(PoolCategory.name == "default") + .filter(PoolCategory.color == "default") + .filter(PoolCategory.version == 1) + .filter(PoolCategory.default == 1) + .one_or_none() + ) + if default_category: + session.delete(default_category) + session.commit() diff --git a/server/szurubooru/migrations/versions/565e01e3cf6d_create_snapshot_table.py b/server/szurubooru/migrations/versions/565e01e3cf6d_create_snapshot_table.py index 475fe96..a4fb89f 100644 --- a/server/szurubooru/migrations/versions/565e01e3cf6d_create_snapshot_table.py +++ b/server/szurubooru/migrations/versions/565e01e3cf6d_create_snapshot_table.py @@ -1,32 +1,33 @@ -''' +""" Create snapshot table Revision ID: 565e01e3cf6d Created at: 2016-04-19 12:07:58.372426 -''' +""" import sqlalchemy as sa from alembic import op -revision = '565e01e3cf6d' -down_revision = '336a76ec1338' +revision = "565e01e3cf6d" +down_revision = "336a76ec1338" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'snapshot', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('resource_type', sa.Unicode(length=32), nullable=False), - sa.Column('resource_id', sa.Integer(), nullable=False), - sa.Column('operation', sa.Unicode(length=16), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('data', sa.PickleType(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('id')) + "snapshot", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("resource_type", sa.Unicode(length=32), nullable=False), + sa.Column("resource_id", sa.Integer(), nullable=False), + sa.Column("operation", sa.Unicode(length=16), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("data", sa.PickleType(), nullable=True), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("id"), + ) def downgrade(): - op.drop_table('snapshot') + op.drop_table("snapshot") diff --git a/server/szurubooru/migrations/versions/5f00af3004a4_add_default_tag_category.py b/server/szurubooru/migrations/versions/5f00af3004a4_add_default_tag_category.py index e097e67..716a04e 100644 --- a/server/szurubooru/migrations/versions/5f00af3004a4_add_default_tag_category.py +++ b/server/szurubooru/migrations/versions/5f00af3004a4_add_default_tag_category.py @@ -1,18 +1,17 @@ -''' +""" Add default tag category Revision ID: 5f00af3004a4 Created at: 2017-02-02 20:06:13.336380 -''' +""" import sqlalchemy as sa -from alembic import op import sqlalchemy.ext.declarative import sqlalchemy.orm.session +from alembic import op - -revision = '5f00af3004a4' -down_revision = '9837fc981ec7' +revision = "5f00af3004a4" +down_revision = "9837fc981ec7" branch_labels = None depends_on = None @@ -21,18 +20,18 @@ Base = sa.ext.declarative.declarative_base() class TagCategory(Base): - __tablename__ = 'tag_category' - __table_args__ = {'extend_existing': True} + __tablename__ = "tag_category" + __table_args__ = {"extend_existing": True} - tag_category_id = sa.Column('id', sa.Integer, primary_key=True) - version = sa.Column('version', sa.Integer, nullable=False) - name = sa.Column('name', sa.Unicode(32), nullable=False) - color = sa.Column('color', sa.Unicode(32), nullable=False) - default = sa.Column('default', sa.Boolean, nullable=False) + tag_category_id = sa.Column("id", sa.Integer, primary_key=True) + version = sa.Column("version", sa.Integer, nullable=False) + name = sa.Column("name", sa.Unicode(32), nullable=False) + color = sa.Column("color", sa.Unicode(32), nullable=False) + default = sa.Column("default", sa.Boolean, nullable=False) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } @@ -40,8 +39,8 @@ def upgrade(): session = sa.orm.session.Session(bind=op.get_bind()) if session.query(TagCategory).count() == 0: category = TagCategory() - category.name = 'default' - category.color = 'default' + category.name = "default" + category.color = "default" category.version = 1 category.default = True session.add(category) @@ -51,13 +50,13 @@ def upgrade(): def downgrade(): session = sa.orm.session.Session(bind=op.get_bind()) default_category = ( - session - .query(TagCategory) - .filter(TagCategory.name == 'default') - .filter(TagCategory.color == 'default') + session.query(TagCategory) + .filter(TagCategory.name == "default") + .filter(TagCategory.color == "default") .filter(TagCategory.version == 1) .filter(TagCategory.default == 1) - .one_or_none()) + .one_or_none() + ) if default_category: session.delete(default_category) session.commit() diff --git a/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py b/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py new file mode 100644 index 0000000..197373d --- /dev/null +++ b/server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py @@ -0,0 +1,67 @@ +""" +create pool tables + +Revision ID: 6a2f424ec9d2 +Created at: 2020-05-03 14:47:59.136410 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "6a2f424ec9d2" +down_revision = "1e280b5d5df1" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "pool_category", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("version", sa.Integer(), nullable=False, default=1), + sa.Column("name", sa.Unicode(length=32), nullable=False), + sa.Column("color", sa.Unicode(length=32), nullable=False), + sa.Column("default", sa.Boolean(), nullable=False, default=False), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_table( + "pool", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("version", sa.Integer(), nullable=False, default=1), + sa.Column("description", sa.UnicodeText(), nullable=True), + sa.Column("category_id", sa.Integer(), nullable=False), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_edit_time", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(["category_id"], ["pool_category.id"]), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_table( + "pool_name", + sa.Column("pool_name_id", sa.Integer(), nullable=False), + sa.Column("pool_id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=256), nullable=False), + sa.Column("ord", sa.Integer(), nullable=False, index=True), + sa.ForeignKeyConstraint(["pool_id"], ["pool.id"]), + sa.PrimaryKeyConstraint("pool_name_id"), + sa.UniqueConstraint("name"), + ) + + op.create_table( + "pool_post", + sa.Column("pool_id", sa.Integer(), nullable=False), + sa.Column("post_id", sa.Integer(), nullable=False, index=True), + sa.Column("ord", sa.Integer(), nullable=False, index=True), + sa.ForeignKeyConstraint(["pool_id"], ["pool.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["post_id"], ["post.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("pool_id", "post_id"), + ) + + +def downgrade(): + op.drop_index(op.f("ix_pool_name_ord"), table_name="pool_name") + op.drop_table("pool_post") + op.drop_table("pool_name") + op.drop_table("pool") + op.drop_table("pool_category") diff --git a/server/szurubooru/migrations/versions/7f6baf38c27c_add_versions.py b/server/szurubooru/migrations/versions/7f6baf38c27c_add_versions.py index 2236026..24056c0 100644 --- a/server/szurubooru/migrations/versions/7f6baf38c27c_add_versions.py +++ b/server/szurubooru/migrations/versions/7f6baf38c27c_add_versions.py @@ -1,31 +1,30 @@ -''' +""" Add entity versions Revision ID: 7f6baf38c27c Created at: 2016-08-06 22:26:58.111763 -''' +""" import sqlalchemy as sa from alembic import op -revision = '7f6baf38c27c' -down_revision = '4c526f869323' +revision = "7f6baf38c27c" +down_revision = "4c526f869323" branch_labels = None depends_on = None -tables = ['tag_category', 'tag', 'user', 'post', 'comment'] +tables = ["tag_category", "tag", "user", "post", "comment"] def upgrade(): for table in tables: - op.add_column(table, sa.Column('version', sa.Integer(), nullable=True)) + op.add_column(table, sa.Column("version", sa.Integer(), nullable=True)) op.execute( - sa.table(table, sa.column('version')) - .update() - .values(version=1)) - op.alter_column(table, 'version', nullable=False) + sa.table(table, sa.column("version")).update().values(version=1) + ) + op.alter_column(table, "version", nullable=False) def downgrade(): for table in tables: - op.drop_column(table, 'version') + op.drop_column(table, "version") diff --git a/server/szurubooru/migrations/versions/840b460c5613_fix_foreignkeys.py b/server/szurubooru/migrations/versions/840b460c5613_fix_foreignkeys.py index 8821dd8..b2b5df5 100644 --- a/server/szurubooru/migrations/versions/840b460c5613_fix_foreignkeys.py +++ b/server/szurubooru/migrations/versions/840b460c5613_fix_foreignkeys.py @@ -1,33 +1,36 @@ -''' +""" Fix ForeignKey constraint definitions Revision ID: 840b460c5613 Created at: 2016-08-15 18:39:30.909867 -''' +""" import sqlalchemy as sa from alembic import op - -revision = '840b460c5613' -down_revision = '7f6baf38c27c' +revision = "840b460c5613" +down_revision = "7f6baf38c27c" branch_labels = None depends_on = None def upgrade(): - op.drop_constraint('post_user_id_fkey', 'post', type_='foreignkey') - op.drop_constraint('snapshot_user_id_fkey', 'snapshot', type_='foreignkey') + op.drop_constraint("post_user_id_fkey", "post", type_="foreignkey") + op.drop_constraint("snapshot_user_id_fkey", "snapshot", type_="foreignkey") op.create_foreign_key( - None, 'post', 'user', ['user_id'], ['id'], ondelete='SET NULL') + None, "post", "user", ["user_id"], ["id"], ondelete="SET NULL" + ) op.create_foreign_key( - None, 'snapshot', 'user', ['user_id'], ['id'], ondelete='set null') + None, "snapshot", "user", ["user_id"], ["id"], ondelete="set null" + ) def downgrade(): - op.drop_constraint(None, 'snapshot', type_='foreignkey') - op.drop_constraint(None, 'post', type_='foreignkey') + op.drop_constraint(None, "snapshot", type_="foreignkey") + op.drop_constraint(None, "post", type_="foreignkey") op.create_foreign_key( - 'snapshot_user_id_fkey', 'snapshot', 'user', ['user_id'], ['id']) + "snapshot_user_id_fkey", "snapshot", "user", ["user_id"], ["id"] + ) op.create_foreign_key( - 'post_user_id_fkey', 'post', 'user', ['user_id'], ['id']) + "post_user_id_fkey", "post", "user", ["user_id"], ["id"] + ) diff --git a/server/szurubooru/migrations/versions/84bd402f15f0_change_flags_column_type.py b/server/szurubooru/migrations/versions/84bd402f15f0_change_flags_column_type.py index 7236641..d495b82 100644 --- a/server/szurubooru/migrations/versions/84bd402f15f0_change_flags_column_type.py +++ b/server/szurubooru/migrations/versions/84bd402f15f0_change_flags_column_type.py @@ -1,26 +1,27 @@ -''' +""" Change flags column type Revision ID: 84bd402f15f0 Created at: 2016-04-22 20:48:32.386159 -''' +""" import sqlalchemy as sa from alembic import op -revision = '84bd402f15f0' -down_revision = '9587de88a84b' +revision = "84bd402f15f0" +down_revision = "9587de88a84b" branch_labels = None depends_on = None def upgrade(): - op.drop_column('post', 'flags') - op.add_column('post', sa.Column('flags', sa.PickleType(), nullable=True)) + op.drop_column("post", "flags") + op.add_column("post", sa.Column("flags", sa.PickleType(), nullable=True)) def downgrade(): - op.drop_column('post', 'flags') + op.drop_column("post", "flags") op.add_column( - 'post', - sa.Column('flags', sa.Integer(), autoincrement=False, nullable=False)) + "post", + sa.Column("flags", sa.Integer(), autoincrement=False, nullable=False), + ) diff --git a/server/szurubooru/migrations/versions/9587de88a84b_create_aux_post_tables.py b/server/szurubooru/migrations/versions/9587de88a84b_create_aux_post_tables.py index eddec24..46647cf 100644 --- a/server/szurubooru/migrations/versions/9587de88a84b_create_aux_post_tables.py +++ b/server/szurubooru/migrations/versions/9587de88a84b_create_aux_post_tables.py @@ -1,61 +1,65 @@ -''' +""" Create auxilliary post tables Revision ID: 9587de88a84b Created at: 2016-04-22 17:42:57.697229 -''' +""" import sqlalchemy as sa from alembic import op -revision = '9587de88a84b' -down_revision = '46cd5229839b' +revision = "9587de88a84b" +down_revision = "46cd5229839b" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'post_favorite', - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('time', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('post_id', 'user_id')) + "post_favorite", + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("time", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("post_id", "user_id"), + ) op.create_table( - 'post_feature', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('time', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('id')) + "post_feature", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("time", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'post_note', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('text', sa.UnicodeText(), nullable=False), - sa.Column('polygon', sa.PickleType(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.PrimaryKeyConstraint('id')) + "post_note", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("text", sa.UnicodeText(), nullable=False), + sa.Column("polygon", sa.PickleType(), nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( - 'post_score', - sa.Column('post_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('time', sa.DateTime(), nullable=False), - sa.Column('score', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.id']), - sa.ForeignKeyConstraint(['user_id'], ['user.id']), - sa.PrimaryKeyConstraint('post_id', 'user_id')) + "post_score", + sa.Column("post_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("time", sa.DateTime(), nullable=False), + sa.Column("score", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["post_id"], ["post.id"]), + sa.ForeignKeyConstraint(["user_id"], ["user.id"]), + sa.PrimaryKeyConstraint("post_id", "user_id"), + ) def downgrade(): - op.drop_table('post_score') - op.drop_table('post_note') - op.drop_table('post_feature') - op.drop_table('post_favorite') + op.drop_table("post_score") + op.drop_table("post_note") + op.drop_table("post_feature") + op.drop_table("post_favorite") diff --git a/server/szurubooru/migrations/versions/9837fc981ec7_add_order_to_tag_names.py b/server/szurubooru/migrations/versions/9837fc981ec7_add_order_to_tag_names.py index 39f9edf..d1e74e9 100644 --- a/server/szurubooru/migrations/versions/9837fc981ec7_add_order_to_tag_names.py +++ b/server/szurubooru/migrations/versions/9837fc981ec7_add_order_to_tag_names.py @@ -1,17 +1,16 @@ -''' +""" Add order to tag names Revision ID: 9837fc981ec7 Created at: 2016-08-28 19:03:59.831527 -''' +""" import sqlalchemy as sa -from alembic import op import sqlalchemy.ext.declarative +from alembic import op - -revision = '9837fc981ec7' -down_revision = '4a020f1d271a' +revision = "9837fc981ec7" +down_revision = "4a020f1d271a" branch_labels = None depends_on = None @@ -20,21 +19,20 @@ Base = sa.ext.declarative.declarative_base() class TagName(Base): - __tablename__ = 'tag_name' - __table_args__ = {'extend_existing': True} + __tablename__ = "tag_name" + __table_args__ = {"extend_existing": True} - tag_name_id = sa.Column('tag_name_id', sa.Integer, primary_key=True) - ord = sa.Column('ord', sa.Integer, nullable=False, index=True) + tag_name_id = sa.Column("tag_name_id", sa.Integer, primary_key=True) + ord = sa.Column("ord", sa.Integer, nullable=False, index=True) def upgrade(): - op.add_column('tag_name', sa.Column('ord', sa.Integer(), nullable=True)) + op.add_column("tag_name", sa.Column("ord", sa.Integer(), nullable=True)) op.execute(TagName.__table__.update().values(ord=TagName.tag_name_id)) - op.alter_column('tag_name', 'ord', nullable=False) - op.create_index( - op.f('ix_tag_name_ord'), 'tag_name', ['ord'], unique=False) + op.alter_column("tag_name", "ord", nullable=False) + op.create_index(op.f("ix_tag_name_ord"), "tag_name", ["ord"], unique=False) def downgrade(): - op.drop_index(op.f('ix_tag_name_ord'), table_name='tag_name') - op.drop_column('tag_name', 'ord') + op.drop_index(op.f("ix_tag_name_ord"), table_name="tag_name") + op.drop_column("tag_name", "ord") diff --git a/server/szurubooru/migrations/versions/9ef1a1643c2a_update_user_table_for_hardened_passwords.py b/server/szurubooru/migrations/versions/9ef1a1643c2a_update_user_table_for_hardened_passwords.py index 3805772..b8763a7 100644 --- a/server/szurubooru/migrations/versions/9ef1a1643c2a_update_user_table_for_hardened_passwords.py +++ b/server/szurubooru/migrations/versions/9ef1a1643c2a_update_user_table_for_hardened_passwords.py @@ -1,19 +1,18 @@ -''' +""" Alter the password_hash field to work with larger output. Particularly libsodium output for greater password security. Revision ID: 9ef1a1643c2a Created at: 2018-02-24 23:00:32.848575 -''' +""" import sqlalchemy as sa import sqlalchemy.ext.declarative import sqlalchemy.orm.session from alembic import op - -revision = '9ef1a1643c2a' -down_revision = '02ef5f73f4ab' +revision = "9ef1a1643c2a" +down_revision = "02ef5f73f4ab" branch_labels = None depends_on = None @@ -21,43 +20,46 @@ Base = sa.ext.declarative.declarative_base() class User(Base): - __tablename__ = 'user' + __tablename__ = "user" - AVATAR_GRAVATAR = 'gravatar' + AVATAR_GRAVATAR = "gravatar" - user_id = sa.Column('id', sa.Integer, primary_key=True) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_login_time = sa.Column('last_login_time', sa.DateTime) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - name = sa.Column('name', sa.Unicode(50), nullable=False, unique=True) - password_hash = sa.Column('password_hash', sa.Unicode(128), nullable=False) - password_salt = sa.Column('password_salt', sa.Unicode(32)) + user_id = sa.Column("id", sa.Integer, primary_key=True) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_login_time = sa.Column("last_login_time", sa.DateTime) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + name = sa.Column("name", sa.Unicode(50), nullable=False, unique=True) + password_hash = sa.Column("password_hash", sa.Unicode(128), nullable=False) + password_salt = sa.Column("password_salt", sa.Unicode(32)) password_revision = sa.Column( - 'password_revision', sa.SmallInteger, default=0, nullable=False) - email = sa.Column('email', sa.Unicode(64), nullable=True) - rank = sa.Column('rank', sa.Unicode(32), nullable=False) + "password_revision", sa.SmallInteger, default=0, nullable=False + ) + email = sa.Column("email", sa.Unicode(64), nullable=True) + rank = sa.Column("rank", sa.Unicode(32), nullable=False) avatar_style = sa.Column( - 'avatar_style', sa.Unicode(32), nullable=False, - default=AVATAR_GRAVATAR) + "avatar_style", sa.Unicode(32), nullable=False, default=AVATAR_GRAVATAR + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } def upgrade(): op.alter_column( - 'user', - 'password_hash', + "user", + "password_hash", existing_type=sa.VARCHAR(length=64), type_=sa.Unicode(length=128), - existing_nullable=False) - op.add_column('user', sa.Column( - 'password_revision', - sa.SmallInteger(), - nullable=True, - default=0)) + existing_nullable=False, + ) + op.add_column( + "user", + sa.Column( + "password_revision", sa.SmallInteger(), nullable=True, default=0 + ), + ) session = sa.orm.session.Session(bind=op.get_bind()) if session.query(User).count() >= 0: @@ -73,17 +75,16 @@ def upgrade(): session.commit() op.alter_column( - 'user', - 'password_revision', - existing_nullable=True, - nullable=False) + "user", "password_revision", existing_nullable=True, nullable=False + ) def downgrade(): op.alter_column( - 'user', - 'password_hash', + "user", + "password_hash", existing_type=sa.Unicode(length=128), type_=sa.VARCHAR(length=64), - existing_nullable=False) - op.drop_column('user', 'password_revision') + existing_nullable=False, + ) + op.drop_column("user", "password_revision") diff --git a/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py b/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py index 899eaa7..57dda5c 100644 --- a/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py +++ b/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py @@ -1,39 +1,40 @@ -''' +""" Added a user_token table for API authorization Revision ID: a39c7f98a7fa Created at: 2018-02-25 01:31:27.345595 -''' +""" import sqlalchemy as sa from alembic import op - -revision = 'a39c7f98a7fa' -down_revision = '9ef1a1643c2a' +revision = "a39c7f98a7fa" +down_revision = "9ef1a1643c2a" branch_labels = None depends_on = None def upgrade(): op.create_table( - 'user_token', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('token', sa.Unicode(length=36), nullable=False), - sa.Column('note', sa.Unicode(length=128), nullable=True), - sa.Column('enabled', sa.Boolean(), nullable=False), - sa.Column('expiration_time', sa.DateTime(), nullable=True), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_edit_time', sa.DateTime(), nullable=True), - sa.Column('last_usage_time', sa.DateTime(), nullable=True), - sa.Column('version', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id')) + "user_token", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("token", sa.Unicode(length=36), nullable=False), + sa.Column("note", sa.Unicode(length=128), nullable=True), + sa.Column("enabled", sa.Boolean(), nullable=False), + sa.Column("expiration_time", sa.DateTime(), nullable=True), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_edit_time", sa.DateTime(), nullable=True), + sa.Column("last_usage_time", sa.DateTime(), nullable=True), + sa.Column("version", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) op.create_index( - op.f('ix_user_token_user_id'), 'user_token', ['user_id'], unique=False) + op.f("ix_user_token_user_id"), "user_token", ["user_id"], unique=False + ) def downgrade(): - op.drop_index(op.f('ix_user_token_user_id'), table_name='user_token') - op.drop_table('user_token') + op.drop_index(op.f("ix_user_token_user_id"), table_name="user_token") + op.drop_table("user_token") diff --git a/server/szurubooru/migrations/versions/c867abb456b1_support_large_file_uploads.py b/server/szurubooru/migrations/versions/c867abb456b1_support_large_file_uploads.py new file mode 100644 index 0000000..212e196 --- /dev/null +++ b/server/szurubooru/migrations/versions/c867abb456b1_support_large_file_uploads.py @@ -0,0 +1,26 @@ +""" +support large file uploads + +Revision ID: c867abb456b1 +Created at: 2020-10-11 15:37:30.965231 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "c867abb456b1" +down_revision = "c97dc1bf184a" +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column( + "post", "file_size", type_=sa.BigInteger, existing_type=sa.Integer + ) + + +def downgrade(): + op.alter_column( + "post", "file_size", type_=sa.Integer, existing_type=sa.BigInteger + ) diff --git a/server/szurubooru/migrations/versions/c97dc1bf184a_add_order_column_to_tag_categories.py b/server/szurubooru/migrations/versions/c97dc1bf184a_add_order_column_to_tag_categories.py new file mode 100644 index 0000000..c5a3124 --- /dev/null +++ b/server/szurubooru/migrations/versions/c97dc1bf184a_add_order_column_to_tag_categories.py @@ -0,0 +1,28 @@ +""" +Add order column to tag categories. + +Revision ID: c97dc1bf184a +Created at: 2020-09-19 17:08:03.225667 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "c97dc1bf184a" +down_revision = "54de8acc6cef" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "tag_category", sa.Column("order", sa.Integer, nullable=True) + ) + op.execute( + sa.table("tag_category", sa.column("order")).update().values(order=1) + ) + op.alter_column("tag_category", "order", nullable=False) + + +def downgrade(): + op.drop_column("tag_category", "order") diff --git a/server/szurubooru/migrations/versions/e5c1216a8503_create_user_table.py b/server/szurubooru/migrations/versions/e5c1216a8503_create_user_table.py index a84e31b..672d4eb 100644 --- a/server/szurubooru/migrations/versions/e5c1216a8503_create_user_table.py +++ b/server/szurubooru/migrations/versions/e5c1216a8503_create_user_table.py @@ -1,14 +1,14 @@ -''' +""" Create user table Revision ID: e5c1216a8503 Created at: 2016-03-20 15:53:25.030415 -''' +""" import sqlalchemy as sa from alembic import op -revision = 'e5c1216a8503' +revision = "e5c1216a8503" down_revision = None branch_labels = None depends_on = None @@ -16,19 +16,20 @@ depends_on = None def upgrade(): op.create_table( - 'user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.Unicode(length=50), nullable=False), - sa.Column('password_hash', sa.Unicode(length=64), nullable=False), - sa.Column('password_salt', sa.Unicode(length=32), nullable=True), - sa.Column('email', sa.Unicode(length=64), nullable=True), - sa.Column('rank', sa.Unicode(length=32), nullable=False), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_login_time', sa.DateTime()), - sa.Column('avatar_style', sa.Unicode(length=32), nullable=False), - sa.PrimaryKeyConstraint('id')) - op.create_unique_constraint('uq_user_name', 'user', ['name']) + "user", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=50), nullable=False), + sa.Column("password_hash", sa.Unicode(length=64), nullable=False), + sa.Column("password_salt", sa.Unicode(length=32), nullable=True), + sa.Column("email", sa.Unicode(length=64), nullable=True), + sa.Column("rank", sa.Unicode(length=32), nullable=False), + sa.Column("creation_time", sa.DateTime(), nullable=False), + sa.Column("last_login_time", sa.DateTime()), + sa.Column("avatar_style", sa.Unicode(length=32), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_unique_constraint("uq_user_name", "user", ["name"]) def downgrade(): - op.drop_table('user') + op.drop_table("user") diff --git a/server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py b/server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py index dffc6c0..3b36c6a 100644 --- a/server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py +++ b/server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py @@ -1,48 +1,49 @@ -''' +""" Delete post columns Revision ID: ed6dd16a30f3 Created at: 2016-04-24 16:29:25.309154 -''' +""" import sqlalchemy as sa from alembic import op -revision = 'ed6dd16a30f3' -down_revision = '46df355634dc' +revision = "ed6dd16a30f3" +down_revision = "46df355634dc" branch_labels = None depends_on = None def upgrade(): for column_name in [ - 'auto_comment_edit_time', - 'auto_fav_count', - 'auto_comment_creation_time', - 'auto_feature_count', - 'auto_comment_count', - 'auto_score', - 'auto_fav_time', - 'auto_feature_time', - 'auto_note_count']: - op.drop_column('post', column_name) + "auto_comment_edit_time", + "auto_fav_count", + "auto_comment_creation_time", + "auto_feature_count", + "auto_comment_count", + "auto_score", + "auto_fav_time", + "auto_feature_time", + "auto_note_count", + ]: + op.drop_column("post", column_name) def downgrade(): for column_name in [ - 'auto_note_count', - 'auto_feature_time', - 'auto_fav_time', - 'auto_score', - 'auto_comment_count', - 'auto_feature_count', - 'auto_comment_creation_time', - 'auto_fav_count', - 'auto_comment_edit_time']: + "auto_note_count", + "auto_feature_time", + "auto_fav_time", + "auto_score", + "auto_comment_count", + "auto_feature_count", + "auto_comment_creation_time", + "auto_fav_count", + "auto_comment_edit_time", + ]: op.add_column( - 'post', + "post", sa.Column( - column_name, - sa.INTEGER(), - autoincrement=False, - nullable=False)) + column_name, sa.INTEGER(), autoincrement=False, nullable=False + ), + ) diff --git a/server/szurubooru/model/__init__.py b/server/szurubooru/model/__init__.py index 202e434..2befe74 100644 --- a/server/szurubooru/model/__init__.py +++ b/server/szurubooru/model/__init__.py @@ -1,16 +1,20 @@ +import szurubooru.model.util from szurubooru.model.base import Base -from szurubooru.model.user import User, UserToken -from szurubooru.model.tag_category import TagCategory -from szurubooru.model.tag import Tag, TagName, TagSuggestion, TagImplication +from szurubooru.model.comment import Comment, CommentScore from szurubooru.model.metric import Metric, PostMetric, PostMetricRange +from szurubooru.model.pool import Pool, PoolName, PoolPost +from szurubooru.model.pool_category import PoolCategory from szurubooru.model.post import ( Post, - PostTag, - PostRelation, PostFavorite, - PostScore, + PostFeature, PostNote, - PostFeature) -from szurubooru.model.comment import Comment, CommentScore + PostRelation, + PostScore, + PostSignature, + PostTag, +) from szurubooru.model.snapshot import Snapshot -import szurubooru.model.util +from szurubooru.model.tag import Tag, TagImplication, TagName, TagSuggestion +from szurubooru.model.tag_category import TagCategory +from szurubooru.model.user import User, UserToken diff --git a/server/szurubooru/model/base.py b/server/szurubooru/model/base.py index e61d35a..860e542 100644 --- a/server/szurubooru/model/base.py +++ b/server/szurubooru/model/base.py @@ -1,4 +1,3 @@ from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() # pylint: disable=invalid-name +Base = declarative_base() diff --git a/server/szurubooru/model/comment.py b/server/szurubooru/model/comment.py index 17b76a0..e64961e 100644 --- a/server/szurubooru/model/comment.py +++ b/server/szurubooru/model/comment.py @@ -1,58 +1,65 @@ import sqlalchemy as sa + from szurubooru.db import get_session from szurubooru.model.base import Base class CommentScore(Base): - __tablename__ = 'comment_score' + __tablename__ = "comment_score" comment_id = sa.Column( - 'comment_id', + "comment_id", sa.Integer, - sa.ForeignKey('comment.id'), + sa.ForeignKey("comment.id"), nullable=False, - primary_key=True) + primary_key=True, + ) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id'), + sa.ForeignKey("user.id"), nullable=False, primary_key=True, - index=True) - time = sa.Column('time', sa.DateTime, nullable=False) - score = sa.Column('score', sa.Integer, nullable=False) + index=True, + ) + time = sa.Column("time", sa.DateTime, nullable=False) + score = sa.Column("score", sa.Integer, nullable=False) - comment = sa.orm.relationship('Comment') + comment = sa.orm.relationship("Comment") user = sa.orm.relationship( - 'User', - backref=sa.orm.backref('comment_scores', cascade='all, delete-orphan')) + "User", + backref=sa.orm.backref("comment_scores", cascade="all, delete-orphan"), + ) class Comment(Base): - __tablename__ = 'comment' + __tablename__ = "comment" - comment_id = sa.Column('id', sa.Integer, primary_key=True) + comment_id = sa.Column("id", sa.Integer, primary_key=True) post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), nullable=False, - index=True) + index=True, + ) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id'), + sa.ForeignKey("user.id"), nullable=True, - index=True) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_edit_time = sa.Column('last_edit_time', sa.DateTime) - text = sa.Column('text', sa.UnicodeText, default=None) + index=True, + ) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_edit_time = sa.Column("last_edit_time", sa.DateTime) + text = sa.Column("text", sa.UnicodeText, default=None) - user = sa.orm.relationship('User') - post = sa.orm.relationship('Post') + user = sa.orm.relationship("User") + post = sa.orm.relationship("Post") scores = sa.orm.relationship( - 'CommentScore', cascade='all, delete-orphan', lazy='joined') + "CommentScore", cascade="all, delete-orphan", lazy="joined" + ) @property def score(self) -> int: @@ -60,9 +67,11 @@ class Comment(Base): get_session() .query(sa.sql.expression.func.sum(CommentScore.score)) .filter(CommentScore.comment_id == self.comment_id) - .one()[0] or 0) + .one()[0] + or 0 + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } diff --git a/server/szurubooru/model/pool.py b/server/szurubooru/model/pool.py new file mode 100644 index 0000000..3dcdd35 --- /dev/null +++ b/server/szurubooru/model/pool.py @@ -0,0 +1,113 @@ +import sqlalchemy as sa +from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.ext.orderinglist import ordering_list + +from szurubooru.model.base import Base + + +class PoolName(Base): + __tablename__ = "pool_name" + + pool_name_id = sa.Column("pool_name_id", sa.Integer, primary_key=True) + pool_id = sa.Column( + "pool_id", + sa.Integer, + sa.ForeignKey("pool.id"), + nullable=False, + index=True, + ) + name = sa.Column("name", sa.Unicode(128), nullable=False, unique=True) + order = sa.Column("ord", sa.Integer, nullable=False, index=True) + + def __init__(self, name: str, order: int) -> None: + self.name = name + self.order = order + + +class PoolPost(Base): + __tablename__ = "pool_post" + + pool_id = sa.Column( + "pool_id", + sa.Integer, + sa.ForeignKey("pool.id"), + nullable=False, + primary_key=True, + index=True, + ) + post_id = sa.Column( + "post_id", + sa.Integer, + sa.ForeignKey("post.id"), + nullable=False, + primary_key=True, + index=True, + ) + order = sa.Column("ord", sa.Integer, nullable=False, index=True) + + pool = sa.orm.relationship("Pool", back_populates="_posts") + post = sa.orm.relationship("Post", back_populates="_pools") + + def __init__(self, post) -> None: + self.post_id = post.post_id + + +class Pool(Base): + __tablename__ = "pool" + + pool_id = sa.Column("id", sa.Integer, primary_key=True) + category_id = sa.Column( + "category_id", + sa.Integer, + sa.ForeignKey("pool_category.id"), + nullable=False, + index=True, + ) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_edit_time = sa.Column("last_edit_time", sa.DateTime) + description = sa.Column("description", sa.UnicodeText, default=None) + + category = sa.orm.relationship("PoolCategory", lazy="joined") + names = sa.orm.relationship( + "PoolName", + cascade="all,delete-orphan", + lazy="joined", + order_by="PoolName.order", + ) + _posts = sa.orm.relationship( + "PoolPost", + cascade="all,delete-orphan", + lazy="joined", + back_populates="pool", + order_by="PoolPost.order", + collection_class=ordering_list("order"), + ) + posts = association_proxy("_posts", "post") + + post_count = sa.orm.column_property( + ( + sa.sql.expression.select( + [sa.sql.expression.func.count(PoolPost.post_id)] + ) + .where(PoolPost.pool_id == pool_id) + .as_scalar() + ), + deferred=True, + ) + + first_name = sa.orm.column_property( + ( + sa.sql.expression.select([PoolName.name]) + .where(PoolName.pool_id == pool_id) + .order_by(PoolName.order) + .limit(1) + .as_scalar() + ), + deferred=True, + ) + + __mapper_args__ = { + "version_id_col": version, + "version_id_generator": False, + } diff --git a/server/szurubooru/model/pool_category.py b/server/szurubooru/model/pool_category.py new file mode 100644 index 0000000..f527d2d --- /dev/null +++ b/server/szurubooru/model/pool_category.py @@ -0,0 +1,34 @@ +from typing import Optional + +import sqlalchemy as sa + +from szurubooru.model.base import Base +from szurubooru.model.pool import Pool + + +class PoolCategory(Base): + __tablename__ = "pool_category" + + pool_category_id = sa.Column("id", sa.Integer, primary_key=True) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + name = sa.Column("name", sa.Unicode(32), nullable=False) + color = sa.Column( + "color", sa.Unicode(32), nullable=False, default="#000000" + ) + default = sa.Column("default", sa.Boolean, nullable=False, default=False) + + def __init__(self, name: Optional[str] = None) -> None: + self.name = name + + pool_count = sa.orm.column_property( + sa.sql.expression.select( + [sa.sql.expression.func.count("Pool.pool_id")] + ) + .where(Pool.category_id == pool_category_id) + .correlate_except(sa.table("Pool")) + ) + + __mapper_args__ = { + "version_id_col": version, + "version_id_generator": False, + } diff --git a/server/szurubooru/model/post.py b/server/szurubooru/model/post.py index 987f720..cf68860 100644 --- a/server/szurubooru/model/post.py +++ b/server/szurubooru/model/post.py @@ -1,119 +1,135 @@ from typing import List + import sqlalchemy as sa +from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.ext.orderinglist import ordering_list + from szurubooru.model.base import Base from szurubooru.model.comment import Comment -from sqlalchemy.ext.hybrid import hybrid_property +from szurubooru.model.pool import PoolPost class PostFeature(Base): - __tablename__ = 'post_feature' + __tablename__ = "post_feature" - post_feature_id = sa.Column('id', sa.Integer, primary_key=True) + post_feature_id = sa.Column("id", sa.Integer, primary_key=True) post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), nullable=False, - index=True) + index=True, + ) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id'), + sa.ForeignKey("user.id"), nullable=False, - index=True) - time = sa.Column('time', sa.DateTime, nullable=False) + index=True, + ) + time = sa.Column("time", sa.DateTime, nullable=False) - post = sa.orm.relationship('Post') # type: Post + post = sa.orm.relationship("Post") # type: Post user = sa.orm.relationship( - 'User', - backref=sa.orm.backref( - 'post_features', cascade='all, delete-orphan')) + "User", + backref=sa.orm.backref("post_features", cascade="all, delete-orphan"), + ) class PostScore(Base): - __tablename__ = 'post_score' + __tablename__ = "post_score" post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id'), + sa.ForeignKey("user.id"), primary_key=True, nullable=False, - index=True) - time = sa.Column('time', sa.DateTime, nullable=False) - score = sa.Column('score', sa.Integer, nullable=False) + index=True, + ) + time = sa.Column("time", sa.DateTime, nullable=False) + score = sa.Column("score", sa.Integer, nullable=False) - post = sa.orm.relationship('Post') + post = sa.orm.relationship("Post") user = sa.orm.relationship( - 'User', - backref=sa.orm.backref('post_scores', cascade='all, delete-orphan')) + "User", + backref=sa.orm.backref("post_scores", cascade="all, delete-orphan"), + ) class PostFavorite(Base): - __tablename__ = 'post_favorite' + __tablename__ = "post_favorite" post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id'), + sa.ForeignKey("user.id"), primary_key=True, nullable=False, - index=True) - time = sa.Column('time', sa.DateTime, nullable=False) + index=True, + ) + time = sa.Column("time", sa.DateTime, nullable=False) - post = sa.orm.relationship('Post') + post = sa.orm.relationship("Post") user = sa.orm.relationship( - 'User', - backref=sa.orm.backref('post_favorites', cascade='all, delete-orphan')) + "User", + backref=sa.orm.backref("post_favorites", cascade="all, delete-orphan"), + ) class PostNote(Base): - __tablename__ = 'post_note' + __tablename__ = "post_note" - post_note_id = sa.Column('id', sa.Integer, primary_key=True) + post_note_id = sa.Column("id", sa.Integer, primary_key=True) post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), nullable=False, - index=True) - polygon = sa.Column('polygon', sa.PickleType, nullable=False) - text = sa.Column('text', sa.UnicodeText, nullable=False) + index=True, + ) + polygon = sa.Column("polygon", sa.PickleType, nullable=False) + text = sa.Column("text", sa.UnicodeText, nullable=False) - post = sa.orm.relationship('Post') + post = sa.orm.relationship("Post") class PostRelation(Base): - __tablename__ = 'post_relation' + __tablename__ = "post_relation" parent_id = sa.Column( - 'parent_id', + "parent_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) child_id = sa.Column( - 'child_id', + "child_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) def __init__(self, parent_id: int, child_id: int) -> None: self.parent_id = parent_id @@ -121,100 +137,150 @@ class PostRelation(Base): class PostTag(Base): - __tablename__ = 'post_tag' + __tablename__ = "post_tag" post_id = sa.Column( - 'post_id', + "post_id", sa.Integer, - sa.ForeignKey('post.id'), + sa.ForeignKey("post.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) tag_id = sa.Column( - 'tag_id', + "tag_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), primary_key=True, nullable=False, - index=True) + index=True, + ) def __init__(self, post_id: int, tag_id: int) -> None: self.post_id = post_id self.tag_id = tag_id +class PostSignature(Base): + __tablename__ = "post_signature" + + post_id = sa.Column( + "post_id", + sa.Integer, + sa.ForeignKey("post.id"), + primary_key=True, + nullable=False, + index=True, + ) + signature = sa.Column("signature", sa.LargeBinary, nullable=False) + words = sa.Column( + "words", + sa.dialects.postgresql.ARRAY(sa.Integer, dimensions=1), + nullable=False, + index=True, + ) + + post = sa.orm.relationship("Post") + + class Post(Base): - __tablename__ = 'post' + __tablename__ = "post" - SAFETY_SAFE = 'safe' - SAFETY_SKETCHY = 'sketchy' - SAFETY_UNSAFE = 'unsafe' + SAFETY_SAFE = "safe" + SAFETY_SKETCHY = "sketchy" + SAFETY_UNSAFE = "unsafe" - TYPE_IMAGE = 'image' - TYPE_ANIMATION = 'animation' - TYPE_VIDEO = 'video' - TYPE_FLASH = 'flash' + TYPE_IMAGE = "image" + TYPE_ANIMATION = "animation" + TYPE_VIDEO = "video" + TYPE_FLASH = "flash" - FLAG_LOOP = 'loop' - FLAG_SOUND = 'sound' + FLAG_LOOP = "loop" + FLAG_SOUND = "sound" # basic meta - post_id = sa.Column('id', sa.Integer, primary_key=True) + post_id = sa.Column("id", sa.Integer, primary_key=True) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id', ondelete='SET NULL'), + sa.ForeignKey("user.id", ondelete="SET NULL"), nullable=True, - index=True) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_edit_time = sa.Column('last_edit_time', sa.DateTime) - safety = sa.Column('safety', sa.Unicode(32), nullable=False) - source = sa.Column('source', sa.Unicode(200)) - flags_string = sa.Column('flags', sa.Unicode(200), default='') + index=True, + ) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_edit_time = sa.Column("last_edit_time", sa.DateTime) + safety = sa.Column("safety", sa.Unicode(32), nullable=False) + source = sa.Column("source", sa.Unicode(2048)) + flags_string = sa.Column("flags", sa.Unicode(32), default="") # content description - type = sa.Column('type', sa.Unicode(32), nullable=False) - checksum = sa.Column('checksum', sa.Unicode(64), nullable=False) - file_size = sa.Column('file_size', sa.Integer) - canvas_width = sa.Column('image_width', sa.Integer) - canvas_height = sa.Column('image_height', sa.Integer) - mime_type = sa.Column('mime-type', sa.Unicode(32), nullable=False) + type = sa.Column("type", sa.Unicode(32), nullable=False) + checksum = sa.Column("checksum", sa.Unicode(64), nullable=False) + file_size = sa.Column("file_size", sa.BigInteger) + canvas_width = sa.Column("image_width", sa.Integer) + canvas_height = sa.Column("image_height", sa.Integer) + mime_type = sa.Column("mime-type", sa.Unicode(32), nullable=False) # foreign tables - user = sa.orm.relationship('User') - tags = sa.orm.relationship('Tag', backref='posts', secondary='post_tag') + user = sa.orm.relationship("User") + tags = sa.orm.relationship("Tag", backref="posts", secondary="post_tag") + signature = sa.orm.relationship( + "PostSignature", + uselist=False, + cascade="all, delete, delete-orphan", + lazy="joined", + ) relations = sa.orm.relationship( - 'Post', - secondary='post_relation', + "Post", + secondary="post_relation", primaryjoin=post_id == PostRelation.parent_id, - secondaryjoin=post_id == PostRelation.child_id, lazy='joined', - backref='related_by') + secondaryjoin=post_id == PostRelation.child_id, + lazy="joined", + backref="related_by", + ) features = sa.orm.relationship( - 'PostFeature', cascade='all, delete-orphan', lazy='joined') + "PostFeature", cascade="all, delete-orphan", lazy="joined" + ) scores = sa.orm.relationship( - 'PostScore', cascade='all, delete-orphan', lazy='joined') + "PostScore", cascade="all, delete-orphan", lazy="joined" + ) favorited_by = sa.orm.relationship( - 'PostFavorite', cascade='all, delete-orphan', lazy='joined') + "PostFavorite", cascade="all, delete-orphan", lazy="joined" + ) notes = sa.orm.relationship( - 'PostNote', cascade='all, delete-orphan', lazy='joined') - comments = sa.orm.relationship('Comment', cascade='all, delete-orphan') + "PostNote", cascade="all, delete-orphan", lazy="joined" + ) + comments = sa.orm.relationship("Comment", cascade="all, delete-orphan") metrics = sa.orm.relationship( - 'PostMetric', cascade='all, delete-orphan', lazy='joined') + "PostMetric", cascade="all, delete-orphan", lazy="joined" + ) metric_ranges = sa.orm.relationship( - 'PostMetricRange', cascade='all, delete-orphan', lazy='joined') + "PostMetricRange", cascade="all, delete-orphan", lazy="joined" + ) + _pools = sa.orm.relationship( + "PoolPost", + cascade="all,delete-orphan", + lazy="select", + order_by="PoolPost.order", + back_populates="post", + ) + pools = association_proxy("_pools", "pool") # dynamic columns tag_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostTag.tag_id)]) + [sa.sql.expression.func.count(PostTag.tag_id)] + ) .where(PostTag.post_id == post_id) - .correlate_except(PostTag)) + .correlate_except(PostTag) + ) canvas_area = sa.orm.column_property(canvas_width * canvas_height) canvas_aspect_ratio = sa.orm.column_property( - sa.sql.expression.func.cast(canvas_width, sa.Float) / - sa.sql.expression.func.cast(canvas_height, sa.Float)) + sa.sql.expression.func.cast(canvas_width, sa.Float) + / sa.sql.expression.func.cast(canvas_height, sa.Float) + ) @property def is_featured(self) -> bool: @@ -222,81 +288,106 @@ class Post(Base): sa.orm.object_session(self) .query(PostFeature) .order_by(PostFeature.time.desc()) - .first()) + .first() + ) return featured_post and featured_post.post_id == self.post_id @hybrid_property def flags(self) -> List[str]: - return sorted([x for x in self.flags_string.split(',') if x]) + return sorted([x for x in self.flags_string.split(",") if x]) @flags.setter def flags(self, data: List[str]) -> None: - self.flags_string = ','.join([x for x in data if x]) + self.flags_string = ",".join([x for x in data if x]) score = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.coalesce( - sa.sql.expression.func.sum(PostScore.score), 0)]) + [ + sa.sql.expression.func.coalesce( + sa.sql.expression.func.sum(PostScore.score), 0 + ) + ] + ) .where(PostScore.post_id == post_id) - .correlate_except(PostScore)) + .correlate_except(PostScore) + ) favorite_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostFavorite.post_id)]) + [sa.sql.expression.func.count(PostFavorite.post_id)] + ) .where(PostFavorite.post_id == post_id) - .correlate_except(PostFavorite)) + .correlate_except(PostFavorite) + ) last_favorite_time = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.max(PostFavorite.time)]) + [sa.sql.expression.func.max(PostFavorite.time)] + ) .where(PostFavorite.post_id == post_id) - .correlate_except(PostFavorite)) + .correlate_except(PostFavorite) + ) feature_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostFeature.post_id)]) + [sa.sql.expression.func.count(PostFeature.post_id)] + ) .where(PostFeature.post_id == post_id) - .correlate_except(PostFeature)) + .correlate_except(PostFeature) + ) last_feature_time = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.max(PostFeature.time)]) + [sa.sql.expression.func.max(PostFeature.time)] + ) .where(PostFeature.post_id == post_id) - .correlate_except(PostFeature)) + .correlate_except(PostFeature) + ) comment_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(Comment.post_id)]) + [sa.sql.expression.func.count(Comment.post_id)] + ) .where(Comment.post_id == post_id) - .correlate_except(Comment)) + .correlate_except(Comment) + ) last_comment_creation_time = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.max(Comment.creation_time)]) + [sa.sql.expression.func.max(Comment.creation_time)] + ) .where(Comment.post_id == post_id) - .correlate_except(Comment)) + .correlate_except(Comment) + ) last_comment_edit_time = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.max(Comment.last_edit_time)]) + [sa.sql.expression.func.max(Comment.last_edit_time)] + ) .where(Comment.post_id == post_id) - .correlate_except(Comment)) + .correlate_except(Comment) + ) note_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostNote.post_id)]) + [sa.sql.expression.func.count(PostNote.post_id)] + ) .where(PostNote.post_id == post_id) - .correlate_except(PostNote)) + .correlate_except(PostNote) + ) relation_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostRelation.child_id)]) + [sa.sql.expression.func.count(PostRelation.child_id)] + ) .where( - (PostRelation.parent_id == post_id) | - (PostRelation.child_id == post_id)) - .correlate_except(PostRelation)) + (PostRelation.parent_id == post_id) + | (PostRelation.child_id == post_id) + ) + .correlate_except(PostRelation) + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } diff --git a/server/szurubooru/model/snapshot.py b/server/szurubooru/model/snapshot.py index 7f8bbdf..d4e3fc8 100644 --- a/server/szurubooru/model/snapshot.py +++ b/server/szurubooru/model/snapshot.py @@ -1,29 +1,32 @@ import sqlalchemy as sa + from szurubooru.model.base import Base class Snapshot(Base): - __tablename__ = 'snapshot' + __tablename__ = "snapshot" - OPERATION_CREATED = 'created' - OPERATION_MODIFIED = 'modified' - OPERATION_DELETED = 'deleted' - OPERATION_MERGED = 'merged' + OPERATION_CREATED = "created" + OPERATION_MODIFIED = "modified" + OPERATION_DELETED = "deleted" + OPERATION_MERGED = "merged" - snapshot_id = sa.Column('id', sa.Integer, primary_key=True) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - operation = sa.Column('operation', sa.Unicode(16), nullable=False) + snapshot_id = sa.Column("id", sa.Integer, primary_key=True) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + operation = sa.Column("operation", sa.Unicode(16), nullable=False) resource_type = sa.Column( - 'resource_type', sa.Unicode(32), nullable=False, index=True) + "resource_type", sa.Unicode(32), nullable=False, index=True + ) resource_pkey = sa.Column( - 'resource_pkey', sa.Integer, nullable=False, index=True) - resource_name = sa.Column( - 'resource_name', sa.Unicode(64), nullable=False) + "resource_pkey", sa.Integer, nullable=False, index=True + ) + resource_name = sa.Column("resource_name", sa.Unicode(128), nullable=False) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id', ondelete='set null'), - nullable=True) - data = sa.Column('data', sa.PickleType) + sa.ForeignKey("user.id", ondelete="set null"), + nullable=True, + ) + data = sa.Column("data", sa.PickleType) - user = sa.orm.relationship('User') + user = sa.orm.relationship("User") diff --git a/server/szurubooru/model/tag.py b/server/szurubooru/model/tag.py index e0d18e6..65dabb1 100644 --- a/server/szurubooru/model/tag.py +++ b/server/szurubooru/model/tag.py @@ -1,25 +1,28 @@ import sqlalchemy as sa + from szurubooru.model.base import Base from szurubooru.model.post import PostTag class TagSuggestion(Base): - __tablename__ = 'tag_suggestion' + __tablename__ = "tag_suggestion" parent_id = sa.Column( - 'parent_id', + "parent_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), nullable=False, primary_key=True, - index=True) + index=True, + ) child_id = sa.Column( - 'child_id', + "child_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), nullable=False, primary_key=True, - index=True) + index=True, + ) def __init__(self, parent_id: int, child_id: int) -> None: self.parent_id = parent_id @@ -27,22 +30,24 @@ class TagSuggestion(Base): class TagImplication(Base): - __tablename__ = 'tag_implication' + __tablename__ = "tag_implication" parent_id = sa.Column( - 'parent_id', + "parent_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), nullable=False, primary_key=True, - index=True) + index=True, + ) child_id = sa.Column( - 'child_id', + "child_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), nullable=False, primary_key=True, - index=True) + index=True, + ) def __init__(self, parent_id: int, child_id: int) -> None: self.parent_id = parent_id @@ -50,17 +55,18 @@ class TagImplication(Base): class TagName(Base): - __tablename__ = 'tag_name' + __tablename__ = "tag_name" - tag_name_id = sa.Column('tag_name_id', sa.Integer, primary_key=True) + tag_name_id = sa.Column("tag_name_id", sa.Integer, primary_key=True) tag_id = sa.Column( - 'tag_id', + "tag_id", sa.Integer, - sa.ForeignKey('tag.id'), + sa.ForeignKey("tag.id"), nullable=False, - index=True) - name = sa.Column('name', sa.Unicode(64), nullable=False, unique=True) - order = sa.Column('ord', sa.Integer, nullable=False, index=True) + index=True, + ) + name = sa.Column("name", sa.Unicode(128), nullable=False, unique=True) + order = sa.Column("ord", sa.Integer, nullable=False, index=True) def __init__(self, name: str, order: int) -> None: self.name = name @@ -68,48 +74,55 @@ class TagName(Base): class Tag(Base): - __tablename__ = 'tag' + __tablename__ = "tag" - tag_id = sa.Column('id', sa.Integer, primary_key=True) + tag_id = sa.Column("id", sa.Integer, primary_key=True) category_id = sa.Column( - 'category_id', + "category_id", sa.Integer, - sa.ForeignKey('tag_category.id'), + sa.ForeignKey("tag_category.id"), nullable=False, - index=True) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_edit_time = sa.Column('last_edit_time', sa.DateTime) - description = sa.Column('description', sa.UnicodeText, default=None) + index=True, + ) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_edit_time = sa.Column("last_edit_time", sa.DateTime) + description = sa.Column("description", sa.UnicodeText, default=None) - category = sa.orm.relationship('TagCategory', lazy='joined') + category = sa.orm.relationship("TagCategory", lazy="joined") names = sa.orm.relationship( - 'TagName', - cascade='all,delete-orphan', - lazy='joined', - order_by='TagName.order') + "TagName", + cascade="all,delete-orphan", + lazy="joined", + order_by="TagName.order", + ) suggestions = sa.orm.relationship( - 'Tag', - secondary='tag_suggestion', + "Tag", + secondary="tag_suggestion", primaryjoin=tag_id == TagSuggestion.parent_id, secondaryjoin=tag_id == TagSuggestion.child_id, - lazy='joined') + lazy="joined", + ) implications = sa.orm.relationship( - 'Tag', - secondary='tag_implication', + "Tag", + secondary="tag_implication", primaryjoin=tag_id == TagImplication.parent_id, secondaryjoin=tag_id == TagImplication.child_id, - lazy='joined') + lazy="joined", + ) metric = sa.orm.relationship( - 'Metric', + "Metric", uselist=False, - cascade='all, delete-orphan') + cascade="all, delete-orphan" + ) post_count = sa.orm.column_property( sa.sql.expression.select( - [sa.sql.expression.func.count(PostTag.post_id)]) + [sa.sql.expression.func.count(PostTag.post_id)] + ) .where(PostTag.tag_id == tag_id) - .correlate_except(PostTag)) + .correlate_except(PostTag) + ) first_name = sa.orm.column_property( ( @@ -119,27 +132,32 @@ class Tag(Base): .limit(1) .as_scalar() ), - deferred=True) + deferred=True, + ) suggestion_count = sa.orm.column_property( ( sa.sql.expression.select( - [sa.sql.expression.func.count(TagSuggestion.child_id)]) + [sa.sql.expression.func.count(TagSuggestion.child_id)] + ) .where(TagSuggestion.parent_id == tag_id) .as_scalar() ), - deferred=True) + deferred=True, + ) implication_count = sa.orm.column_property( ( sa.sql.expression.select( - [sa.sql.expression.func.count(TagImplication.child_id)]) + [sa.sql.expression.func.count(TagImplication.child_id)] + ) .where(TagImplication.parent_id == tag_id) .as_scalar() ), - deferred=True) + deferred=True, + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } diff --git a/server/szurubooru/model/tag_category.py b/server/szurubooru/model/tag_category.py index 2c961ed..a336a21 100644 --- a/server/szurubooru/model/tag_category.py +++ b/server/szurubooru/model/tag_category.py @@ -1,28 +1,33 @@ from typing import Optional + import sqlalchemy as sa + from szurubooru.model.base import Base from szurubooru.model.tag import Tag class TagCategory(Base): - __tablename__ = 'tag_category' + __tablename__ = "tag_category" - tag_category_id = sa.Column('id', sa.Integer, primary_key=True) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - name = sa.Column('name', sa.Unicode(32), nullable=False) + tag_category_id = sa.Column("id", sa.Integer, primary_key=True) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + name = sa.Column("name", sa.Unicode(32), nullable=False) color = sa.Column( - 'color', sa.Unicode(32), nullable=False, default='#000000') - default = sa.Column('default', sa.Boolean, nullable=False, default=False) + "color", sa.Unicode(32), nullable=False, default="#000000" + ) + default = sa.Column("default", sa.Boolean, nullable=False, default=False) + order = sa.Column("order", sa.Integer, nullable=False, default=1) def __init__(self, name: Optional[str] = None) -> None: self.name = name tag_count = sa.orm.column_property( - sa.sql.expression.select([sa.sql.expression.func.count('Tag.tag_id')]) + sa.sql.expression.select([sa.sql.expression.func.count("Tag.tag_id")]) .where(Tag.category_id == tag_category_id) - .correlate_except(sa.table('Tag'))) + .correlate_except(sa.table("Tag")) + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } diff --git a/server/szurubooru/model/user.py b/server/szurubooru/model/user.py index 2d599e8..41a9b30 100644 --- a/server/szurubooru/model/user.py +++ b/server/szurubooru/model/user.py @@ -1,110 +1,123 @@ import sqlalchemy as sa + from szurubooru.model.base import Base -from szurubooru.model.post import Post, PostScore, PostFavorite from szurubooru.model.comment import Comment +from szurubooru.model.post import Post, PostFavorite, PostScore class User(Base): - __tablename__ = 'user' + __tablename__ = "user" - AVATAR_GRAVATAR = 'gravatar' - AVATAR_MANUAL = 'manual' + AVATAR_GRAVATAR = "gravatar" + AVATAR_MANUAL = "manual" - RANK_ANONYMOUS = 'anonymous' - RANK_RESTRICTED = 'restricted' - RANK_REGULAR = 'regular' - RANK_POWER = 'power' - RANK_MODERATOR = 'moderator' - RANK_ADMINISTRATOR = 'administrator' - RANK_NOBODY = 'nobody' # unattainable, used for privileges + RANK_ANONYMOUS = "anonymous" + RANK_RESTRICTED = "restricted" + RANK_REGULAR = "regular" + RANK_POWER = "power" + RANK_MODERATOR = "moderator" + RANK_ADMINISTRATOR = "administrator" + RANK_NOBODY = "nobody" # unattainable, used for privileges - user_id = sa.Column('id', sa.Integer, primary_key=True) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_login_time = sa.Column('last_login_time', sa.DateTime) - version = sa.Column('version', sa.Integer, default=1, nullable=False) - name = sa.Column('name', sa.Unicode(50), nullable=False, unique=True) - password_hash = sa.Column('password_hash', sa.Unicode(128), nullable=False) - password_salt = sa.Column('password_salt', sa.Unicode(32)) + user_id = sa.Column("id", sa.Integer, primary_key=True) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_login_time = sa.Column("last_login_time", sa.DateTime) + version = sa.Column("version", sa.Integer, default=1, nullable=False) + name = sa.Column("name", sa.Unicode(50), nullable=False, unique=True) + password_hash = sa.Column("password_hash", sa.Unicode(128), nullable=False) + password_salt = sa.Column("password_salt", sa.Unicode(32)) password_revision = sa.Column( - 'password_revision', sa.SmallInteger, default=0, nullable=False) - email = sa.Column('email', sa.Unicode(64), nullable=True) - rank = sa.Column('rank', sa.Unicode(32), nullable=False) + "password_revision", sa.SmallInteger, default=0, nullable=False + ) + email = sa.Column("email", sa.Unicode(64), nullable=True) + rank = sa.Column("rank", sa.Unicode(32), nullable=False) avatar_style = sa.Column( - 'avatar_style', sa.Unicode(32), nullable=False, - default=AVATAR_GRAVATAR) + "avatar_style", sa.Unicode(32), nullable=False, default=AVATAR_GRAVATAR + ) - comments = sa.orm.relationship('Comment') + comments = sa.orm.relationship("Comment") @property def post_count(self) -> int: from szurubooru.db import session + return ( - session - .query(sa.sql.expression.func.sum(1)) + session.query(sa.sql.expression.func.sum(1)) .filter(Post.user_id == self.user_id) - .one()[0] or 0) + .one()[0] + or 0 + ) @property def comment_count(self) -> int: from szurubooru.db import session + return ( - session - .query(sa.sql.expression.func.sum(1)) + session.query(sa.sql.expression.func.sum(1)) .filter(Comment.user_id == self.user_id) - .one()[0] or 0) + .one()[0] + or 0 + ) @property def favorite_post_count(self) -> int: from szurubooru.db import session + return ( - session - .query(sa.sql.expression.func.sum(1)) + session.query(sa.sql.expression.func.sum(1)) .filter(PostFavorite.user_id == self.user_id) - .one()[0] or 0) + .one()[0] + or 0 + ) @property def liked_post_count(self) -> int: from szurubooru.db import session + return ( - session - .query(sa.sql.expression.func.sum(1)) + session.query(sa.sql.expression.func.sum(1)) .filter(PostScore.user_id == self.user_id) .filter(PostScore.score == 1) - .one()[0] or 0) + .one()[0] + or 0 + ) @property def disliked_post_count(self) -> int: from szurubooru.db import session + return ( - session - .query(sa.sql.expression.func.sum(1)) + session.query(sa.sql.expression.func.sum(1)) .filter(PostScore.user_id == self.user_id) .filter(PostScore.score == -1) - .one()[0] or 0) + .one()[0] + or 0 + ) __mapper_args__ = { - 'version_id_col': version, - 'version_id_generator': False, + "version_id_col": version, + "version_id_generator": False, } class UserToken(Base): - __tablename__ = 'user_token' + __tablename__ = "user_token" - user_token_id = sa.Column('id', sa.Integer, primary_key=True) + user_token_id = sa.Column("id", sa.Integer, primary_key=True) user_id = sa.Column( - 'user_id', + "user_id", sa.Integer, - sa.ForeignKey('user.id', ondelete='CASCADE'), + sa.ForeignKey("user.id", ondelete="CASCADE"), nullable=False, - index=True) - token = sa.Column('token', sa.Unicode(36), nullable=False) - note = sa.Column('note', sa.Unicode(128), nullable=True) - enabled = sa.Column('enabled', sa.Boolean, nullable=False, default=True) - expiration_time = sa.Column('expiration_time', sa.DateTime, nullable=True) - creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) - last_edit_time = sa.Column('last_edit_time', sa.DateTime) - last_usage_time = sa.Column('last_usage_time', sa.DateTime) - version = sa.Column('version', sa.Integer, default=1, nullable=False) + index=True, + ) + token = sa.Column("token", sa.Unicode(36), nullable=False) + note = sa.Column("note", sa.Unicode(128), nullable=True) + enabled = sa.Column("enabled", sa.Boolean, nullable=False, default=True) + expiration_time = sa.Column("expiration_time", sa.DateTime, nullable=True) + creation_time = sa.Column("creation_time", sa.DateTime, nullable=False) + last_edit_time = sa.Column("last_edit_time", sa.DateTime) + last_usage_time = sa.Column("last_usage_time", sa.DateTime) + version = sa.Column("version", sa.Integer, default=1, nullable=False) - user = sa.orm.relationship('User') + user = sa.orm.relationship("User") diff --git a/server/szurubooru/model/util.py b/server/szurubooru/model/util.py index e82539f..bece9e9 100644 --- a/server/szurubooru/model/util.py +++ b/server/szurubooru/model/util.py @@ -1,15 +1,19 @@ -from typing import Tuple, Any, Dict, Callable, Union, Optional +from typing import Any, Callable, Dict, Optional, Tuple, Union + import sqlalchemy as sa + from szurubooru.model.base import Base from szurubooru.model.user import User def get_resource_info(entity: Base) -> Tuple[Any, Any, Union[str, int]]: serializers = { - 'tag': lambda tag: tag.first_name, - 'tag_category': lambda category: category.name, - 'comment': lambda comment: comment.comment_id, - 'post': lambda post: post.post_id, + "tag": lambda tag: tag.first_name, + "tag_category": lambda category: category.name, + "comment": lambda comment: comment.comment_id, + "post": lambda post: post.post_id, + "pool": lambda pool: pool.pool_id, + "pool_category": lambda category: category.name, } # type: Dict[str, Callable[[Base], Any]] resource_type = entity.__table__.name @@ -29,14 +33,15 @@ def get_resource_info(entity: Base) -> Tuple[Any, Any, Union[str, int]]: def get_aux_entity( - session: Any, - get_table_info: Callable[[Base], Tuple[Base, Callable[[Base], Any]]], - entity: Base, - user: User) -> Optional[Base]: + session: Any, + get_table_info: Callable[[Base], Tuple[Base, Callable[[Base], Any]]], + entity: Base, + user: User, +) -> Optional[Base]: table, get_column = get_table_info(entity) return ( - session - .query(table) + session.query(table) .filter(get_column(table) == get_column(entity)) .filter(table.user_id == user.user_id) - .one_or_none()) + .one_or_none() + ) diff --git a/server/szurubooru/rest/__init__.py b/server/szurubooru/rest/__init__.py index d6b3ef2..6db22e2 100644 --- a/server/szurubooru/rest/__init__.py +++ b/server/szurubooru/rest/__init__.py @@ -1,3 +1,3 @@ +import szurubooru.rest.routes from szurubooru.rest.app import application from szurubooru.rest.context import Context, Response -import szurubooru.rest.routes diff --git a/server/szurubooru/rest/app.py b/server/szurubooru/rest/app.py index 8c9efba..a6f10fb 100644 --- a/server/szurubooru/rest/app.py +++ b/server/szurubooru/rest/app.py @@ -1,20 +1,21 @@ -import urllib.parse import cgi import json import re -from typing import Dict, Any, Callable, Tuple +import urllib.parse from datetime import datetime +from typing import Any, Callable, Dict, Tuple + from szurubooru import db from szurubooru.func import util -from szurubooru.rest import errors, middleware, routes, context +from szurubooru.rest import context, errors, middleware, routes def _json_serializer(obj: Any) -> str: - ''' JSON serializer for objects not serializable by default JSON code ''' + """ JSON serializer for objects not serializable by default JSON code """ if isinstance(obj, datetime): - serial = obj.isoformat('T') + 'Z' + serial = obj.isoformat("T") + "Z" return serial - raise TypeError('Type not serializable') + raise TypeError("Type not serializable") def _dump_json(obj: Any) -> str: @@ -24,71 +25,75 @@ def _dump_json(obj: Any) -> str: def _get_headers(env: Dict[str, Any]) -> Dict[str, str]: headers = {} # type: Dict[str, str] for key, value in env.items(): - if key.startswith('HTTP_'): + if key.startswith("HTTP_"): key = util.snake_case_to_upper_train_case(key[5:]) headers[key] = value return headers def _create_context(env: Dict[str, Any]) -> context.Context: - method = env['REQUEST_METHOD'] - path = '/' + env['PATH_INFO'].lstrip('/') - path = path.encode('latin-1').decode('utf-8') # PEP-3333 + method = env["REQUEST_METHOD"] + path = "/" + env["PATH_INFO"].lstrip("/") + path = path.encode("latin-1").decode("utf-8") # PEP-3333 headers = _get_headers(env) files = {} - params = dict(urllib.parse.parse_qsl(env.get('QUERY_STRING', ''))) + params = dict(urllib.parse.parse_qsl(env.get("QUERY_STRING", ""))) - if 'multipart' in env.get('CONTENT_TYPE', ''): - form = cgi.FieldStorage(fp=env['wsgi.input'], environ=env) + if "multipart" in env.get("CONTENT_TYPE", ""): + form = cgi.FieldStorage(fp=env["wsgi.input"], environ=env) if not form.list: raise errors.HttpBadRequest( - 'ValidationError', 'No files attached.') - body = form.getvalue('metadata') + "ValidationError", "No files attached." + ) + body = form.getvalue("metadata") for key in form: files[key] = form.getvalue(key) else: - body = env['wsgi.input'].read() + body = env["wsgi.input"].read() if body: try: if isinstance(body, bytes): - body = body.decode('utf-8') + body = body.decode("utf-8") for key, value in json.loads(body).items(): params[key] = value except (ValueError, UnicodeDecodeError): raise errors.HttpBadRequest( - 'ValidationError', - 'Could not decode the request body. The JSON ' - 'was incorrect or was not encoded as UTF-8.') + "ValidationError", + "Could not decode the request body. The JSON " + "was incorrect or was not encoded as UTF-8.", + ) return context.Context(env, method, path, headers, params, files) def application( - env: Dict[str, Any], - start_response: Callable[[str, Any], Any]) -> Tuple[bytes]: + env: Dict[str, Any], start_response: Callable[[str, Any], Any] +) -> Tuple[bytes]: try: ctx = _create_context(env) - if 'application/json' not in ctx.get_header('Accept'): + if "application/json" not in ctx.get_header("Accept"): raise errors.HttpNotAcceptable( - 'ValidationError', - 'This API only supports JSON responses.') + "ValidationError", "This API only supports JSON responses." + ) for url, allowed_methods in routes.routes.items(): match = re.fullmatch(url, ctx.url) if match: if ctx.method not in allowed_methods: raise errors.HttpMethodNotAllowed( - 'ValidationError', - 'Allowed methods: %r' % allowed_methods) + "ValidationError", + "Allowed methods: %r" % allowed_methods, + ) handler = allowed_methods[ctx.method] break else: raise errors.HttpNotFound( - 'ValidationError', - 'Requested path ' + ctx.url + ' was not found.') + "ValidationError", + "Requested path " + ctx.url + " was not found.", + ) try: ctx.session = db.session() @@ -106,8 +111,8 @@ def application( finally: db.session.remove() - start_response('200', [('content-type', 'application/json')]) - return (_dump_json(response).encode('utf-8'),) + start_response("200", [("content-type", "application/json")]) + return (_dump_json(response).encode("utf-8"),) except Exception as ex: for exception_type, ex_handler in errors.error_handlers.items(): @@ -117,14 +122,15 @@ def application( except errors.BaseHttpError as ex: start_response( - '%d %s' % (ex.code, ex.reason), - [('content-type', 'application/json')]) + "%d %s" % (ex.code, ex.reason), + [("content-type", "application/json")], + ) blob = { - 'name': ex.name, - 'title': ex.title, - 'description': ex.description, + "name": ex.name, + "title": ex.title, + "description": ex.description, } if ex.extra_fields is not None: for key, value in ex.extra_fields.items(): blob[key] = value - return (_dump_json(blob).encode('utf-8'),) + return (_dump_json(blob).encode("utf-8"),) diff --git a/server/szurubooru/rest/context.py b/server/szurubooru/rest/context.py index 0d4535a..cc5393c 100644 --- a/server/szurubooru/rest/context.py +++ b/server/szurubooru/rest/context.py @@ -1,7 +1,7 @@ -from typing import Any, Union, List, Dict, Optional, cast -from szurubooru import model, errors -from szurubooru.func import net, file_uploads +from typing import Any, Dict, List, Optional, Union, cast +from szurubooru import errors, model +from szurubooru.func import file_uploads, net MISSING = object() Request = Dict[str, Any] @@ -10,13 +10,14 @@ Response = Optional[Dict[str, Any]] class Context: def __init__( - self, - env: Dict[str, Any], - method: str, - url: str, - headers: Dict[str, str] = None, - params: Request = None, - files: Dict[str, bytes] = None) -> None: + self, + env: Dict[str, Any], + method: str, + url: str, + headers: Dict[str, str] = None, + params: Request = None, + files: Dict[str, bytes] = None, + ) -> None: self.env = env self.method = method self.url = url @@ -26,7 +27,7 @@ class Context: self.user = model.User() self.user.name = None - self.user.rank = 'anonymous' + self.user.rank = "anonymous" self.session = None # type: Any @@ -34,37 +35,45 @@ class Context: return name in self._headers def get_header(self, name: str) -> str: - return self._headers.get(name, '') + return self._headers.get(name, "") def has_file(self, name: str, allow_tokens: bool = True) -> bool: return ( - name in self._files or - name + 'Url' in self._params or - (allow_tokens and name + 'Token' in self._params)) + name in self._files + or name + "Url" in self._params + or (allow_tokens and name + "Token" in self._params) + ) def get_file( - self, - name: str, - default: Union[object, bytes] = MISSING, - allow_tokens: bool = True) -> bytes: + self, + name: str, + default: Union[object, bytes] = MISSING, + use_video_downloader: bool = False, + allow_tokens: bool = True, + ) -> bytes: if name in self._files and self._files[name]: return self._files[name] - if name + 'Url' in self._params: - return net.download(self._params[name + 'Url']) + if name + "Url" in self._params: + return net.download( + self._params[name + "Url"], + use_video_downloader=use_video_downloader, + ) - if allow_tokens and name + 'Token' in self._params: - ret = file_uploads.get(self._params[name + 'Token']) + if allow_tokens and name + "Token" in self._params: + ret = file_uploads.get(self._params[name + "Token"]) if ret: return ret elif default is not MISSING: raise errors.MissingOrExpiredRequiredFileError( - 'Required file %r is missing or has expired.' % name) + "Required file %r is missing or has expired." % name + ) if default is not MISSING: return cast(bytes, default) raise errors.MissingRequiredFileError( - 'Required file %r is missing.' % name) + "Required file %r is missing." % name + ) def has_param(self, name: str) -> bool: return name in self._params @@ -73,61 +82,62 @@ class Context: return self._params[name] def get_param_as_list( - self, - name: str, - default: Union[object, List[Any]] = MISSING) -> List[Any]: + self, name: str, default: Union[object, List[Any]] = MISSING + ) -> List[Any]: if name not in self._params: if default is not MISSING: return cast(List[Any], default) raise errors.MissingRequiredParameterError( - 'Required parameter %r is missing.' % name) + "Required parameter %r is missing." % name + ) value = self._params[name] if type(value) is str: - if ',' in value: - return value.split(',') + if "," in value: + return value.split(",") return [value] if type(value) is list: return value raise errors.InvalidParameterError( - 'Parameter %r must be a list.' % name) + "Parameter %r must be a list." % name + ) def get_param_as_int_list( - self, - name: str, - default: Union[object, List[int]] = MISSING) -> List[int]: + self, name: str, default: Union[object, List[int]] = MISSING + ) -> List[int]: ret = self.get_param_as_list(name, default) for item in ret: if type(item) is not int: raise errors.InvalidParameterError( - 'Parameter %r must be a list of integer values.' % name) + "Parameter %r must be a list of integer values." % name + ) return ret def get_param_as_string_list( - self, - name: str, - default: Union[object, List[str]] = MISSING) -> List[str]: + self, name: str, default: Union[object, List[str]] = MISSING + ) -> List[str]: ret = self.get_param_as_list(name, default) for item in ret: if type(item) is not str: raise errors.InvalidParameterError( - 'Parameter %r must be a list of string values.' % name) + "Parameter %r must be a list of string values." % name + ) return ret def get_param_as_string( - self, - name: str, - default: Union[object, str] = MISSING) -> str: + self, name: str, default: Union[object, str] = MISSING + ) -> str: if name not in self._params: if default is not MISSING: return cast(str, default) raise errors.MissingRequiredParameterError( - 'Required parameter %r is missing.' % name) + "Required parameter %r is missing." % name + ) value = self._params[name] try: if value is None: - return '' + return "" if type(value) is list: - return ','.join(value) + return ",".join(value) if type(value) is int or type(value) is float: return str(value) if type(value) is str: @@ -135,33 +145,39 @@ class Context: except TypeError: pass raise errors.InvalidParameterError( - 'Parameter %r must be a string value.' % name) + "Parameter %r must be a string value." % name + ) def get_param_as_int( - self, - name: str, - default: Union[object, int] = MISSING, - min: Optional[int] = None, - max: Optional[int] = None) -> int: + self, + name: str, + default: Union[object, int] = MISSING, + min: Optional[int] = None, + max: Optional[int] = None, + ) -> int: if name not in self._params: if default is not MISSING: return cast(int, default) raise errors.MissingRequiredParameterError( - 'Required parameter %r is missing.' % name) + "Required parameter %r is missing." % name + ) value = self._params[name] try: value = int(value) if min is not None and value < min: raise errors.InvalidParameterError( - 'Parameter %r must be at least %r.' % (name, min)) + "Parameter %r must be at least %r." % (name, min) + ) if max is not None and value > max: raise errors.InvalidParameterError( - 'Parameter %r may not exceed %r.' % (name, max)) + "Parameter %r may not exceed %r." % (name, max) + ) return value except (ValueError, TypeError): pass raise errors.InvalidParameterError( - 'Parameter %r must be an integer value.' % name) + "Parameter %r must be an integer value." % name + ) def get_param_as_float( self, @@ -190,22 +206,23 @@ class Context: 'Parameter %r must be a float value.' % name) def get_param_as_bool( - self, - name: str, - default: Union[object, bool] = MISSING) -> bool: + self, name: str, default: Union[object, bool] = MISSING + ) -> bool: if name not in self._params: if default is not MISSING: return cast(bool, default) raise errors.MissingRequiredParameterError( - 'Required parameter %r is missing.' % name) + "Required parameter %r is missing." % name + ) value = self._params[name] try: value = str(value).lower() except TypeError: pass - if value in ['1', 'y', 'yes', 'yeah', 'yep', 'yup', 't', 'true']: + if value in ["1", "y", "yes", "yeah", "yep", "yup", "t", "true"]: return True - if value in ['0', 'n', 'no', 'nope', 'f', 'false']: + if value in ["0", "n", "no", "nope", "f", "false"]: return False raise errors.InvalidParameterError( - 'Parameter %r must be a boolean value.' % name) + "Parameter %r must be a boolean value." % name + ) diff --git a/server/szurubooru/rest/errors.py b/server/szurubooru/rest/errors.py index f90ac25..446b757 100644 --- a/server/szurubooru/rest/errors.py +++ b/server/szurubooru/rest/errors.py @@ -1,19 +1,19 @@ -from typing import Optional, Callable, Type, Dict +from typing import Callable, Dict, Optional, Type - -error_handlers = {} # pylint: disable=invalid-name +error_handlers = {} class BaseHttpError(RuntimeError): code = -1 - reason = '' + reason = "" def __init__( - self, - name: str, - description: str, - title: Optional[str] = None, - extra_fields: Optional[Dict[str, str]] = None) -> None: + self, + name: str, + description: str, + title: Optional[str] = None, + extra_fields: Optional[Dict[str, str]] = None, + ) -> None: super().__init__() # error name for programmers self.name = name @@ -27,40 +27,40 @@ class BaseHttpError(RuntimeError): class HttpBadRequest(BaseHttpError): code = 400 - reason = 'Bad Request' + reason = "Bad Request" class HttpForbidden(BaseHttpError): code = 403 - reason = 'Forbidden' + reason = "Forbidden" class HttpNotFound(BaseHttpError): code = 404 - reason = 'Not Found' + reason = "Not Found" class HttpNotAcceptable(BaseHttpError): code = 406 - reason = 'Not Acceptable' + reason = "Not Acceptable" class HttpConflict(BaseHttpError): code = 409 - reason = 'Conflict' + reason = "Conflict" class HttpMethodNotAllowed(BaseHttpError): code = 405 - reason = 'Method Not Allowed' + reason = "Method Not Allowed" class HttpInternalServerError(BaseHttpError): code = 500 - reason = 'Internal Server Error' + reason = "Internal Server Error" def handle( - exception_type: Type[Exception], - handler: Callable[[Exception], None]) -> None: + exception_type: Type[Exception], handler: Callable[[Exception], None] +) -> None: error_handlers[exception_type] = handler diff --git a/server/szurubooru/rest/middleware.py b/server/szurubooru/rest/middleware.py index ce457e0..2936abf 100644 --- a/server/szurubooru/rest/middleware.py +++ b/server/szurubooru/rest/middleware.py @@ -1,8 +1,7 @@ -from typing import List, Callable -from szurubooru.rest.context import Context +from typing import Callable, List +from szurubooru.rest.context import Context -# pylint: disable=invalid-name pre_hooks = [] # type: List[Callable[[Context], None]] post_hooks = [] # type: List[Callable[[Context], None]] diff --git a/server/szurubooru/rest/routes.py b/server/szurubooru/rest/routes.py index 569cbe1..b0946fb 100644 --- a/server/szurubooru/rest/routes.py +++ b/server/szurubooru/rest/routes.py @@ -1,36 +1,39 @@ -from typing import Callable, Dict from collections import defaultdict -from szurubooru.rest.context import Context, Response +from typing import Callable, Dict +from szurubooru.rest.context import Context, Response -# pylint: disable=invalid-name RouteHandler = Callable[[Context, Dict[str, str]], Response] routes = defaultdict(dict) # type: Dict[str, Dict[str, RouteHandler]] def get(url: str) -> Callable[[RouteHandler], RouteHandler]: def wrapper(handler: RouteHandler) -> RouteHandler: - routes[url]['GET'] = handler + routes[url]["GET"] = handler return handler + return wrapper def put(url: str) -> Callable[[RouteHandler], RouteHandler]: def wrapper(handler: RouteHandler) -> RouteHandler: - routes[url]['PUT'] = handler + routes[url]["PUT"] = handler return handler + return wrapper def post(url: str) -> Callable[[RouteHandler], RouteHandler]: def wrapper(handler: RouteHandler) -> RouteHandler: - routes[url]['POST'] = handler + routes[url]["POST"] = handler return handler + return wrapper def delete(url: str) -> Callable[[RouteHandler], RouteHandler]: def wrapper(handler: RouteHandler) -> RouteHandler: - routes[url]['DELETE'] = handler + routes[url]["DELETE"] = handler return handler + return wrapper diff --git a/server/szurubooru/search/__init__.py b/server/szurubooru/search/__init__.py index 919475f..6ad1920 100644 --- a/server/szurubooru/search/__init__.py +++ b/server/szurubooru/search/__init__.py @@ -1,2 +1,2 @@ -from szurubooru.search.executor import Executor import szurubooru.search.configs +from szurubooru.search.executor import Executor diff --git a/server/szurubooru/search/configs/__init__.py b/server/szurubooru/search/configs/__init__.py index 73cc604..72bd5bc 100644 --- a/server/szurubooru/search/configs/__init__.py +++ b/server/szurubooru/search/configs/__init__.py @@ -1,6 +1,7 @@ -from .user_search_config import UserSearchConfig -from .tag_search_config import TagSearchConfig -from .post_search_config import PostSearchConfig -from .snapshot_search_config import SnapshotSearchConfig from .comment_search_config import CommentSearchConfig from .post_metric_search_config import PostMetricSearchConfig +from .pool_search_config import PoolSearchConfig +from .post_search_config import PostSearchConfig +from .snapshot_search_config import SnapshotSearchConfig +from .tag_search_config import TagSearchConfig +from .user_search_config import UserSearchConfig diff --git a/server/szurubooru/search/configs/base_search_config.py b/server/szurubooru/search/configs/base_search_config.py index 0cb814d..d60f361 100644 --- a/server/szurubooru/search/configs/base_search_config.py +++ b/server/szurubooru/search/configs/base_search_config.py @@ -1,5 +1,6 @@ -from typing import Optional, Tuple, Dict, Callable -from szurubooru.search import tokens, criteria +from typing import Callable, Dict, Optional, Tuple + +from szurubooru.search import criteria, tokens from szurubooru.search.query import SearchQuery from szurubooru.search.typing import SaColumn, SaQuery diff --git a/server/szurubooru/search/configs/comment_search_config.py b/server/szurubooru/search/configs/comment_search_config.py index 8b15446..1145e51 100644 --- a/server/szurubooru/search/configs/comment_search_config.py +++ b/server/szurubooru/search/configs/comment_search_config.py @@ -1,10 +1,14 @@ -from typing import Tuple, Dict +from typing import Dict, Tuple + import sqlalchemy as sa + from szurubooru import db, model -from szurubooru.search.typing import SaColumn, SaQuery from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( - BaseSearchConfig, Filter) + BaseSearchConfig, + Filter, +) +from szurubooru.search.typing import SaColumn, SaQuery class CommentSearchConfig(BaseSearchConfig): @@ -27,36 +31,42 @@ class CommentSearchConfig(BaseSearchConfig): @property def named_filters(self) -> Dict[str, Filter]: return { - 'id': search_util.create_num_filter(model.Comment.comment_id), - 'post': search_util.create_num_filter(model.Comment.post_id), - 'user': search_util.create_str_filter(model.User.name), - 'author': search_util.create_str_filter(model.User.name), - 'text': search_util.create_str_filter(model.Comment.text), - 'creation-date': - search_util.create_date_filter(model.Comment.creation_time), - 'creation-time': - search_util.create_date_filter(model.Comment.creation_time), - 'last-edit-date': - search_util.create_date_filter(model.Comment.last_edit_time), - 'last-edit-time': - search_util.create_date_filter(model.Comment.last_edit_time), - 'edit-date': - search_util.create_date_filter(model.Comment.last_edit_time), - 'edit-time': - search_util.create_date_filter(model.Comment.last_edit_time), + "id": search_util.create_num_filter(model.Comment.comment_id), + "post": search_util.create_num_filter(model.Comment.post_id), + "user": search_util.create_str_filter(model.User.name), + "author": search_util.create_str_filter(model.User.name), + "text": search_util.create_str_filter(model.Comment.text), + "creation-date": search_util.create_date_filter( + model.Comment.creation_time + ), + "creation-time": search_util.create_date_filter( + model.Comment.creation_time + ), + "last-edit-date": search_util.create_date_filter( + model.Comment.last_edit_time + ), + "last-edit-time": search_util.create_date_filter( + model.Comment.last_edit_time + ), + "edit-date": search_util.create_date_filter( + model.Comment.last_edit_time + ), + "edit-time": search_util.create_date_filter( + model.Comment.last_edit_time + ), } @property def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: return { - 'random': (sa.sql.expression.func.random(), self.SORT_NONE), - 'user': (model.User.name, self.SORT_ASC), - 'author': (model.User.name, self.SORT_ASC), - 'post': (model.Comment.post_id, self.SORT_DESC), - 'creation-date': (model.Comment.creation_time, self.SORT_DESC), - 'creation-time': (model.Comment.creation_time, self.SORT_DESC), - 'last-edit-date': (model.Comment.last_edit_time, self.SORT_DESC), - 'last-edit-time': (model.Comment.last_edit_time, self.SORT_DESC), - 'edit-date': (model.Comment.last_edit_time, self.SORT_DESC), - 'edit-time': (model.Comment.last_edit_time, self.SORT_DESC), + "random": (sa.sql.expression.func.random(), self.SORT_NONE), + "user": (model.User.name, self.SORT_ASC), + "author": (model.User.name, self.SORT_ASC), + "post": (model.Comment.post_id, self.SORT_DESC), + "creation-date": (model.Comment.creation_time, self.SORT_DESC), + "creation-time": (model.Comment.creation_time, self.SORT_DESC), + "last-edit-date": (model.Comment.last_edit_time, self.SORT_DESC), + "last-edit-time": (model.Comment.last_edit_time, self.SORT_DESC), + "edit-date": (model.Comment.last_edit_time, self.SORT_DESC), + "edit-time": (model.Comment.last_edit_time, self.SORT_DESC), } diff --git a/server/szurubooru/search/configs/pool_search_config.py b/server/szurubooru/search/configs/pool_search_config.py new file mode 100644 index 0000000..88b30a6 --- /dev/null +++ b/server/szurubooru/search/configs/pool_search_config.py @@ -0,0 +1,111 @@ +from typing import Dict, Tuple + +import sqlalchemy as sa + +from szurubooru import db, model +from szurubooru.func import util +from szurubooru.search.configs import util as search_util +from szurubooru.search.configs.base_search_config import ( + BaseSearchConfig, + Filter, +) +from szurubooru.search.typing import SaColumn, SaQuery + + +class PoolSearchConfig(BaseSearchConfig): + def create_filter_query(self, _disable_eager_loads: bool) -> SaQuery: + strategy = ( + sa.orm.lazyload if _disable_eager_loads else sa.orm.subqueryload + ) + return ( + db.session.query(model.Pool) + .join(model.PoolCategory) + .options(strategy(model.Pool.names)) + ) + + def create_count_query(self, _disable_eager_loads: bool) -> SaQuery: + return db.session.query(model.Pool) + + def create_around_query(self) -> SaQuery: + raise NotImplementedError() + + def finalize_query(self, query: SaQuery) -> SaQuery: + return query.order_by(model.Pool.first_name.asc()) + + @property + def anonymous_filter(self) -> Filter: + return search_util.create_subquery_filter( + model.Pool.pool_id, + model.PoolName.pool_id, + model.PoolName.name, + search_util.create_str_filter, + ) + + @property + def named_filters(self) -> Dict[str, Filter]: + return util.unalias_dict( + [ + ( + ["name"], + search_util.create_subquery_filter( + model.Pool.pool_id, + model.PoolName.pool_id, + model.PoolName.name, + search_util.create_str_filter, + ), + ), + ( + ["category"], + search_util.create_subquery_filter( + model.Pool.category_id, + model.PoolCategory.pool_category_id, + model.PoolCategory.name, + search_util.create_str_filter, + ), + ), + ( + ["creation-date", "creation-time"], + search_util.create_date_filter(model.Pool.creation_time), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + search_util.create_date_filter(model.Pool.last_edit_time), + ), + ( + ["post-count"], + search_util.create_num_filter(model.Pool.post_count), + ), + ] + ) + + @property + def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: + return util.unalias_dict( + [ + ( + ["random"], + (sa.sql.expression.func.random(), self.SORT_NONE), + ), + (["name"], (model.Pool.first_name, self.SORT_ASC)), + (["category"], (model.PoolCategory.name, self.SORT_ASC)), + ( + ["creation-date", "creation-time"], + (model.Pool.creation_time, self.SORT_DESC), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + (model.Pool.last_edit_time, self.SORT_DESC), + ), + (["post-count"], (model.Pool.post_count, self.SORT_DESC)), + ] + ) diff --git a/server/szurubooru/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py index 8596bce..71bd16a 100644 --- a/server/szurubooru/search/configs/post_search_config.py +++ b/server/szurubooru/search/configs/post_search_config.py @@ -1,87 +1,92 @@ -from typing import Any, Optional, Tuple, Dict +from typing import Any, Dict, Optional, Tuple + import sqlalchemy as sa -from szurubooru import db, model, errors -from szurubooru.func import util, metrics + +from szurubooru import db, errors, model +from szurubooru.func import metrics, util from szurubooru.search import criteria, tokens -from szurubooru.search.typing import SaColumn, SaQuery -from szurubooru.search.query import SearchQuery from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( - BaseSearchConfig, Filter) + BaseSearchConfig, + Filter, +) +from szurubooru.search.query import SearchQuery +from szurubooru.search.typing import SaColumn, SaQuery def _type_transformer(value: str) -> str: available_values = { - 'image': model.Post.TYPE_IMAGE, - 'animation': model.Post.TYPE_ANIMATION, - 'animated': model.Post.TYPE_ANIMATION, - 'anim': model.Post.TYPE_ANIMATION, - 'gif': model.Post.TYPE_ANIMATION, - 'video': model.Post.TYPE_VIDEO, - 'webm': model.Post.TYPE_VIDEO, - 'flash': model.Post.TYPE_FLASH, - 'swf': model.Post.TYPE_FLASH, + "image": model.Post.TYPE_IMAGE, + "animation": model.Post.TYPE_ANIMATION, + "animated": model.Post.TYPE_ANIMATION, + "anim": model.Post.TYPE_ANIMATION, + "gif": model.Post.TYPE_ANIMATION, + "video": model.Post.TYPE_VIDEO, + "webm": model.Post.TYPE_VIDEO, + "flash": model.Post.TYPE_FLASH, + "swf": model.Post.TYPE_FLASH, } return search_util.enum_transformer(available_values, value) def _safety_transformer(value: str) -> str: available_values = { - 'safe': model.Post.SAFETY_SAFE, - 'sketchy': model.Post.SAFETY_SKETCHY, - 'questionable': model.Post.SAFETY_SKETCHY, - 'unsafe': model.Post.SAFETY_UNSAFE, + "safe": model.Post.SAFETY_SAFE, + "sketchy": model.Post.SAFETY_SKETCHY, + "questionable": model.Post.SAFETY_SKETCHY, + "unsafe": model.Post.SAFETY_UNSAFE, } return search_util.enum_transformer(available_values, value) def _flag_transformer(value: str) -> str: available_values = { - 'loop': model.Post.FLAG_LOOP, - 'sound': model.Post.FLAG_SOUND, + "loop": model.Post.FLAG_LOOP, + "sound": model.Post.FLAG_SOUND, } - return '%' + search_util.enum_transformer(available_values, value) + '%' + return "%" + search_util.enum_transformer(available_values, value) + "%" def _source_transformer(value: str) -> str: - return search_util.wildcard_transformer('*' + value + '*') + return search_util.wildcard_transformer("*" + value + "*") def _create_score_filter(score: int) -> Filter: def wrapper( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: assert criterion - if not getattr(criterion, 'internal', False): + if not getattr(criterion, "internal", False): raise errors.SearchError( - 'Votes cannot be seen publicly. Did you mean %r?' - % 'special:liked') + "Votes cannot be seen publicly. Did you mean %r?" + % "special:liked" + ) user_alias = sa.orm.aliased(model.User) score_alias = sa.orm.aliased(model.PostScore) expr = score_alias.score == score expr = expr & search_util.apply_str_criterion_to_column( - user_alias.name, criterion) + user_alias.name, criterion + ) if negated: expr = ~expr ret = ( - query - .join(score_alias, score_alias.post_id == model.Post.post_id) + query.join(score_alias, score_alias.post_id == model.Post.post_id) .join(user_alias, user_alias.user_id == score_alias.user_id) - .filter(expr)) + .filter(expr) + ) return ret + return wrapper def _user_filter( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool +) -> SaQuery: assert criterion - if isinstance(criterion, criteria.PlainCriterion) \ - and not criterion.value: - # pylint: disable=singleton-comparison - expr = model.Post.user_id == None + if isinstance(criterion, criteria.PlainCriterion) and not criterion.value: + expr = model.Post.user_id == None # noqa: E711 if negated: expr = ~expr return query.filter(expr) @@ -89,25 +94,40 @@ def _user_filter( model.Post.user_id, model.User.user_id, model.User.name, - search_util.create_str_filter)(query, criterion, negated) + search_util.create_str_filter, + )(query, criterion, negated) def _note_filter( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool +) -> SaQuery: assert criterion return search_util.create_subquery_filter( model.Post.post_id, model.PostNote.post_id, model.PostNote.text, - search_util.create_str_filter)(query, criterion, negated) + search_util.create_str_filter, + )(query, criterion, negated) + + +def _pool_filter( + query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool +) -> SaQuery: + assert criterion + return search_util.create_subquery_filter( + model.Post.post_id, + model.PoolPost.post_id, + model.PoolPost.pool_id, + search_util.create_num_filter, + )(query, criterion, negated) def _create_metric_num_filter(name: str): - def wrapper(query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + def wrapper( + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: assert criterion t = sa.orm.aliased(model.TagName) pm = sa.orm.aliased(model.PostMetric) @@ -126,9 +146,10 @@ def _create_metric_num_filter(name: str): def _metric_presence_filter( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, +) -> SaQuery: assert criterion t = sa.orm.aliased(model.TagName) tag_name_filter = search_util.apply_str_criterion_to_column( @@ -168,53 +189,54 @@ class PostSearchConfig(BaseSearchConfig): def on_search_query_parsed(self, search_query: SearchQuery) -> SaQuery: new_special_tokens = [] for token in search_query.special_tokens: - if token.value in ('fav', 'liked', 'disliked'): + if token.value in ("fav", "liked", "disliked"): assert self.user - if self.user.rank == 'anonymous': + if self.user.rank == "anonymous": raise errors.SearchError( - 'Must be logged in to use this feature.') + "Must be logged in to use this feature." + ) criterion = criteria.PlainCriterion( - original_text=self.user.name, - value=self.user.name) - setattr(criterion, 'internal', True) + original_text=self.user.name, value=self.user.name + ) + setattr(criterion, "internal", True) search_query.named_tokens.append( tokens.NamedToken( name=token.value, criterion=criterion, - negated=token.negated)) + negated=token.negated, + ) + ) else: new_special_tokens.append(token) search_query.special_tokens = new_special_tokens def create_around_query(self) -> SaQuery: self.refresh_metrics() - return db.session.query(model.Post).options(sa.orm.lazyload('*')) + return db.session.query(model.Post).options(sa.orm.lazyload("*")) def create_filter_query(self, disable_eager_loads: bool) -> SaQuery: self.refresh_metrics() strategy = ( - sa.orm.lazyload - if disable_eager_loads - else sa.orm.subqueryload) - return ( - db.session.query(model.Post) - .options( - sa.orm.lazyload('*'), - # use config optimized for official client - # sa.orm.defer(model.Post.score), - # sa.orm.defer(model.Post.favorite_count), - # sa.orm.defer(model.Post.comment_count), - sa.orm.defer(model.Post.last_favorite_time), - sa.orm.defer(model.Post.feature_count), - sa.orm.defer(model.Post.last_feature_time), - sa.orm.defer(model.Post.last_comment_creation_time), - sa.orm.defer(model.Post.last_comment_edit_time), - sa.orm.defer(model.Post.note_count), - sa.orm.defer(model.Post.tag_count), - strategy(model.Post.tags).subqueryload(model.Tag.names), - strategy(model.Post.tags).defer(model.Tag.post_count), - strategy(model.Post.tags).lazyload(model.Tag.implications), - strategy(model.Post.tags).lazyload(model.Tag.suggestions))) + sa.orm.lazyload if disable_eager_loads else sa.orm.subqueryload + ) + return db.session.query(model.Post).options( + sa.orm.lazyload("*"), + # use config optimized for official client + # sa.orm.defer(model.Post.score), + # sa.orm.defer(model.Post.favorite_count), + # sa.orm.defer(model.Post.comment_count), + sa.orm.defer(model.Post.last_favorite_time), + sa.orm.defer(model.Post.feature_count), + sa.orm.defer(model.Post.last_feature_time), + sa.orm.defer(model.Post.last_comment_creation_time), + sa.orm.defer(model.Post.last_comment_edit_time), + sa.orm.defer(model.Post.note_count), + sa.orm.defer(model.Post.tag_count), + strategy(model.Post.tags).subqueryload(model.Tag.names), + strategy(model.Post.tags).defer(model.Tag.post_count), + strategy(model.Post.tags).lazyload(model.Tag.implications), + strategy(model.Post.tags).lazyload(model.Tag.suggestions), + ) def create_count_query(self, _disable_eager_loads: bool) -> SaQuery: return db.session.query(model.Post) @@ -233,316 +255,264 @@ class PostSearchConfig(BaseSearchConfig): model.PostTag.post_id, model.TagName.name, search_util.create_str_filter, - lambda subquery: subquery.join(model.Tag).join(model.TagName)) + lambda subquery: subquery.join(model.Tag).join(model.TagName), + ) @property def named_filters(self) -> Dict[str, Filter]: - filters = {'metric-' + name: _create_metric_num_filter(name) + filters = {"metric-" + name: _create_metric_num_filter(name) for name in self.all_metric_names} - filters.update(util.unalias_dict([ - ( - ['id'], - search_util.create_num_filter(model.Post.post_id) - ), - - ( - ['tag'], - search_util.create_subquery_filter( - model.Post.post_id, - model.PostTag.post_id, - model.TagName.name, - search_util.create_str_filter, - lambda subquery: - subquery.join(model.Tag).join(model.TagName)) - ), - - ( - ['metric'], - _metric_presence_filter - ), - - ( - ['score'], - search_util.create_num_filter(model.Post.score) - ), - - ( - ['uploader', 'upload', 'submit'], - _user_filter - ), - - ( - ['comment'], - search_util.create_subquery_filter( - model.Post.post_id, - model.Comment.post_id, - model.User.name, - search_util.create_str_filter, - lambda subquery: subquery.join(model.User)) - ), - - ( - ['fav'], - search_util.create_subquery_filter( - model.Post.post_id, - model.PostFavorite.post_id, - model.User.name, - search_util.create_str_filter, - lambda subquery: subquery.join(model.User)) - ), - - ( - ['liked'], - _create_score_filter(1) - ), - ( - ['disliked'], - _create_score_filter(-1) - ), - - ( - ['source'], - search_util.create_str_filter( - model.Post.source, _source_transformer) - ), - - ( - ['tag-count'], - search_util.create_num_filter(model.Post.tag_count) - ), - - ( - ['comment-count'], - search_util.create_num_filter(model.Post.comment_count) - ), - - ( - ['fav-count'], - search_util.create_num_filter(model.Post.favorite_count) - ), - - ( - ['note-count'], - search_util.create_num_filter(model.Post.note_count) - ), - - ( - ['relation-count'], - search_util.create_num_filter(model.Post.relation_count) - ), - - ( - ['feature-count'], - search_util.create_num_filter(model.Post.feature_count) - ), - - ( - ['type'], - search_util.create_str_filter( - model.Post.type, _type_transformer) - ), - - ( - ['content-checksum'], - search_util.create_str_filter(model.Post.checksum) - ), - - ( - ['file-size'], - search_util.create_num_filter(model.Post.file_size) - ), - - ( - ['image-width', 'width'], - search_util.create_num_filter(model.Post.canvas_width) - ), - - ( - ['image-height', 'height'], - search_util.create_num_filter(model.Post.canvas_height) - ), - - ( - ['image-area', 'area'], - search_util.create_num_filter(model.Post.canvas_area) - ), - - ( - ['image-aspect-ratio', 'image-ar', 'aspect-ratio', 'ar'], - search_util.create_num_filter( - model.Post.canvas_aspect_ratio, - transformer=search_util.float_transformer) - ), - - ( - ['creation-date', 'creation-time', 'date', 'time'], - search_util.create_date_filter(model.Post.creation_time) - ), - - ( - ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], - search_util.create_date_filter(model.Post.last_edit_time) - ), - - ( - ['comment-date', 'comment-time'], - search_util.create_date_filter( - model.Post.last_comment_creation_time) - ), - - ( - ['fav-date', 'fav-time'], - search_util.create_date_filter(model.Post.last_favorite_time) - ), - - ( - ['feature-date', 'feature-time'], - search_util.create_date_filter(model.Post.last_feature_time) - ), - - ( - ['safety', 'rating'], - search_util.create_str_filter( - model.Post.safety, _safety_transformer) - ), - - ( - ['note-text'], - _note_filter - ), - - ( - ['flag'], - search_util.create_str_filter( - model.Post.flags_string, _flag_transformer) - ), - ])) + filters.update(util.unalias_dict( + [ + (["id"], search_util.create_num_filter(model.Post.post_id)), + ( + ["tag"], + search_util.create_subquery_filter( + model.Post.post_id, + model.PostTag.post_id, + model.TagName.name, + search_util.create_str_filter, + lambda subquery: subquery.join(model.Tag).join( + model.TagName + ), + ), + ), + (["metric"], _metric_presence_filter), + (["score"], search_util.create_num_filter(model.Post.score)), + (["uploader", "upload", "submit"], _user_filter), + ( + ["comment"], + search_util.create_subquery_filter( + model.Post.post_id, + model.Comment.post_id, + model.User.name, + search_util.create_str_filter, + lambda subquery: subquery.join(model.User), + ), + ), + ( + ["fav"], + search_util.create_subquery_filter( + model.Post.post_id, + model.PostFavorite.post_id, + model.User.name, + search_util.create_str_filter, + lambda subquery: subquery.join(model.User), + ), + ), + (["liked"], _create_score_filter(1)), + (["disliked"], _create_score_filter(-1)), + ( + ["source"], + search_util.create_str_filter( + model.Post.source, _source_transformer + ), + ), + ( + ["tag-count"], + search_util.create_num_filter(model.Post.tag_count), + ), + ( + ["comment-count"], + search_util.create_num_filter(model.Post.comment_count), + ), + ( + ["fav-count"], + search_util.create_num_filter(model.Post.favorite_count), + ), + ( + ["note-count"], + search_util.create_num_filter(model.Post.note_count), + ), + ( + ["relation-count"], + search_util.create_num_filter(model.Post.relation_count), + ), + ( + ["feature-count"], + search_util.create_num_filter(model.Post.feature_count), + ), + ( + ["type"], + search_util.create_str_filter( + model.Post.type, _type_transformer + ), + ), + ( + ["content-checksum"], + search_util.create_str_filter(model.Post.checksum), + ), + ( + ["file-size"], + search_util.create_num_filter(model.Post.file_size), + ), + ( + ["image-width", "width"], + search_util.create_num_filter(model.Post.canvas_width), + ), + ( + ["image-height", "height"], + search_util.create_num_filter(model.Post.canvas_height), + ), + ( + ["image-area", "area"], + search_util.create_num_filter(model.Post.canvas_area), + ), + ( + ["image-aspect-ratio", "image-ar", "aspect-ratio", "ar"], + search_util.create_num_filter( + model.Post.canvas_aspect_ratio, + transformer=search_util.float_transformer, + ), + ), + ( + ["creation-date", "creation-time", "date", "time"], + search_util.create_date_filter(model.Post.creation_time), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + search_util.create_date_filter(model.Post.last_edit_time), + ), + ( + ["comment-date", "comment-time"], + search_util.create_date_filter( + model.Post.last_comment_creation_time + ), + ), + ( + ["fav-date", "fav-time"], + search_util.create_date_filter( + model.Post.last_favorite_time + ), + ), + ( + ["feature-date", "feature-time"], + search_util.create_date_filter( + model.Post.last_feature_time + ), + ), + ( + ["safety", "rating"], + search_util.create_str_filter( + model.Post.safety, _safety_transformer + ), + ), + (["note-text"], _note_filter), + ( + ["flag"], + search_util.create_str_filter( + model.Post.flags_string, _flag_transformer + ), + ), + (["pool"], _pool_filter), + ] + )) return filters @property def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: - filters = {'metric-' + name: + filters = {"metric-" + name: (_create_metric_sort_column(name), self.SORT_ASC) for name in self.all_metric_names} - filters.update(util.unalias_dict([ - ( - ['random'], - (sa.sql.expression.func.random(), self.SORT_NONE) - ), - - ( - ['id'], - (model.Post.post_id, self.SORT_DESC) - ), - - ( - ['score'], - (model.Post.score, self.SORT_DESC) - ), - - ( - ['tag-count'], - (model.Post.tag_count, self.SORT_DESC) - ), - - ( - ['comment-count'], - (model.Post.comment_count, self.SORT_DESC) - ), - - ( - ['fav-count'], - (model.Post.favorite_count, self.SORT_DESC) - ), - - ( - ['note-count'], - (model.Post.note_count, self.SORT_DESC) - ), - - ( - ['relation-count'], - (model.Post.relation_count, self.SORT_DESC) - ), - - ( - ['feature-count'], - (model.Post.feature_count, self.SORT_DESC) - ), - - ( - ['file-size'], - (model.Post.file_size, self.SORT_DESC) - ), - - ( - ['image-width', 'width'], - (model.Post.canvas_width, self.SORT_DESC) - ), - - ( - ['image-height', 'height'], - (model.Post.canvas_height, self.SORT_DESC) - ), - - ( - ['image-area', 'area'], - (model.Post.canvas_area, self.SORT_DESC) - ), - - ( - ['creation-date', 'creation-time', 'date', 'time'], - (model.Post.creation_time, self.SORT_DESC) - ), - - ( - ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], - (model.Post.last_edit_time, self.SORT_DESC) - ), - - ( - ['comment-date', 'comment-time'], - (model.Post.last_comment_creation_time, self.SORT_DESC) - ), - - ( - ['fav-date', 'fav-time'], - (model.Post.last_favorite_time, self.SORT_DESC) - ), - - ( - ['feature-date', 'feature-time'], - (model.Post.last_feature_time, self.SORT_DESC) - ), - ])) + filters.update(util.unalias_dict( + [ + ( + ["random"], + (sa.sql.expression.func.random(), self.SORT_NONE), + ), + (["id"], (model.Post.post_id, self.SORT_DESC)), + (["score"], (model.Post.score, self.SORT_DESC)), + (["tag-count"], (model.Post.tag_count, self.SORT_DESC)), + ( + ["comment-count"], + (model.Post.comment_count, self.SORT_DESC), + ), + (["fav-count"], (model.Post.favorite_count, self.SORT_DESC)), + (["note-count"], (model.Post.note_count, self.SORT_DESC)), + ( + ["relation-count"], + (model.Post.relation_count, self.SORT_DESC), + ), + ( + ["feature-count"], + (model.Post.feature_count, self.SORT_DESC), + ), + (["file-size"], (model.Post.file_size, self.SORT_DESC)), + ( + ["image-width", "width"], + (model.Post.canvas_width, self.SORT_DESC), + ), + ( + ["image-height", "height"], + (model.Post.canvas_height, self.SORT_DESC), + ), + ( + ["image-area", "area"], + (model.Post.canvas_area, self.SORT_DESC), + ), + ( + ["creation-date", "creation-time", "date", "time"], + (model.Post.creation_time, self.SORT_DESC), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + (model.Post.last_edit_time, self.SORT_DESC), + ), + ( + ["comment-date", "comment-time"], + (model.Post.last_comment_creation_time, self.SORT_DESC), + ), + ( + ["fav-date", "fav-time"], + (model.Post.last_favorite_time, self.SORT_DESC), + ), + ( + ["feature-date", "feature-time"], + (model.Post.last_feature_time, self.SORT_DESC), + ), + ] + )) return filters @property def special_filters(self) -> Dict[str, Filter]: return { # handled by parser - 'fav': self.noop_filter, - 'liked': self.noop_filter, - 'disliked': self.noop_filter, - 'tumbleweed': self.tumbleweed_filter, + "fav": self.noop_filter, + "liked": self.noop_filter, + "disliked": self.noop_filter, + "tumbleweed": self.tumbleweed_filter, } def noop_filter( - self, - query: SaQuery, - _criterion: Optional[criteria.BaseCriterion], - _negated: bool) -> SaQuery: + self, + query: SaQuery, + _criterion: Optional[criteria.BaseCriterion], + _negated: bool, + ) -> SaQuery: return query def tumbleweed_filter( - self, - query: SaQuery, - _criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + self, + query: SaQuery, + _criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: expr = ( (model.Post.comment_count == 0) & (model.Post.favorite_count == 0) - & (model.Post.score == 0)) + & (model.Post.score == 0) + ) if negated: expr = ~expr return query.filter(expr) diff --git a/server/szurubooru/search/configs/snapshot_search_config.py b/server/szurubooru/search/configs/snapshot_search_config.py index 0fdb69d..df722d2 100644 --- a/server/szurubooru/search/configs/snapshot_search_config.py +++ b/server/szurubooru/search/configs/snapshot_search_config.py @@ -1,9 +1,12 @@ from typing import Dict + from szurubooru import db, model -from szurubooru.search.typing import SaQuery from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( - BaseSearchConfig, Filter) + BaseSearchConfig, + Filter, +) +from szurubooru.search.typing import SaQuery class SnapshotSearchConfig(BaseSearchConfig): @@ -22,16 +25,18 @@ class SnapshotSearchConfig(BaseSearchConfig): @property def named_filters(self) -> Dict[str, Filter]: return { - 'type': - search_util.create_str_filter(model.Snapshot.resource_type), - 'id': - search_util.create_str_filter(model.Snapshot.resource_name), - 'date': - search_util.create_date_filter(model.Snapshot.creation_time), - 'time': - search_util.create_date_filter(model.Snapshot.creation_time), - 'operation': - search_util.create_str_filter(model.Snapshot.operation), - 'user': - search_util.create_str_filter(model.User.name), + "type": search_util.create_str_filter( + model.Snapshot.resource_type + ), + "id": search_util.create_str_filter(model.Snapshot.resource_name), + "date": search_util.create_date_filter( + model.Snapshot.creation_time + ), + "time": search_util.create_date_filter( + model.Snapshot.creation_time + ), + "operation": search_util.create_str_filter( + model.Snapshot.operation + ), + "user": search_util.create_str_filter(model.User.name), } diff --git a/server/szurubooru/search/configs/tag_search_config.py b/server/szurubooru/search/configs/tag_search_config.py index db3b4b2..5d41603 100644 --- a/server/szurubooru/search/configs/tag_search_config.py +++ b/server/szurubooru/search/configs/tag_search_config.py @@ -1,19 +1,22 @@ -from typing import Tuple, Dict +from typing import Dict, Tuple + import sqlalchemy as sa + from szurubooru import db, model from szurubooru.func import util -from szurubooru.search.typing import SaColumn, SaQuery from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( - BaseSearchConfig, Filter) + BaseSearchConfig, + Filter, +) +from szurubooru.search.typing import SaColumn, SaQuery class TagSearchConfig(BaseSearchConfig): def create_filter_query(self, _disable_eager_loads: bool) -> SaQuery: strategy = ( - sa.orm.lazyload - if _disable_eager_loads - else sa.orm.subqueryload) + sa.orm.lazyload if _disable_eager_loads else sa.orm.subqueryload + ) return ( db.session.query(model.Tag) .join(model.TagCategory) @@ -24,7 +27,9 @@ class TagSearchConfig(BaseSearchConfig): sa.orm.defer(model.Tag.post_count), strategy(model.Tag.names), strategy(model.Tag.suggestions).joinedload(model.Tag.names), - strategy(model.Tag.implications).joinedload(model.Tag.names))) + strategy(model.Tag.implications).joinedload(model.Tag.names), + ) + ) def create_count_query(self, _disable_eager_loads: bool) -> SaQuery: return db.session.query(model.Tag) @@ -41,95 +46,93 @@ class TagSearchConfig(BaseSearchConfig): model.Tag.tag_id, model.TagName.tag_id, model.TagName.name, - search_util.create_str_filter) + search_util.create_str_filter, + ) @property def named_filters(self) -> Dict[str, Filter]: - return util.unalias_dict([ - ( - ['name'], - search_util.create_subquery_filter( - model.Tag.tag_id, - model.TagName.tag_id, - model.TagName.name, - search_util.create_str_filter) - ), - - ( - ['category'], - search_util.create_subquery_filter( - model.Tag.category_id, - model.TagCategory.tag_category_id, - model.TagCategory.name, - search_util.create_str_filter) - ), - - ( - ['creation-date', 'creation-time'], - search_util.create_date_filter(model.Tag.creation_time) - ), - - ( - ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], - search_util.create_date_filter(model.Tag.last_edit_time) - ), - - ( - ['usage-count', 'post-count', 'usages'], - search_util.create_num_filter(model.Tag.post_count) - ), - - ( - ['suggestion-count'], - search_util.create_num_filter(model.Tag.suggestion_count) - ), - - ( - ['implication-count'], - search_util.create_num_filter(model.Tag.implication_count) - ), - ]) + return util.unalias_dict( + [ + ( + ["name"], + search_util.create_subquery_filter( + model.Tag.tag_id, + model.TagName.tag_id, + model.TagName.name, + search_util.create_str_filter, + ), + ), + ( + ["category"], + search_util.create_subquery_filter( + model.Tag.category_id, + model.TagCategory.tag_category_id, + model.TagCategory.name, + search_util.create_str_filter, + ), + ), + ( + ["creation-date", "creation-time"], + search_util.create_date_filter(model.Tag.creation_time), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + search_util.create_date_filter(model.Tag.last_edit_time), + ), + ( + ["usage-count", "post-count", "usages"], + search_util.create_num_filter(model.Tag.post_count), + ), + ( + ["suggestion-count"], + search_util.create_num_filter(model.Tag.suggestion_count), + ), + ( + ["implication-count"], + search_util.create_num_filter(model.Tag.implication_count), + ), + ] + ) @property def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: - return util.unalias_dict([ - ( - ['random'], - (sa.sql.expression.func.random(), self.SORT_NONE) - ), - - ( - ['name'], - (model.Tag.first_name, self.SORT_ASC) - ), - - ( - ['category'], - (model.TagCategory.name, self.SORT_ASC) - ), - - ( - ['creation-date', 'creation-time'], - (model.Tag.creation_time, self.SORT_DESC) - ), - - ( - ['last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'], - (model.Tag.last_edit_time, self.SORT_DESC) - ), - - ( - ['usage-count', 'post-count', 'usages'], - (model.Tag.post_count, self.SORT_DESC) - ), - - ( - ['suggestion-count'], - (model.Tag.suggestion_count, self.SORT_DESC) - ), - - ( - ['implication-count'], - (model.Tag.implication_count, self.SORT_DESC) - ), - ]) + return util.unalias_dict( + [ + ( + ["random"], + (sa.sql.expression.func.random(), self.SORT_NONE), + ), + (["name"], (model.Tag.first_name, self.SORT_ASC)), + (["category"], (model.TagCategory.name, self.SORT_ASC)), + ( + ["creation-date", "creation-time"], + (model.Tag.creation_time, self.SORT_DESC), + ), + ( + [ + "last-edit-date", + "last-edit-time", + "edit-date", + "edit-time", + ], + (model.Tag.last_edit_time, self.SORT_DESC), + ), + ( + ["usage-count", "post-count", "usages"], + (model.Tag.post_count, self.SORT_DESC), + ), + ( + ["suggestion-count"], + (model.Tag.suggestion_count, self.SORT_DESC), + ), + ( + ["implication-count"], + (model.Tag.implication_count, self.SORT_DESC), + ), + ] + ) diff --git a/server/szurubooru/search/configs/user_search_config.py b/server/szurubooru/search/configs/user_search_config.py index 6453400..bbf4034 100644 --- a/server/szurubooru/search/configs/user_search_config.py +++ b/server/szurubooru/search/configs/user_search_config.py @@ -1,10 +1,14 @@ -from typing import Tuple, Dict +from typing import Dict, Tuple + import sqlalchemy as sa + from szurubooru import db, model -from szurubooru.search.typing import SaColumn, SaQuery from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import ( - BaseSearchConfig, Filter) + BaseSearchConfig, + Filter, +) +from szurubooru.search.typing import SaColumn, SaQuery class UserSearchConfig(BaseSearchConfig): @@ -27,31 +31,36 @@ class UserSearchConfig(BaseSearchConfig): @property def named_filters(self) -> Dict[str, Filter]: return { - 'name': - search_util.create_str_filter(model.User.name), - 'creation-date': - search_util.create_date_filter(model.User.creation_time), - 'creation-time': - search_util.create_date_filter(model.User.creation_time), - 'last-login-date': - search_util.create_date_filter(model.User.last_login_time), - 'last-login-time': - search_util.create_date_filter(model.User.last_login_time), - 'login-date': - search_util.create_date_filter(model.User.last_login_time), - 'login-time': - search_util.create_date_filter(model.User.last_login_time), + "name": search_util.create_str_filter(model.User.name), + "creation-date": search_util.create_date_filter( + model.User.creation_time + ), + "creation-time": search_util.create_date_filter( + model.User.creation_time + ), + "last-login-date": search_util.create_date_filter( + model.User.last_login_time + ), + "last-login-time": search_util.create_date_filter( + model.User.last_login_time + ), + "login-date": search_util.create_date_filter( + model.User.last_login_time + ), + "login-time": search_util.create_date_filter( + model.User.last_login_time + ), } @property def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: return { - 'random': (sa.sql.expression.func.random(), self.SORT_NONE), - 'name': (model.User.name, self.SORT_ASC), - 'creation-date': (model.User.creation_time, self.SORT_DESC), - 'creation-time': (model.User.creation_time, self.SORT_DESC), - 'last-login-date': (model.User.last_login_time, self.SORT_DESC), - 'last-login-time': (model.User.last_login_time, self.SORT_DESC), - 'login-date': (model.User.last_login_time, self.SORT_DESC), - 'login-time': (model.User.last_login_time, self.SORT_DESC), + "random": (sa.sql.expression.func.random(), self.SORT_NONE), + "name": (model.User.name, self.SORT_ASC), + "creation-date": (model.User.creation_time, self.SORT_DESC), + "creation-time": (model.User.creation_time, self.SORT_DESC), + "last-login-date": (model.User.last_login_time, self.SORT_DESC), + "last-login-time": (model.User.last_login_time, self.SORT_DESC), + "login-date": (model.User.last_login_time, self.SORT_DESC), + "login-time": (model.User.last_login_time, self.SORT_DESC), } diff --git a/server/szurubooru/search/configs/util.py b/server/szurubooru/search/configs/util.py index 183dbab..659f546 100644 --- a/server/szurubooru/search/configs/util.py +++ b/server/szurubooru/search/configs/util.py @@ -1,33 +1,36 @@ -from typing import Any, Optional, Union, Dict, Callable +from typing import Any, Callable, Dict, Optional, Union + import sqlalchemy as sa + from szurubooru import db, errors from szurubooru.func import util from szurubooru.search import criteria -from szurubooru.search.typing import SaColumn, SaQuery from szurubooru.search.configs.base_search_config import Filter - +from szurubooru.search.typing import SaColumn, SaQuery Number = Union[int, float] -WILDCARD = '(--wildcard--)' # something unlikely to be used by the users +WILDCARD = "(--wildcard--)" # something unlikely to be used by the users def unescape(text: str, make_wildcards_special: bool = False) -> str: - output = '' + output = "" i = 0 while i < len(text): - if text[i] == '\\': + if text[i] == "\\": try: - char = text[i+1] + char = text[i + 1] i += 1 except IndexError: raise errors.SearchError( - 'Unterminated escape sequence (did you forget to escape ' - 'the ending backslash?)') - if char not in '*\\:-.,': + "Unterminated escape sequence (did you forget to escape " + "the ending backslash?)" + ) + if char not in "*\\:-.,": raise errors.SearchError( - 'Unknown escape sequence (did you forget to escape ' - 'the backslash?)') - elif text[i] == '*' and make_wildcards_special: + "Unknown escape sequence (did you forget to escape " + "the backslash?)" + ) + elif text[i] == "*" and make_wildcards_special: char = WILDCARD else: char = text[i] @@ -39,10 +42,11 @@ def unescape(text: str, make_wildcards_special: bool = False) -> str: def wildcard_transformer(value: str) -> str: return ( unescape(value, make_wildcards_special=True) - .replace('\\', '\\\\') - .replace('%', '\\%') - .replace('_', '\\_') - .replace(WILDCARD, '%')) + .replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_") + .replace(WILDCARD, "%") + ) def enum_transformer(available_values: Dict[str, Any], value: str) -> str: @@ -50,8 +54,9 @@ def enum_transformer(available_values: Dict[str, Any], value: str) -> str: return available_values[unescape(value.lower())] except KeyError: raise errors.SearchError( - 'Invalid value: %r. Possible values: %r.' % ( - value, list(sorted(available_values.keys())))) + "Invalid value: %r. Possible values: %r." + % (value, list(sorted(available_values.keys()))) + ) def integer_transformer(value: str) -> int: @@ -59,7 +64,7 @@ def integer_transformer(value: str) -> int: def float_transformer(value: str) -> float: - for sep in list('/:'): + for sep in list("/:"): if sep in value: a, b = value.split(sep, 1) return float(unescape(a)) / float(unescape(b)) @@ -67,9 +72,10 @@ def float_transformer(value: str) -> float: def apply_num_criterion_to_column( - column: Any, - criterion: criteria.BaseCriterion, - transformer: Callable[[str], Number] = integer_transformer) -> SaQuery: + column: Any, + criterion: criteria.BaseCriterion, + transformer: Callable[[str], Number] = integer_transformer, +) -> SaQuery: try: if isinstance(criterion, criteria.PlainCriterion): expr = column == transformer(criterion.value) @@ -80,7 +86,8 @@ def apply_num_criterion_to_column( if criterion.min_value and criterion.max_value: expr = column.between( transformer(criterion.min_value), - transformer(criterion.max_value)) + transformer(criterion.max_value), + ) elif criterion.min_value: expr = column >= transformer(criterion.min_value) elif criterion.max_value: @@ -89,22 +96,25 @@ def apply_num_criterion_to_column( assert False except ValueError: raise errors.SearchError( - 'Criterion value %r must be a number.' % (criterion,)) + "Criterion value %r must be a number." % (criterion,) + ) return expr def create_num_filter( - column: Any, - transformer: Callable[[str], Number] = integer_transformer) -> SaQuery: + column: Any, transformer: Callable[[str], Number] = integer_transformer +) -> SaQuery: def wrapper( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: assert criterion expr = apply_num_criterion_to_column(column, criterion, transformer) if negated: expr = ~expr return query.filter(expr) + return wrapper @@ -113,9 +123,10 @@ def create_float_filter(column: Any) -> SaQuery: def apply_str_criterion_to_column( - column: SaColumn, - criterion: criteria.BaseCriterion, - transformer: Callable[[str], str] = wildcard_transformer) -> SaQuery: + column: SaColumn, + criterion: criteria.BaseCriterion, + transformer: Callable[[str], str] = wildcard_transformer, +) -> SaQuery: if isinstance(criterion, criteria.PlainCriterion): expr = column.ilike(transformer(criterion.value)) elif isinstance(criterion, criteria.ArrayCriterion): @@ -124,30 +135,34 @@ def apply_str_criterion_to_column( expr = expr | column.ilike(transformer(value)) elif isinstance(criterion, criteria.RangedCriterion): raise errors.SearchError( - 'Ranged criterion is invalid in this context. ' - 'Did you forget to escape the dots?') + "Ranged criterion is invalid in this context. " + "Did you forget to escape the dots?" + ) else: assert False return expr def create_str_filter( - column: SaColumn, - transformer: Callable[[str], str] = wildcard_transformer) -> Filter: + column: SaColumn, transformer: Callable[[str], str] = wildcard_transformer +) -> Filter: def wrapper( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: assert criterion expr = apply_str_criterion_to_column(column, criterion, transformer) if negated: expr = ~expr return query.filter(expr) + return wrapper def apply_date_criterion_to_column( - column: SaQuery, criterion: criteria.BaseCriterion) -> SaQuery: + column: SaQuery, criterion: criteria.BaseCriterion +) -> SaQuery: if isinstance(criterion, criteria.PlainCriterion): min_date, max_date = util.parse_time_range(criterion.value) expr = column.between(min_date, max_date) @@ -175,36 +190,40 @@ def apply_date_criterion_to_column( def create_date_filter(column: SaColumn) -> Filter: def wrapper( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: assert criterion expr = apply_date_criterion_to_column(column, criterion) if negated: expr = ~expr return query.filter(expr) + return wrapper def create_subquery_filter( - left_id_column: SaColumn, - right_id_column: SaColumn, - filter_column: SaColumn, - filter_factory: SaColumn, - subquery_decorator: Callable[[SaQuery], None] = None) -> Filter: + left_id_column: SaColumn, + right_id_column: SaColumn, + filter_column: SaColumn, + filter_factory: SaColumn, + subquery_decorator: Callable[[SaQuery], None] = None, +) -> Filter: filter_func = filter_factory(filter_column) def wrapper( - query: SaQuery, - criterion: Optional[criteria.BaseCriterion], - negated: bool) -> SaQuery: + query: SaQuery, + criterion: Optional[criteria.BaseCriterion], + negated: bool, + ) -> SaQuery: assert criterion - subquery = db.session.query(right_id_column.label('foreign_id')) + subquery = db.session.query(right_id_column.label("foreign_id")) if subquery_decorator: subquery = subquery_decorator(subquery) - subquery = subquery.options(sa.orm.lazyload('*')) + subquery = subquery.options(sa.orm.lazyload("*")) subquery = filter_func(subquery, criterion, False) - subquery = subquery.subquery('t') + subquery = subquery.subquery("t") expression = left_id_column.in_(subquery) if negated: expression = ~expression diff --git a/server/szurubooru/search/criteria.py b/server/szurubooru/search/criteria.py index 4512b0f..633c6f2 100644 --- a/server/szurubooru/search/criteria.py +++ b/server/szurubooru/search/criteria.py @@ -1,4 +1,5 @@ -from typing import Optional, List +from typing import List, Optional + from szurubooru.search.typing import SaQuery @@ -12,16 +13,17 @@ class BaseCriterion: class RangedCriterion(BaseCriterion): def __init__( - self, - original_text: str, - min_value: Optional[str], - max_value: Optional[str]) -> None: + self, + original_text: str, + min_value: Optional[str], + max_value: Optional[str], + ) -> None: super().__init__(original_text) self.min_value = min_value self.max_value = max_value def __hash__(self) -> int: - return hash(('range', self.min_value, self.max_value)) + return hash(("range", self.min_value, self.max_value)) class PlainCriterion(BaseCriterion): @@ -39,4 +41,4 @@ class ArrayCriterion(BaseCriterion): self.values = values def __hash__(self) -> int: - return hash(tuple(['array'] + self.values)) + return hash(tuple(["array"] + self.values)) diff --git a/server/szurubooru/search/executor.py b/server/szurubooru/search/executor.py index 3a8ec68..92a7e2f 100644 --- a/server/szurubooru/search/executor.py +++ b/server/szurubooru/search/executor.py @@ -1,11 +1,13 @@ -from typing import Union, Tuple, List, Dict, Callable +from typing import Callable, Dict, List, Tuple, Union + import sqlalchemy as sa -from szurubooru import db, model, errors, rest + +from szurubooru import db, errors, model, rest from szurubooru.func import cache -from szurubooru.search import tokens, parser -from szurubooru.search.typing import SaQuery -from szurubooru.search.query import SearchQuery +from szurubooru.search import parser, tokens from szurubooru.search.configs.base_search_config import BaseSearchConfig +from szurubooru.search.query import SearchQuery +from szurubooru.search.typing import SaQuery def _format_dict_keys(source: Dict) -> List[str]: @@ -25,10 +27,10 @@ def _get_order(order: str, default_order: str) -> Union[bool, str]: class Executor: - ''' + """ Class for search parsing and execution. Handles plaintext parsing and delegates sqlalchemy filter decoration to SearchConfig instances. - ''' + """ AROUND_NEXT = 'up' AROUND_PREV = 'down' @@ -37,9 +39,8 @@ class Executor: self.parser = parser.Parser() def get_around( - self, - query_text: str, - entity_id: int) -> Tuple[model.Base, model.Base, model.Base]: + self, query_text: str, entity_id: int + ) -> Tuple[model.Base, model.Base, model.Base]: search_query = self.parser.parse(query_text) self.config.on_search_query_parsed(search_query) filter_query = ( @@ -68,21 +69,19 @@ class Executor: self, ctx: rest.Context, entity_id: int, - serializer: Callable[[model.Base], rest.Response] + serializer: Callable[[model.Base], rest.Response], ) -> rest.Response: entities = self.get_around( - ctx.get_param_as_string('query', default=''), entity_id) + ctx.get_param_as_string("query", default=""), entity_id + ) return { - 'prev': serializer(entities[0]), - 'next': serializer(entities[1]), - 'random': serializer(entities[2]), + "prev": serializer(entities[0]), + "next": serializer(entities[1]), + "random": serializer(entities[2]), } def execute( - self, - query_text: str, - offset: int, - limit: int + self, query_text: str, offset: int, limit: int ) -> Tuple[int, List[model.Base]]: search_query = self.parser.parse(query_text) self.config.on_search_query_parsed(search_query) @@ -93,7 +92,7 @@ class Executor: disable_eager_loads = False for token in search_query.sort_tokens: - if token.name == 'random': + if token.name == "random": disable_eager_loads = True key = (id(self.config), hash(search_query), offset, limit) @@ -101,22 +100,16 @@ class Executor: return cache.get(key) filter_query = self.config.create_filter_query(disable_eager_loads) - filter_query = filter_query.options(sa.orm.lazyload('*')) + filter_query = filter_query.options(sa.orm.lazyload("*")) filter_query = self._prepare_db_query(filter_query, search_query, True) - entities = ( - filter_query - .offset(offset) - .limit(limit) - .all()) + entities = filter_query.offset(offset).limit(limit).all() count_query = self.config.create_count_query(disable_eager_loads) - count_query = count_query.options(sa.orm.lazyload('*')) + count_query = count_query.options(sa.orm.lazyload("*")) count_query = self._prepare_db_query(count_query, search_query, False) - count_statement = ( - count_query - .statement - .with_only_columns([sa.func.count()]) - .order_by(None)) + count_statement = count_query.statement.with_only_columns( + [sa.func.count()] + ).order_by(None) count = db.session.execute(count_statement).scalar() ret = (count, entities) @@ -126,18 +119,18 @@ class Executor: def execute_and_serialize( self, ctx: rest.Context, - serializer: Callable[[model.Base], rest.Response] + serializer: Callable[[model.Base], rest.Response], ) -> rest.Response: - query = ctx.get_param_as_string('query', default='') - offset = ctx.get_param_as_int('offset', default=0, min=0) - limit = ctx.get_param_as_int('limit', default=100, min=1, max=100) + query = ctx.get_param_as_string("query", default="") + offset = ctx.get_param_as_int("offset", default=0, min=0) + limit = ctx.get_param_as_int("limit", default=100, min=1, max=100) count, entities = self.execute(query, offset, limit) return { - 'query': query, - 'offset': offset, - 'limit': limit, - 'total': count, - 'results': list([serializer(entity) for entity in entities]), + "query": query, + "offset": offset, + "limit": limit, + "total": count, + "results": list([serializer(entity) for entity in entities]), } def count(self, query_text:str) -> int: @@ -155,46 +148,58 @@ class Executor: return count def _prepare_db_query( - self, - db_query: SaQuery, - search_query: SearchQuery, - use_sort: bool) -> SaQuery: + self, db_query: SaQuery, search_query: SearchQuery, use_sort: bool + ) -> SaQuery: for anon_token in search_query.anonymous_tokens: if not self.config.anonymous_filter: raise errors.SearchError( - 'Anonymous tokens are not valid in this context.') + "Anonymous tokens are not valid in this context." + ) db_query = self.config.anonymous_filter( - db_query, anon_token.criterion, anon_token.negated) + db_query, anon_token.criterion, anon_token.negated + ) for named_token in search_query.named_tokens: if named_token.name not in self.config.named_filters: raise errors.SearchError( - 'Unknown named token: %r. Available named tokens: %r.' % ( + "Unknown named token: %r. Available named tokens: %r." + % ( named_token.name, - _format_dict_keys(self.config.named_filters))) + _format_dict_keys(self.config.named_filters), + ) + ) db_query = self.config.named_filters[named_token.name]( - db_query, named_token.criterion, named_token.negated) + db_query, named_token.criterion, named_token.negated + ) for sp_token in search_query.special_tokens: if sp_token.value not in self.config.special_filters: raise errors.SearchError( - 'Unknown special token: %r. ' - 'Available special tokens: %r.' % ( + "Unknown special token: %r. " + "Available special tokens: %r." + % ( sp_token.value, - _format_dict_keys(self.config.special_filters))) + _format_dict_keys(self.config.special_filters), + ) + ) db_query = self.config.special_filters[sp_token.value]( - db_query, None, sp_token.negated) + db_query, None, sp_token.negated + ) if use_sort: for sort_token in search_query.sort_tokens: if sort_token.name not in self.config.sort_columns: raise errors.SearchError( - 'Unknown sort token: %r. ' - 'Available sort tokens: %r.' % ( + "Unknown sort token: %r. " + "Available sort tokens: %r." + % ( sort_token.name, - _format_dict_keys(self.config.sort_columns))) - column, default_order = ( - self.config.sort_columns[sort_token.name]) + _format_dict_keys(self.config.sort_columns), + ) + ) + column, default_order = self.config.sort_columns[ + sort_token.name + ] order = _get_order(sort_token.order, default_order) if order == sort_token.SORT_ASC: db_query = db_query.order_by(column.asc()) diff --git a/server/szurubooru/search/parser.py b/server/szurubooru/search/parser.py index 79cf968..d5e0f2f 100644 --- a/server/szurubooru/search/parser.py +++ b/server/szurubooru/search/parser.py @@ -1,21 +1,23 @@ import re + from szurubooru import errors from szurubooru.search import criteria, tokens -from szurubooru.search.query import SearchQuery from szurubooru.search.configs import util +from szurubooru.search.query import SearchQuery def _create_criterion( - original_value: str, value: str) -> criteria.BaseCriterion: - if re.search(r'(?<!\\),', value): - values = re.split(r'(?<!\\),', value) + original_value: str, value: str +) -> criteria.BaseCriterion: + if re.search(r"(?<!\\),", value): + values = re.split(r"(?<!\\),", value) if any(not term.strip() for term in values): - raise errors.SearchError('Empty compound value') + raise errors.SearchError("Empty compound value") return criteria.ArrayCriterion(original_value, values) - if re.search(r'(?<!\\)\.(?<!\\)\.', value): - low, high = re.split(r'(?<!\\)\.(?<!\\)\.', value, 1) + if re.search(r"(?<!\\)\.(?<!\\)\.", value): + low, high = re.split(r"(?<!\\)\.(?<!\\)\.", value, 1) if not low and not high: - raise errors.SearchError('Empty ranged value') + raise errors.SearchError("Empty ranged value") return criteria.RangedCriterion(original_value, low, high) return criteria.PlainCriterion(original_value, value) @@ -27,12 +29,12 @@ def _parse_anonymous(value: str, negated: bool) -> tokens.AnonymousToken: def _parse_named(key: str, value: str, negated: bool) -> tokens.NamedToken: original_value = value - if key.endswith('-min'): + if key.endswith("-min"): key = key[:-4] - value += '..' - elif key.endswith('-max'): + value += ".." + elif key.endswith("-max"): key = key[:-4] - value = '..' + value + value = ".." + value criterion = _create_criterion(original_value, value) return tokens.NamedToken(key, criterion, negated) @@ -42,32 +44,27 @@ def _parse_special(value: str, negated: bool) -> tokens.SpecialToken: def _parse_sort(value: str, negated: bool) -> tokens.SortToken: - if value.count(',') == 0: + if value.count(",") == 0: order_str = None - elif value.count(',') == 1: - value, order_str = value.split(',') + elif value.count(",") == 1: + value, order_str = value.split(",") else: - raise errors.SearchError('Too many commas in sort style token.') + raise errors.SearchError("Too many commas in sort style token.") try: order = { - 'asc': tokens.SortToken.SORT_ASC, - 'desc': tokens.SortToken.SORT_DESC, - '': tokens.SortToken.SORT_DEFAULT, + "asc": tokens.SortToken.SORT_ASC, + "desc": tokens.SortToken.SORT_DESC, + "": tokens.SortToken.SORT_DEFAULT, None: tokens.SortToken.SORT_DEFAULT, }[order_str] except KeyError: - raise errors.SearchError( - 'Unknown search direction: %r.' % order_str) + raise errors.SearchError("Unknown search direction: %r." % order_str) if negated: order = { - tokens.SortToken.SORT_ASC: - tokens.SortToken.SORT_DESC, - tokens.SortToken.SORT_DESC: - tokens.SortToken.SORT_ASC, - tokens.SortToken.SORT_DEFAULT: - tokens.SortToken.SORT_NEGATED_DEFAULT, - tokens.SortToken.SORT_NEGATED_DEFAULT: - tokens.SortToken.SORT_DEFAULT, + tokens.SortToken.SORT_ASC: tokens.SortToken.SORT_DESC, + tokens.SortToken.SORT_DESC: tokens.SortToken.SORT_ASC, + tokens.SortToken.SORT_DEFAULT: tokens.SortToken.SORT_NEGATED_DEFAULT, # noqa: E501 + tokens.SortToken.SORT_NEGATED_DEFAULT: tokens.SortToken.SORT_DEFAULT, # noqa: E501 }[order] return tokens.SortToken(value, order) @@ -75,29 +72,27 @@ def _parse_sort(value: str, negated: bool) -> tokens.SortToken: class Parser: def parse(self, query_text: str) -> SearchQuery: query = SearchQuery() - for chunk in re.split(r'\s+', (query_text or '').lower()): + for chunk in re.split(r"\s+", (query_text or "").lower()): if not chunk: continue negated = False - if chunk[0] == '-': + if chunk[0] == "-": chunk = chunk[1:] negated = True if not chunk: - raise errors.SearchError('Empty negated token.') - match = re.match(r'^(.*?)(?<!\\):(.*)$', chunk) + raise errors.SearchError("Empty negated token.") + match = re.match(r"^(.*?)(?<!\\):(.*)$", chunk) if match: key, value = list(match.groups()) key = util.unescape(key) - if key == 'sort': - query.sort_tokens.append( - _parse_sort(value, negated)) - elif key == 'special': - query.special_tokens.append( - _parse_special(value, negated)) + if key == "sort": + query.sort_tokens.append(_parse_sort(value, negated)) + elif key == "special": + query.special_tokens.append(_parse_special(value, negated)) else: query.named_tokens.append( - _parse_named(key, value, negated)) + _parse_named(key, value, negated) + ) else: - query.anonymous_tokens.append( - _parse_anonymous(chunk, negated)) + query.anonymous_tokens.append(_parse_anonymous(chunk, negated)) return query diff --git a/server/szurubooru/search/query.py b/server/szurubooru/search/query.py index 3d304f8..50255d6 100644 --- a/server/szurubooru/search/query.py +++ b/server/szurubooru/search/query.py @@ -1,6 +1,7 @@ -from szurubooru.search import tokens from typing import List +from szurubooru.search import tokens + class SearchQuery: def __init__(self) -> None: @@ -10,8 +11,11 @@ class SearchQuery: self.sort_tokens = [] # type: List[tokens.SortToken] def __hash__(self) -> int: - return hash(( - tuple(self.anonymous_tokens), - tuple(self.named_tokens), - tuple(self.special_tokens), - tuple(self.sort_tokens))) + return hash( + ( + tuple(self.anonymous_tokens), + tuple(self.named_tokens), + tuple(self.special_tokens), + tuple(self.sort_tokens), + ) + ) diff --git a/server/szurubooru/search/tokens.py b/server/szurubooru/search/tokens.py index 0cd7fd7..9f4eeed 100644 --- a/server/szurubooru/search/tokens.py +++ b/server/szurubooru/search/tokens.py @@ -12,7 +12,8 @@ class AnonymousToken: class NamedToken(AnonymousToken): def __init__( - self, name: str, criterion: BaseCriterion, negated: bool) -> None: + self, name: str, criterion: BaseCriterion, negated: bool + ) -> None: super().__init__(criterion, negated) self.name = name @@ -21,11 +22,11 @@ class NamedToken(AnonymousToken): class SortToken: - SORT_DESC = 'desc' - SORT_ASC = 'asc' - SORT_NONE = '' - SORT_DEFAULT = 'default' - SORT_NEGATED_DEFAULT = 'negated default' + SORT_DESC = "desc" + SORT_ASC = "asc" + SORT_NONE = "" + SORT_DEFAULT = "default" + SORT_NEGATED_DEFAULT = "negated default" def __init__(self, name: str, order: str) -> None: self.name = name diff --git a/server/szurubooru/search/typing.py b/server/szurubooru/search/typing.py index ebb1b30..686c2cb 100644 --- a/server/szurubooru/search/typing.py +++ b/server/szurubooru/search/typing.py @@ -1,6 +1,5 @@ from typing import Any, Callable - SaColumn = Any SaQuery = Any SaQueryFactory = Callable[[], SaQuery] diff --git a/server/szurubooru/tests/api/test_comment_creating.py b/server/szurubooru/tests/api/test_comment_creating.py index ad24366..b16ce65 100644 --- a/server/szurubooru/tests/api/test_comment_creating.py +++ b/server/szurubooru/tests/api/test_comment_creating.py @@ -1,70 +1,82 @@ from datetime import datetime from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import comments, posts @pytest.fixture(autouse=True) def inject_config(config_injector): config_injector( - {'privileges': {'comments:create': model.User.RANK_REGULAR}}) + {"privileges": {"comments:create": model.User.RANK_REGULAR}} + ) def test_creating_comment( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): post = post_factory() user = user_factory(rank=model.User.RANK_REGULAR) db.session.add_all([post, user]) db.session.flush() - with patch('szurubooru.func.comments.serialize_comment'), \ - fake_datetime('1997-01-01'): - comments.serialize_comment.return_value = 'serialized comment' + with patch("szurubooru.func.comments.serialize_comment"), fake_datetime( + "1997-01-01" + ): + comments.serialize_comment.return_value = "serialized comment" result = api.comment_api.create_comment( context_factory( - params={'text': 'input', 'postId': post.post_id}, - user=user)) - assert result == 'serialized comment' + params={"text": "input", "postId": post.post_id}, user=user + ) + ) + assert result == "serialized comment" comment = db.session.query(model.Comment).one() - assert comment.text == 'input' + assert comment.text == "input" assert comment.creation_time == datetime(1997, 1, 1) assert comment.last_edit_time is None assert comment.user and comment.user.user_id == user.user_id assert comment.post and comment.post.post_id == post.post_id -@pytest.mark.parametrize('params', [ - {'text': None}, - {'text': ''}, - {'text': [None]}, - {'text': ['']}, -]) +@pytest.mark.parametrize( + "params", + [ + {"text": None}, + {"text": ""}, + {"text": [None]}, + {"text": [""]}, + ], +) def test_trying_to_pass_invalid_params( - user_factory, post_factory, context_factory, params): + user_factory, post_factory, context_factory, params +): post = post_factory() user = user_factory(rank=model.User.RANK_REGULAR) db.session.add_all([post, user]) db.session.flush() - real_params = {'text': 'input', 'postId': post.post_id} + real_params = {"text": "input", "postId": post.post_id} for key, value in params.items(): real_params[key] = value with pytest.raises(errors.ValidationError): api.comment_api.create_comment( - context_factory(params=real_params, user=user)) + context_factory(params=real_params, user=user) + ) -@pytest.mark.parametrize('field', ['text', 'postId']) +@pytest.mark.parametrize("field", ["text", "postId"]) def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): params = { - 'text': 'input', - 'postId': 1, + "text": "input", + "postId": 1, } del params[field] with pytest.raises(errors.ValidationError): api.comment_api.create_comment( context_factory( - params={}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={}, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_comment_non_existing(user_factory, context_factory): @@ -73,13 +85,14 @@ def test_trying_to_comment_non_existing(user_factory, context_factory): db.session.flush() with pytest.raises(posts.PostNotFoundError): api.comment_api.create_comment( - context_factory( - params={'text': 'bad', 'postId': 5}, user=user)) + context_factory(params={"text": "bad", "postId": 5}, user=user) + ) def test_trying_to_create_without_privileges(user_factory, context_factory): with pytest.raises(errors.AuthError): api.comment_api.create_comment( context_factory( - params={}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={}, user=user_factory(rank=model.User.RANK_ANONYMOUS) + ) + ) diff --git a/server/szurubooru/tests/api/test_comment_deleting.py b/server/szurubooru/tests/api/test_comment_deleting.py index e1d1baa..71df1ff 100644 --- a/server/szurubooru/tests/api/test_comment_deleting.py +++ b/server/szurubooru/tests/api/test_comment_deleting.py @@ -1,16 +1,19 @@ import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import comments @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'comments:delete:own': model.User.RANK_REGULAR, - 'comments:delete:any': model.User.RANK_MODERATOR, - }, - }) + config_injector( + { + "privileges": { + "comments:delete:own": model.User.RANK_REGULAR, + "comments:delete:any": model.User.RANK_MODERATOR, + }, + } + ) def test_deleting_own_comment(user_factory, comment_factory, context_factory): @@ -19,27 +22,31 @@ def test_deleting_own_comment(user_factory, comment_factory, context_factory): db.session.add(comment) db.session.commit() result = api.comment_api.delete_comment( - context_factory(params={'version': 1}, user=user), - {'comment_id': comment.comment_id}) + context_factory(params={"version": 1}, user=user), + {"comment_id": comment.comment_id}, + ) assert result == {} assert db.session.query(model.Comment).count() == 0 def test_deleting_someones_else_comment( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user1 = user_factory(rank=model.User.RANK_REGULAR) user2 = user_factory(rank=model.User.RANK_MODERATOR) comment = comment_factory(user=user1) db.session.add(comment) db.session.commit() api.comment_api.delete_comment( - context_factory(params={'version': 1}, user=user2), - {'comment_id': comment.comment_id}) + context_factory(params={"version": 1}, user=user2), + {"comment_id": comment.comment_id}, + ) assert db.session.query(model.Comment).count() == 0 def test_trying_to_delete_someones_else_comment_without_privileges( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user1 = user_factory(rank=model.User.RANK_REGULAR) user2 = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user1) @@ -47,8 +54,9 @@ def test_trying_to_delete_someones_else_comment_without_privileges( db.session.commit() with pytest.raises(errors.AuthError): api.comment_api.delete_comment( - context_factory(params={'version': 1}, user=user2), - {'comment_id': comment.comment_id}) + context_factory(params={"version": 1}, user=user2), + {"comment_id": comment.comment_id}, + ) assert db.session.query(model.Comment).count() == 1 @@ -56,6 +64,8 @@ def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(comments.CommentNotFoundError): api.comment_api.delete_comment( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'comment_id': 1}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"comment_id": 1}, + ) diff --git a/server/szurubooru/tests/api/test_comment_rating.py b/server/szurubooru/tests/api/test_comment_rating.py index aae5e24..efb4bc2 100644 --- a/server/szurubooru/tests/api/test_comment_rating.py +++ b/server/szurubooru/tests/api/test_comment_rating.py @@ -1,116 +1,134 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import comments @pytest.fixture(autouse=True) def inject_config(config_injector): config_injector( - {'privileges': {'comments:score': model.User.RANK_REGULAR}}) + {"privileges": {"comments:score": model.User.RANK_REGULAR}} + ) def test_simple_rating( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'), \ - fake_datetime('1997-12-01'): - comments.serialize_comment.return_value = 'serialized comment' + with patch("szurubooru.func.comments.serialize_comment"), fake_datetime( + "1997-12-01" + ): + comments.serialize_comment.return_value = "serialized comment" result = api.comment_api.set_comment_score( - context_factory(params={'score': 1}, user=user), - {'comment_id': comment.comment_id}) - assert result == 'serialized comment' + context_factory(params={"score": 1}, user=user), + {"comment_id": comment.comment_id}, + ) + assert result == "serialized comment" assert db.session.query(model.CommentScore).count() == 1 assert comment is not None assert comment.score == 1 def test_updating_rating( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.comments.serialize_comment"): + with fake_datetime("1997-12-01"): api.comment_api.set_comment_score( - context_factory(params={'score': 1}, user=user), - {'comment_id': comment.comment_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"comment_id": comment.comment_id}, + ) + with fake_datetime("1997-12-02"): api.comment_api.set_comment_score( - context_factory(params={'score': -1}, user=user), - {'comment_id': comment.comment_id}) + context_factory(params={"score": -1}, user=user), + {"comment_id": comment.comment_id}, + ) comment = db.session.query(model.Comment).one() assert db.session.query(model.CommentScore).count() == 1 assert comment.score == -1 def test_updating_rating_to_zero( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.comments.serialize_comment"): + with fake_datetime("1997-12-01"): api.comment_api.set_comment_score( - context_factory(params={'score': 1}, user=user), - {'comment_id': comment.comment_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"comment_id": comment.comment_id}, + ) + with fake_datetime("1997-12-02"): api.comment_api.set_comment_score( - context_factory(params={'score': 0}, user=user), - {'comment_id': comment.comment_id}) + context_factory(params={"score": 0}, user=user), + {"comment_id": comment.comment_id}, + ) comment = db.session.query(model.Comment).one() assert db.session.query(model.CommentScore).count() == 0 assert comment.score == 0 def test_deleting_rating( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.comments.serialize_comment"): + with fake_datetime("1997-12-01"): api.comment_api.set_comment_score( - context_factory(params={'score': 1}, user=user), - {'comment_id': comment.comment_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"comment_id": comment.comment_id}, + ) + with fake_datetime("1997-12-02"): api.comment_api.delete_comment_score( - context_factory(user=user), - {'comment_id': comment.comment_id}) + context_factory(user=user), {"comment_id": comment.comment_id} + ) comment = db.session.query(model.Comment).one() assert db.session.query(model.CommentScore).count() == 0 assert comment.score == 0 def test_ratings_from_multiple_users( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user1 = user_factory(rank=model.User.RANK_REGULAR) user2 = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory() db.session.add_all([user1, user2, comment]) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.comments.serialize_comment"): + with fake_datetime("1997-12-01"): api.comment_api.set_comment_score( - context_factory(params={'score': 1}, user=user1), - {'comment_id': comment.comment_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user1), + {"comment_id": comment.comment_id}, + ) + with fake_datetime("1997-12-02"): api.comment_api.set_comment_score( - context_factory(params={'score': -1}, user=user2), - {'comment_id': comment.comment_id}) + context_factory(params={"score": -1}, user=user2), + {"comment_id": comment.comment_id}, + ) comment = db.session.query(model.Comment).one() assert db.session.query(model.CommentScore).count() == 2 assert comment.score == 0 def test_trying_to_omit_mandatory_field( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user = user_factory() comment = comment_factory(user=user) db.session.add(comment) @@ -118,26 +136,32 @@ def test_trying_to_omit_mandatory_field( with pytest.raises(errors.ValidationError): api.comment_api.set_comment_score( context_factory(params={}, user=user), - {'comment_id': comment.comment_id}) + {"comment_id": comment.comment_id}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(comments.CommentNotFoundError): api.comment_api.set_comment_score( context_factory( - params={'score': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'comment_id': 5}) + params={"score": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"comment_id": 5}, + ) def test_trying_to_rate_without_privileges( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): comment = comment_factory() db.session.add(comment) db.session.commit() with pytest.raises(errors.AuthError): api.comment_api.set_comment_score( context_factory( - params={'score': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'comment_id': comment.comment_id}) + params={"score": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"comment_id": comment.comment_id}, + ) diff --git a/server/szurubooru/tests/api/test_comment_retrieving.py b/server/szurubooru/tests/api/test_comment_retrieving.py index 5c846bb..404af76 100644 --- a/server/szurubooru/tests/api/test_comment_retrieving.py +++ b/server/szurubooru/tests/api/test_comment_retrieving.py @@ -1,72 +1,83 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import comments @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'comments:list': model.User.RANK_REGULAR, - 'comments:view': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "comments:list": model.User.RANK_REGULAR, + "comments:view": model.User.RANK_REGULAR, + }, + } + ) def test_retrieving_multiple(user_factory, comment_factory, context_factory): - comment1 = comment_factory(text='text 1') - comment2 = comment_factory(text='text 2') + comment1 = comment_factory(text="text 1") + comment2 = comment_factory(text="text 2") db.session.add_all([comment1, comment2]) db.session.flush() - with patch('szurubooru.func.comments.serialize_comment'): - comments.serialize_comment.return_value = 'serialized comment' + with patch("szurubooru.func.comments.serialize_comment"): + comments.serialize_comment.return_value = "serialized comment" result = api.comment_api.get_comments( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) assert result == { - 'query': '', - 'offset': 0, - 'limit': 100, - 'total': 2, - 'results': ['serialized comment', 'serialized comment'], + "query": "", + "offset": 0, + "limit": 100, + "total": 2, + "results": ["serialized comment", "serialized comment"], } def test_trying_to_retrieve_multiple_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.comment_api.get_comments( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_retrieving_single(user_factory, comment_factory, context_factory): - comment = comment_factory(text='dummy text') + comment = comment_factory(text="dummy text") db.session.add(comment) db.session.flush() - with patch('szurubooru.func.comments.serialize_comment'): - comments.serialize_comment.return_value = 'serialized comment' + with patch("szurubooru.func.comments.serialize_comment"): + comments.serialize_comment.return_value = "serialized comment" result = api.comment_api.get_comment( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR)), - {'comment_id': comment.comment_id}) - assert result == 'serialized comment' + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"comment_id": comment.comment_id}, + ) + assert result == "serialized comment" def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): with pytest.raises(comments.CommentNotFoundError): api.comment_api.get_comment( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR)), - {'comment_id': 5}) + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"comment_id": 5}, + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.comment_api.get_comment( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'comment_id': 5}) + {"comment_id": 5}, + ) diff --git a/server/szurubooru/tests/api/test_comment_updating.py b/server/szurubooru/tests/api/test_comment_updating.py index 761b1ce..e7c4fbe 100644 --- a/server/szurubooru/tests/api/test_comment_updating.py +++ b/server/szurubooru/tests/api/test_comment_updating.py @@ -1,84 +1,97 @@ from datetime import datetime from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import comments @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'comments:edit:own': model.User.RANK_REGULAR, - 'comments:edit:any': model.User.RANK_MODERATOR, - }, - }) + config_injector( + { + "privileges": { + "comments:edit:own": model.User.RANK_REGULAR, + "comments:edit:any": model.User.RANK_MODERATOR, + }, + } + ) def test_simple_updating( - user_factory, comment_factory, context_factory, fake_datetime): + user_factory, comment_factory, context_factory, fake_datetime +): user = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'), \ - fake_datetime('1997-12-01'): - comments.serialize_comment.return_value = 'serialized comment' + with patch("szurubooru.func.comments.serialize_comment"), fake_datetime( + "1997-12-01" + ): + comments.serialize_comment.return_value = "serialized comment" result = api.comment_api.update_comment( context_factory( - params={'text': 'new text', 'version': 1}, user=user), - {'comment_id': comment.comment_id}) - assert result == 'serialized comment' + params={"text": "new text", "version": 1}, user=user + ), + {"comment_id": comment.comment_id}, + ) + assert result == "serialized comment" assert comment.last_edit_time == datetime(1997, 12, 1) -@pytest.mark.parametrize('params,expected_exception', [ - ({'text': None}, comments.EmptyCommentTextError), - ({'text': ''}, comments.EmptyCommentTextError), - ({'text': []}, comments.EmptyCommentTextError), - ({'text': [None]}, errors.ValidationError), - ({'text': ['']}, comments.EmptyCommentTextError), -]) +@pytest.mark.parametrize( + "params,expected_exception", + [ + ({"text": None}, comments.EmptyCommentTextError), + ({"text": ""}, comments.EmptyCommentTextError), + ({"text": []}, comments.EmptyCommentTextError), + ({"text": [None]}, errors.ValidationError), + ({"text": [""]}, comments.EmptyCommentTextError), + ], +) def test_trying_to_pass_invalid_params( - user_factory, - comment_factory, - context_factory, - params, - expected_exception): + user_factory, comment_factory, context_factory, params, expected_exception +): user = user_factory() comment = comment_factory(user=user) db.session.add(comment) db.session.commit() with pytest.raises(expected_exception): api.comment_api.update_comment( - context_factory( - params={**params, **{'version': 1}}, user=user), - {'comment_id': comment.comment_id}) + context_factory(params={**params, **{"version": 1}}, user=user), + {"comment_id": comment.comment_id}, + ) def test_trying_to_omit_mandatory_field( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user = user_factory() comment = comment_factory(user=user) db.session.add(comment) db.session.commit() with pytest.raises(errors.ValidationError): api.comment_api.update_comment( - context_factory(params={'version': 1}, user=user), - {'comment_id': comment.comment_id}) + context_factory(params={"version": 1}, user=user), + {"comment_id": comment.comment_id}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(comments.CommentNotFoundError): api.comment_api.update_comment( context_factory( - params={'text': 'new text'}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'comment_id': 5}) + params={"text": "new text"}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"comment_id": 5}, + ) def test_trying_to_update_someones_comment_without_privileges( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user = user_factory(rank=model.User.RANK_REGULAR) user2 = user_factory(rank=model.User.RANK_REGULAR) comment = comment_factory(user=user) @@ -87,19 +100,24 @@ def test_trying_to_update_someones_comment_without_privileges( with pytest.raises(errors.AuthError): api.comment_api.update_comment( context_factory( - params={'text': 'new text', 'version': 1}, user=user2), - {'comment_id': comment.comment_id}) + params={"text": "new text", "version": 1}, user=user2 + ), + {"comment_id": comment.comment_id}, + ) def test_updating_someones_comment_with_privileges( - user_factory, comment_factory, context_factory): + user_factory, comment_factory, context_factory +): user = user_factory(rank=model.User.RANK_REGULAR) user2 = user_factory(rank=model.User.RANK_MODERATOR) comment = comment_factory(user=user) db.session.add(comment) db.session.commit() - with patch('szurubooru.func.comments.serialize_comment'): + with patch("szurubooru.func.comments.serialize_comment"): api.comment_api.update_comment( context_factory( - params={'text': 'new text', 'version': 1}, user=user2), - {'comment_id': comment.comment_id}) + params={"text": "new text", "version": 1}, user=user2 + ), + {"comment_id": comment.comment_id}, + ) diff --git a/server/szurubooru/tests/api/test_info.py b/server/szurubooru/tests/api/test_info.py index cd15727..37099e8 100644 --- a/server/szurubooru/tests/api/test_info.py +++ b/server/szurubooru/tests/api/test_info.py @@ -1,88 +1,96 @@ from datetime import datetime + from szurubooru import api, db, model def test_info_api( - tmpdir, config_injector, context_factory, post_factory, user_factory, - fake_datetime): - directory = tmpdir.mkdir('data') - directory.join('test.txt').write('abc') + tmpdir, + config_injector, + context_factory, + post_factory, + user_factory, + fake_datetime, +): + directory = tmpdir.mkdir("data") + directory.join("test.txt").write("abc") auth_user = user_factory(rank=model.User.RANK_REGULAR) anon_user = user_factory(rank=model.User.RANK_ANONYMOUS) - config_injector({ - 'name': 'test installation', - 'contact_email': 'test@example.com', - 'enable_safety': True, - 'data_dir': str(directory), - 'user_name_regex': '1', - 'password_regex': '2', - 'tag_name_regex': '3', - 'tag_category_name_regex': '4', - 'default_rank': '5', - 'privileges': { - 'test_key1': 'test_value1', - 'test_key2': 'test_value2', - 'posts:view:featured': 'regular', - }, - 'smtp': { - 'host': 'example.com', + config_injector( + { + "name": "test installation", + "contact_email": "test@example.com", + "enable_safety": True, + "data_dir": str(directory), + "user_name_regex": "1", + "password_regex": "2", + "tag_name_regex": "3", + "tag_category_name_regex": "4", + "default_rank": "5", + "privileges": { + "test_key1": "test_value1", + "test_key2": "test_value2", + "posts:view:featured": "regular", + }, + "smtp": { + "host": "example.com", + }, } - }) + ) db.session.add_all([post_factory(), post_factory()]) db.session.flush() expected_config_key = { - 'name': 'test installation', - 'contactEmail': 'test@example.com', - 'enableSafety': True, - 'userNameRegex': '1', - 'passwordRegex': '2', - 'tagNameRegex': '3', - 'tagCategoryNameRegex': '4', - 'defaultUserRank': '5', - 'privileges': { - 'testKey1': 'test_value1', - 'testKey2': 'test_value2', - 'posts:view:featured': 'regular', + "name": "test installation", + "contactEmail": "test@example.com", + "enableSafety": True, + "userNameRegex": "1", + "passwordRegex": "2", + "tagNameRegex": "3", + "tagCategoryNameRegex": "4", + "defaultUserRank": "5", + "privileges": { + "testKey1": "test_value1", + "testKey2": "test_value2", + "posts:view:featured": "regular", }, - 'canSendMails': True + "canSendMails": True, } - with fake_datetime('2016-01-01 13:00'): + with fake_datetime("2016-01-01 13:00"): assert api.info_api.get_info(context_factory(user=auth_user)) == { - 'postCount': 2, - 'diskUsage': 3, - 'featuredPost': None, - 'featuringTime': None, - 'featuringUser': None, - 'serverTime': datetime(2016, 1, 1, 13, 0), - 'config': expected_config_key, + "postCount": 2, + "diskUsage": 3, + "featuredPost": None, + "featuringTime": None, + "featuringUser": None, + "serverTime": datetime(2016, 1, 1, 13, 0), + "config": expected_config_key, } - directory.join('test2.txt').write('abc') - with fake_datetime('2016-01-03 12:59'): + directory.join("test2.txt").write("abc") + with fake_datetime("2016-01-03 12:59"): assert api.info_api.get_info(context_factory(user=auth_user)) == { - 'postCount': 2, - 'diskUsage': 3, # still 3 - it's cached - 'featuredPost': None, - 'featuringTime': None, - 'featuringUser': None, - 'serverTime': datetime(2016, 1, 3, 12, 59), - 'config': expected_config_key, + "postCount": 2, + "diskUsage": 3, # still 3 - it's cached + "featuredPost": None, + "featuringTime": None, + "featuringUser": None, + "serverTime": datetime(2016, 1, 3, 12, 59), + "config": expected_config_key, } - with fake_datetime('2016-01-03 13:01'): + with fake_datetime("2016-01-03 13:01"): assert api.info_api.get_info(context_factory(user=auth_user)) == { - 'postCount': 2, - 'diskUsage': 6, # cache expired - 'featuredPost': None, - 'featuringTime': None, - 'featuringUser': None, - 'serverTime': datetime(2016, 1, 3, 13, 1), - 'config': expected_config_key, + "postCount": 2, + "diskUsage": 6, # cache expired + "featuredPost": None, + "featuringTime": None, + "featuringUser": None, + "serverTime": datetime(2016, 1, 3, 13, 1), + "config": expected_config_key, } - with fake_datetime('2016-01-03 13:01'): + with fake_datetime("2016-01-03 13:01"): assert api.info_api.get_info(context_factory(user=anon_user)) == { - 'postCount': 2, - 'diskUsage': 6, # cache expired - 'serverTime': datetime(2016, 1, 3, 13, 1), - 'config': expected_config_key, + "postCount": 2, + "diskUsage": 6, # cache expired + "serverTime": datetime(2016, 1, 3, 13, 1), + "config": expected_config_key, } diff --git a/server/szurubooru/tests/api/test_password_reset.py b/server/szurubooru/tests/api/test_password_reset.py index e46dbbe..bf1ab5c 100644 --- a/server/szurubooru/tests/api/test_password_reset.py +++ b/server/szurubooru/tests/api/test_password_reset.py @@ -1,84 +1,114 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import auth, mailer @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'secret': 'x', - 'base_url': 'http://example.com/', - 'name': 'Test instance', - }) + config_injector( + { + "secret": "x", + "domain": "http://example.com", + "name": "Test instance", + "smtp": { + "from": "noreply@example.com", + }, + } + ) def test_reset_sending_email(context_factory, user_factory): - db.session.add(user_factory( - name='u1', rank=model.User.RANK_REGULAR, email='user@example.com')) + db.session.add( + user_factory( + name="u1", rank=model.User.RANK_REGULAR, email="user@example.com" + ) + ) db.session.flush() - for initiating_user in ['u1', 'user@example.com']: - with patch('szurubooru.func.mailer.send_mail'): - assert api.password_reset_api.start_password_reset( - context_factory(), {'user_name': initiating_user}) == {} + for initiating_user in ["u1", "user@example.com"]: + with patch("szurubooru.func.mailer.send_mail"): + assert ( + api.password_reset_api.start_password_reset( + context_factory(), {"user_name": initiating_user} + ) + == {} + ) mailer.send_mail.assert_called_once_with( - 'noreply@Test instance', - 'user@example.com', - 'Password reset for Test instance', - 'You (or someone else) requested to reset your password ' + - 'on Test instance.\nIf you wish to proceed, click this l' + - 'ink: http://example.com/password-reset/u1:4ac0be176fb36' + - '4f13ee6b634c43220e2\nOtherwise, please ignore this email.') + "noreply@example.com", + "user@example.com", + "Password reset for Test instance", + "You (or someone else) requested to reset your password " + + "on Test instance.\nIf you wish to proceed, click this l" + + "ink: http://example.com/password-reset/u1:4ac0be176fb36" + + "4f13ee6b634c43220e2\nOtherwise, please ignore this email.", + ) def test_trying_to_reset_non_existing(context_factory): with pytest.raises(errors.NotFoundError): api.password_reset_api.start_password_reset( - context_factory(), {'user_name': 'u1'}) + context_factory(), {"user_name": "u1"} + ) def test_trying_to_reset_without_email(context_factory, user_factory): db.session.add( - user_factory(name='u1', rank=model.User.RANK_REGULAR, email=None)) + user_factory(name="u1", rank=model.User.RANK_REGULAR, email=None) + ) db.session.flush() with pytest.raises(errors.ValidationError): api.password_reset_api.start_password_reset( - context_factory(), {'user_name': 'u1'}) + context_factory(), {"user_name": "u1"} + ) def test_confirming_with_good_token(context_factory, user_factory): user = user_factory( - name='u1', rank=model.User.RANK_REGULAR, email='user@example.com') + name="u1", rank=model.User.RANK_REGULAR, email="user@example.com" + ) old_hash = user.password_hash db.session.add(user) db.session.flush() context = context_factory( - params={'token': '4ac0be176fb364f13ee6b634c43220e2'}) + params={"token": "4ac0be176fb364f13ee6b634c43220e2"} + ) result = api.password_reset_api.finish_password_reset( - context, {'user_name': 'u1'}) + context, {"user_name": "u1"} + ) assert user.password_hash != old_hash - assert auth.is_valid_password(user, result['password']) is True + assert auth.is_valid_password(user, result["password"]) is True def test_trying_to_confirm_non_existing(context_factory): with pytest.raises(errors.NotFoundError): api.password_reset_api.finish_password_reset( - context_factory(), {'user_name': 'u1'}) + context_factory(), {"user_name": "u1"} + ) def test_trying_to_confirm_without_token(context_factory, user_factory): - db.session.add(user_factory( - name='u1', rank=model.User.RANK_REGULAR, email='user@example.com')) + db.session.add( + user_factory( + name="u1", rank=model.User.RANK_REGULAR, email="user@example.com" + ) + ) db.session.flush() with pytest.raises(errors.ValidationError): api.password_reset_api.finish_password_reset( - context_factory(params={}), {'user_name': 'u1'}) + context_factory(params={}), {"user_name": "u1"} + ) def test_trying_to_confirm_with_bad_token(context_factory, user_factory): - db.session.add(user_factory( - name='u1', rank=model.User.RANK_REGULAR, email='user@example.com')) + db.session.add( + user_factory( + name="u1", rank=model.User.RANK_REGULAR, email="user@example.com" + ) + ) db.session.flush() with pytest.raises(errors.ValidationError): api.password_reset_api.finish_password_reset( - context_factory(params={'token': 'bad'}), {'user_name': 'u1'}) + context_factory(params={"token": "bad"}), {"user_name": "u1"} + ) diff --git a/server/szurubooru/tests/api/test_pool_category_creating.py b/server/szurubooru/tests/api/test_pool_category_creating.py new file mode 100644 index 0000000..b9235d0 --- /dev/null +++ b/server/szurubooru/tests/api/test_pool_category_creating.py @@ -0,0 +1,73 @@ +from unittest.mock import patch + +import pytest + +from szurubooru import api, db, errors, model +from szurubooru.func import pool_categories, snapshots + + +def _update_category_name(category, name): + category.name = name + + +@pytest.fixture(autouse=True) +def inject_config(config_injector): + config_injector( + { + "privileges": {"pool_categories:create": model.User.RANK_REGULAR}, + } + ) + + +def test_creating_category( + pool_category_factory, user_factory, context_factory +): + auth_user = user_factory(rank=model.User.RANK_REGULAR) + category = pool_category_factory(name="meta") + db.session.add(category) + + with patch("szurubooru.func.pool_categories.create_category"), patch( + "szurubooru.func.pool_categories.serialize_category" + ), patch("szurubooru.func.pool_categories.update_category_name"), patch( + "szurubooru.func.snapshots.create" + ): + pool_categories.create_category.return_value = category + pool_categories.update_category_name.side_effect = ( + _update_category_name + ) + pool_categories.serialize_category.return_value = "serialized category" + result = api.pool_category_api.create_pool_category( + context_factory( + params={"name": "meta", "color": "black"}, user=auth_user + ) + ) + assert result == "serialized category" + pool_categories.create_category.assert_called_once_with( + "meta", "black" + ) + snapshots.create.assert_called_once_with(category, auth_user) + + +@pytest.mark.parametrize("field", ["name", "color"]) +def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): + params = { + "name": "meta", + "color": "black", + } + del params[field] + with pytest.raises(errors.ValidationError): + api.pool_category_api.create_pool_category( + context_factory( + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) + + +def test_trying_to_create_without_privileges(user_factory, context_factory): + with pytest.raises(errors.AuthError): + api.pool_category_api.create_pool_category( + context_factory( + params={"name": "meta", "color": "black"}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_pool_category_deleting.py b/server/szurubooru/tests/api/test_pool_category_deleting.py new file mode 100644 index 0000000..b50f961 --- /dev/null +++ b/server/szurubooru/tests/api/test_pool_category_deleting.py @@ -0,0 +1,91 @@ +from unittest.mock import patch + +import pytest + +from szurubooru import api, db, errors, model +from szurubooru.func import pool_categories, snapshots + + +@pytest.fixture(autouse=True) +def inject_config(config_injector): + config_injector( + { + "privileges": {"pool_categories:delete": model.User.RANK_REGULAR}, + } + ) + + +def test_deleting(user_factory, pool_category_factory, context_factory): + auth_user = user_factory(rank=model.User.RANK_REGULAR) + category = pool_category_factory(name="category") + db.session.add(pool_category_factory(name="root")) + db.session.add(category) + db.session.flush() + with patch("szurubooru.func.snapshots.delete"): + result = api.pool_category_api.delete_pool_category( + context_factory(params={"version": 1}, user=auth_user), + {"category_name": "category"}, + ) + assert result == {} + assert db.session.query(model.PoolCategory).count() == 1 + assert db.session.query(model.PoolCategory).one().name == "root" + snapshots.delete.assert_called_once_with(category, auth_user) + + +def test_trying_to_delete_used( + user_factory, pool_category_factory, pool_factory, context_factory +): + category = pool_category_factory(name="category") + db.session.add(category) + db.session.flush() + pool = pool_factory(names=["pool"], category=category) + db.session.add(pool) + db.session.commit() + with pytest.raises(pool_categories.PoolCategoryIsInUseError): + api.pool_category_api.delete_pool_category( + context_factory( + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "category"}, + ) + assert db.session.query(model.PoolCategory).count() == 1 + + +def test_trying_to_delete_last( + user_factory, pool_category_factory, context_factory +): + db.session.add(pool_category_factory(name="root")) + db.session.commit() + with pytest.raises(pool_categories.PoolCategoryIsInUseError): + api.pool_category_api.delete_pool_category( + context_factory( + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "root"}, + ) + + +def test_trying_to_delete_non_existing(user_factory, context_factory): + with pytest.raises(pool_categories.PoolCategoryNotFoundError): + api.pool_category_api.delete_pool_category( + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"category_name": "bad"}, + ) + + +def test_trying_to_delete_without_privileges( + user_factory, pool_category_factory, context_factory +): + db.session.add(pool_category_factory(name="category")) + db.session.commit() + with pytest.raises(errors.AuthError): + api.pool_category_api.delete_pool_category( + context_factory( + params={"version": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"category_name": "category"}, + ) + assert db.session.query(model.PoolCategory).count() == 1 diff --git a/server/szurubooru/tests/api/test_pool_category_retrieving.py b/server/szurubooru/tests/api/test_pool_category_retrieving.py new file mode 100644 index 0000000..7820567 --- /dev/null +++ b/server/szurubooru/tests/api/test_pool_category_retrieving.py @@ -0,0 +1,68 @@ +import pytest + +from szurubooru import api, db, errors, model +from szurubooru.func import pool_categories + + +@pytest.fixture(autouse=True) +def inject_config(config_injector): + config_injector( + { + "privileges": { + "pool_categories:list": model.User.RANK_REGULAR, + "pool_categories:view": model.User.RANK_REGULAR, + }, + } + ) + + +def test_retrieving_multiple( + user_factory, pool_category_factory, context_factory +): + db.session.add_all( + [ + pool_category_factory(name="c1"), + pool_category_factory(name="c2"), + ] + ) + db.session.flush() + result = api.pool_category_api.get_pool_categories( + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)) + ) + assert [cat["name"] for cat in result["results"]] == ["c1", "c2"] + + +def test_retrieving_single( + user_factory, pool_category_factory, context_factory +): + db.session.add(pool_category_factory(name="cat")) + db.session.flush() + result = api.pool_category_api.get_pool_category( + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"category_name": "cat"}, + ) + assert result == { + "name": "cat", + "color": "dummy", + "usages": 0, + "default": False, + "version": 1, + } + + +def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): + with pytest.raises(pool_categories.PoolCategoryNotFoundError): + api.pool_category_api.get_pool_category( + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"category_name": "-"}, + ) + + +def test_trying_to_retrieve_single_without_privileges( + user_factory, context_factory +): + with pytest.raises(errors.AuthError): + api.pool_category_api.get_pool_category( + context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), + {"category_name": "-"}, + ) diff --git a/server/szurubooru/tests/api/test_pool_category_updating.py b/server/szurubooru/tests/api/test_pool_category_updating.py new file mode 100644 index 0000000..5e26209 --- /dev/null +++ b/server/szurubooru/tests/api/test_pool_category_updating.py @@ -0,0 +1,136 @@ +from unittest.mock import patch + +import pytest + +from szurubooru import api, db, errors, model +from szurubooru.func import pool_categories, snapshots + + +def _update_category_name(category, name): + category.name = name + + +@pytest.fixture(autouse=True) +def inject_config(config_injector): + config_injector( + { + "privileges": { + "pool_categories:edit:name": model.User.RANK_REGULAR, + "pool_categories:edit:color": model.User.RANK_REGULAR, + "pool_categories:set_default": model.User.RANK_REGULAR, + }, + } + ) + + +def test_simple_updating(user_factory, pool_category_factory, context_factory): + auth_user = user_factory(rank=model.User.RANK_REGULAR) + category = pool_category_factory(name="name", color="black") + db.session.add(category) + db.session.flush() + with patch("szurubooru.func.pool_categories.serialize_category"), patch( + "szurubooru.func.pool_categories.update_category_name" + ), patch("szurubooru.func.pool_categories.update_category_color"), patch( + "szurubooru.func.snapshots.modify" + ): + pool_categories.update_category_name.side_effect = ( + _update_category_name + ) + pool_categories.serialize_category.return_value = "serialized category" + result = api.pool_category_api.update_pool_category( + context_factory( + params={"name": "changed", "color": "white", "version": 1}, + user=auth_user, + ), + {"category_name": "name"}, + ) + assert result == "serialized category" + pool_categories.update_category_name.assert_called_once_with( + category, "changed" + ) + pool_categories.update_category_color.assert_called_once_with( + category, "white" + ) + snapshots.modify.assert_called_once_with(category, auth_user) + + +@pytest.mark.parametrize("field", ["name", "color"]) +def test_omitting_optional_field( + user_factory, pool_category_factory, context_factory, field +): + db.session.add(pool_category_factory(name="name", color="black")) + db.session.commit() + params = { + "name": "changed", + "color": "white", + } + del params[field] + with patch("szurubooru.func.pool_categories.serialize_category"), patch( + "szurubooru.func.pool_categories.update_category_name" + ), patch("szurubooru.func.snapshots._post_to_webhooks"): + api.pool_category_api.update_pool_category( + context_factory( + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "name"}, + ) + + +def test_trying_to_update_non_existing(user_factory, context_factory): + with pytest.raises(pool_categories.PoolCategoryNotFoundError): + api.pool_category_api.update_pool_category( + context_factory( + params={"name": ["dummy"]}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "bad"}, + ) + + +@pytest.mark.parametrize( + "params", + [ + {"name": "whatever"}, + {"color": "whatever"}, + ], +) +def test_trying_to_update_without_privileges( + user_factory, pool_category_factory, context_factory, params +): + db.session.add(pool_category_factory(name="dummy")) + db.session.commit() + with pytest.raises(errors.AuthError): + api.pool_category_api.update_pool_category( + context_factory( + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"category_name": "dummy"}, + ) + + +def test_set_as_default(user_factory, pool_category_factory, context_factory): + category = pool_category_factory(name="name", color="black") + db.session.add(category) + db.session.commit() + with patch("szurubooru.func.pool_categories.serialize_category"), patch( + "szurubooru.func.pool_categories.set_default_category" + ): + pool_categories.update_category_name.side_effect = ( + _update_category_name + ) + pool_categories.serialize_category.return_value = "serialized category" + result = api.pool_category_api.set_pool_category_as_default( + context_factory( + params={ + "name": "changed", + "color": "white", + "version": 1, + }, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "name"}, + ) + assert result == "serialized category" + pool_categories.set_default_category.assert_called_once_with(category) diff --git a/server/szurubooru/tests/api/test_pool_creating.py b/server/szurubooru/tests/api/test_pool_creating.py new file mode 100644 index 0000000..4fd8939 --- /dev/null +++ b/server/szurubooru/tests/api/test_pool_creating.py @@ -0,0 +1,95 @@ +from unittest.mock import patch + +import pytest + +from szurubooru import api, errors, model +from szurubooru.func import pools, posts, snapshots + + +@pytest.fixture(autouse=True) +def inject_config(config_injector): + config_injector({"privileges": {"pools:create": model.User.RANK_REGULAR}}) + + +def test_creating_simple_pools(pool_factory, user_factory, context_factory): + auth_user = user_factory(rank=model.User.RANK_REGULAR) + pool = pool_factory() + with patch("szurubooru.func.pools.create_pool"), patch( + "szurubooru.func.pools.get_or_create_pools_by_names" + ), patch("szurubooru.func.pools.serialize_pool"), patch( + "szurubooru.func.snapshots.create" + ): + posts.get_posts_by_ids.return_value = ([], []) + pools.create_pool.return_value = pool + pools.serialize_pool.return_value = "serialized pool" + result = api.pool_api.create_pool( + context_factory( + params={ + "names": ["pool1", "pool2"], + "category": "default", + "description": "desc", + "posts": [1, 2], + }, + user=auth_user, + ) + ) + assert result == "serialized pool" + pools.create_pool.assert_called_once_with( + ["pool1", "pool2"], "default", [1, 2] + ) + snapshots.create.assert_called_once_with(pool, auth_user) + + +@pytest.mark.parametrize("field", ["names", "category"]) +def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): + params = { + "names": ["pool1", "pool2"], + "category": "default", + "description": "desc", + "posts": [], + } + del params[field] + with pytest.raises(errors.ValidationError): + api.pool_api.create_pool( + context_factory( + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) + + +@pytest.mark.parametrize("field", ["description", "posts"]) +def test_omitting_optional_field( + pool_factory, user_factory, context_factory, field +): + params = { + "names": ["pool1", "pool2"], + "category": "default", + "description": "desc", + "posts": [], + } + del params[field] + with patch("szurubooru.func.pools.create_pool"), patch( + "szurubooru.func.pools.serialize_pool" + ), patch("szurubooru.func.snapshots._post_to_webhooks"): + pools.create_pool.return_value = pool_factory() + api.pool_api.create_pool( + context_factory( + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) + + +def test_trying_to_create_pool_without_privileges( + user_factory, context_factory +): + with pytest.raises(errors.AuthError): + api.pool_api.create_pool( + context_factory( + params={ + "names": ["pool"], + "category": "default", + "posts": [], + }, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_pool_deleting.py b/server/szurubooru/tests/api/test_pool_deleting.py new file mode 100644 index 0000000..5c5adcc --- /dev/null +++ b/server/szurubooru/tests/api/test_pool_deleting.py @@ -0,0 +1,72 @@ +from unittest.mock import patch + +import pytest + +from szurubooru import api, db, errors, model +from szurubooru.func import pools, snapshots + + +@pytest.fixture(autouse=True) +def inject_config(config_injector): + config_injector({"privileges": {"pools:delete": model.User.RANK_REGULAR}}) + + +def test_deleting(user_factory, pool_factory, context_factory): + auth_user = user_factory(rank=model.User.RANK_REGULAR) + pool = pool_factory(id=1) + db.session.add(pool) + db.session.commit() + with patch("szurubooru.func.snapshots.delete"): + result = api.pool_api.delete_pool( + context_factory(params={"version": 1}, user=auth_user), + {"pool_id": 1}, + ) + assert result == {} + assert db.session.query(model.Pool).count() == 0 + snapshots.delete.assert_called_once_with(pool, auth_user) + + +def test_deleting_used( + user_factory, pool_factory, context_factory, post_factory +): + pool = pool_factory(id=1) + post = post_factory(id=1) + pool.posts.append(post) + db.session.add_all([pool, post]) + db.session.commit() + with patch("szurubooru.func.snapshots._post_to_webhooks"): + api.pool_api.delete_pool( + context_factory( + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"pool_id": 1}, + ) + db.session.refresh(post) + assert db.session.query(model.Pool).count() == 0 + assert db.session.query(model.PoolPost).count() == 0 + assert post.pools == [] + + +def test_trying_to_delete_non_existing(user_factory, context_factory): + with pytest.raises(pools.PoolNotFoundError): + api.pool_api.delete_pool( + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"pool_id": 9999}, + ) + + +def test_trying_to_delete_without_privileges( + user_factory, pool_factory, context_factory +): + db.session.add(pool_factory(id=1)) + db.session.commit() + with pytest.raises(errors.AuthError): + api.pool_api.delete_pool( + context_factory( + params={"version": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"pool_id": 1}, + ) + assert db.session.query(model.Pool).count() == 1 diff --git a/server/szurubooru/tests/api/test_pool_merging.py b/server/szurubooru/tests/api/test_pool_merging.py new file mode 100644 index 0000000..3357780 --- /dev/null +++ b/server/szurubooru/tests/api/test_pool_merging.py @@ -0,0 +1,118 @@ +from unittest.mock import patch + +import pytest + +from szurubooru import api, db, errors, model +from szurubooru.func import pools, snapshots + + +@pytest.fixture(autouse=True) +def inject_config(config_injector): + config_injector({"privileges": {"pools:merge": model.User.RANK_REGULAR}}) + + +def test_merging(user_factory, pool_factory, context_factory, post_factory): + auth_user = user_factory(rank=model.User.RANK_REGULAR) + source_pool = pool_factory(id=1) + target_pool = pool_factory(id=2) + db.session.add_all([source_pool, target_pool]) + db.session.flush() + assert source_pool.post_count == 0 + assert target_pool.post_count == 0 + post = post_factory(id=1) + source_pool.posts = [post] + db.session.add(post) + db.session.commit() + assert source_pool.post_count == 1 + assert target_pool.post_count == 0 + with patch("szurubooru.func.pools.serialize_pool"), patch( + "szurubooru.func.pools.merge_pools" + ), patch("szurubooru.func.snapshots.merge"): + api.pool_api.merge_pools( + context_factory( + params={ + "removeVersion": 1, + "mergeToVersion": 1, + "remove": 1, + "mergeTo": 2, + }, + user=auth_user, + ) + ) + pools.merge_pools.called_once_with(source_pool, target_pool) + snapshots.merge.assert_called_once_with( + source_pool, target_pool, auth_user + ) + + +@pytest.mark.parametrize( + "field", ["remove", "mergeTo", "removeVersion", "mergeToVersion"] +) +def test_trying_to_omit_mandatory_field( + user_factory, pool_factory, context_factory, field +): + db.session.add_all( + [ + pool_factory(id=1), + pool_factory(id=2), + ] + ) + db.session.commit() + params = { + "removeVersion": 1, + "mergeToVersion": 1, + "remove": 1, + "mergeTo": 2, + } + del params[field] + with pytest.raises(errors.ValidationError): + api.pool_api.merge_pools( + context_factory( + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) + + +def test_trying_to_merge_non_existing( + user_factory, pool_factory, context_factory +): + db.session.add(pool_factory(id=1)) + db.session.commit() + with pytest.raises(pools.PoolNotFoundError): + api.pool_api.merge_pools( + context_factory( + params={"remove": 1, "mergeTo": 9999}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) + with pytest.raises(pools.PoolNotFoundError): + api.pool_api.merge_pools( + context_factory( + params={"remove": 9999, "mergeTo": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) + + +def test_trying_to_merge_without_privileges( + user_factory, pool_factory, context_factory +): + db.session.add_all( + [ + pool_factory(id=1), + pool_factory(id=2), + ] + ) + db.session.commit() + with pytest.raises(errors.AuthError): + api.pool_api.merge_pools( + context_factory( + params={ + "removeVersion": 1, + "mergeToVersion": 1, + "remove": 1, + "mergeTo": 2, + }, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_pool_retrieving.py b/server/szurubooru/tests/api/test_pool_retrieving.py new file mode 100644 index 0000000..688dfa7 --- /dev/null +++ b/server/szurubooru/tests/api/test_pool_retrieving.py @@ -0,0 +1,82 @@ +from unittest.mock import patch + +import pytest + +from szurubooru import api, db, errors, model +from szurubooru.func import pools + + +@pytest.fixture(autouse=True) +def inject_config(config_injector): + config_injector( + { + "privileges": { + "pools:list": model.User.RANK_REGULAR, + "pools:view": model.User.RANK_REGULAR, + }, + } + ) + + +def test_retrieving_multiple(user_factory, pool_factory, context_factory): + pool1 = pool_factory(id=1) + pool2 = pool_factory(id=2) + db.session.add_all([pool2, pool1]) + db.session.flush() + with patch("szurubooru.func.pools.serialize_pool"): + pools.serialize_pool.return_value = "serialized pool" + result = api.pool_api.get_pools( + context_factory( + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) + assert result == { + "query": "", + "offset": 0, + "limit": 100, + "total": 2, + "results": ["serialized pool", "serialized pool"], + } + + +def test_trying_to_retrieve_multiple_without_privileges( + user_factory, context_factory +): + with pytest.raises(errors.AuthError): + api.pool_api.get_pools( + context_factory( + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) + + +def test_retrieving_single(user_factory, pool_factory, context_factory): + db.session.add(pool_factory(id=1)) + db.session.flush() + with patch("szurubooru.func.pools.serialize_pool"): + pools.serialize_pool.return_value = "serialized pool" + result = api.pool_api.get_pool( + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"pool_id": 1}, + ) + assert result == "serialized pool" + + +def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): + with pytest.raises(pools.PoolNotFoundError): + api.pool_api.get_pool( + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"pool_id": 1}, + ) + + +def test_trying_to_retrieve_single_without_privileges( + user_factory, context_factory +): + with pytest.raises(errors.AuthError): + api.pool_api.get_pool( + context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), + {"pool_id": 1}, + ) diff --git a/server/szurubooru/tests/api/test_pool_updating.py b/server/szurubooru/tests/api/test_pool_updating.py new file mode 100644 index 0000000..507289f --- /dev/null +++ b/server/szurubooru/tests/api/test_pool_updating.py @@ -0,0 +1,160 @@ +from unittest.mock import patch + +import pytest + +from szurubooru import api, db, errors, model +from szurubooru.func import pools, posts, snapshots + + +@pytest.fixture(autouse=True) +def inject_config(config_injector): + config_injector( + { + "privileges": { + "pools:create": model.User.RANK_REGULAR, + "pools:edit:names": model.User.RANK_REGULAR, + "pools:edit:category": model.User.RANK_REGULAR, + "pools:edit:description": model.User.RANK_REGULAR, + "pools:edit:posts": model.User.RANK_REGULAR, + }, + } + ) + + +def test_simple_updating(user_factory, pool_factory, context_factory): + auth_user = user_factory(rank=model.User.RANK_REGULAR) + pool = pool_factory(id=1, names=["pool1", "pool2"]) + db.session.add(pool) + db.session.commit() + with patch("szurubooru.func.pools.create_pool"), patch( + "szurubooru.func.posts.get_posts_by_ids" + ), patch("szurubooru.func.pools.update_pool_names"), patch( + "szurubooru.func.pools.update_pool_category_name" + ), patch( + "szurubooru.func.pools.update_pool_description" + ), patch( + "szurubooru.func.pools.update_pool_posts" + ), patch( + "szurubooru.func.pools.serialize_pool" + ), patch( + "szurubooru.func.snapshots.modify" + ): + posts.get_posts_by_ids.return_value = ([], []) + pools.serialize_pool.return_value = "serialized pool" + result = api.pool_api.update_pool( + context_factory( + params={ + "version": 1, + "names": ["pool3"], + "category": "series", + "description": "desc", + "posts": [1, 2], + }, + user=auth_user, + ), + {"pool_id": 1}, + ) + assert result == "serialized pool" + pools.create_pool.assert_not_called() + pools.update_pool_names.assert_called_once_with(pool, ["pool3"]) + pools.update_pool_category_name.assert_called_once_with(pool, "series") + pools.update_pool_description.assert_called_once_with(pool, "desc") + pools.update_pool_posts.assert_called_once_with(pool, [1, 2]) + pools.serialize_pool.assert_called_once_with(pool, options=[]) + snapshots.modify.assert_called_once_with(pool, auth_user) + + +@pytest.mark.parametrize( + "field", + [ + "names", + "category", + "description", + "posts", + ], +) +def test_omitting_optional_field( + user_factory, pool_factory, context_factory, field +): + db.session.add(pool_factory(id=1)) + db.session.commit() + params = { + "names": ["pool1", "pool2"], + "category": "default", + "description": "desc", + "posts": [], + } + del params[field] + with patch("szurubooru.func.pools.create_pool"), patch( + "szurubooru.func.pools.update_pool_names" + ), patch("szurubooru.func.pools.update_pool_category_name"), patch( + "szurubooru.func.pools.serialize_pool" + ): + api.pool_api.update_pool( + context_factory( + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"pool_id": 1}, + ) + + +def test_trying_to_update_non_existing(user_factory, context_factory): + with pytest.raises(pools.PoolNotFoundError): + api.pool_api.update_pool( + context_factory( + params={"names": ["dummy"]}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"pool_id": 9999}, + ) + + +@pytest.mark.parametrize( + "params", + [ + {"names": ["whatever"]}, + {"category": "whatever"}, + {"posts": [1]}, + ], +) +def test_trying_to_update_without_privileges( + user_factory, pool_factory, context_factory, params +): + db.session.add(pool_factory(id=1)) + db.session.commit() + with pytest.raises(errors.AuthError): + api.pool_api.update_pool( + context_factory( + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"pool_id": 1}, + ) + + +def test_trying_to_create_pools_without_privileges( + config_injector, context_factory, pool_factory, user_factory +): + pool = pool_factory(id=1) + db.session.add(pool) + db.session.commit() + config_injector( + { + "privileges": { + "pools:create": model.User.RANK_ADMINISTRATOR, + "pools:edit:posts": model.User.RANK_REGULAR, + }, + "delete_source_files": False, + } + ) + with patch("szurubooru.func.posts.get_posts_by_ids"): + posts.get_posts_by_ids.return_value = ([], ["new-post"]) + with pytest.raises(errors.AuthError): + api.pool_api.create_pool( + context_factory( + params={"posts": [1, 2], "version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"pool_id": 1}, + ) diff --git a/server/szurubooru/tests/api/test_post_creating.py b/server/szurubooru/tests/api/test_post_creating.py index 6edcdd3..a1ad4de 100644 --- a/server/szurubooru/tests/api/test_post_creating.py +++ b/server/szurubooru/tests/api/test_post_creating.py @@ -1,65 +1,82 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import posts, tags, snapshots, net + +from szurubooru import api, db, errors, model +from szurubooru.func import net, posts, snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'posts:create:anonymous': model.User.RANK_REGULAR, - 'posts:create:identified': model.User.RANK_REGULAR, - 'tags:create': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "posts:create:anonymous": model.User.RANK_REGULAR, + "posts:create:identified": model.User.RANK_REGULAR, + "tags:create": model.User.RANK_REGULAR, + "uploads:use_downloader": model.User.RANK_REGULAR, + }, + "allow_broken_uploads": False, + } + ) -def test_creating_minimal_posts( - context_factory, post_factory, user_factory): +def test_creating_minimal_posts(context_factory, post_factory, user_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_safety'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.posts.update_post_relations'), \ - patch('szurubooru.func.posts.update_post_notes'), \ - patch('szurubooru.func.posts.update_post_flags'), \ - patch('szurubooru.func.posts.update_post_thumbnail'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_safety" + ), patch("szurubooru.func.posts.update_post_source"), patch( + "szurubooru.func.posts.update_post_relations" + ), patch( + "szurubooru.func.posts.update_post_notes" + ), patch( + "szurubooru.func.posts.update_post_flags" + ), patch( + "szurubooru.func.posts.update_post_thumbnail" + ), patch( + "szurubooru.func.posts.serialize_post" + ), patch( + "szurubooru.func.snapshots.create" + ): posts.create_post.return_value = (post, []) - posts.serialize_post.return_value = 'serialized post' + posts.serialize_post.return_value = "serialized post" result = api.post_api.create_post( context_factory( params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], + "safety": "safe", + "tags": ["tag1", "tag2"], }, files={ - 'content': 'post-content', - 'thumbnail': 'post-thumbnail', + "content": "post-content", + "thumbnail": "post-thumbnail", }, - user=auth_user)) + user=auth_user, + ) + ) - assert result == 'serialized post' + assert result == "serialized post" posts.create_post.assert_called_once_with( - 'post-content', ['tag1', 'tag2'], auth_user) + "post-content", ["tag1", "tag2"], auth_user + ) posts.update_post_thumbnail.assert_called_once_with( - post, 'post-thumbnail') - posts.update_post_safety.assert_called_once_with(post, 'safe') - posts.update_post_source.assert_called_once_with(post, '') + post, "post-thumbnail" + ) + posts.update_post_safety.assert_called_once_with(post, "safe") + posts.update_post_source.assert_called_once_with(post, "") posts.update_post_relations.assert_called_once_with(post, []) posts.update_post_notes.assert_called_once_with(post, []) posts.update_post_flags.assert_called_once_with(post, []) posts.update_post_thumbnail.assert_called_once_with( - post, 'post-thumbnail') + post, "post-thumbnail" + ) posts.serialize_post.assert_called_once_with( - post, auth_user, options=[]) + post, auth_user, options=[] + ) snapshots.create.assert_called_once_with(post, auth_user) @@ -69,232 +86,302 @@ def test_creating_full_posts(context_factory, post_factory, user_factory): db.session.add(post) db.session.flush() - with patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_safety'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.posts.update_post_relations'), \ - patch('szurubooru.func.posts.update_post_notes'), \ - patch('szurubooru.func.posts.update_post_flags'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_safety" + ), patch("szurubooru.func.posts.update_post_source"), patch( + "szurubooru.func.posts.update_post_relations" + ), patch( + "szurubooru.func.posts.update_post_notes" + ), patch( + "szurubooru.func.posts.update_post_flags" + ), patch( + "szurubooru.func.posts.serialize_post" + ), patch( + "szurubooru.func.snapshots.create" + ): posts.create_post.return_value = (post, []) - posts.serialize_post.return_value = 'serialized post' + posts.serialize_post.return_value = "serialized post" result = api.post_api.create_post( context_factory( params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'relations': [1, 2], - 'source': 'source', - 'notes': ['note1', 'note2'], - 'flags': ['flag1', 'flag2'], + "safety": "safe", + "tags": ["tag1", "tag2"], + "relations": [1, 2], + "source": "source", + "notes": ["note1", "note2"], + "flags": ["flag1", "flag2"], }, files={ - 'content': 'post-content', + "content": "post-content", }, - user=auth_user)) + user=auth_user, + ) + ) - assert result == 'serialized post' + assert result == "serialized post" posts.create_post.assert_called_once_with( - 'post-content', ['tag1', 'tag2'], auth_user) - posts.update_post_safety.assert_called_once_with(post, 'safe') - posts.update_post_source.assert_called_once_with(post, 'source') + "post-content", ["tag1", "tag2"], auth_user + ) + posts.update_post_safety.assert_called_once_with(post, "safe") + posts.update_post_source.assert_called_once_with(post, "source") posts.update_post_relations.assert_called_once_with(post, [1, 2]) posts.update_post_notes.assert_called_once_with( - post, ['note1', 'note2']) + post, ["note1", "note2"] + ) posts.update_post_flags.assert_called_once_with( - post, ['flag1', 'flag2']) + post, ["flag1", "flag2"] + ) posts.serialize_post.assert_called_once_with( - post, auth_user, options=[]) + post, auth_user, options=[] + ) snapshots.create.assert_called_once_with(post, auth_user) def test_anonymous_uploads( - config_injector, context_factory, post_factory, user_factory): + config_injector, context_factory, post_factory, user_factory +): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_source'): - config_injector({ - 'privileges': {'posts:create:anonymous': model.User.RANK_REGULAR}, - }) + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.posts.create_post" + ), patch("szurubooru.func.posts.update_post_source"), patch( + "szurubooru.func.snapshots._post_to_webhooks" + ): + config_injector( + { + "privileges": { + "posts:create:anonymous": model.User.RANK_REGULAR, + "uploads:use_downloader": model.User.RANK_POWER, + }, + } + ) posts.create_post.return_value = [post, []] api.post_api.create_post( context_factory( params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'anonymous': 'True', + "safety": "safe", + "tags": ["tag1", "tag2"], + "anonymous": "True", }, files={ - 'content': 'post-content', + "content": "post-content", }, - user=auth_user)) + user=auth_user, + ) + ) posts.create_post.assert_called_once_with( - 'post-content', ['tag1', 'tag2'], None) + "post-content", ["tag1", "tag2"], None + ) def test_creating_from_url_saves_source( - config_injector, context_factory, post_factory, user_factory): + config_injector, context_factory, post_factory, user_factory +): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.net.download'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_source'): - config_injector({ - 'privileges': {'posts:create:identified': model.User.RANK_REGULAR}, - }) - net.download.return_value = b'content' + with patch("szurubooru.func.net.download"), patch( + "szurubooru.func.posts.serialize_post" + ), patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_source" + ), patch( + "szurubooru.func.snapshots._post_to_webhooks" + ): + config_injector( + { + "privileges": { + "posts:create:identified": model.User.RANK_REGULAR, + "uploads:use_downloader": model.User.RANK_POWER, + }, + } + ) + net.download.return_value = b"content" posts.create_post.return_value = [post, []] api.post_api.create_post( context_factory( params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'contentUrl': 'example.com', + "safety": "safe", + "tags": ["tag1", "tag2"], + "contentUrl": "example.com", }, - user=auth_user)) - net.download.assert_called_once_with('example.com') + user=auth_user, + ) + ) + net.download.assert_called_once_with( + "example.com", use_video_downloader=False + ) posts.create_post.assert_called_once_with( - b'content', ['tag1', 'tag2'], auth_user) - posts.update_post_source.assert_called_once_with(post, 'example.com') + b"content", ["tag1", "tag2"], auth_user + ) + posts.update_post_source.assert_called_once_with(post, "example.com") def test_creating_from_url_with_source_specified( - config_injector, context_factory, post_factory, user_factory): + config_injector, context_factory, post_factory, user_factory +): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.net.download'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_source'): - config_injector({ - 'privileges': {'posts:create:identified': model.User.RANK_REGULAR}, - }) - net.download.return_value = b'content' + with patch("szurubooru.func.net.download"), patch( + "szurubooru.func.posts.serialize_post" + ), patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_source" + ), patch( + "szurubooru.func.snapshots._post_to_webhooks" + ): + config_injector( + { + "privileges": { + "posts:create:identified": model.User.RANK_REGULAR, + "uploads:use_downloader": model.User.RANK_REGULAR, + }, + } + ) + net.download.return_value = b"content" posts.create_post.return_value = [post, []] api.post_api.create_post( context_factory( params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'contentUrl': 'example.com', - 'source': 'example2.com', + "safety": "safe", + "tags": ["tag1", "tag2"], + "contentUrl": "example.com", + "source": "example2.com", }, - user=auth_user)) - net.download.assert_called_once_with('example.com') + user=auth_user, + ) + ) + net.download.assert_called_once_with( + "example.com", use_video_downloader=True + ) posts.create_post.assert_called_once_with( - b'content', ['tag1', 'tag2'], auth_user) - posts.update_post_source.assert_called_once_with(post, 'example2.com') + b"content", ["tag1", "tag2"], auth_user + ) + posts.update_post_source.assert_called_once_with(post, "example2.com") -@pytest.mark.parametrize('field', ['safety']) +@pytest.mark.parametrize("field", ["safety"]) def test_trying_to_omit_mandatory_field(context_factory, user_factory, field): params = { - 'safety': 'safe', + "safety": "safe", } del params[field] with pytest.raises(errors.MissingRequiredParameterError): api.post_api.create_post( context_factory( params=params, - files={'content': '...'}, - user=user_factory(rank=model.User.RANK_REGULAR))) + files={"content": "..."}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) @pytest.mark.parametrize( - 'field', ['tags', 'relations', 'source', 'notes', 'flags']) + "field", ["tags", "relations", "source", "notes", "flags"] +) def test_omitting_optional_field( - field, context_factory, post_factory, user_factory): + field, context_factory, post_factory, user_factory +): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() params = { - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'relations': [1, 2], - 'source': 'source', - 'notes': ['note1', 'note2'], - 'flags': ['flag1', 'flag2'], + "safety": "safe", + "tags": ["tag1", "tag2"], + "relations": [1, 2], + "source": "source", + "notes": ["note1", "note2"], + "flags": ["flag1", "flag2"], } del params[field] - with patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_safety'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.posts.update_post_relations'), \ - patch('szurubooru.func.posts.update_post_notes'), \ - patch('szurubooru.func.posts.update_post_flags'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_safety" + ), patch("szurubooru.func.posts.update_post_source"), patch( + "szurubooru.func.posts.update_post_relations" + ), patch( + "szurubooru.func.posts.update_post_notes" + ), patch( + "szurubooru.func.posts.update_post_flags" + ), patch( + "szurubooru.func.posts.serialize_post" + ), patch( + "szurubooru.func.snapshots.create" + ): posts.create_post.return_value = (post, []) - posts.serialize_post.return_value = 'serialized post' + posts.serialize_post.return_value = "serialized post" result = api.post_api.create_post( context_factory( params=params, - files={'content': 'post-content'}, - user=auth_user)) - assert result == 'serialized post' + files={"content": "post-content"}, + user=auth_user, + ) + ) + assert result == "serialized post" def test_errors_not_spending_ids( - config_injector, tmpdir, context_factory, read_asset, user_factory, - skip_post_hashing): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'data_url': 'example.com', - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'privileges': { - 'posts:create:identified': model.User.RANK_REGULAR, - }, - 'secret': 'test', - }) + config_injector, tmpdir, context_factory, read_asset, user_factory +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "data_url": "example.com", + "thumbnails": { + "post_width": 300, + "post_height": 300, + }, + "privileges": { + "posts:create:identified": model.User.RANK_REGULAR, + "uploads:use_downloader": model.User.RANK_POWER, + }, + "secret": "test", + } + ) auth_user = user_factory(rank=model.User.RANK_REGULAR) # successful request - with patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.update_post_tags'): + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.posts.update_post_tags" + ), patch("szurubooru.func.snapshots._post_to_webhooks"): posts.serialize_post.side_effect = lambda post, *_, **__: post.post_id post1_id = api.post_api.create_post( context_factory( - params={'safety': 'safe', 'tags': []}, - files={'content': read_asset('png.png')}, - user=auth_user)) - db.session.commit() + params={"safety": "safe", "tags": []}, + files={"content": read_asset("png.png")}, + user=auth_user, + ) + ) # erroreous request (duplicate post) with pytest.raises(posts.PostAlreadyUploadedError): api.post_api.create_post( context_factory( - params={'safety': 'safe', 'tags': []}, - files={'content': read_asset('png.png')}, - user=auth_user)) - db.session.rollback() + params={"safety": "safe", "tags": []}, + files={"content": read_asset("png.png")}, + user=auth_user, + ) + ) # successful request - with patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.update_post_tags'): + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.posts.update_post_tags" + ), patch("szurubooru.func.snapshots._post_to_webhooks"): posts.serialize_post.side_effect = lambda post, *_, **__: post.post_id post2_id = api.post_api.create_post( context_factory( - params={'safety': 'safe', 'tags': []}, - files={'content': read_asset('jpeg.jpg')}, - user=auth_user)) + params={"safety": "safe", "tags": []}, + files={"content": read_asset("jpeg.jpg")}, + user=auth_user, + ) + ) assert post1_id > 0 assert post2_id > 0 @@ -306,40 +393,52 @@ def test_trying_to_omit_content(context_factory, user_factory): api.post_api.create_post( context_factory( params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], + "safety": "safe", + "tags": ["tag1", "tag2"], }, - user=user_factory(rank=model.User.RANK_REGULAR))) + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) def test_trying_to_create_post_without_privileges( - context_factory, user_factory): + context_factory, user_factory +): with pytest.raises(errors.AuthError): - api.post_api.create_post(context_factory( - params='whatever', - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + api.post_api.create_post( + context_factory( + params="whatever", + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_trying_to_create_tags_without_privileges( - config_injector, context_factory, user_factory): - config_injector({ - 'privileges': { - 'posts:create:anonymous': model.User.RANK_REGULAR, - 'posts:create:identified': model.User.RANK_REGULAR, - 'tags:create': model.User.RANK_ADMINISTRATOR, - }, - }) - with pytest.raises(errors.AuthError), \ - patch('szurubooru.func.posts.update_post_content'), \ - patch('szurubooru.func.posts.update_post_tags'): - posts.update_post_tags.return_value = ['new-tag'] + config_injector, context_factory, user_factory +): + config_injector( + { + "privileges": { + "posts:create:anonymous": model.User.RANK_REGULAR, + "posts:create:identified": model.User.RANK_REGULAR, + "tags:create": model.User.RANK_ADMINISTRATOR, + "uploads:use_downloader": model.User.RANK_POWER, + }, + } + ) + with pytest.raises(errors.AuthError), patch( + "szurubooru.func.posts.update_post_content" + ), patch("szurubooru.func.posts.update_post_tags"): + posts.update_post_tags.return_value = ["new-tag"] api.post_api.create_post( context_factory( params={ - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], + "safety": "safe", + "tags": ["tag1", "tag2"], }, files={ - 'content': posts.EMPTY_PIXEL, + "content": posts.EMPTY_PIXEL, }, - user=user_factory(rank=model.User.RANK_REGULAR))) + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) diff --git a/server/szurubooru/tests/api/test_post_deleting.py b/server/szurubooru/tests/api/test_post_deleting.py index bb5f9ce..fd2cb75 100644 --- a/server/szurubooru/tests/api/test_post_deleting.py +++ b/server/szurubooru/tests/api/test_post_deleting.py @@ -1,19 +1,21 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts, snapshots @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'secret': 'secret', - 'data_dir': '', - 'delete_source_files': False, - 'privileges': { - 'posts:delete': model.User.RANK_REGULAR + config_injector( + { + "secret": "secret", + "data_dir": "", + "delete_source_files": False, + "privileges": {"posts:delete": model.User.RANK_REGULAR}, } - }) + ) def test_deleting(user_factory, post_factory, context_factory): @@ -21,10 +23,11 @@ def test_deleting(user_factory, post_factory, context_factory): post = post_factory(id=1) db.session.add(post) db.session.flush() - with patch('szurubooru.func.snapshots.delete'): + with patch("szurubooru.func.snapshots.delete"): result = api.post_api.delete_post( - context_factory(params={'version': 1}, user=auth_user), - {'post_id': 1}) + context_factory(params={"version": 1}, user=auth_user), + {"post_id": 1}, + ) assert result == {} assert db.session.query(model.Post).count() == 0 snapshots.delete.assert_called_once_with(post, auth_user) @@ -34,15 +37,18 @@ def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.delete_post( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': 999}) + {"post_id": 999}, + ) def test_trying_to_delete_without_privileges( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): db.session.add(post_factory(id=1)) db.session.commit() with pytest.raises(errors.AuthError): api.post_api.delete_post( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'post_id': 1}) + {"post_id": 1}, + ) assert db.session.query(model.Post).count() == 1 diff --git a/server/szurubooru/tests/api/test_post_favoriting.py b/server/szurubooru/tests/api/test_post_favoriting.py index ce91a02..dc92bd9 100644 --- a/server/szurubooru/tests/api/test_post_favoriting.py +++ b/server/szurubooru/tests/api/test_post_favoriting.py @@ -1,29 +1,34 @@ from datetime import datetime from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts @pytest.fixture(autouse=True) def inject_config(config_injector): config_injector( - {'privileges': {'posts:favorite': model.User.RANK_REGULAR}}) + {"privileges": {"posts:favorite": model.User.RANK_REGULAR}} + ) def test_adding_to_favorites( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): post = post_factory() db.session.add(post) db.session.commit() assert post.score == 0 - with patch('szurubooru.func.posts.serialize_post'), \ - fake_datetime('1997-12-01'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.serialize_post"), fake_datetime( + "1997-12-01" + ): + posts.serialize_post.return_value = "serialized post" result = api.post_api.add_post_to_favorites( - context_factory(user=user_factory()), - {'post_id': post.post_id}) - assert result == 'serialized post' + context_factory(user=user_factory()), {"post_id": post.post_id} + ) + assert result == "serialized post" post = db.session.query(model.Post).one() assert db.session.query(model.PostFavorite).count() == 1 assert post is not None @@ -32,22 +37,23 @@ def test_adding_to_favorites( def test_removing_from_favorites( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() assert post.score == 0 - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.add_post_to_favorites( - context_factory(user=user), - {'post_id': post.post_id}) + context_factory(user=user), {"post_id": post.post_id} + ) assert post.score == 1 - with fake_datetime('1997-12-02'): + with fake_datetime("1997-12-02"): api.post_api.delete_post_from_favorites( - context_factory(user=user), - {'post_id': post.post_id}) + context_factory(user=user), {"post_id": post.post_id} + ) post = db.session.query(model.Post).one() assert post.score == 1 assert db.session.query(model.PostFavorite).count() == 0 @@ -55,65 +61,68 @@ def test_removing_from_favorites( def test_favoriting_twice( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.add_post_to_favorites( - context_factory(user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(user=user), {"post_id": post.post_id} + ) + with fake_datetime("1997-12-02"): api.post_api.add_post_to_favorites( - context_factory(user=user), - {'post_id': post.post_id}) + context_factory(user=user), {"post_id": post.post_id} + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostFavorite).count() == 1 assert post.favorite_count == 1 def test_removing_twice( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.add_post_to_favorites( - context_factory(user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(user=user), {"post_id": post.post_id} + ) + with fake_datetime("1997-12-02"): api.post_api.delete_post_from_favorites( - context_factory(user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(user=user), {"post_id": post.post_id} + ) + with fake_datetime("1997-12-02"): api.post_api.delete_post_from_favorites( - context_factory(user=user), - {'post_id': post.post_id}) + context_factory(user=user), {"post_id": post.post_id} + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostFavorite).count() == 0 assert post.favorite_count == 0 def test_favorites_from_multiple_users( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user1 = user_factory() user2 = user_factory() post = post_factory() db.session.add_all([user1, user2, post]) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.add_post_to_favorites( - context_factory(user=user1), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(user=user1), {"post_id": post.post_id} + ) + with fake_datetime("1997-12-02"): api.post_api.add_post_to_favorites( - context_factory(user=user2), - {'post_id': post.post_id}) + context_factory(user=user2), {"post_id": post.post_id} + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostFavorite).count() == 2 assert post.favorite_count == 2 @@ -123,16 +132,18 @@ def test_favorites_from_multiple_users( def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.add_post_to_favorites( - context_factory(user=user_factory()), - {'post_id': 5}) + context_factory(user=user_factory()), {"post_id": 5} + ) def test_trying_to_rate_without_privileges( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): post = post_factory() db.session.add(post) db.session.commit() with pytest.raises(errors.AuthError): api.post_api.add_post_to_favorites( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'post_id': post.post_id}) + {"post_id": post.post_id}, + ) diff --git a/server/szurubooru/tests/api/test_post_featuring.py b/server/szurubooru/tests/api/test_post_featuring.py index 6e9e756..d83d4b6 100644 --- a/server/szurubooru/tests/api/test_post_featuring.py +++ b/server/szurubooru/tests/api/test_post_featuring.py @@ -1,18 +1,22 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts, snapshots @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'posts:feature': model.User.RANK_REGULAR, - 'posts:view': model.User.RANK_REGULAR, - 'posts:view:featured': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "posts:feature": model.User.RANK_REGULAR, + "posts:view": model.User.RANK_REGULAR, + "posts:view:featured": model.User.RANK_REGULAR, + }, + } + ) def test_featuring(user_factory, post_factory, context_factory): @@ -21,64 +25,80 @@ def test_featuring(user_factory, post_factory, context_factory): db.session.add(post) db.session.flush() assert not posts.get_post_by_id(1).is_featured - with patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.snapshots.modify'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.snapshots.modify" + ): + posts.serialize_post.return_value = "serialized post" result = api.post_api.set_featured_post( - context_factory(params={'id': 1}, user=auth_user)) - assert result == 'serialized post' + context_factory(params={"id": 1}, user=auth_user) + ) + assert result == "serialized post" assert posts.try_get_featured_post() is not None assert posts.try_get_featured_post().post_id == 1 assert posts.get_post_by_id(1).is_featured result = api.post_api.get_featured_post( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR))) - assert result == 'serialized post' + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)) + ) + assert result == "serialized post" snapshots.modify.assert_called_once_with(post, auth_user) def test_trying_to_omit_required_parameter(user_factory, context_factory): with pytest.raises(errors.MissingRequiredParameterError): api.post_api.set_featured_post( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR))) + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)) + ) def test_trying_to_feature_the_same_post_twice( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): db.session.add(post_factory(id=1)) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.snapshots._post_to_webhooks" + ): api.post_api.set_featured_post( context_factory( - params={'id': 1}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"id": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) with pytest.raises(posts.PostAlreadyFeaturedError): api.post_api.set_featured_post( context_factory( - params={'id': 1}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"id": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) def test_featuring_one_post_after_another( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): db.session.add(post_factory(id=1)) db.session.add(post_factory(id=2)) db.session.commit() assert posts.try_get_featured_post() is None assert not posts.get_post_by_id(1).is_featured assert not posts.get_post_by_id(2).is_featured - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997'): + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.snapshots._post_to_webhooks" + ): + with fake_datetime("1997"): api.post_api.set_featured_post( context_factory( - params={'id': 1}, - user=user_factory(rank=model.User.RANK_REGULAR))) - with fake_datetime('1998'): + params={"id": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) + with fake_datetime("1998"): api.post_api.set_featured_post( context_factory( - params={'id': 2}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"id": 2}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) assert posts.try_get_featured_post() is not None assert posts.try_get_featured_post().post_id == 2 assert not posts.get_post_by_id(1).is_featured @@ -89,18 +109,21 @@ def test_trying_to_feature_non_existing(user_factory, context_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.set_featured_post( context_factory( - params={'id': 1}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"id": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) -def test_trying_to_retrieve_without_privileges( - user_factory, context_factory): +def test_trying_to_retrieve_without_privileges(user_factory, context_factory): with pytest.raises(errors.AuthError): api.post_api.get_featured_post( - context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS))) + context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)) + ) def test_trying_to_feature_without_privileges(user_factory, context_factory): with pytest.raises(errors.AuthError): api.post_api.set_featured_post( - context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS))) + context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)) + ) diff --git a/server/szurubooru/tests/api/test_post_merging.py b/server/szurubooru/tests/api/test_post_merging.py index eb8464f..cdcb0af 100644 --- a/server/szurubooru/tests/api/test_post_merging.py +++ b/server/szurubooru/tests/api/test_post_merging.py @@ -1,12 +1,14 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts, snapshots @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'posts:merge': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"posts:merge": model.User.RANK_REGULAR}}) def test_merging(user_factory, context_factory, post_factory): @@ -15,66 +17,78 @@ def test_merging(user_factory, context_factory, post_factory): target_post = post_factory() db.session.add_all([source_post, target_post]) db.session.flush() - with patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.merge_posts'), \ - patch('szurubooru.func.snapshots.merge'): + with patch("szurubooru.func.posts.serialize_post"), patch( + "szurubooru.func.posts.merge_posts" + ), patch("szurubooru.func.snapshots.merge"): api.post_api.merge_posts( context_factory( params={ - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': source_post.post_id, - 'mergeTo': target_post.post_id, - 'replaceContent': False, + "removeVersion": 1, + "mergeToVersion": 1, + "remove": source_post.post_id, + "mergeTo": target_post.post_id, + "replaceContent": False, }, - user=auth_user)) + user=auth_user, + ) + ) posts.merge_posts.called_once_with(source_post, target_post) snapshots.merge.assert_called_once_with( - source_post, target_post, auth_user) + source_post, target_post, auth_user + ) @pytest.mark.parametrize( - 'field', ['remove', 'mergeTo', 'removeVersion', 'mergeToVersion']) + "field", ["remove", "mergeTo", "removeVersion", "mergeToVersion"] +) def test_trying_to_omit_mandatory_field( - user_factory, post_factory, context_factory, field): + user_factory, post_factory, context_factory, field +): source_post = post_factory() target_post = post_factory() db.session.add_all([source_post, target_post]) db.session.commit() params = { - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': source_post.post_id, - 'mergeTo': target_post.post_id, - 'replaceContent': False, + "removeVersion": 1, + "mergeToVersion": 1, + "remove": source_post.post_id, + "mergeTo": target_post.post_id, + "replaceContent": False, } del params[field] with pytest.raises(errors.ValidationError): api.post_api.merge_posts( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_merge_non_existing( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): post = post_factory() db.session.add(post) db.session.commit() with pytest.raises(posts.PostNotFoundError): api.post_api.merge_posts( context_factory( - params={'remove': post.post_id, 'mergeTo': 999}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"remove": post.post_id, "mergeTo": 999}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) with pytest.raises(posts.PostNotFoundError): api.post_api.merge_posts( context_factory( - params={'remove': 999, 'mergeTo': post.post_id}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"remove": 999, "mergeTo": post.post_id}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) def test_trying_to_merge_without_privileges( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): source_post = post_factory() target_post = post_factory() db.session.add_all([source_post, target_post]) @@ -83,10 +97,12 @@ def test_trying_to_merge_without_privileges( api.post_api.merge_posts( context_factory( params={ - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': source_post.post_id, - 'mergeTo': target_post.post_id, - 'replaceContent': False, + "removeVersion": 1, + "mergeToVersion": 1, + "remove": source_post.post_id, + "mergeTo": target_post.post_id, + "replaceContent": False, }, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_post_rating.py b/server/szurubooru/tests/api/test_post_rating.py index 0fca2f5..2db17db 100644 --- a/server/szurubooru/tests/api/test_post_rating.py +++ b/server/szurubooru/tests/api/test_post_rating.py @@ -1,27 +1,31 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'posts:score': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"posts:score": model.User.RANK_REGULAR}}) def test_simple_rating( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'), \ - fake_datetime('1997-12-01'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.serialize_post"), fake_datetime( + "1997-12-01" + ): + posts.serialize_post.return_value = "serialized post" result = api.post_api.set_post_score( - context_factory( - params={'score': 1}, user=user_factory()), - {'post_id': post.post_id}) - assert result == 'serialized post' + context_factory(params={"score": 1}, user=user_factory()), + {"post_id": post.post_id}, + ) + assert result == "serialized post" post = db.session.query(model.Post).one() assert db.session.query(model.PostScore).count() == 1 assert post is not None @@ -29,112 +33,129 @@ def test_simple_rating( def test_updating_rating( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.set_post_score( - context_factory(params={'score': 1}, user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"post_id": post.post_id}, + ) + with fake_datetime("1997-12-02"): api.post_api.set_post_score( - context_factory(params={'score': -1}, user=user), - {'post_id': post.post_id}) + context_factory(params={"score": -1}, user=user), + {"post_id": post.post_id}, + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostScore).count() == 1 assert post.score == -1 def test_updating_rating_to_zero( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.set_post_score( - context_factory(params={'score': 1}, user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"post_id": post.post_id}, + ) + with fake_datetime("1997-12-02"): api.post_api.set_post_score( - context_factory(params={'score': 0}, user=user), - {'post_id': post.post_id}) + context_factory(params={"score": 0}, user=user), + {"post_id": post.post_id}, + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostScore).count() == 0 assert post.score == 0 def test_deleting_rating( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user = user_factory() post = post_factory() db.session.add(post) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.set_post_score( - context_factory(params={'score': 1}, user=user), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user), + {"post_id": post.post_id}, + ) + with fake_datetime("1997-12-02"): api.post_api.delete_post_score( - context_factory(user=user), - {'post_id': post.post_id}) + context_factory(user=user), {"post_id": post.post_id} + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostScore).count() == 0 assert post.score == 0 def test_ratings_from_multiple_users( - user_factory, post_factory, context_factory, fake_datetime): + user_factory, post_factory, context_factory, fake_datetime +): user1 = user_factory() user2 = user_factory() post = post_factory() db.session.add_all([user1, user2, post]) db.session.commit() - with patch('szurubooru.func.posts.serialize_post'): - with fake_datetime('1997-12-01'): + with patch("szurubooru.func.posts.serialize_post"): + with fake_datetime("1997-12-01"): api.post_api.set_post_score( - context_factory(params={'score': 1}, user=user1), - {'post_id': post.post_id}) - with fake_datetime('1997-12-02'): + context_factory(params={"score": 1}, user=user1), + {"post_id": post.post_id}, + ) + with fake_datetime("1997-12-02"): api.post_api.set_post_score( - context_factory(params={'score': -1}, user=user2), - {'post_id': post.post_id}) + context_factory(params={"score": -1}, user=user2), + {"post_id": post.post_id}, + ) post = db.session.query(model.Post).one() assert db.session.query(model.PostScore).count() == 2 assert post.score == 0 def test_trying_to_omit_mandatory_field( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): post = post_factory() db.session.add(post) db.session.commit() with pytest.raises(errors.ValidationError): api.post_api.set_post_score( context_factory(params={}, user=user_factory()), - {'post_id': post.post_id}) + {"post_id": post.post_id}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.set_post_score( - context_factory(params={'score': 1}, user=user_factory()), - {'post_id': 5}) + context_factory(params={"score": 1}, user=user_factory()), + {"post_id": 5}, + ) def test_trying_to_rate_without_privileges( - user_factory, post_factory, context_factory): + user_factory, post_factory, context_factory +): post = post_factory() db.session.add(post) db.session.commit() with pytest.raises(errors.AuthError): api.post_api.set_post_score( context_factory( - params={'score': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'post_id': post.post_id}) + params={"score": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"post_id": post.post_id}, + ) diff --git a/server/szurubooru/tests/api/test_post_retrieving.py b/server/szurubooru/tests/api/test_post_retrieving.py index 1e4ed03..b64074c 100644 --- a/server/szurubooru/tests/api/test_post_retrieving.py +++ b/server/szurubooru/tests/api/test_post_retrieving.py @@ -1,20 +1,24 @@ from datetime import datetime from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import posts @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'data_url': 'http://example.com/', - 'secret': 'test', - 'privileges': { - 'posts:list': model.User.RANK_REGULAR, - 'posts:view': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "data_url": "http://example.com/", + "secret": "test", + "privileges": { + "posts:list": model.User.RANK_REGULAR, + "posts:view": model.User.RANK_REGULAR, + }, + } + ) def test_retrieving_multiple(user_factory, post_factory, context_factory): @@ -22,18 +26,20 @@ def test_retrieving_multiple(user_factory, post_factory, context_factory): post2 = post_factory(id=2) db.session.add_all([post1, post2]) db.session.flush() - with patch('szurubooru.func.posts.serialize_post'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.serialize_post"): + posts.serialize_post.return_value = "serialized post" result = api.post_api.get_posts( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) assert result == { - 'query': '', - 'offset': 0, - 'limit': 100, - 'total': 2, - 'results': ['serialized post', 'serialized post'], + "query": "", + "offset": 0, + "limit": 100, + "total": 2, + "results": ["serialized post", "serialized post"], } @@ -41,83 +47,98 @@ def test_using_special_tokens(user_factory, post_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) post1 = post_factory(id=1) post2 = post_factory(id=2) - post1.favorited_by = [model.PostFavorite( - user=auth_user, time=datetime.utcnow())] + post1.favorited_by = [ + model.PostFavorite(user=auth_user, time=datetime.utcnow()) + ] db.session.add_all([post1, post2, auth_user]) db.session.flush() - with patch('szurubooru.func.posts.serialize_post'): - posts.serialize_post.side_effect = lambda post, *_args, **_kwargs: \ - 'serialized post %d' % post.post_id + with patch("szurubooru.func.posts.serialize_post"): + posts.serialize_post.side_effect = ( + lambda post, *_args, **_kwargs: "serialized post %d" % post.post_id + ) result = api.post_api.get_posts( context_factory( - params={'query': 'special:fav', 'offset': 0}, - user=auth_user)) + params={"query": "special:fav", "offset": 0}, user=auth_user + ) + ) assert result == { - 'query': 'special:fav', - 'offset': 0, - 'limit': 100, - 'total': 1, - 'results': ['serialized post 1'], + "query": "special:fav", + "offset": 0, + "limit": 100, + "total": 1, + "results": ["serialized post 1"], } def test_trying_to_use_special_tokens_without_logging_in( - user_factory, context_factory, config_injector): - config_injector({ - 'privileges': {'posts:list': 'anonymous'}, - }) + user_factory, context_factory, config_injector +): + config_injector( + { + "privileges": {"posts:list": "anonymous"}, + } + ) with pytest.raises(errors.SearchError): api.post_api.get_posts( context_factory( - params={'query': 'special:fav', 'offset': 0}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "special:fav", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_trying_to_retrieve_multiple_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.post_api.get_posts( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_retrieving_single(user_factory, post_factory, context_factory): db.session.add(post_factory(id=1)) db.session.flush() - with patch('szurubooru.func.posts.serialize_post'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.serialize_post"): + posts.serialize_post.return_value = "serialized post" result = api.post_api.get_post( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': 1}) - assert result == 'serialized post' + {"post_id": 1}, + ) + assert result == "serialized post" def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.get_post( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': 999}) + {"post_id": 999}, + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.post_api.get_post( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'post_id': 999}) + {"post_id": 999}, + ) -@pytest.mark.parametrize('query,expected_id', [ - ('sort:id,asc', 2), - ('sort:id,asc id:2..', 2), - ('sort:id,desc id:2..', 3), - ('sort:id,asc id:3..', 3), - ('sort:id,desc id:3..', 3), - ('sort:id id:4..', None), - ('sort:tag-count', 3), - ('sort:tag-count,asc id:..2', 1), - ('sort:tag-count,desc id:..2', 2), +@pytest.mark.parametrize("query,expected_id", [ + ("sort:id,asc", 2), + ("sort:id,asc id:2..", 2), + ("sort:id,desc id:2..", 3), + ("sort:id,asc id:3..", 3), + ("sort:id,desc id:3..", 3), + ("sort:id id:4..", None), + ("sort:tag-count", 3), + ("sort:tag-count,asc id:..2", 1), + ("sort:tag-count,desc id:..2", 2), ]) def test_median( query, @@ -134,16 +155,16 @@ def test_median( post3 = post_factory(id=3, tags=[tag1, tag2]) db.session.add_all([tag1, tag2, tag3, post1, post2, post3]) db.session.flush() - with patch('szurubooru.func.comments.serialize_comment'), \ - patch('szurubooru.func.users.serialize_micro_user'), \ - patch('szurubooru.func.posts.files.has'): + with patch("szurubooru.func.comments.serialize_comment"), \ + patch("szurubooru.func.users.serialize_micro_user"), \ + patch("szurubooru.func.posts.files.has"): response = api.post_api.get_posts_median( context_factory( - params={'query': query}, + params={"query": query}, user=user_factory(rank=model.User.RANK_REGULAR))) if not expected_id: - assert response['total'] == 0 - assert len(response['results']) == 0 + assert response["total"] == 0 + assert len(response["results"]) == 0 else: - assert response['total'] == 1 - assert response['results'][0]['id'] == expected_id + assert response["total"] == 1 + assert response["results"][0]["id"] == expected_id diff --git a/server/szurubooru/tests/api/test_post_updating.py b/server/szurubooru/tests/api/test_post_updating.py index 7675488..7d830c9 100644 --- a/server/szurubooru/tests/api/test_post_updating.py +++ b/server/szurubooru/tests/api/test_post_updating.py @@ -1,184 +1,235 @@ from datetime import datetime from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import posts, tags, metrics, snapshots, net + +from szurubooru import api, db, errors, model +from szurubooru.func import metrics, net, posts, snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'posts:edit:tags': model.User.RANK_REGULAR, - 'posts:edit:content': model.User.RANK_REGULAR, - 'posts:edit:safety': model.User.RANK_REGULAR, - 'posts:edit:source': model.User.RANK_REGULAR, - 'posts:edit:relations': model.User.RANK_REGULAR, - 'posts:edit:notes': model.User.RANK_REGULAR, - 'posts:edit:flags': model.User.RANK_REGULAR, - 'posts:edit:thumbnail': model.User.RANK_REGULAR, - 'tags:create': model.User.RANK_MODERATOR, - 'metrics:edit:posts': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "posts:edit:tags": model.User.RANK_REGULAR, + "posts:edit:content": model.User.RANK_REGULAR, + "posts:edit:safety": model.User.RANK_REGULAR, + "posts:edit:source": model.User.RANK_REGULAR, + "posts:edit:relations": model.User.RANK_REGULAR, + "posts:edit:notes": model.User.RANK_REGULAR, + "posts:edit:flags": model.User.RANK_REGULAR, + "posts:edit:thumbnail": model.User.RANK_REGULAR, + "tags:create": model.User.RANK_MODERATOR, + "metrics:edit:posts": model.User.RANK_REGULAR, + "uploads:use_downloader": model.User.RANK_REGULAR, + }, + "allow_broken_uploads": False, + } + ) def test_post_updating( - context_factory, post_factory, user_factory, fake_datetime): + context_factory, post_factory, user_factory, fake_datetime +): auth_user = user_factory(rank=model.User.RANK_REGULAR) post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.posts.create_post'), \ - patch('szurubooru.func.posts.update_post_tags'), \ - patch('szurubooru.func.posts.update_post_content'), \ - patch('szurubooru.func.posts.update_post_thumbnail'), \ - patch('szurubooru.func.posts.update_post_safety'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.posts.update_post_relations'), \ - patch('szurubooru.func.posts.update_post_notes'), \ - patch('szurubooru.func.posts.update_post_flags'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.snapshots.modify'), \ - patch('szurubooru.func.metrics.update_or_create_post_metrics'), \ - patch('szurubooru.func.metrics.update_or_create_post_metric_ranges'), \ - fake_datetime('1997-01-01'): - posts.serialize_post.return_value = 'serialized post' + with patch("szurubooru.func.posts.create_post"), patch( + "szurubooru.func.posts.update_post_tags" + ), patch("szurubooru.func.posts.update_post_content"), patch( + "szurubooru.func.posts.update_post_thumbnail" + ), patch( + "szurubooru.func.posts.update_post_safety" + ), patch( + "szurubooru.func.posts.update_post_source" + ), patch( + "szurubooru.func.posts.update_post_relations" + ), patch( + "szurubooru.func.posts.update_post_notes" + ), patch( + "szurubooru.func.posts.update_post_flags" + ), patch( + "szurubooru.func.posts.serialize_post" + ), patch( + "szurubooru.func.snapshots.modify" + ), patch( + "szurubooru.func.metrics.update_or_create_post_metrics" + ), patch( + "szurubooru.func.metrics.update_or_create_post_metric_ranges" + ), fake_datetime( + "1997-01-01" + ): + posts.serialize_post.return_value = "serialized post" result = api.post_api.update_post( context_factory( params={ - 'version': 1, - 'safety': 'safe', - 'tags': ['tag1', 'tag2'], - 'relations': [1, 2], - 'source': 'source', - 'notes': ['note1', 'note2'], - 'flags': ['flag1', 'flag2'], - 'metrics': [{'tag_name': 'tag1', 'value': 1.2}], - 'metricRanges': [{'tag_name': 'tag2', 'low': 1, 'high': 2}], + "version": 1, + "safety": "safe", + "tags": ["tag1", "tag2"], + "relations": [1, 2], + "source": "source", + "notes": ["note1", "note2"], + "flags": ["flag1", "flag2"], + "metrics": [{"tag_name": "tag1", "value": 1.2}], + "metricRanges": [{"tag_name": "tag2", "low": 1, "high": 2}], }, files={ - 'content': 'post-content', - 'thumbnail': 'post-thumbnail', + "content": "post-content", + "thumbnail": "post-thumbnail", }, - user=auth_user), - {'post_id': post.post_id}) + user=auth_user, + ), + {"post_id": post.post_id}, + ) - assert result == 'serialized post' + assert result == "serialized post" posts.create_post.assert_not_called() - posts.update_post_tags.assert_called_once_with(post, ['tag1', 'tag2']) - posts.update_post_content.assert_called_once_with(post, 'post-content') + posts.update_post_tags.assert_called_once_with(post, ["tag1", "tag2"]) + posts.update_post_content.assert_called_once_with(post, "post-content") posts.update_post_thumbnail.assert_called_once_with( - post, 'post-thumbnail') - posts.update_post_safety.assert_called_once_with(post, 'safe') - posts.update_post_source.assert_called_once_with(post, 'source') + post, "post-thumbnail" + ) + posts.update_post_safety.assert_called_once_with(post, "safe") + posts.update_post_source.assert_called_once_with(post, "source") posts.update_post_relations.assert_called_once_with(post, [1, 2]) posts.update_post_notes.assert_called_once_with( - post, ['note1', 'note2']) + post, ["note1", "note2"] + ) posts.update_post_flags.assert_called_once_with( - post, ['flag1', 'flag2']) + post, ["flag1", "flag2"] + ) posts.serialize_post.assert_called_once_with( - post, auth_user, options=[]) + post, auth_user, options=[] + ) snapshots.modify.assert_called_once_with(post, auth_user) metrics.update_or_create_post_metrics.assert_called_once_with( - post, [{'tag_name': 'tag1', 'value': 1.2}]) + post, [{"tag_name": "tag1", "value": 1.2}]) metrics.update_or_create_post_metric_ranges.assert_called_once_with( - post, [{'tag_name': 'tag2', 'low': 1, 'high': 2}]) + post, [{"tag_name": "tag2", "low": 1, "high": 2}]) assert post.last_edit_time == datetime(1997, 1, 1) def test_uploading_from_url_saves_source( - context_factory, post_factory, user_factory): + context_factory, post_factory, user_factory +): post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.net.download'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.update_post_content'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.snapshots.modify'): - net.download.return_value = b'content' + with patch("szurubooru.func.net.download"), patch( + "szurubooru.func.posts.serialize_post" + ), patch("szurubooru.func.posts.update_post_content"), patch( + "szurubooru.func.posts.update_post_source" + ), patch( + "szurubooru.func.snapshots.modify" + ): + net.download.return_value = b"content" api.post_api.update_post( context_factory( - params={'contentUrl': 'example.com', 'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': post.post_id}) - net.download.assert_called_once_with('example.com') - posts.update_post_content.assert_called_once_with(post, b'content') - posts.update_post_source.assert_called_once_with(post, 'example.com') + params={"contentUrl": "example.com", "version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"post_id": post.post_id}, + ) + net.download.assert_called_once_with( + "example.com", use_video_downloader=True + ) + posts.update_post_content.assert_called_once_with(post, b"content") + posts.update_post_source.assert_called_once_with(post, "example.com") def test_uploading_from_url_with_source_specified( - context_factory, post_factory, user_factory): + context_factory, post_factory, user_factory +): post = post_factory() db.session.add(post) db.session.flush() - with patch('szurubooru.func.net.download'), \ - patch('szurubooru.func.posts.serialize_post'), \ - patch('szurubooru.func.posts.update_post_content'), \ - patch('szurubooru.func.posts.update_post_source'), \ - patch('szurubooru.func.snapshots.modify'): - net.download.return_value = b'content' + with patch("szurubooru.func.net.download"), patch( + "szurubooru.func.posts.serialize_post" + ), patch("szurubooru.func.posts.update_post_content"), patch( + "szurubooru.func.posts.update_post_source" + ), patch( + "szurubooru.func.snapshots.modify" + ): + net.download.return_value = b"content" api.post_api.update_post( context_factory( params={ - 'contentUrl': 'example.com', - 'source': 'example2.com', - 'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': post.post_id}) - net.download.assert_called_once_with('example.com') - posts.update_post_content.assert_called_once_with(post, b'content') - posts.update_post_source.assert_called_once_with(post, 'example2.com') + "contentUrl": "example.com", + "source": "example2.com", + "version": 1, + }, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"post_id": post.post_id}, + ) + net.download.assert_called_once_with( + "example.com", use_video_downloader=True + ) + posts.update_post_content.assert_called_once_with(post, b"content") + posts.update_post_source.assert_called_once_with(post, "example2.com") def test_trying_to_update_non_existing(context_factory, user_factory): with pytest.raises(posts.PostNotFoundError): api.post_api.update_post( context_factory( - params='whatever', - user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': 1}) + params="whatever", + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"post_id": 1}, + ) -@pytest.mark.parametrize('files,params', [ - ({}, {'tags': '...'}), - ({}, {'safety': '...'}), - ({}, {'source': '...'}), - ({}, {'relations': '...'}), - ({}, {'notes': '...'}), - ({}, {'flags': '...'}), - ({'content': '...'}, {}), - ({'thumbnail': '...'}, {}), - ({}, {'metrics': '...'}), - ({}, {'metricRanges': '...'}), -]) +@pytest.mark.parametrize( + "files,params", + [ + ({}, {"tags": "..."}), + ({}, {"safety": "..."}), + ({}, {"source": "..."}), + ({}, {"relations": "..."}), + ({}, {"notes": "..."}), + ({}, {"flags": "..."}), + ({"content": "..."}, {}), + ({"thumbnail": "..."}, {}), + ({}, {"metrics": "..."}), + ({}, {"metricRanges": "..."}), + ], +) def test_trying_to_update_field_without_privileges( - context_factory, post_factory, user_factory, files, params): + context_factory, post_factory, user_factory, files, params +): post = post_factory() db.session.add(post) db.session.flush() with pytest.raises(errors.AuthError): api.post_api.update_post( context_factory( - params={**params, **{'version': 1}}, + params={**params, **{"version": 1}}, files=files, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'post_id': post.post_id}) + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"post_id": post.post_id}, + ) def test_trying_to_create_tags_without_privileges( - context_factory, post_factory, user_factory): + context_factory, post_factory, user_factory +): post = post_factory() db.session.add(post) db.session.flush() - with pytest.raises(errors.AuthError), \ - patch('szurubooru.func.posts.update_post_tags'): - posts.update_post_tags.return_value = ['new-tag'] + with pytest.raises(errors.AuthError), patch( + "szurubooru.func.posts.update_post_tags" + ): + posts.update_post_tags.return_value = ["new-tag"] api.post_api.update_post( context_factory( - params={'tags': ['tag1', 'tag2'], 'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'post_id': post.post_id}) + params={"tags": ["tag1", "tag2"], "version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"post_id": post.post_id}, + ) diff --git a/server/szurubooru/tests/api/test_snapshot_retrieving.py b/server/szurubooru/tests/api/test_snapshot_retrieving.py index 41f0beb..ea59d77 100644 --- a/server/szurubooru/tests/api/test_snapshot_retrieving.py +++ b/server/szurubooru/tests/api/test_snapshot_retrieving.py @@ -1,24 +1,28 @@ from datetime import datetime + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model def snapshot_factory(): snapshot = model.Snapshot() snapshot.creation_time = datetime(1999, 1, 1) - snapshot.resource_type = 'dummy' + snapshot.resource_type = "dummy" snapshot.resource_pkey = 1 - snapshot.resource_name = 'dummy' - snapshot.operation = 'added' - snapshot.data = '{}' + snapshot.resource_name = "dummy" + snapshot.operation = "added" + snapshot.data = "{}" return snapshot @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': {'snapshots:list': model.User.RANK_REGULAR}, - }) + config_injector( + { + "privileges": {"snapshots:list": model.User.RANK_REGULAR}, + } + ) def test_retrieving_multiple(user_factory, context_factory): @@ -28,19 +32,24 @@ def test_retrieving_multiple(user_factory, context_factory): db.session.flush() result = api.snapshot_api.get_snapshots( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_REGULAR))) - assert result['query'] == '' - assert result['offset'] == 0 - assert result['limit'] == 100 - assert result['total'] == 2 - assert len(result['results']) == 2 + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) + assert result["query"] == "" + assert result["offset"] == 0 + assert result["limit"] == 100 + assert result["total"] == 2 + assert len(result["results"]) == 2 def test_trying_to_retrieve_multiple_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.snapshot_api.get_snapshots( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_tag_category_creating.py b/server/szurubooru/tests/api/test_tag_category_creating.py index 47e8405..6798cbe 100644 --- a/server/szurubooru/tests/api/test_tag_category_creating.py +++ b/server/szurubooru/tests/api/test_tag_category_creating.py @@ -1,7 +1,9 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tag_categories, tags, snapshots + +from szurubooru import api, db, errors, model +from szurubooru.func import snapshots, tag_categories, tags def _update_category_name(category, name): @@ -10,49 +12,61 @@ def _update_category_name(category, name): @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': {'tag_categories:create': model.User.RANK_REGULAR}, - }) + config_injector( + { + "privileges": {"tag_categories:create": model.User.RANK_REGULAR}, + } + ) def test_creating_category( - tag_category_factory, user_factory, context_factory): + tag_category_factory, user_factory, context_factory +): auth_user = user_factory(rank=model.User.RANK_REGULAR) - category = tag_category_factory(name='meta') + category = tag_category_factory(name="meta") db.session.add(category) - with patch('szurubooru.func.tag_categories.create_category'), \ - patch('szurubooru.func.tag_categories.serialize_category'), \ - patch('szurubooru.func.tag_categories.update_category_name'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.tag_categories.create_category"), patch( + "szurubooru.func.tag_categories.serialize_category" + ), patch("szurubooru.func.tag_categories.update_category_name"), patch( + "szurubooru.func.snapshots.create" + ): tag_categories.create_category.return_value = category tag_categories.update_category_name.side_effect = _update_category_name - tag_categories.serialize_category.return_value = 'serialized category' + tag_categories.serialize_category.return_value = "serialized category" result = api.tag_category_api.create_tag_category( context_factory( - params={'name': 'meta', 'color': 'black'}, user=auth_user)) - assert result == 'serialized category' - tag_categories.create_category.assert_called_once_with('meta', 'black') + params={"name": "meta", "color": "black", "order": 0}, + user=auth_user, + ) + ) + assert result == "serialized category" + tag_categories.create_category.assert_called_once_with( + "meta", "black", 0 + ) snapshots.create.assert_called_once_with(category, auth_user) -@pytest.mark.parametrize('field', ['name', 'color']) +@pytest.mark.parametrize("field", ["name", "color"]) def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): params = { - 'name': 'meta', - 'color': 'black', + "name": "meta", + "color": "black", } del params[field] with pytest.raises(errors.ValidationError): api.tag_category_api.create_tag_category( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_create_without_privileges(user_factory, context_factory): with pytest.raises(errors.AuthError): api.tag_category_api.create_tag_category( context_factory( - params={'name': 'meta', 'color': 'black'}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"name": "meta", "color": "black"}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_tag_category_deleting.py b/server/szurubooru/tests/api/test_tag_category_deleting.py index 2bee513..23a3a42 100644 --- a/server/szurubooru/tests/api/test_tag_category_deleting.py +++ b/server/szurubooru/tests/api/test_tag_category_deleting.py @@ -1,76 +1,91 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tag_categories, tags, snapshots + +from szurubooru import api, db, errors, model +from szurubooru.func import snapshots, tag_categories, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': {'tag_categories:delete': model.User.RANK_REGULAR}, - }) + config_injector( + { + "privileges": {"tag_categories:delete": model.User.RANK_REGULAR}, + } + ) def test_deleting(user_factory, tag_category_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - category = tag_category_factory(name='category') - db.session.add(tag_category_factory(name='root')) + category = tag_category_factory(name="category") + db.session.add(tag_category_factory(name="root")) db.session.add(category) db.session.flush() - with patch('szurubooru.func.snapshots.delete'): + with patch("szurubooru.func.snapshots.delete"): result = api.tag_category_api.delete_tag_category( - context_factory(params={'version': 1}, user=auth_user), - {'category_name': 'category'}) + context_factory(params={"version": 1}, user=auth_user), + {"category_name": "category"}, + ) assert result == {} assert db.session.query(model.TagCategory).count() == 1 - assert db.session.query(model.TagCategory).one().name == 'root' + assert db.session.query(model.TagCategory).one().name == "root" snapshots.delete.assert_called_once_with(category, auth_user) def test_trying_to_delete_used( - user_factory, tag_category_factory, tag_factory, context_factory): - category = tag_category_factory(name='category') + user_factory, tag_category_factory, tag_factory, context_factory +): + category = tag_category_factory(name="category") db.session.add(category) db.session.flush() - tag = tag_factory(names=['tag'], category=category) + tag = tag_factory(names=["tag"], category=category) db.session.add(tag) db.session.commit() with pytest.raises(tag_categories.TagCategoryIsInUseError): api.tag_category_api.delete_tag_category( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'category'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "category"}, + ) assert db.session.query(model.TagCategory).count() == 1 def test_trying_to_delete_last( - user_factory, tag_category_factory, context_factory): - db.session.add(tag_category_factory(name='root')) + user_factory, tag_category_factory, context_factory +): + db.session.add(tag_category_factory(name="root")) db.session.commit() with pytest.raises(tag_categories.TagCategoryIsInUseError): api.tag_category_api.delete_tag_category( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'root'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "root"}, + ) def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(tag_categories.TagCategoryNotFoundError): api.tag_category_api.delete_tag_category( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'bad'}) + {"category_name": "bad"}, + ) def test_trying_to_delete_without_privileges( - user_factory, tag_category_factory, context_factory): - db.session.add(tag_category_factory(name='category')) + user_factory, tag_category_factory, context_factory +): + db.session.add(tag_category_factory(name="category")) db.session.commit() with pytest.raises(errors.AuthError): api.tag_category_api.delete_tag_category( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'category_name': 'category'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"category_name": "category"}, + ) assert db.session.query(model.TagCategory).count() == 1 diff --git a/server/szurubooru/tests/api/test_tag_category_retrieving.py b/server/szurubooru/tests/api/test_tag_category_retrieving.py index 0b98d74..cec3657 100644 --- a/server/szurubooru/tests/api/test_tag_category_retrieving.py +++ b/server/szurubooru/tests/api/test_tag_category_retrieving.py @@ -1,43 +1,53 @@ import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import tag_categories @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'tag_categories:list': model.User.RANK_REGULAR, - 'tag_categories:view': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "tag_categories:list": model.User.RANK_REGULAR, + "tag_categories:view": model.User.RANK_REGULAR, + }, + } + ) def test_retrieving_multiple( - user_factory, tag_category_factory, context_factory): - db.session.add_all([ - tag_category_factory(name='c1'), - tag_category_factory(name='c2'), - ]) + user_factory, tag_category_factory, context_factory +): + db.session.add_all( + [ + tag_category_factory(name="c1"), + tag_category_factory(name="c2"), + ] + ) db.session.flush() result = api.tag_category_api.get_tag_categories( - context_factory(user=user_factory(rank=model.User.RANK_REGULAR))) - assert [cat['name'] for cat in result['results']] == ['c1', 'c2'] + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)) + ) + assert [cat["name"] for cat in result["results"]] == ["c1", "c2"] def test_retrieving_single( - user_factory, tag_category_factory, context_factory): - db.session.add(tag_category_factory(name='cat')) + user_factory, tag_category_factory, context_factory +): + db.session.add(tag_category_factory(name="cat")) db.session.flush() result = api.tag_category_api.get_tag_category( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'cat'}) + {"category_name": "cat"}, + ) assert result == { - 'name': 'cat', - 'color': 'dummy', - 'usages': 0, - 'default': False, - 'version': 1, + "name": "cat", + "color": "dummy", + "usages": 0, + "default": False, + "order": 1, + "version": 1, } @@ -45,12 +55,15 @@ def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): with pytest.raises(tag_categories.TagCategoryNotFoundError): api.tag_category_api.get_tag_category( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': '-'}) + {"category_name": "-"}, + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.tag_category_api.get_tag_category( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'category_name': '-'}) + {"category_name": "-"}, + ) diff --git a/server/szurubooru/tests/api/test_tag_category_updating.py b/server/szurubooru/tests/api/test_tag_category_updating.py index 24a9f6e..f12ce36 100644 --- a/server/szurubooru/tests/api/test_tag_category_updating.py +++ b/server/szurubooru/tests/api/test_tag_category_updating.py @@ -1,7 +1,9 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tag_categories, tags, snapshots + +from szurubooru import api, db, errors, model +from szurubooru.func import snapshots, tag_categories, tags def _update_category_name(category, name): @@ -10,99 +12,122 @@ def _update_category_name(category, name): @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'tag_categories:edit:name': model.User.RANK_REGULAR, - 'tag_categories:edit:color': model.User.RANK_REGULAR, - 'tag_categories:set_default': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "tag_categories:edit:name": model.User.RANK_REGULAR, + "tag_categories:edit:color": model.User.RANK_REGULAR, + "tag_categories:edit:order": model.User.RANK_REGULAR, + "tag_categories:set_default": model.User.RANK_REGULAR, + }, + } + ) def test_simple_updating(user_factory, tag_category_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - category = tag_category_factory(name='name', color='black') + category = tag_category_factory(name="name", color="black") db.session.add(category) db.session.flush() - with patch('szurubooru.func.tag_categories.serialize_category'), \ - patch('szurubooru.func.tag_categories.update_category_name'), \ - patch('szurubooru.func.tag_categories.update_category_color'), \ - patch('szurubooru.func.snapshots.modify'): + with patch("szurubooru.func.tag_categories.serialize_category"), patch( + "szurubooru.func.tag_categories.update_category_name" + ), patch("szurubooru.func.tag_categories.update_category_color"), patch( + "szurubooru.func.snapshots.modify" + ): tag_categories.update_category_name.side_effect = _update_category_name - tag_categories.serialize_category.return_value = 'serialized category' + tag_categories.serialize_category.return_value = "serialized category" result = api.tag_category_api.update_tag_category( context_factory( - params={'name': 'changed', 'color': 'white', 'version': 1}, - user=auth_user), - {'category_name': 'name'}) - assert result == 'serialized category' + params={"name": "changed", "color": "white", "version": 1}, + user=auth_user, + ), + {"category_name": "name"}, + ) + assert result == "serialized category" tag_categories.update_category_name.assert_called_once_with( - category, 'changed') + category, "changed" + ) tag_categories.update_category_color.assert_called_once_with( - category, 'white') + category, "white" + ) snapshots.modify.assert_called_once_with(category, auth_user) -@pytest.mark.parametrize('field', ['name', 'color']) +@pytest.mark.parametrize("field", ["name", "color"]) def test_omitting_optional_field( - user_factory, tag_category_factory, context_factory, field): - db.session.add(tag_category_factory(name='name', color='black')) + user_factory, tag_category_factory, context_factory, field +): + db.session.add(tag_category_factory(name="name", color="black")) db.session.commit() params = { - 'name': 'changed', - 'color': 'white', + "name": "changed", + "color": "white", } del params[field] - with patch('szurubooru.func.tag_categories.serialize_category'), \ - patch('szurubooru.func.tag_categories.update_category_name'): + with patch("szurubooru.func.tag_categories.serialize_category"), patch( + "szurubooru.func.tag_categories.update_category_name" + ), patch("szurubooru.func.snapshots._post_to_webhooks"): api.tag_category_api.update_tag_category( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'name'}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "name"}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(tag_categories.TagCategoryNotFoundError): api.tag_category_api.update_tag_category( context_factory( - params={'name': ['dummy']}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'bad'}) + params={"name": ["dummy"]}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "bad"}, + ) -@pytest.mark.parametrize('params', [ - {'name': 'whatever'}, - {'color': 'whatever'}, -]) +@pytest.mark.parametrize( + "params", + [ + {"name": "whatever"}, + {"color": "whatever"}, + ], +) def test_trying_to_update_without_privileges( - user_factory, tag_category_factory, context_factory, params): - db.session.add(tag_category_factory(name='dummy')) + user_factory, tag_category_factory, context_factory, params +): + db.session.add(tag_category_factory(name="dummy")) db.session.commit() with pytest.raises(errors.AuthError): api.tag_category_api.update_tag_category( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'category_name': 'dummy'}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"category_name": "dummy"}, + ) def test_set_as_default(user_factory, tag_category_factory, context_factory): - category = tag_category_factory(name='name', color='black') + category = tag_category_factory(name="name", color="black") db.session.add(category) db.session.commit() - with patch('szurubooru.func.tag_categories.serialize_category'), \ - patch('szurubooru.func.tag_categories.set_default_category'): + with patch("szurubooru.func.tag_categories.serialize_category"), patch( + "szurubooru.func.tag_categories.set_default_category" + ): tag_categories.update_category_name.side_effect = _update_category_name - tag_categories.serialize_category.return_value = 'serialized category' + tag_categories.serialize_category.return_value = "serialized category" result = api.tag_category_api.set_tag_category_as_default( context_factory( params={ - 'name': 'changed', - 'color': 'white', - 'version': 1, + "name": "changed", + "color": "white", + "version": 1, }, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'category_name': 'name'}) - assert result == 'serialized category' + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"category_name": "name"}, + ) + assert result == "serialized category" tag_categories.set_default_category.assert_called_once_with(category) diff --git a/server/szurubooru/tests/api/test_tag_creating.py b/server/szurubooru/tests/api/test_tag_creating.py index 4cee710..2f4264d 100644 --- a/server/szurubooru/tests/api/test_tag_creating.py +++ b/server/szurubooru/tests/api/test_tag_creating.py @@ -1,84 +1,97 @@ from unittest.mock import patch + import pytest -from szurubooru import api, model, errors -from szurubooru.func import tags, snapshots + +from szurubooru import api, errors, model +from szurubooru.func import snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'tags:create': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"tags:create": model.User.RANK_REGULAR}}) def test_creating_simple_tags(tag_factory, user_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) tag = tag_factory() - with patch('szurubooru.func.tags.create_tag'), \ - patch('szurubooru.func.tags.get_or_create_tags_by_names'), \ - patch('szurubooru.func.tags.serialize_tag'), \ - patch('szurubooru.func.snapshots.create'): + with patch("szurubooru.func.tags.create_tag"), patch( + "szurubooru.func.tags.get_or_create_tags_by_names" + ), patch("szurubooru.func.tags.serialize_tag"), patch( + "szurubooru.func.snapshots.create" + ): tags.get_or_create_tags_by_names.return_value = ([], []) tags.create_tag.return_value = tag - tags.serialize_tag.return_value = 'serialized tag' + tags.serialize_tag.return_value = "serialized tag" result = api.tag_api.create_tag( context_factory( params={ - 'names': ['tag1', 'tag2'], - 'category': 'meta', - 'description': 'desc', - 'suggestions': ['sug1', 'sug2'], - 'implications': ['imp1', 'imp2'], + "names": ["tag1", "tag2"], + "category": "meta", + "description": "desc", + "suggestions": ["sug1", "sug2"], + "implications": ["imp1", "imp2"], }, - user=auth_user)) - assert result == 'serialized tag' + user=auth_user, + ) + ) + assert result == "serialized tag" tags.create_tag.assert_called_once_with( - ['tag1', 'tag2'], 'meta', ['sug1', 'sug2'], ['imp1', 'imp2']) + ["tag1", "tag2"], "meta", ["sug1", "sug2"], ["imp1", "imp2"] + ) snapshots.create.assert_called_once_with(tag, auth_user) -@pytest.mark.parametrize('field', ['names', 'category']) +@pytest.mark.parametrize("field", ["names", "category"]) def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): params = { - 'names': ['tag1', 'tag2'], - 'category': 'meta', - 'suggestions': [], - 'implications': [], + "names": ["tag1", "tag2"], + "category": "meta", + "suggestions": [], + "implications": [], } del params[field] with pytest.raises(errors.ValidationError): api.tag_api.create_tag( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) -@pytest.mark.parametrize('field', ['implications', 'suggestions']) +@pytest.mark.parametrize("field", ["implications", "suggestions"]) def test_omitting_optional_field( - tag_factory, user_factory, context_factory, field): + tag_factory, user_factory, context_factory, field +): params = { - 'names': ['tag1', 'tag2'], - 'category': 'meta', - 'suggestions': [], - 'implications': [], + "names": ["tag1", "tag2"], + "category": "meta", + "suggestions": [], + "implications": [], } del params[field] - with patch('szurubooru.func.tags.create_tag'), \ - patch('szurubooru.func.tags.serialize_tag'): + with patch("szurubooru.func.tags.create_tag"), patch( + "szurubooru.func.tags.serialize_tag" + ), patch("szurubooru.func.snapshots._post_to_webhooks"): tags.create_tag.return_value = tag_factory() api.tag_api.create_tag( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_create_tag_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.tag_api.create_tag( context_factory( params={ - 'names': ['tag'], - 'category': 'meta', - 'suggestions': ['tag'], - 'implications': [], + "names": ["tag"], + "category": "meta", + "suggestions": ["tag"], + "implications": [], }, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_tag_deleting.py b/server/szurubooru/tests/api/test_tag_deleting.py index a0367f2..59a19f8 100644 --- a/server/szurubooru/tests/api/test_tag_deleting.py +++ b/server/szurubooru/tests/api/test_tag_deleting.py @@ -1,60 +1,71 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tags, snapshots + +from szurubooru import api, db, errors, model +from szurubooru.func import snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'tags:delete': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"tags:delete": model.User.RANK_REGULAR}}) def test_deleting(user_factory, tag_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - tag = tag_factory(names=['tag']) + tag = tag_factory(names=["tag"]) db.session.add(tag) db.session.commit() - with patch('szurubooru.func.snapshots.delete'): + with patch("szurubooru.func.snapshots.delete"): result = api.tag_api.delete_tag( - context_factory(params={'version': 1}, user=auth_user), - {'tag_name': 'tag'}) + context_factory(params={"version": 1}, user=auth_user), + {"tag_name": "tag"}, + ) assert result == {} assert db.session.query(model.Tag).count() == 0 snapshots.delete.assert_called_once_with(tag, auth_user) def test_deleting_used( - user_factory, tag_factory, context_factory, post_factory): - tag = tag_factory(names=['tag']) + user_factory, tag_factory, context_factory, post_factory +): + tag = tag_factory(names=["tag"]) post = post_factory() post.tags.append(tag) db.session.add_all([tag, post]) db.session.commit() - api.tag_api.delete_tag( - context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) - db.session.refresh(post) - assert db.session.query(model.Tag).count() == 0 - assert post.tags == [] + with patch("szurubooru.func.snapshots._post_to_webhooks"): + api.tag_api.delete_tag( + context_factory( + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"tag_name": "tag"}, + ) + db.session.refresh(post) + assert db.session.query(model.Tag).count() == 0 + assert post.tags == [] def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(tags.TagNotFoundError): api.tag_api.delete_tag( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'bad'}) + {"tag_name": "bad"}, + ) def test_trying_to_delete_without_privileges( - user_factory, tag_factory, context_factory): - db.session.add(tag_factory(names=['tag'])) + user_factory, tag_factory, context_factory +): + db.session.add(tag_factory(names=["tag"])) db.session.commit() with pytest.raises(errors.AuthError): api.tag_api.delete_tag( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'tag_name': 'tag'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"tag_name": "tag"}, + ) assert db.session.query(model.Tag).count() == 1 diff --git a/server/szurubooru/tests/api/test_tag_merging.py b/server/szurubooru/tests/api/test_tag_merging.py index 671e2e4..84c13d3 100644 --- a/server/szurubooru/tests/api/test_tag_merging.py +++ b/server/szurubooru/tests/api/test_tag_merging.py @@ -1,18 +1,20 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tags, snapshots + +from szurubooru import api, db, errors, model +from szurubooru.func import snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'tags:merge': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"tags:merge": model.User.RANK_REGULAR}}) def test_merging(user_factory, tag_factory, context_factory, post_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) db.session.add_all([source_tag, target_tag]) db.session.flush() assert source_tag.post_count == 0 @@ -23,76 +25,94 @@ def test_merging(user_factory, tag_factory, context_factory, post_factory): db.session.commit() assert source_tag.post_count == 1 assert target_tag.post_count == 0 - with patch('szurubooru.func.tags.serialize_tag'), \ - patch('szurubooru.func.tags.merge_tags'), \ - patch('szurubooru.func.snapshots.merge'): + with patch("szurubooru.func.tags.serialize_tag"), patch( + "szurubooru.func.tags.merge_tags" + ), patch("szurubooru.func.snapshots.merge"): api.tag_api.merge_tags( context_factory( params={ - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': 'source', - 'mergeTo': 'target', + "removeVersion": 1, + "mergeToVersion": 1, + "remove": "source", + "mergeTo": "target", }, - user=auth_user)) + user=auth_user, + ) + ) tags.merge_tags.called_once_with(source_tag, target_tag) snapshots.merge.assert_called_once_with( - source_tag, target_tag, auth_user) + source_tag, target_tag, auth_user + ) @pytest.mark.parametrize( - 'field', ['remove', 'mergeTo', 'removeVersion', 'mergeToVersion']) + "field", ["remove", "mergeTo", "removeVersion", "mergeToVersion"] +) def test_trying_to_omit_mandatory_field( - user_factory, tag_factory, context_factory, field): - db.session.add_all([ - tag_factory(names=['source']), - tag_factory(names=['target']), - ]) + user_factory, tag_factory, context_factory, field +): + db.session.add_all( + [ + tag_factory(names=["source"]), + tag_factory(names=["target"]), + ] + ) db.session.commit() params = { - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': 'source', - 'mergeTo': 'target', + "removeVersion": 1, + "mergeToVersion": 1, + "remove": "source", + "mergeTo": "target", } del params[field] with pytest.raises(errors.ValidationError): api.tag_api.merge_tags( context_factory( - params=params, - user=user_factory(rank=model.User.RANK_REGULAR))) + params=params, user=user_factory(rank=model.User.RANK_REGULAR) + ) + ) def test_trying_to_merge_non_existing( - user_factory, tag_factory, context_factory): - db.session.add(tag_factory(names=['good'])) + user_factory, tag_factory, context_factory +): + db.session.add(tag_factory(names=["good"])) db.session.commit() with pytest.raises(tags.TagNotFoundError): api.tag_api.merge_tags( context_factory( - params={'remove': 'good', 'mergeTo': 'bad'}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"remove": "good", "mergeTo": "bad"}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) with pytest.raises(tags.TagNotFoundError): api.tag_api.merge_tags( context_factory( - params={'remove': 'bad', 'mergeTo': 'good'}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"remove": "bad", "mergeTo": "good"}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) def test_trying_to_merge_without_privileges( - user_factory, tag_factory, context_factory): - db.session.add_all([ - tag_factory(names=['source']), - tag_factory(names=['target']), - ]) + user_factory, tag_factory, context_factory +): + db.session.add_all( + [ + tag_factory(names=["source"]), + tag_factory(names=["target"]), + ] + ) db.session.commit() with pytest.raises(errors.AuthError): api.tag_api.merge_tags( context_factory( params={ - 'removeVersion': 1, - 'mergeToVersion': 1, - 'remove': 'source', - 'mergeTo': 'target', + "removeVersion": 1, + "mergeToVersion": 1, + "remove": "source", + "mergeTo": "target", }, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_tag_retrieving.py b/server/szurubooru/tests/api/test_tag_retrieving.py index 43a0766..8b51de5 100644 --- a/server/szurubooru/tests/api/test_tag_retrieving.py +++ b/server/szurubooru/tests/api/test_tag_retrieving.py @@ -1,72 +1,82 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'tags:list': model.User.RANK_REGULAR, - 'tags:view': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "tags:list": model.User.RANK_REGULAR, + "tags:view": model.User.RANK_REGULAR, + }, + } + ) def test_retrieving_multiple(user_factory, tag_factory, context_factory): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) - db.session.add_all([tag1, tag2]) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) + db.session.add_all([tag2, tag1]) db.session.flush() - with patch('szurubooru.func.tags.serialize_tag'): - tags.serialize_tag.return_value = 'serialized tag' + with patch("szurubooru.func.tags.serialize_tag"): + tags.serialize_tag.return_value = "serialized tag" result = api.tag_api.get_tags( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) assert result == { - 'query': '', - 'offset': 0, - 'limit': 100, - 'total': 2, - 'results': ['serialized tag', 'serialized tag'], + "query": "", + "offset": 0, + "limit": 100, + "total": 2, + "results": ["serialized tag", "serialized tag"], } def test_trying_to_retrieve_multiple_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.tag_api.get_tags( context_factory( - params={'query': '', 'offset': 0}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "", "offset": 0}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_retrieving_single(user_factory, tag_factory, context_factory): - db.session.add(tag_factory(names=['tag'])) + db.session.add(tag_factory(names=["tag"])) db.session.flush() - with patch('szurubooru.func.tags.serialize_tag'): - tags.serialize_tag.return_value = 'serialized tag' + with patch("szurubooru.func.tags.serialize_tag"): + tags.serialize_tag.return_value = "serialized tag" result = api.tag_api.get_tag( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) - assert result == 'serialized tag' + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"tag_name": "tag"}, + ) + assert result == "serialized tag" def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): with pytest.raises(tags.TagNotFoundError): api.tag_api.get_tag( - context_factory( - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': '-'}) + context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), + {"tag_name": "-"}, + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.tag_api.get_tag( - context_factory( - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'tag_name': '-'}) + context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), + {"tag_name": "-"}, + ) diff --git a/server/szurubooru/tests/api/test_tag_siblings_retrieving.py b/server/szurubooru/tests/api/test_tag_siblings_retrieving.py index fc2f5aa..7453b98 100644 --- a/server/szurubooru/tests/api/test_tag_siblings_retrieving.py +++ b/server/szurubooru/tests/api/test_tag_siblings_retrieving.py @@ -1,37 +1,43 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'tags:view': model.User.RANK_REGULAR}}) + config_injector({"privileges": {"tags:view": model.User.RANK_REGULAR}}) def test_get_tag_siblings(user_factory, tag_factory, context_factory): - db.session.add(tag_factory(names=['tag'])) + db.session.add(tag_factory(names=["tag"])) db.session.flush() - with patch('szurubooru.func.tags.serialize_tag'), \ - patch('szurubooru.func.tags.get_tag_siblings'): - tags.serialize_tag.side_effect = lambda tag, *args, **kwargs: \ - 'serialized tag %s' % tag.names[0].name + with patch("szurubooru.func.tags.serialize_tag"), patch( + "szurubooru.func.tags.get_tag_siblings" + ): + tags.serialize_tag.side_effect = ( + lambda tag, *args, **kwargs: "serialized tag %s" + % tag.names[0].name + ) tags.get_tag_siblings.return_value = [ - (tag_factory(names=['sib1']), 1), - (tag_factory(names=['sib2']), 3), + (tag_factory(names=["sib1"]), 1), + (tag_factory(names=["sib2"]), 3), ] result = api.tag_api.get_tag_siblings( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) + {"tag_name": "tag"}, + ) assert result == { - 'results': [ + "results": [ { - 'tag': 'serialized tag sib1', - 'occurrences': 1, + "tag": "serialized tag sib1", + "occurrences": 1, }, { - 'tag': 'serialized tag sib2', - 'occurrences': 3, + "tag": "serialized tag sib2", + "occurrences": 3, }, ], } @@ -41,11 +47,13 @@ def test_trying_to_retrieve_non_existing(user_factory, context_factory): with pytest.raises(tags.TagNotFoundError): api.tag_api.get_tag_siblings( context_factory(user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': '-'}) + {"tag_name": "-"}, + ) def test_trying_to_retrieve_without_privileges(user_factory, context_factory): with pytest.raises(errors.AuthError): api.tag_api.get_tag_siblings( context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'tag_name': '-'}) + {"tag_name": "-"}, + ) diff --git a/server/szurubooru/tests/api/test_tag_updating.py b/server/szurubooru/tests/api/test_tag_updating.py index d5d59c9..9112c29 100644 --- a/server/szurubooru/tests/api/test_tag_updating.py +++ b/server/szurubooru/tests/api/test_tag_updating.py @@ -1,163 +1,203 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors -from szurubooru.func import tags, snapshots, metrics + +from szurubooru import api, db, errors, model +from szurubooru.func import metrics, snapshots, tags @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'tags:create': model.User.RANK_REGULAR, - 'tags:edit:names': model.User.RANK_REGULAR, - 'tags:edit:category': model.User.RANK_REGULAR, - 'tags:edit:description': model.User.RANK_REGULAR, - 'tags:edit:suggestions': model.User.RANK_REGULAR, - 'tags:edit:implications': model.User.RANK_REGULAR, - 'metrics:create': model.User.RANK_REGULAR, - 'metrics:edit:bounds': model.User.RANK_REGULAR, - }, - }) + config_injector( + { + "privileges": { + "tags:create": model.User.RANK_REGULAR, + "tags:edit:names": model.User.RANK_REGULAR, + "tags:edit:category": model.User.RANK_REGULAR, + "tags:edit:description": model.User.RANK_REGULAR, + "tags:edit:suggestions": model.User.RANK_REGULAR, + "tags:edit:implications": model.User.RANK_REGULAR, + "metrics:create": model.User.RANK_REGULAR, + "metrics:edit:bounds": model.User.RANK_REGULAR, + }, + } + ) def test_simple_updating(user_factory, tag_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) - tag = tag_factory(names=['tag1', 'tag2']) + tag = tag_factory(names=["tag1", "tag2"]) db.session.add(tag) db.session.commit() - with patch('szurubooru.func.tags.create_tag'), \ - patch('szurubooru.func.tags.get_or_create_tags_by_names'), \ - patch('szurubooru.func.tags.update_tag_names'), \ - patch('szurubooru.func.tags.update_tag_category_name'), \ - patch('szurubooru.func.tags.update_tag_description'), \ - patch('szurubooru.func.tags.update_tag_suggestions'), \ - patch('szurubooru.func.tags.update_tag_implications'), \ - patch('szurubooru.func.tags.serialize_tag'), \ - patch('szurubooru.func.metrics.update_or_create_metric'), \ - patch('szurubooru.func.snapshots.modify'): + with patch("szurubooru.func.tags.create_tag"), patch( + "szurubooru.func.tags.get_or_create_tags_by_names" + ), patch("szurubooru.func.tags.update_tag_names"), patch( + "szurubooru.func.tags.update_tag_category_name" + ), patch( + "szurubooru.func.tags.update_tag_description" + ), patch( + "szurubooru.func.tags.update_tag_suggestions" + ), patch( + "szurubooru.func.tags.update_tag_implications" + ), patch( + "szurubooru.func.tags.serialize_tag" + ), patch( + "szurubooru.func.metrics.update_or_create_metric" + ), patch( + "szurubooru.func.snapshots.modify" + ): tags.get_or_create_tags_by_names.return_value = ([], []) - tags.serialize_tag.return_value = 'serialized tag' + tags.serialize_tag.return_value = "serialized tag" result = api.tag_api.update_tag( context_factory( params={ - 'version': 1, - 'names': ['tag3'], - 'category': 'character', - 'description': 'desc', - 'suggestions': ['sug1', 'sug2'], - 'implications': ['imp1', 'imp2'], - 'metric': {'min': -1, 'max': 1}, + "version": 1, + "names": ["tag3"], + "category": "character", + "description": "desc", + "suggestions": ["sug1", "sug2"], + "implications": ["imp1", "imp2"], + "metric": {"min": -1, "max": 1}, }, - user=auth_user), - {'tag_name': 'tag1'}) - assert result == 'serialized tag' + user=auth_user, + ), + {"tag_name": "tag1"}, + ) + assert result == "serialized tag" tags.create_tag.assert_not_called() - tags.update_tag_names.assert_called_once_with(tag, ['tag3']) - tags.update_tag_category_name.assert_called_once_with(tag, 'character') - tags.update_tag_description.assert_called_once_with(tag, 'desc') + tags.update_tag_names.assert_called_once_with(tag, ["tag3"]) + tags.update_tag_category_name.assert_called_once_with(tag, "character") + tags.update_tag_description.assert_called_once_with(tag, "desc") tags.update_tag_suggestions.assert_called_once_with( - tag, ['sug1', 'sug2']) + tag, ["sug1", "sug2"] + ) tags.update_tag_implications.assert_called_once_with( - tag, ['imp1', 'imp2']) + tag, ["imp1", "imp2"] + ) tags.serialize_tag.assert_called_once_with(tag, options=[]) metrics.update_or_create_metric.assert_called_once_with( - tag, {'min': -1, 'max': 1}) + tag, {"min": -1, "max": 1}) snapshots.modify.assert_called_once_with(tag, auth_user) @pytest.mark.parametrize( - 'field', [ - 'names', - 'category', - 'description', - 'implications', - 'suggestions', - ]) + "field", + [ + "names", + "category", + "description", + "implications", + "suggestions", + ], +) def test_omitting_optional_field( - user_factory, tag_factory, context_factory, field): - db.session.add(tag_factory(names=['tag'])) + user_factory, tag_factory, context_factory, field +): + db.session.add(tag_factory(names=["tag"])) db.session.commit() params = { - 'names': ['tag1', 'tag2'], - 'category': 'meta', - 'description': 'desc', - 'suggestions': [], - 'implications': [], + "names": ["tag1", "tag2"], + "category": "meta", + "description": "desc", + "suggestions": [], + "implications": [], } del params[field] - with patch('szurubooru.func.tags.create_tag'), \ - patch('szurubooru.func.tags.update_tag_names'), \ - patch('szurubooru.func.tags.update_tag_category_name'), \ - patch('szurubooru.func.tags.serialize_tag'): + with patch("szurubooru.func.tags.create_tag"), patch( + "szurubooru.func.tags.update_tag_names" + ), patch("szurubooru.func.tags.update_tag_category_name"), patch( + "szurubooru.func.tags.serialize_tag" + ): api.tag_api.update_tag( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"tag_name": "tag"}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): with pytest.raises(tags.TagNotFoundError): api.tag_api.update_tag( context_factory( - params={'names': ['dummy']}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag1'}) + params={"names": ["dummy"]}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"tag_name": "tag1"}, + ) -@pytest.mark.parametrize('params', [ - {'names': 'whatever'}, - {'category': 'whatever'}, - {'suggestions': ['whatever']}, - {'implications': ['whatever']}, - {'metric': ['whatever']}, -]) +@pytest.mark.parametrize( + "params", + [ + {"names": "whatever"}, + {"category": "whatever"}, + {"suggestions": ["whatever"]}, + {"implications": ["whatever"]}, + {"metric": ["whatever"]}, + ], +) def test_trying_to_update_without_privileges( - user_factory, tag_factory, context_factory, params): - db.session.add(tag_factory(names=['tag'])) + user_factory, tag_factory, context_factory, params +): + db.session.add(tag_factory(names=["tag"])) db.session.commit() with pytest.raises(errors.AuthError): api.tag_api.update_tag( context_factory( - params={**params, **{'version': 1}}, - user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'tag_name': 'tag'}) + params={**params, **{"version": 1}}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ), + {"tag_name": "tag"}, + ) def test_trying_to_create_metric_without_privileges( - user_factory, tag_factory, context_factory): - db.session.add(tag_factory(names=['tag'])) + user_factory, tag_factory, context_factory +): + db.session.add(tag_factory(names=["tag"])) db.session.commit() with pytest.raises(errors.AuthError): api.tag_api.update_tag( context_factory( - params={'metric': {'min': 0, 'max': 10}, **{'version': 1}}, + params={"metric": {"min": 0, "max": 10}, **{"version": 1}}, user=user_factory(rank=model.User.RANK_ANONYMOUS)), - {'tag_name': 'tag'}) + {"tag_name": "tag"} + ) def test_trying_to_create_tags_without_privileges( - config_injector, context_factory, tag_factory, user_factory): - tag = tag_factory(names=['tag']) + config_injector, context_factory, tag_factory, user_factory +): + tag = tag_factory(names=["tag"]) db.session.add(tag) db.session.commit() - config_injector({'privileges': { - 'tags:create': model.User.RANK_ADMINISTRATOR, - 'tags:edit:suggestions': model.User.RANK_REGULAR, - 'tags:edit:implications': model.User.RANK_REGULAR, - }}) - with patch('szurubooru.func.tags.get_or_create_tags_by_names'): - tags.get_or_create_tags_by_names.return_value = ([], ['new-tag']) + config_injector( + { + "privileges": { + "tags:create": model.User.RANK_ADMINISTRATOR, + "tags:edit:suggestions": model.User.RANK_REGULAR, + "tags:edit:implications": model.User.RANK_REGULAR, + } + } + ) + with patch("szurubooru.func.tags.get_or_create_tags_by_names"): + tags.get_or_create_tags_by_names.return_value = ([], ["new-tag"]) with pytest.raises(errors.AuthError): api.tag_api.update_tag( context_factory( - params={'suggestions': ['tag1', 'tag2'], 'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) + params={"suggestions": ["tag1", "tag2"], "version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"tag_name": "tag"}, + ) db.session.rollback() with pytest.raises(errors.AuthError): api.tag_api.update_tag( context_factory( - params={'implications': ['tag1', 'tag2'], 'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'tag_name': 'tag'}) + params={"implications": ["tag1", "tag2"], "version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"tag_name": "tag"}, + ) diff --git a/server/szurubooru/tests/api/test_user_creating.py b/server/szurubooru/tests/api/test_user_creating.py index 699bfef..d55e1f7 100644 --- a/server/szurubooru/tests/api/test_user_creating.py +++ b/server/szurubooru/tests/api/test_user_creating.py @@ -1,87 +1,104 @@ from unittest.mock import patch + import pytest -from szurubooru import api, model, errors + +from szurubooru import api, errors, model from szurubooru.func import users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'users:create:self': 'regular'}}) + config_injector({"privileges": {"users:create:self": "regular"}}) def test_creating_user(user_factory, context_factory, fake_datetime): user = user_factory() - with patch('szurubooru.func.users.create_user'), \ - patch('szurubooru.func.users.update_user_name'), \ - patch('szurubooru.func.users.update_user_password'), \ - patch('szurubooru.func.users.update_user_email'), \ - patch('szurubooru.func.users.update_user_rank'), \ - patch('szurubooru.func.users.update_user_avatar'), \ - patch('szurubooru.func.users.serialize_user'), \ - fake_datetime('1969-02-12'): - users.serialize_user.return_value = 'serialized user' + with patch("szurubooru.func.users.create_user"), patch( + "szurubooru.func.users.update_user_name" + ), patch("szurubooru.func.users.update_user_password"), patch( + "szurubooru.func.users.update_user_email" + ), patch( + "szurubooru.func.users.update_user_rank" + ), patch( + "szurubooru.func.users.update_user_avatar" + ), patch( + "szurubooru.func.users.serialize_user" + ), fake_datetime( + "1969-02-12" + ): + users.serialize_user.return_value = "serialized user" users.create_user.return_value = user result = api.user_api.create_user( context_factory( params={ - 'name': 'chewie1', - 'email': 'asd@asd.asd', - 'password': 'oks', - 'rank': 'moderator', - 'avatarStyle': 'manual', + "name": "chewie1", + "email": "asd@asd.asd", + "password": "oks", + "rank": "moderator", + "avatarStyle": "manual", }, - files={'avatar': b'...'}, - user=user_factory(rank=model.User.RANK_REGULAR))) - assert result == 'serialized user' + files={"avatar": b"..."}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) + assert result == "serialized user" users.create_user.assert_called_once_with( - 'chewie1', 'oks', 'asd@asd.asd') + "chewie1", "oks", "asd@asd.asd" + ) assert not users.update_user_name.called assert not users.update_user_password.called assert not users.update_user_email.called - users.update_user_rank.called_once_with(user, 'moderator') - users.update_user_avatar.called_once_with(user, 'manual', b'...') + users.update_user_rank.called_once_with(user, "moderator") + users.update_user_avatar.called_once_with(user, "manual", b"...") -@pytest.mark.parametrize('field', ['name', 'password']) +@pytest.mark.parametrize("field", ["name", "password"]) def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): params = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', + "name": "chewie", + "email": "asd@asd.asd", + "password": "oks", } user = user_factory() auth_user = user_factory(rank=model.User.RANK_REGULAR) del params[field] - with patch('szurubooru.func.users.create_user'), \ - pytest.raises(errors.MissingRequiredParameterError): + with patch("szurubooru.func.users.create_user"), pytest.raises( + errors.MissingRequiredParameterError + ): users.create_user.return_value = user api.user_api.create_user( - context_factory(params=params, user=auth_user)) + context_factory(params=params, user=auth_user) + ) -@pytest.mark.parametrize('field', ['rank', 'email', 'avatarStyle']) +@pytest.mark.parametrize("field", ["rank", "email", "avatarStyle"]) def test_omitting_optional_field(user_factory, context_factory, field): params = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - 'rank': 'moderator', - 'avatarStyle': 'gravatar', + "name": "chewie", + "email": "asd@asd.asd", + "password": "oks", + "rank": "moderator", + "avatarStyle": "gravatar", } del params[field] user = user_factory() auth_user = user_factory(rank=model.User.RANK_MODERATOR) - with patch('szurubooru.func.users.create_user'), \ - patch('szurubooru.func.users.update_user_avatar'), \ - patch('szurubooru.func.users.serialize_user'): + with patch("szurubooru.func.users.create_user"), patch( + "szurubooru.func.users.update_user_avatar" + ), patch("szurubooru.func.users.serialize_user"): users.create_user.return_value = user api.user_api.create_user( - context_factory(params=params, user=auth_user)) + context_factory(params=params, user=auth_user) + ) def test_trying_to_create_user_without_privileges( - context_factory, user_factory): + context_factory, user_factory +): with pytest.raises(errors.AuthError): - api.user_api.create_user(context_factory( - params='whatever', - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + api.user_api.create_user( + context_factory( + params="whatever", + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) diff --git a/server/szurubooru/tests/api/test_user_deleting.py b/server/szurubooru/tests/api/test_user_deleting.py index 2bd53e2..6ab3f1d 100644 --- a/server/szurubooru/tests/api/test_user_deleting.py +++ b/server/szurubooru/tests/api/test_user_deleting.py @@ -1,50 +1,55 @@ import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'users:delete:self': model.User.RANK_REGULAR, - 'users:delete:any': model.User.RANK_MODERATOR, - }, - }) + config_injector( + { + "privileges": { + "users:delete:self": model.User.RANK_REGULAR, + "users:delete:any": model.User.RANK_MODERATOR, + }, + } + ) def test_deleting_oneself(user_factory, context_factory): - user = user_factory(name='u', rank=model.User.RANK_REGULAR) + user = user_factory(name="u", rank=model.User.RANK_REGULAR) db.session.add(user) db.session.commit() result = api.user_api.delete_user( - context_factory( - params={'version': 1}, user=user), {'user_name': 'u'}) + context_factory(params={"version": 1}, user=user), {"user_name": "u"} + ) assert result == {} assert db.session.query(model.User).count() == 0 def test_deleting_someone_else(user_factory, context_factory): - user1 = user_factory(name='u1', rank=model.User.RANK_REGULAR) - user2 = user_factory(name='u2', rank=model.User.RANK_MODERATOR) + user1 = user_factory(name="u1", rank=model.User.RANK_REGULAR) + user2 = user_factory(name="u2", rank=model.User.RANK_MODERATOR) db.session.add_all([user1, user2]) db.session.commit() api.user_api.delete_user( - context_factory( - params={'version': 1}, user=user2), {'user_name': 'u1'}) + context_factory(params={"version": 1}, user=user2), {"user_name": "u1"} + ) assert db.session.query(model.User).count() == 1 def test_trying_to_delete_someone_else_without_privileges( - user_factory, context_factory): - user1 = user_factory(name='u1', rank=model.User.RANK_REGULAR) - user2 = user_factory(name='u2', rank=model.User.RANK_REGULAR) + user_factory, context_factory +): + user1 = user_factory(name="u1", rank=model.User.RANK_REGULAR) + user2 = user_factory(name="u2", rank=model.User.RANK_REGULAR) db.session.add_all([user1, user2]) db.session.commit() with pytest.raises(errors.AuthError): api.user_api.delete_user( - context_factory( - params={'version': 1}, user=user2), {'user_name': 'u1'}) + context_factory(params={"version": 1}, user=user2), + {"user_name": "u1"}, + ) assert db.session.query(model.User).count() == 2 @@ -52,6 +57,8 @@ def test_trying_to_delete_non_existing(user_factory, context_factory): with pytest.raises(users.UserNotFoundError): api.user_api.delete_user( context_factory( - params={'version': 1}, - user=user_factory(rank=model.User.RANK_REGULAR)), - {'user_name': 'bad'}) + params={"version": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ), + {"user_name": "bad"}, + ) diff --git a/server/szurubooru/tests/api/test_user_retrieving.py b/server/szurubooru/tests/api/test_user_retrieving.py index 2e797e8..b18b4d5 100644 --- a/server/szurubooru/tests/api/test_user_retrieving.py +++ b/server/szurubooru/tests/api/test_user_retrieving.py @@ -1,73 +1,86 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'users:list': model.User.RANK_REGULAR, - 'users:view': model.User.RANK_REGULAR, - 'users:edit:any:email': model.User.RANK_MODERATOR, - }, - }) + config_injector( + { + "privileges": { + "users:list": model.User.RANK_REGULAR, + "users:view": model.User.RANK_REGULAR, + "users:edit:any:email": model.User.RANK_MODERATOR, + }, + } + ) def test_retrieving_multiple(user_factory, context_factory): - user1 = user_factory(name='u1', rank=model.User.RANK_MODERATOR) - user2 = user_factory(name='u2', rank=model.User.RANK_MODERATOR) + user1 = user_factory(name="u1", rank=model.User.RANK_MODERATOR) + user2 = user_factory(name="u2", rank=model.User.RANK_MODERATOR) db.session.add_all([user1, user2]) db.session.flush() - with patch('szurubooru.func.users.serialize_user'): - users.serialize_user.return_value = 'serialized user' + with patch("szurubooru.func.users.serialize_user"): + users.serialize_user.return_value = "serialized user" result = api.user_api.get_users( context_factory( - params={'query': '', 'page': 1}, - user=user_factory(rank=model.User.RANK_REGULAR))) + params={"query": "", "page": 1}, + user=user_factory(rank=model.User.RANK_REGULAR), + ) + ) assert result == { - 'query': '', - 'offset': 0, - 'limit': 100, - 'total': 2, - 'results': ['serialized user', 'serialized user'], + "query": "", + "offset": 0, + "limit": 100, + "total": 2, + "results": ["serialized user", "serialized user"], } def test_trying_to_retrieve_multiple_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): with pytest.raises(errors.AuthError): api.user_api.get_users( context_factory( - params={'query': '', 'page': 1}, - user=user_factory(rank=model.User.RANK_ANONYMOUS))) + params={"query": "", "page": 1}, + user=user_factory(rank=model.User.RANK_ANONYMOUS), + ) + ) def test_retrieving_single(user_factory, context_factory): - user = user_factory(name='u1', rank=model.User.RANK_REGULAR) + user = user_factory(name="u1", rank=model.User.RANK_REGULAR) auth_user = user_factory(rank=model.User.RANK_REGULAR) db.session.add(user) db.session.flush() - with patch('szurubooru.func.users.serialize_user'): - users.serialize_user.return_value = 'serialized user' + with patch("szurubooru.func.users.serialize_user"): + users.serialize_user.return_value = "serialized user" result = api.user_api.get_user( - context_factory(user=auth_user), {'user_name': 'u1'}) - assert result == 'serialized user' + context_factory(user=auth_user), {"user_name": "u1"} + ) + assert result == "serialized user" def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): auth_user = user_factory(rank=model.User.RANK_REGULAR) with pytest.raises(users.UserNotFoundError): api.user_api.get_user( - context_factory(user=auth_user), {'user_name': '-'}) + context_factory(user=auth_user), {"user_name": "-"} + ) def test_trying_to_retrieve_single_without_privileges( - user_factory, context_factory): + user_factory, context_factory +): auth_user = user_factory(rank=model.User.RANK_ANONYMOUS) - db.session.add(user_factory(name='u1', rank=model.User.RANK_REGULAR)) + db.session.add(user_factory(name="u1", rank=model.User.RANK_REGULAR)) db.session.flush() with pytest.raises(errors.AuthError): api.user_api.get_user( - context_factory(user=auth_user), {'user_name': 'u1'}) + context_factory(user=auth_user), {"user_name": "u1"} + ) diff --git a/server/szurubooru/tests/api/test_user_token_creating.py b/server/szurubooru/tests/api/test_user_token_creating.py index f550f63..bce41ec 100644 --- a/server/szurubooru/tests/api/test_user_token_creating.py +++ b/server/szurubooru/tests/api/test_user_token_creating.py @@ -1,29 +1,33 @@ from unittest.mock import patch + import pytest + from szurubooru import api from szurubooru.func import user_tokens, users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'user_tokens:create:self': 'regular'}}) + config_injector({"privileges": {"user_tokens:create:self": "regular"}}) def test_creating_user_token( - user_token_factory, context_factory, fake_datetime): + user_token_factory, context_factory, fake_datetime +): user_token = user_token_factory() - with patch('szurubooru.func.user_tokens.create_user_token'), \ - patch('szurubooru.func.user_tokens.serialize_user_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - fake_datetime('1969-02-12'): + with patch("szurubooru.func.user_tokens.create_user_token"), patch( + "szurubooru.func.user_tokens.serialize_user_token" + ), patch("szurubooru.func.users.get_user_by_name"), fake_datetime( + "1969-02-12" + ): users.get_user_by_name.return_value = user_token.user - user_tokens.serialize_user_token.return_value = 'serialized user token' + user_tokens.serialize_user_token.return_value = "serialized user token" user_tokens.create_user_token.return_value = user_token result = api.user_token_api.create_user_token( context_factory(user=user_token.user), - { - 'user_name': user_token.user.name - }) - assert result == 'serialized user token' + {"user_name": user_token.user.name}, + ) + assert result == "serialized user token" user_tokens.create_user_token.assert_called_once_with( - user_token.user, True) + user_token.user, True + ) diff --git a/server/szurubooru/tests/api/test_user_token_deleting.py b/server/szurubooru/tests/api/test_user_token_deleting.py index 8534152..19f93fa 100644 --- a/server/szurubooru/tests/api/test_user_token_deleting.py +++ b/server/szurubooru/tests/api/test_user_token_deleting.py @@ -1,30 +1,35 @@ from unittest.mock import patch + import pytest + from szurubooru import api, db from szurubooru.func import user_tokens, users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'user_tokens:delete:self': 'regular'}}) + config_injector({"privileges": {"user_tokens:delete:self": "regular"}}) def test_deleting_user_token( - user_token_factory, context_factory, fake_datetime): + user_token_factory, context_factory, fake_datetime +): user_token = user_token_factory() db.session.add(user_token) db.session.commit() - with patch('szurubooru.func.user_tokens.get_by_user_and_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - fake_datetime('1969-02-12'): + with patch("szurubooru.func.user_tokens.get_by_user_and_token"), patch( + "szurubooru.func.users.get_user_by_name" + ), fake_datetime("1969-02-12"): users.get_user_by_name.return_value = user_token.user user_tokens.get_by_user_and_token.return_value = user_token result = api.user_token_api.delete_user_token( context_factory(user=user_token.user), { - 'user_name': user_token.user.name, - 'user_token': user_token.token - }) + "user_name": user_token.user.name, + "user_token": user_token.token, + }, + ) assert result == {} user_tokens.get_by_user_and_token.assert_called_once_with( - user_token.user, user_token.token) + user_token.user, user_token.token + ) diff --git a/server/szurubooru/tests/api/test_user_token_retrieving.py b/server/szurubooru/tests/api/test_user_token_retrieving.py index 01b2534..e3351b4 100644 --- a/server/szurubooru/tests/api/test_user_token_retrieving.py +++ b/server/szurubooru/tests/api/test_user_token_retrieving.py @@ -1,31 +1,37 @@ from unittest.mock import patch + import pytest + from szurubooru import api from szurubooru.func import user_tokens, users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'user_tokens:list:self': 'regular'}}) + config_injector({"privileges": {"user_tokens:list:self": "regular"}}) def test_retrieving_user_tokens( - user_token_factory, context_factory, fake_datetime): + user_token_factory, context_factory, fake_datetime +): user_token1 = user_token_factory() user_token2 = user_token_factory(user=user_token1.user) user_token3 = user_token_factory(user=user_token1.user) - with patch('szurubooru.func.user_tokens.get_user_tokens'), \ - patch('szurubooru.func.user_tokens.serialize_user_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - fake_datetime('1969-02-12'): + with patch("szurubooru.func.user_tokens.get_user_tokens"), patch( + "szurubooru.func.user_tokens.serialize_user_token" + ), patch("szurubooru.func.users.get_user_by_name"), fake_datetime( + "1969-02-12" + ): users.get_user_by_name.return_value = user_token1.user - user_tokens.serialize_user_token.return_value = 'serialized user token' - user_tokens.get_user_tokens.return_value = [user_token1, user_token2, - user_token3] + user_tokens.serialize_user_token.return_value = "serialized user token" + user_tokens.get_user_tokens.return_value = [ + user_token1, + user_token2, + user_token3, + ] result = api.user_token_api.get_user_tokens( context_factory(user=user_token1.user), - { - 'user_name': user_token1.user.name - }) - assert result == {'results': ['serialized user token'] * 3} + {"user_name": user_token1.user.name}, + ) + assert result == {"results": ["serialized user token"] * 3} user_tokens.get_user_tokens.assert_called_once_with(user_token1.user) diff --git a/server/szurubooru/tests/api/test_user_token_updating.py b/server/szurubooru/tests/api/test_user_token_updating.py index bf725a3..3f66041 100644 --- a/server/szurubooru/tests/api/test_user_token_updating.py +++ b/server/szurubooru/tests/api/test_user_token_updating.py @@ -1,42 +1,52 @@ from unittest.mock import patch + import pytest + from szurubooru import api, db from szurubooru.func import user_tokens, users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'privileges': {'user_tokens:edit:self': 'regular'}}) + config_injector({"privileges": {"user_tokens:edit:self": "regular"}}) def test_edit_user_token(user_token_factory, context_factory, fake_datetime): user_token = user_token_factory() db.session.add(user_token) db.session.commit() - with patch('szurubooru.func.user_tokens.get_by_user_and_token'), \ - patch('szurubooru.func.user_tokens.update_user_token_enabled'), \ - patch('szurubooru.func.user_tokens.update_user_token_edit_time'), \ - patch('szurubooru.func.user_tokens.serialize_user_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - fake_datetime('1969-02-12'): + with patch("szurubooru.func.user_tokens.get_by_user_and_token"), patch( + "szurubooru.func.user_tokens.update_user_token_enabled" + ), patch("szurubooru.func.user_tokens.update_user_token_edit_time"), patch( + "szurubooru.func.user_tokens.serialize_user_token" + ), patch( + "szurubooru.func.users.get_user_by_name" + ), fake_datetime( + "1969-02-12" + ): users.get_user_by_name.return_value = user_token.user - user_tokens.serialize_user_token.return_value = 'serialized user token' + user_tokens.serialize_user_token.return_value = "serialized user token" user_tokens.get_by_user_and_token.return_value = user_token result = api.user_token_api.update_user_token( context_factory( params={ - 'version': user_token.version, - 'enabled': False, + "version": user_token.version, + "enabled": False, }, - user=user_token.user), + user=user_token.user, + ), { - 'user_name': user_token.user.name, - 'user_token': user_token.token - }) - assert result == 'serialized user token' + "user_name": user_token.user.name, + "user_token": user_token.token, + }, + ) + assert result == "serialized user token" user_tokens.get_by_user_and_token.assert_called_once_with( - user_token.user, user_token.token) + user_token.user, user_token.token + ) user_tokens.update_user_token_enabled.assert_called_once_with( - user_token, False) + user_token, False + ) user_tokens.update_user_token_edit_time.assert_called_once_with( - user_token) + user_token + ) diff --git a/server/szurubooru/tests/api/test_user_updating.py b/server/szurubooru/tests/api/test_user_updating.py index af75049..304e489 100644 --- a/server/szurubooru/tests/api/test_user_updating.py +++ b/server/szurubooru/tests/api/test_user_updating.py @@ -1,125 +1,149 @@ from unittest.mock import patch + import pytest -from szurubooru import api, db, model, errors + +from szurubooru import api, db, errors, model from szurubooru.func import users @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'privileges': { - 'users:edit:self:name': model.User.RANK_REGULAR, - 'users:edit:self:pass': model.User.RANK_REGULAR, - 'users:edit:self:email': model.User.RANK_REGULAR, - 'users:edit:self:rank': model.User.RANK_MODERATOR, - 'users:edit:self:avatar': model.User.RANK_MODERATOR, - 'users:edit:any:name': model.User.RANK_MODERATOR, - 'users:edit:any:pass': model.User.RANK_MODERATOR, - 'users:edit:any:email': model.User.RANK_MODERATOR, - 'users:edit:any:rank': model.User.RANK_ADMINISTRATOR, - 'users:edit:any:avatar': model.User.RANK_ADMINISTRATOR, - }, - }) + config_injector( + { + "privileges": { + "users:edit:self:name": model.User.RANK_REGULAR, + "users:edit:self:pass": model.User.RANK_REGULAR, + "users:edit:self:email": model.User.RANK_REGULAR, + "users:edit:self:rank": model.User.RANK_MODERATOR, + "users:edit:self:avatar": model.User.RANK_MODERATOR, + "users:edit:any:name": model.User.RANK_MODERATOR, + "users:edit:any:pass": model.User.RANK_MODERATOR, + "users:edit:any:email": model.User.RANK_MODERATOR, + "users:edit:any:rank": model.User.RANK_ADMINISTRATOR, + "users:edit:any:avatar": model.User.RANK_ADMINISTRATOR, + }, + } + ) def test_updating_user(context_factory, user_factory): - user = user_factory(name='u1', rank=model.User.RANK_ADMINISTRATOR) + user = user_factory(name="u1", rank=model.User.RANK_ADMINISTRATOR) auth_user = user_factory(rank=model.User.RANK_ADMINISTRATOR) db.session.add(user) db.session.flush() - with patch('szurubooru.func.users.create_user'), \ - patch('szurubooru.func.users.update_user_name'), \ - patch('szurubooru.func.users.update_user_password'), \ - patch('szurubooru.func.users.update_user_email'), \ - patch('szurubooru.func.users.update_user_rank'), \ - patch('szurubooru.func.users.update_user_avatar'), \ - patch('szurubooru.func.users.serialize_user'): - users.serialize_user.return_value = 'serialized user' + with patch("szurubooru.func.users.create_user"), patch( + "szurubooru.func.users.update_user_name" + ), patch("szurubooru.func.users.update_user_password"), patch( + "szurubooru.func.users.update_user_email" + ), patch( + "szurubooru.func.users.update_user_rank" + ), patch( + "szurubooru.func.users.update_user_avatar" + ), patch( + "szurubooru.func.users.serialize_user" + ): + users.serialize_user.return_value = "serialized user" result = api.user_api.update_user( context_factory( params={ - 'version': 1, - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - 'rank': 'moderator', - 'avatarStyle': 'manual', + "version": 1, + "name": "chewie", + "email": "asd@asd.asd", + "password": "oks", + "rank": "moderator", + "avatarStyle": "manual", }, files={ - 'avatar': b'...', + "avatar": b"...", }, - user=auth_user), - {'user_name': 'u1'}) + user=auth_user, + ), + {"user_name": "u1"}, + ) - assert result == 'serialized user' + assert result == "serialized user" users.create_user.assert_not_called() - users.update_user_name.assert_called_once_with(user, 'chewie') - users.update_user_password.assert_called_once_with(user, 'oks') - users.update_user_email.assert_called_once_with(user, 'asd@asd.asd') + users.update_user_name.assert_called_once_with(user, "chewie") + users.update_user_password.assert_called_once_with(user, "oks") + users.update_user_email.assert_called_once_with(user, "asd@asd.asd") users.update_user_rank.assert_called_once_with( - user, 'moderator', auth_user) + user, "moderator", auth_user + ) users.update_user_avatar.assert_called_once_with( - user, 'manual', b'...') + user, "manual", b"..." + ) users.serialize_user.assert_called_once_with( - user, auth_user, options=[]) + user, auth_user, options=[] + ) @pytest.mark.parametrize( - 'field', ['name', 'email', 'password', 'rank', 'avatarStyle']) + "field", ["name", "email", "password", "rank", "avatarStyle"] +) def test_omitting_optional_field(user_factory, context_factory, field): - user = user_factory(name='u1', rank=model.User.RANK_ADMINISTRATOR) + user = user_factory(name="u1", rank=model.User.RANK_ADMINISTRATOR) db.session.add(user) db.session.flush() params = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - 'rank': 'moderator', - 'avatarStyle': 'gravatar', + "name": "chewie", + "email": "asd@asd.asd", + "password": "oks", + "rank": "moderator", + "avatarStyle": "gravatar", } del params[field] - with patch('szurubooru.func.users.create_user'), \ - patch('szurubooru.func.users.update_user_name'), \ - patch('szurubooru.func.users.update_user_password'), \ - patch('szurubooru.func.users.update_user_email'), \ - patch('szurubooru.func.users.update_user_rank'), \ - patch('szurubooru.func.users.update_user_avatar'), \ - patch('szurubooru.func.users.serialize_user'): + with patch("szurubooru.func.users.create_user"), patch( + "szurubooru.func.users.update_user_name" + ), patch("szurubooru.func.users.update_user_password"), patch( + "szurubooru.func.users.update_user_email" + ), patch( + "szurubooru.func.users.update_user_rank" + ), patch( + "szurubooru.func.users.update_user_avatar" + ), patch( + "szurubooru.func.users.serialize_user" + ): api.user_api.update_user( context_factory( - params={**params, **{'version': 1}}, - files={'avatar': b'...'}, - user=user), - {'user_name': 'u1'}) + params={**params, **{"version": 1}}, + files={"avatar": b"..."}, + user=user, + ), + {"user_name": "u1"}, + ) def test_trying_to_update_non_existing(user_factory, context_factory): - user = user_factory(name='u1', rank=model.User.RANK_ADMINISTRATOR) + user = user_factory(name="u1", rank=model.User.RANK_ADMINISTRATOR) db.session.add(user) db.session.flush() with pytest.raises(users.UserNotFoundError): api.user_api.update_user( - context_factory(user=user), {'user_name': 'u2'}) + context_factory(user=user), {"user_name": "u2"} + ) -@pytest.mark.parametrize('params', [ - {'name': 'whatever'}, - {'email': 'whatever'}, - {'rank': 'whatever'}, - {'password': 'whatever'}, - {'avatarStyle': 'whatever'}, -]) +@pytest.mark.parametrize( + "params", + [ + {"name": "whatever"}, + {"email": "whatever"}, + {"rank": "whatever"}, + {"password": "whatever"}, + {"avatarStyle": "whatever"}, + ], +) def test_trying_to_update_field_without_privileges( - user_factory, context_factory, params): - user1 = user_factory(name='u1', rank=model.User.RANK_REGULAR) - user2 = user_factory(name='u2', rank=model.User.RANK_REGULAR) + user_factory, context_factory, params +): + user1 = user_factory(name="u1", rank=model.User.RANK_REGULAR) + user2 = user_factory(name="u2", rank=model.User.RANK_REGULAR) db.session.add_all([user1, user2]) db.session.flush() with pytest.raises(errors.AuthError): api.user_api.update_user( - context_factory( - params={**params, **{'version': 1}}, - user=user1), - {'user_name': user2.name}) + context_factory(params={**params, **{"version": 1}}, user=user1), + {"user_name": user2.name}, + ) diff --git a/server/szurubooru/tests/assets/webp.webp b/server/szurubooru/tests/assets/webp.webp Binary files differnew file mode 100644 index 0000000..d116e1e --- /dev/null +++ b/server/szurubooru/tests/assets/webp.webp diff --git a/server/szurubooru/tests/conftest.py b/server/szurubooru/tests/conftest.py index d327bb2..50cbf7d 100644 --- a/server/szurubooru/tests/conftest.py +++ b/server/szurubooru/tests/conftest.py @@ -1,62 +1,20 @@ -# pylint: disable=redefined-outer-name import contextlib import os import random import string -from unittest.mock import patch from datetime import datetime -import pytest +from unittest.mock import patch + import freezegun +import pytest import sqlalchemy as sa -from szurubooru import config, db, model, rest - - -class QueryCounter: - def __init__(self): - self._statements = [] - - def __enter__(self): - self._statements = [] - - def __exit__(self, *args, **kwargs): - self._statements = [] - - def create_before_cursor_execute(self): - def before_cursor_execute( - _conn, _cursor, statement, _params, _context, _executemany): - self._statements.append(statement) - return before_cursor_execute - - @property - def statements(self): - return self._statements - - -def _set_sqlite_pragma(dbapi_connection, connection_record): - cursor = dbapi_connection.cursor() - cursor.execute('PRAGMA foreign_keys=ON') - cursor.close() - - -_test_db_url = config.config['test_database'] -if not _test_db_url: - raise RuntimeError('Test database not configured.') -_query_counter = QueryCounter() -_engine = sa.create_engine(_test_db_url) -if _test_db_url.startswith('sqlite'): - sa.event.listen(_engine, 'connect', _set_sqlite_pragma) -model.Base.metadata.drop_all(bind=_engine) -model.Base.metadata.create_all(bind=_engine) -sa.event.listen( - _engine, - 'before_cursor_execute', - _query_counter.create_before_cursor_execute()) +from szurubooru import config, db, model, rest def get_unique_name(): alphabet = string.ascii_letters + string.digits - return ''.join(random.choice(alphabet) for _ in range(8)) + return "".join(random.choice(alphabet) for _ in range(8)) @pytest.fixture @@ -67,52 +25,49 @@ def fake_datetime(): freezer.start() yield freezer.stop() - return injector - -@pytest.fixture() -def query_counter(): - return _query_counter + return injector -@pytest.fixture -def query_logger(): - if pytest.config.option.verbose > 0: +@pytest.fixture(scope="session") +def query_logger(pytestconfig): + if pytestconfig.option.verbose > 0: import logging + import coloredlogs + coloredlogs.install( - fmt='[%(asctime)-15s] %(name)s %(message)s', isatty=True) + fmt="[%(asctime)-15s] %(name)s %(message)s", isatty=True + ) logging.basicConfig() - logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) + logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) -@pytest.yield_fixture(scope='function', autouse=True) -def session(query_logger): # pylint: disable=unused-argument - db.sessionmaker = sa.orm.sessionmaker( - bind=_engine, autoflush=False) - db.session = sa.orm.scoped_session(db.sessionmaker) +@pytest.yield_fixture(scope="function", autouse=True) +def session(query_logger, postgresql_db): + db.session = postgresql_db.session + postgresql_db.create_table(*model.Base.metadata.sorted_tables) try: - yield db.session + yield postgresql_db.session finally: - db.session.remove() - for table in reversed(model.Base.metadata.sorted_tables): - db.session.execute(table.delete()) - db.session.commit() + postgresql_db.reset_db() @pytest.fixture def context_factory(session): def factory(params=None, files=None, user=None, headers=None): ctx = rest.Context( - env={'HTTP_ORIGIN': 'http://example.com'}, + env={"HTTP_ORIGIN": "http://example.com"}, method=None, url=None, headers=headers or {}, params=params or {}, - files=files or {}) + files=files or {}, + ) ctx.session = session ctx.user = user or model.User() return ctx + return factory @@ -120,58 +75,65 @@ def context_factory(session): def config_injector(): def injector(new_config_content): config.config = new_config_content + return injector @pytest.fixture def user_factory(): def factory( - name=None, - rank=model.User.RANK_REGULAR, - email='dummy', - password_salt=None, - password_hash=None): + name=None, + rank=model.User.RANK_REGULAR, + email="dummy", + password_salt=None, + password_hash=None, + ): user = model.User() user.name = name or get_unique_name() - user.password_salt = password_salt or 'dummy' - user.password_hash = password_hash or 'dummy' + user.password_salt = password_salt or "dummy" + user.password_hash = password_hash or "dummy" user.email = email user.rank = rank user.creation_time = datetime(1997, 1, 1) user.avatar_style = model.User.AVATAR_GRAVATAR return user + return factory @pytest.fixture def user_token_factory(user_factory): def factory( - user=None, - token=None, - expiration_time=None, - enabled=None, - creation_time=None): + user=None, + token=None, + expiration_time=None, + enabled=None, + creation_time=None, + ): if user is None: user = user_factory() db.session.add(user) user_token = model.UserToken() user_token.user = user - user_token.token = token or 'dummy' + user_token.token = token or "dummy" user_token.expiration_time = expiration_time user_token.enabled = enabled if enabled is not None else True user_token.creation_time = creation_time or datetime(1997, 1, 1) return user_token + return factory @pytest.fixture def tag_category_factory(): - def factory(name=None, color='dummy', default=False): + def factory(name=None, color="dummy", order=1, default=False): category = model.TagCategory() category.name = name or get_unique_name() category.color = color + category.order = order category.default = default return category + return factory @@ -190,41 +152,36 @@ def tag_factory(): if metric: tag.metric = metric return tag - return factory - -@pytest.yield_fixture -def skip_post_hashing(): - with patch('szurubooru.func.image_hash.add_image'), \ - patch('szurubooru.func.image_hash.delete_image'): - yield + return factory @pytest.fixture -def post_factory(skip_post_hashing): - # pylint: disable=invalid-name +def post_factory(): def factory( - id=None, - safety=model.Post.SAFETY_SAFE, - type=model.Post.TYPE_IMAGE, - checksum='...', - tags=[]): + id=None, + safety=model.Post.SAFETY_SAFE, + type=model.Post.TYPE_IMAGE, + checksum="...", + tags=[], + ): post = model.Post() post.post_id = id post.safety = safety post.type = type post.checksum = checksum post.flags = [] - post.mime_type = 'application/octet-stream' + post.mime_type = "application/octet-stream" post.creation_time = datetime(1996, 1, 1) post.tags = tags return post + return factory @pytest.fixture def comment_factory(user_factory, post_factory): - def factory(user=None, post=None, text='dummy', time=None): + def factory(user=None, post=None, text="dummy", time=None): if not user: user = user_factory() db.session.add(user) @@ -237,6 +194,7 @@ def comment_factory(user_factory, post_factory): comment.text = text comment.creation_time = time or datetime(1996, 1, 1) return comment + return factory @@ -248,7 +206,9 @@ def post_score_factory(user_factory, post_factory): if post is None: post = post_factory() return model.PostScore( - post=post, user=user, score=score, time=datetime(1999, 1, 1)) + post=post, user=user, score=score, time=datetime(1999, 1, 1) + ) + return factory @@ -260,7 +220,60 @@ def post_favorite_factory(user_factory, post_factory): if post is None: post = post_factory() return model.PostFavorite( - post=post, user=user, time=datetime(1999, 1, 1)) + post=post, user=user, time=datetime(1999, 1, 1) + ) + + return factory + + +@pytest.fixture +def pool_category_factory(): + def factory(name=None, color="dummy", default=False): + category = model.PoolCategory() + category.name = name or get_unique_name() + category.color = color + category.default = default + return category + + return factory + + +@pytest.fixture +def pool_factory(): + def factory( + id=None, names=None, description=None, category=None, time=None + ): + if not category: + category = model.PoolCategory(get_unique_name()) + db.session.add(category) + pool = model.Pool() + pool.pool_id = id + pool.names = [] + for i, name in enumerate(names or [get_unique_name()]): + pool.names.append(model.PoolName(name, i)) + pool.description = description + pool.category = category + pool.creation_time = time or datetime(1996, 1, 1) + return pool + + return factory + + +@pytest.fixture +def pool_post_factory(pool_factory, post_factory): + def factory(pool=None, post=None, order=None): + if not pool: + pool = pool_factory() + db.session.add(pool) + if not post: + post = post_factory() + db.session.add(post) + pool_post = model.PoolPost(post) + pool_post.pool = pool + pool_post.post = post + pool_post.order = order or 0 + return pool_post + return factory @@ -314,7 +327,8 @@ def post_metric_range_factory(post_factory, tag_factory, metric_factory): @pytest.fixture def read_asset(): def get(path): - path = os.path.join(os.path.dirname(__file__), 'assets', path) - with open(path, 'rb') as handle: + path = os.path.join(os.path.dirname(__file__), "assets", path) + with open(path, "rb") as handle: return handle.read() + return get diff --git a/server/szurubooru/tests/func/test_auth.py b/server/szurubooru/tests/func/test_auth.py index 6dc79bb..2141d3c 100644 --- a/server/szurubooru/tests/func/test_auth.py +++ b/server/szurubooru/tests/func/test_auth.py @@ -1,41 +1,44 @@ from datetime import datetime, timedelta + import pytest + from szurubooru.func import auth @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({'secret': 'testSecret'}) + config_injector({"secret": "testSecret"}) def test_get_password_hash(): - salt, password = ('testSalt', 'pass') + salt, password = ("testSalt", "pass") result, revision = auth.get_password_hash(salt, password) assert result assert revision == 3 hash_parts = list( - filter(lambda e: e is not None and e != '', result.split('$'))) + filter(lambda e: e is not None and e != "", result.split("$")) + ) assert len(hash_parts) == 5 - assert hash_parts[0] == 'argon2id' + assert hash_parts[0] == "argon2id" def test_get_sha256_legacy_password_hash(): - salt, password = ('testSalt', 'pass') + salt, password = ("testSalt", "pass") result, revision = auth.get_sha256_legacy_password_hash(salt, password) - hash = '2031ac9631353ac9303719a7f808a24f79aa1d71712c98523e4bb4cce579428a' + hash = "2031ac9631353ac9303719a7f808a24f79aa1d71712c98523e4bb4cce579428a" assert result == hash assert revision == 2 def test_get_sha1_legacy_password_hash(): - salt, password = ('testSalt', 'pass') + salt, password = ("testSalt", "pass") result, revision = auth.get_sha1_legacy_password_hash(salt, password) - assert result == '1eb1f953d9be303a1b54627e903e6124cfb1245b' + assert result == "1eb1f953d9be303a1b54627e903e6124cfb1245b" assert revision == 1 def test_is_valid_password_auto_upgrades_user_password_hash(user_factory): - salt, password = ('testSalt', 'pass') + salt, password = ("testSalt", "pass") hash, revision = auth.get_sha256_legacy_password_hash(salt, password) user = user_factory(password_salt=salt, password_hash=hash) result = auth.is_valid_password(user, password) @@ -50,7 +53,7 @@ def test_is_valid_token(user_token_factory): def test_expired_token_is_invalid(user_token_factory): - past_expiration = (datetime.utcnow() - timedelta(minutes=30)) + past_expiration = datetime.utcnow() - timedelta(minutes=30) user_token = user_token_factory(expiration_time=past_expiration) assert not auth.is_valid_token(user_token) diff --git a/server/szurubooru/tests/func/test_comments.py b/server/szurubooru/tests/func/test_comments.py index f1e5d0f..ef5ca4f 100644 --- a/server/szurubooru/tests/func/test_comments.py +++ b/server/szurubooru/tests/func/test_comments.py @@ -1,34 +1,36 @@ -from unittest.mock import patch from datetime import datetime +from unittest.mock import patch + import pytest + from szurubooru import db from szurubooru.func import comments, users def test_serialize_user(user_factory, comment_factory): - with patch('szurubooru.func.users.get_avatar_url'): - users.get_avatar_url.return_value = 'https://example.com/avatar.png' - comment = comment_factory(user=user_factory(name='dummy')) + with patch("szurubooru.func.users.get_avatar_url"): + users.get_avatar_url.return_value = "https://example.com/avatar.png" + comment = comment_factory(user=user_factory(name="dummy")) comment.comment_id = 77 comment.creation_time = datetime(1997, 1, 1) comment.last_edit_time = datetime(1998, 1, 1) - comment.text = 'text' + comment.text = "text" db.session.add(comment) db.session.flush() auth_user = user_factory() assert comments.serialize_comment(comment, auth_user) == { - 'id': comment.comment_id, - 'postId': comment.post.post_id, - 'creationTime': datetime(1997, 1, 1, 0, 0), - 'lastEditTime': datetime(1998, 1, 1, 0, 0), - 'score': 0, - 'ownScore': 0, - 'text': 'text', - 'user': { - 'name': 'dummy', - 'avatarUrl': 'https://example.com/avatar.png', + "id": comment.comment_id, + "postId": comment.post.post_id, + "creationTime": datetime(1997, 1, 1, 0, 0), + "lastEditTime": datetime(1998, 1, 1, 0, 0), + "score": 0, + "ownScore": 0, + "text": "text", + "user": { + "name": "dummy", + "avatarUrl": "https://example.com/avatar.png", }, - 'version': 1, + "version": 1, } @@ -53,13 +55,14 @@ def test_create_comment(user_factory, post_factory, fake_datetime): user = user_factory() post = post_factory() db.session.add_all([user, post]) - with patch('szurubooru.func.comments.update_comment_text'), \ - fake_datetime('1997-01-01'): - comment = comments.create_comment(user, post, 'text') + with patch("szurubooru.func.comments.update_comment_text"), fake_datetime( + "1997-01-01" + ): + comment = comments.create_comment(user, post, "text") assert comment.creation_time == datetime(1997, 1, 1) assert comment.user == user assert comment.post == post - comments.update_comment_text.assert_called_once_with(comment, 'text') + comments.update_comment_text.assert_called_once_with(comment, "text") def test_update_comment_text_with_emptry_string(comment_factory): @@ -70,5 +73,5 @@ def test_update_comment_text_with_emptry_string(comment_factory): def test_update_comment_text(comment_factory): comment = comment_factory() - comments.update_comment_text(comment, 'text') - assert comment.text == 'text' + comments.update_comment_text(comment, "text") + assert comment.text == "text" diff --git a/server/szurubooru/tests/func/test_diff.py b/server/szurubooru/tests/func/test_diff.py index 0134a3f..5e415b5 100644 --- a/server/szurubooru/tests/func/test_diff.py +++ b/server/szurubooru/tests/func/test_diff.py @@ -1,275 +1,238 @@ import pytest -from szurubooru.func import diff - - -@pytest.mark.parametrize('old,new,expected', [ - ( - [], [], None, - ), - - ( - [], - ['added'], - {'type': 'list change', 'added': ['added'], 'removed': []}, - ), - ( - ['removed'], - [], - {'type': 'list change', 'added': [], 'removed': ['removed']}, - ), - - ( - ['untouched'], - ['untouched'], - None, - ), +from szurubooru.func import diff - ( - ['untouched'], - ['untouched', 'added'], - {'type': 'list change', 'added': ['added'], 'removed': []}, - ), - ( - ['untouched', 'removed'], - ['untouched'], - {'type': 'list change', 'added': [], 'removed': ['removed']}, - ), -]) +@pytest.mark.parametrize( + "old,new,expected", + [ + ( + [], + [], + None, + ), + ( + [], + ["added"], + {"type": "list change", "added": ["added"], "removed": []}, + ), + ( + ["removed"], + [], + {"type": "list change", "added": [], "removed": ["removed"]}, + ), + ( + ["untouched"], + ["untouched"], + None, + ), + ( + ["untouched"], + ["untouched", "added"], + {"type": "list change", "added": ["added"], "removed": []}, + ), + ( + ["untouched", "removed"], + ["untouched"], + {"type": "list change", "added": [], "removed": ["removed"]}, + ), + ], +) def test_get_list_diff(old, new, expected): assert diff.get_list_diff(old, new) == expected -@pytest.mark.parametrize('old,new,expected', [ - ( - {}, {}, None, - ), - - ( - {'removed key': 'removed value'}, - {}, - { - 'type': 'object change', - 'value': +@pytest.mark.parametrize( + "old,new,expected", + [ + ( + {}, + {}, + None, + ), + ( + {"removed key": "removed value"}, + {}, { - 'removed key': - { - 'type': 'deleted property', - 'value': 'removed value', + "type": "object change", + "value": { + "removed key": { + "type": "deleted property", + "value": "removed value", + }, }, }, - }, - ), - - ( - {}, - {'added key': 'added value'}, - { - 'type': 'object change', - 'value': + ), + ( + {}, + {"added key": "added value"}, { - 'added key': - { - 'type': 'added property', - 'value': 'added value', + "type": "object change", + "value": { + "added key": { + "type": "added property", + "value": "added value", + }, }, }, - }, - ), - - ( - {'key': 'old value'}, - {'key': 'new value'}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": "old value"}, + {"key": "new value"}, { - 'key': - { - 'type': 'primitive change', - 'old-value': 'old value', - 'new-value': 'new value', + "type": "object change", + "value": { + "key": { + "type": "primitive change", + "old-value": "old value", + "new-value": "new value", + }, }, }, - }, - ), - - ( - {'key': 'untouched'}, - {'key': 'untouched'}, - None, - ), - - ( - {'key': 'untouched', 'removed key': 'removed value'}, - {'key': 'untouched'}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": "untouched"}, + {"key": "untouched"}, + None, + ), + ( + {"key": "untouched", "removed key": "removed value"}, + {"key": "untouched"}, { - 'removed key': - { - 'type': 'deleted property', - 'value': 'removed value', + "type": "object change", + "value": { + "removed key": { + "type": "deleted property", + "value": "removed value", + }, }, }, - }, - ), - - ( - {'key': 'untouched'}, - {'key': 'untouched', 'added key': 'added value'}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": "untouched"}, + {"key": "untouched", "added key": "added value"}, { - 'added key': - { - 'type': 'added property', - 'value': 'added value', + "type": "object change", + "value": { + "added key": { + "type": "added property", + "value": "added value", + }, }, }, - }, - ), - - ( - {'key': 'untouched', 'changed key': 'old value'}, - {'key': 'untouched', 'changed key': 'new value'}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": "untouched", "changed key": "old value"}, + {"key": "untouched", "changed key": "new value"}, { - 'changed key': - { - 'type': 'primitive change', - 'old-value': 'old value', - 'new-value': 'new value', + "type": "object change", + "value": { + "changed key": { + "type": "primitive change", + "old-value": "old value", + "new-value": "new value", + }, }, }, - }, - ), - - ( - {'key': {'subkey': 'old value'}}, - {'key': {'subkey': 'new value'}}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": {"subkey": "old value"}}, + {"key": {"subkey": "new value"}}, { - 'key': - { - 'type': 'object change', - 'value': - { - 'subkey': - { - 'type': 'primitive change', - 'old-value': 'old value', - 'new-value': 'new value', + "type": "object change", + "value": { + "key": { + "type": "object change", + "value": { + "subkey": { + "type": "primitive change", + "old-value": "old value", + "new-value": "new value", + }, }, }, }, }, - }, - ), - - ( - {'key': {}}, - {'key': {'subkey': 'removed value'}}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": {}}, + {"key": {"subkey": "removed value"}}, { - 'key': - { - 'type': 'object change', - 'value': - { - 'subkey': - { - 'type': 'added property', - 'value': 'removed value', + "type": "object change", + "value": { + "key": { + "type": "object change", + "value": { + "subkey": { + "type": "added property", + "value": "removed value", + }, }, }, }, }, - }, - ), - - ( - {'key': {'subkey': 'removed value'}}, - {'key': {}}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": {"subkey": "removed value"}}, + {"key": {}}, { - 'key': - { - 'type': 'object change', - 'value': - { - 'subkey': - { - 'type': 'deleted property', - 'value': 'removed value', + "type": "object change", + "value": { + "key": { + "type": "object change", + "value": { + "subkey": { + "type": "deleted property", + "value": "removed value", + }, }, }, }, }, - }, - ), - - ( - {'key': ['old value']}, - {'key': ['new value']}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": ["old value"]}, + {"key": ["new value"]}, { - 'key': - { - 'type': 'list change', - 'added': ['new value'], - 'removed': ['old value'], + "type": "object change", + "value": { + "key": { + "type": "list change", + "added": ["new value"], + "removed": ["old value"], + }, }, }, - }, - ), - - ( - {'key': []}, - {'key': ['new value']}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": []}, + {"key": ["new value"]}, { - 'key': - { - 'type': 'list change', - 'added': ['new value'], - 'removed': [], + "type": "object change", + "value": { + "key": { + "type": "list change", + "added": ["new value"], + "removed": [], + }, }, }, - }, - ), - - ( - {'key': ['removed value']}, - {'key': []}, - { - 'type': 'object change', - 'value': + ), + ( + {"key": ["removed value"]}, + {"key": []}, { - 'key': - { - 'type': 'list change', - 'added': [], - 'removed': ['removed value'], + "type": "object change", + "value": { + "key": { + "type": "list change", + "added": [], + "removed": ["removed value"], + }, }, }, - }, - ), -]) + ), + ], +) def test_get_dict_diff(old, new, expected): assert diff.get_dict_diff(old, new) == expected diff --git a/server/szurubooru/tests/func/test_image_hash.py b/server/szurubooru/tests/func/test_image_hash.py index 192d175..e7028b6 100644 --- a/server/szurubooru/tests/func/test_image_hash.py +++ b/server/szurubooru/tests/func/test_image_hash.py @@ -1,28 +1,29 @@ +import pytest +from numpy import array_equal + from szurubooru.func import image_hash -def test_hashing(read_asset, config_injector): - config_injector({ - 'elasticsearch': { - 'host': 'localhost', - 'port': 9200, - 'index': 'szurubooru_test', - }, - }) - image_hash.purge() - image_hash.add_image('test', read_asset('jpeg.jpg')) +def test_signature_functions(read_asset, config_injector): + sig1 = image_hash.generate_signature(read_asset("jpeg.jpg")) + sig2 = image_hash.generate_signature(read_asset("jpeg-similar.jpg")) + + sig1_repacked = image_hash.unpack_signature( + image_hash.pack_signature(sig1) + ) + sig2_repacked = image_hash.unpack_signature( + image_hash.pack_signature(sig2) + ) + assert array_equal(sig1, sig1_repacked) + assert array_equal(sig2, sig2_repacked) + + dist1 = image_hash.normalized_distance([sig1], sig2) + assert abs(dist1[0] - 0.19713075553164386) < 1e-8 - paths = image_hash.get_all_paths() - results_exact = image_hash.search_by_image(read_asset('jpeg.jpg')) - results_similar = image_hash.search_by_image( - read_asset('jpeg-similar.jpg')) + dist2 = image_hash.normalized_distance([sig2], sig2) + assert abs(dist2[0]) < 1e-8 - assert len(paths) == 1 - assert len(results_exact) == 1 - assert len(results_similar) == 1 - assert results_exact[0].path == 'test' - assert results_exact[0].score == 63 - assert results_exact[0].distance == 0 - assert results_similar[0].path == 'test' - assert results_similar[0].score == 17 - assert abs(results_similar[0].distance - 0.20599895341812172) < 1e-8 + words1 = image_hash.generate_words(sig1) + words2 = image_hash.generate_words(sig2) + words_match = sum(word1 == word2 for word1, word2 in zip(words1, words2)) + assert words_match == 18 diff --git a/server/szurubooru/tests/func/test_mime.py b/server/szurubooru/tests/func/test_mime.py index 4b8dcfa..0d8f645 100644 --- a/server/szurubooru/tests/func/test_mime.py +++ b/server/szurubooru/tests/func/test_mime.py @@ -1,76 +1,97 @@ import pytest + from szurubooru.func import mime -@pytest.mark.parametrize('input_path,expected_mime_type', [ - ('mp4.mp4', 'video/mp4'), - ('webm.webm', 'video/webm'), - ('flash.swf', 'application/x-shockwave-flash'), - ('png.png', 'image/png'), - ('jpeg.jpg', 'image/jpeg'), - ('gif.gif', 'image/gif'), - ('text.txt', 'application/octet-stream'), -]) +@pytest.mark.parametrize( + "input_path,expected_mime_type", + [ + ("mp4.mp4", "video/mp4"), + ("webm.webm", "video/webm"), + ("flash.swf", "application/x-shockwave-flash"), + ("png.png", "image/png"), + ("jpeg.jpg", "image/jpeg"), + ("gif.gif", "image/gif"), + ("webp.webp", "image/webp"), + ("text.txt", "application/octet-stream"), + ], +) def test_get_mime_type(read_asset, input_path, expected_mime_type): assert mime.get_mime_type(read_asset(input_path)) == expected_mime_type def test_get_mime_type_for_empty_file(): - assert mime.get_mime_type(b'') == 'application/octet-stream' + assert mime.get_mime_type(b"") == "application/octet-stream" -@pytest.mark.parametrize('mime_type,expected_extension', [ - ('video/mp4', 'mp4'), - ('video/webm', 'webm'), - ('application/x-shockwave-flash', 'swf'), - ('image/png', 'png'), - ('image/jpeg', 'jpg'), - ('image/gif', 'gif'), - ('application/octet-stream', 'dat'), -]) +@pytest.mark.parametrize( + "mime_type,expected_extension", + [ + ("video/mp4", "mp4"), + ("video/webm", "webm"), + ("application/x-shockwave-flash", "swf"), + ("image/png", "png"), + ("image/jpeg", "jpg"), + ("image/gif", "gif"), + ("image/webp", "webp"), + ("application/octet-stream", "dat"), + ], +) def test_get_extension(mime_type, expected_extension): assert mime.get_extension(mime_type) == expected_extension -@pytest.mark.parametrize('input_mime_type,expected_state', [ - ('application/x-shockwave-flash', True), - ('APPLICATION/X-SHOCKWAVE-FLASH', True), - ('application/x-shockwave', False), -]) +@pytest.mark.parametrize( + "input_mime_type,expected_state", + [ + ("application/x-shockwave-flash", True), + ("APPLICATION/X-SHOCKWAVE-FLASH", True), + ("application/x-shockwave", False), + ], +) def test_is_flash(input_mime_type, expected_state): assert mime.is_flash(input_mime_type) == expected_state -@pytest.mark.parametrize('input_mime_type,expected_state', [ - ('video/webm', True), - ('VIDEO/WEBM', True), - ('video/mp4', True), - ('VIDEO/MP4', True), - ('video/anything_else', False), - ('application/ogg', True), - ('not a video', False), -]) +@pytest.mark.parametrize( + "input_mime_type,expected_state", + [ + ("video/webm", True), + ("VIDEO/WEBM", True), + ("video/mp4", True), + ("VIDEO/MP4", True), + ("video/anything_else", False), + ("application/ogg", True), + ("not a video", False), + ], +) def test_is_video(input_mime_type, expected_state): assert mime.is_video(input_mime_type) == expected_state -@pytest.mark.parametrize('input_mime_type,expected_state', [ - ('image/gif', True), - ('image/png', True), - ('image/jpeg', True), - ('IMAGE/GIF', True), - ('IMAGE/PNG', True), - ('IMAGE/JPEG', True), - ('image/anything_else', False), - ('not an image', False), -]) +@pytest.mark.parametrize( + "input_mime_type,expected_state", + [ + ("image/gif", True), + ("image/png", True), + ("image/jpeg", True), + ("IMAGE/GIF", True), + ("IMAGE/PNG", True), + ("IMAGE/JPEG", True), + ("image/anything_else", False), + ("not an image", False), + ], +) def test_is_image(input_mime_type, expected_state): assert mime.is_image(input_mime_type) == expected_state -@pytest.mark.parametrize('input_path,expected_state', [ - ('gif.gif', False), - ('gif-animated.gif', True), -]) +@pytest.mark.parametrize( + "input_path,expected_state", + [ + ("gif.gif", False), + ("gif-animated.gif", True), + ], +) def test_is_animated_gif(read_asset, input_path, expected_state): assert mime.is_animated_gif(read_asset(input_path)) == expected_state diff --git a/server/szurubooru/tests/func/test_net.py b/server/szurubooru/tests/func/test_net.py index fb149b0..65e9048 100644 --- a/server/szurubooru/tests/func/test_net.py +++ b/server/szurubooru/tests/func/test_net.py @@ -1,49 +1,144 @@ +from datetime import datetime +from unittest.mock import patch + +import pytest + +from szurubooru import errors from szurubooru.func import net +from szurubooru.func.util import get_sha1 + +@pytest.fixture(autouse=True) +def inject_config(tmpdir, config_injector): + config_injector( + { + "user_agent": None, + "max_dl_filesize": 1.0e6, + "data_dir": str(tmpdir.mkdir("data")), + } + ) -def test_download(config_injector): - config_injector({ - 'user_agent': None - }) - url = 'http://info.cern.ch/hypertext/WWW/TheProject.html' + +def test_download(): + url = "http://info.cern.ch/hypertext/WWW/TheProject.html" expected_content = ( - b'<HEADER>\n<TITLE>The World Wide Web project</TITLE>\n<NEXTID N="' + - b'55">\n</HEADER>\n<BODY>\n<H1>World Wide Web</H1>The WorldWideWeb' + - b' (W3) is a wide-area<A\nNAME=0 HREF="WhatIs.html">\nhypermedia</' + - b'A> information retrieval\ninitiative aiming to give universal\na' + - b'ccess to a large universe of documents.<P>\nEverything there is ' + - b'online about\nW3 is linked directly or indirectly\nto this docum' + - b'ent, including an <A\nNAME=24 HREF="Summary.html">executive\nsum' + - b'mary</A> of the project, <A\nNAME=29 HREF="Administration/Mailin' + - b'g/Overview.html">Mailing lists</A>\n, <A\nNAME=30 HREF="Policy.h' + - b'tml">Policy</A> , November\'s <A\nNAME=34 HREF="News/9211.html"' + - b'>W3 news</A> ,\n<A\nNAME=41 HREF="FAQ/List.html">Frequently Ask' + - b'ed Questions</A> .\n<DL>\n<DT><A\nNAME=44 HREF="../DataSources/T' + - b'op.html">What\'s out there?</A>\n<DD> Pointers to the\nworld\'s ' + - b'online information,<A\nNAME=45 HREF="../DataSources/bySubject/Ov' + - b'erview.html"> subjects</A>\n, <A\nNAME=z54 HREF="../DataSources/' + - b'WWW/Servers.html">W3 servers</A>, etc.\n<DT><A\nNAME=46 HREF="He' + - b'lp.html">Help</A>\n<DD> on the browser you are using\n<DT><A\nNA' + - b'ME=13 HREF="Status.html">Software Products</A>\n<DD> A list of W' + - b'3 project\ncomponents and their current state.\n(e.g. <A\nNAME=2' + - b'7 HREF="LineMode/Browser.html">Line Mode</A> ,X11 <A\nNAME=35 HR' + - b'EF="Status.html#35">Viola</A> , <A\nNAME=26 HREF="NeXT/WorldWid' + - b'eWeb.html">NeXTStep</A>\n, <A\nNAME=25 HREF="Daemon/Overview.htm' + - b'l">Servers</A> , <A\nNAME=51 HREF="Tools/Overview.html">Tools</A' + - b'> ,<A\nNAME=53 HREF="MailRobot/Overview.html"> Mail robot</A> ,<' + - b'A\nNAME=52 HREF="Status.html#57">\nLibrary</A> )\n<DT><A\nNAME=4' + - b'7 HREF="Technical.html">Technical</A>\n<DD> Details of protocols' + - b', formats,\nprogram internals etc\n<DT><A\nNAME=40 HREF="Bibliog' + - b'raphy.html">Bibliography</A>\n<DD> Paper documentation\non W3 a' + - b'nd references.\n<DT><A\nNAME=14 HREF="People.html">People</A>\n<' + - b'DD> A list of some people involved\nin the project.\n<DT><A\nNAM' + - b'E=15 HREF="History.html">History</A>\n<DD> A summary of the hist' + - b'ory\nof the project.\n<DT><A\nNAME=37 HREF="Helping.html">How ca' + - b'n I help</A> ?\n<DD> If you would like\nto support the web..\n<D' + - b'T><A\nNAME=48 HREF="../README.html">Getting code</A>\n<DD> Getti' + - b'ng the code by<A\nNAME=49 HREF="LineMode/Defaults/Distribution.h' + - b'tml">\nanonymous FTP</A> , etc.</A>\n</DL>\n</BODY>\n') + b'<HEADER>\n<TITLE>The World Wide Web project</TITLE>\n<NEXTID N="' + + b'55">\n</HEADER>\n<BODY>\n<H1>World Wide Web</H1>The WorldWideWeb' + + b' (W3) is a wide-area<A\nNAME=0 HREF="WhatIs.html">\nhypermedia</' + + b"A> information retrieval\ninitiative aiming to give universal\na" + + b"ccess to a large universe of documents.<P>\nEverything there is " + + b"online about\nW3 is linked directly or indirectly\nto this docum" + + b'ent, including an <A\nNAME=24 HREF="Summary.html">executive\nsum' + + b'mary</A> of the project, <A\nNAME=29 HREF="Administration/Mailin' + + b'g/Overview.html">Mailing lists</A>\n, <A\nNAME=30 HREF="Policy.h' + + b'tml">Policy</A> , November\'s <A\nNAME=34 HREF="News/9211.html"' + + b'>W3 news</A> ,\n<A\nNAME=41 HREF="FAQ/List.html">Frequently Ask' + + b'ed Questions</A> .\n<DL>\n<DT><A\nNAME=44 HREF="../DataSources/T' + + b"op.html\">What's out there?</A>\n<DD> Pointers to the\nworld's " + + b'online information,<A\nNAME=45 HREF="../DataSources/bySubject/Ov' + + b'erview.html"> subjects</A>\n, <A\nNAME=z54 HREF="../DataSources/' + + b'WWW/Servers.html">W3 servers</A>, etc.\n<DT><A\nNAME=46 HREF="He' + + b'lp.html">Help</A>\n<DD> on the browser you are using\n<DT><A\nNA' + + b'ME=13 HREF="Status.html">Software Products</A>\n<DD> A list of W' + + b"3 project\ncomponents and their current state.\n(e.g. <A\nNAME=2" + + b'7 HREF="LineMode/Browser.html">Line Mode</A> ,X11 <A\nNAME=35 HR' + + b'EF="Status.html#35">Viola</A> , <A\nNAME=26 HREF="NeXT/WorldWid' + + b'eWeb.html">NeXTStep</A>\n, <A\nNAME=25 HREF="Daemon/Overview.htm' + + b'l">Servers</A> , <A\nNAME=51 HREF="Tools/Overview.html">Tools</A' + + b'> ,<A\nNAME=53 HREF="MailRobot/Overview.html"> Mail robot</A> ,<' + + b'A\nNAME=52 HREF="Status.html#57">\nLibrary</A> )\n<DT><A\nNAME=4' + + b'7 HREF="Technical.html">Technical</A>\n<DD> Details of protocols' + + b', formats,\nprogram internals etc\n<DT><A\nNAME=40 HREF="Bibliog' + + b'raphy.html">Bibliography</A>\n<DD> Paper documentation\non W3 a' + + b'nd references.\n<DT><A\nNAME=14 HREF="People.html">People</A>\n<' + + b"DD> A list of some people involved\nin the project.\n<DT><A\nNAM" + + b'E=15 HREF="History.html">History</A>\n<DD> A summary of the hist' + + b'ory\nof the project.\n<DT><A\nNAME=37 HREF="Helping.html">How ca' + + b"n I help</A> ?\n<DD> If you would like\nto support the web..\n<D" + + b'T><A\nNAME=48 HREF="../README.html">Getting code</A>\n<DD> Getti' + + b'ng the code by<A\nNAME=49 HREF="LineMode/Defaults/Distribution.h' + + b'tml">\nanonymous FTP</A> , etc.</A>\n</DL>\n</BODY>\n' + ) actual_content = net.download(url) assert actual_content == expected_content + + +@pytest.mark.parametrize( + "url", + [ + "https://samples.ffmpeg.org/MPEG-4/video.mp4", + ], +) +def test_too_large_download(url): + pytest.xfail("Download limit not implemented yet") + with pytest.raises(errors.ProcessingError): + net.download(url) + + +@pytest.mark.parametrize( + "url,expected_sha1", + [ + ( + "https://www.youtube.com/watch?v=C0DPdy98e4c", + "365af1c8f59c6865e1a84c6e13e3e25ff89e0ba1", + ), + ( + "https://gfycat.com/immaterialchillyiberianmole", + "953000e81d7bd1da95ce264f872e7b6c4a6484be", + ), + ], +) +def test_video_download(url, expected_sha1): + actual_content = net.download(url, use_video_downloader=True) + assert get_sha1(actual_content) == expected_sha1 + + +@pytest.mark.parametrize( + "url", + [ + "https://samples.ffmpeg.org/flac/short.flac", # not a video + "https://www.youtube.com/watch?v=dQw4w9WgXcQ", # video too large + ], +) +def test_failed_video_download(url): + with pytest.raises(errors.ThirdPartyError): + net.download(url, use_video_downloader=True) + + +def test_no_webhooks(config_injector): + config_injector({"webhooks": []}) + res = net.post_to_webhooks(None) + assert len(res) == 0 + + +@pytest.mark.parametrize( + "webhook,status_code", + [ + ("https://postman-echo.com/post", 200), + ("http://localhost/", 400), + ("https://postman-echo.com/get", 400), + ], +) +def test_single_webhook(config_injector, webhook, status_code): + ret = net._post_to_webhook(webhook, {"test_arg": "test_value"}) + assert ret == status_code + + +def test_multiple_webhooks(config_injector): + config_injector( + { + "webhooks": [ + "https://postman-echo.com/post", + "https://postman-echo.com/get", + ] + } + ) + threads = net.post_to_webhooks({"test_arg": "test_value"}) + assert len(threads) == 2 + + +def test_malformed_webhooks(config_injector): + with pytest.raises(ValueError): + net._post_to_webhook("malformed_url", {"test_arg": "test_value"}) diff --git a/server/szurubooru/tests/func/test_posts.py b/server/szurubooru/tests/func/test_posts.py index 70a521a..af6121a 100644 --- a/server/szurubooru/tests/func/test_posts.py +++ b/server/szurubooru/tests/func/test_posts.py @@ -1,40 +1,58 @@ +import os from datetime import datetime from unittest.mock import patch -import os + import pytest + from szurubooru import db, model from szurubooru.func import ( - posts, users, comments, tags, images, files, util, image_hash) + comments, + files, + image_hash, + images, + posts, + tags, + users, + util, +) -@pytest.mark.parametrize('input_mime_type,expected_url', [ - ('image/jpeg', 'http://example.com/posts/1_244c8840887984c4.jpg'), - ('image/gif', 'http://example.com/posts/1_244c8840887984c4.gif'), - ('totally/unknown', 'http://example.com/posts/1_244c8840887984c4.dat'), -]) +@pytest.mark.parametrize( + "input_mime_type,expected_url", + [ + ("image/jpeg", "http://example.com/posts/1_244c8840887984c4.jpg"), + ("image/gif", "http://example.com/posts/1_244c8840887984c4.gif"), + ("totally/unknown", "http://example.com/posts/1_244c8840887984c4.dat"), + ], +) def test_get_post_url(input_mime_type, expected_url, config_injector): - config_injector({'data_url': 'http://example.com/', 'secret': 'test'}) + config_injector({"data_url": "http://example.com/", "secret": "test"}) post = model.Post() post.post_id = 1 post.mime_type = input_mime_type assert posts.get_post_content_url(post) == expected_url -@pytest.mark.parametrize('input_mime_type', ['image/jpeg', 'image/gif']) +@pytest.mark.parametrize("input_mime_type", ["image/jpeg", "image/gif"]) def test_get_post_thumbnail_url(input_mime_type, config_injector): - config_injector({'data_url': 'http://example.com/', 'secret': 'test'}) + config_injector({"data_url": "http://example.com/", "secret": "test"}) post = model.Post() post.post_id = 1 post.mime_type = input_mime_type - assert posts.get_post_thumbnail_url(post) \ - == 'http://example.com/generated-thumbnails/1_244c8840887984c4.jpg' + assert ( + posts.get_post_thumbnail_url(post) + == "http://example.com/generated-thumbnails/1_244c8840887984c4.jpg" + ) -@pytest.mark.parametrize('input_mime_type,expected_path', [ - ('image/jpeg', 'posts/1_244c8840887984c4.jpg'), - ('image/gif', 'posts/1_244c8840887984c4.gif'), - ('totally/unknown', 'posts/1_244c8840887984c4.dat'), -]) +@pytest.mark.parametrize( + "input_mime_type,expected_path", + [ + ("image/jpeg", "posts/1_244c8840887984c4.jpg"), + ("image/gif", "posts/1_244c8840887984c4.gif"), + ("totally/unknown", "posts/1_244c8840887984c4.dat"), + ], +) def test_get_post_content_path(input_mime_type, expected_path): post = model.Post() post.post_id = 1 @@ -42,31 +60,35 @@ def test_get_post_content_path(input_mime_type, expected_path): assert posts.get_post_content_path(post) == expected_path -@pytest.mark.parametrize('input_mime_type', ['image/jpeg', 'image/gif']) +@pytest.mark.parametrize("input_mime_type", ["image/jpeg", "image/gif"]) def test_get_post_thumbnail_path(input_mime_type): post = model.Post() post.post_id = 1 post.mime_type = input_mime_type - assert posts.get_post_thumbnail_path(post) \ - == 'generated-thumbnails/1_244c8840887984c4.jpg' + assert ( + posts.get_post_thumbnail_path(post) + == "generated-thumbnails/1_244c8840887984c4.jpg" + ) -@pytest.mark.parametrize('input_mime_type', ['image/jpeg', 'image/gif']) +@pytest.mark.parametrize("input_mime_type", ["image/jpeg", "image/gif"]) def test_get_post_thumbnail_backup_path(input_mime_type): post = model.Post() post.post_id = 1 post.mime_type = input_mime_type - assert posts.get_post_thumbnail_backup_path(post) \ - == 'posts/custom-thumbnails/1_244c8840887984c4.dat' + assert ( + posts.get_post_thumbnail_backup_path(post) + == "posts/custom-thumbnails/1_244c8840887984c4.dat" + ) def test_serialize_note(): note = model.PostNote() note.polygon = [[0, 1], [1, 1], [1, 0], [0, 0]] - note.text = '...' + note.text = "..." assert posts.serialize_note(note) == { - 'polygon': [[0, 1], [1, 1], [1, 0], [0, 0]], - 'text': '...' + "polygon": [[0, 1], [1, 1], [1, 0], [0, 0]], + "text": "...", } @@ -75,174 +97,227 @@ def test_serialize_post_when_empty(): def test_serialize_post( - user_factory, - comment_factory, - tag_factory, - tag_category_factory, - metric_factory, - post_metric_factory, - post_metric_range_factory, - config_injector): - config_injector({'data_url': 'http://example.com/', 'secret': 'test'}) - with patch('szurubooru.func.comments.serialize_comment'), \ - patch('szurubooru.func.users.serialize_micro_user'), \ - patch('szurubooru.func.posts.files.has'): + user_factory, + comment_factory, + tag_factory, + tag_category_factory, + metric_factory, + post_metric_factory, + post_metric_range_factory, + pool_factory, + pool_category_factory, + config_injector, +): + config_injector({"data_url": "http://example.com/", "secret": "test"}) + with patch("szurubooru.func.comments.serialize_comment"), patch( + "szurubooru.func.users.serialize_micro_user" + ), patch("szurubooru.func.posts.files.has"): files.has.return_value = True - users.serialize_micro_user.side_effect \ - = lambda user, auth_user: user.name - comments.serialize_comment.side_effect \ - = lambda comment, auth_user: comment.user.name + users.serialize_micro_user.side_effect = ( + lambda user, auth_user: user.name + ) + comments.serialize_comment.side_effect = ( + lambda comment, auth_user: comment.user.name + ) - auth_user = user_factory(name='auth user') + auth_user = user_factory(name="auth user") post = model.Post() post.post_id = 1 post.creation_time = datetime(1997, 1, 1) post.last_edit_time = datetime(1998, 1, 1) tag1 = tag_factory( - names=['tag1', 'tag2'], - category=tag_category_factory('test-cat1')) + names=["tag1", "tag2"], + category=tag_category_factory("test-cat1") + ) tag1.metric = metric_factory(tag=tag1, min=-2.5, max=2.5) tag3 = tag_factory( - names=['tag3'], - category=tag_category_factory('test-cat2')) + names=["tag3"], + category=tag_category_factory("test-cat2") + ) post.tags = [tag1, tag3] post.metrics = [ post_metric_factory(post=post, metric=tag1.metric, value=-1.2) ] post.metric_ranges = [ - post_metric_range_factory(post=post, metric=tag1.metric, - low=2, high=3) + post_metric_range_factory( + post=post, metric=tag1.metric, low=2, high=3 + ) ] post.safety = model.Post.SAFETY_SAFE - post.source = '4gag' + post.source = "4gag" post.type = model.Post.TYPE_IMAGE - post.checksum = 'deadbeef' - post.mime_type = 'image/jpeg' + post.checksum = "deadbeef" + post.mime_type = "image/jpeg" post.file_size = 100 - post.user = user_factory(name='post author') + post.user = user_factory(name="post author") post.canvas_width = 200 post.canvas_height = 300 - post.flags = ['loop'] + post.flags = ["loop"] db.session.add(post) db.session.flush() - db.session.add_all([ - comment_factory( - user=user_factory(name='commenter1'), - post=post, - time=datetime(1999, 1, 1)), - comment_factory( - user=user_factory(name='commenter2'), - post=post, - time=datetime(1999, 1, 2)), - model.PostFavorite( - post=post, - user=user_factory(name='fav1'), - time=datetime(1800, 1, 1)), - model.PostFeature( - post=post, - user=user_factory(), - time=datetime(1999, 1, 1)), - model.PostScore( - post=post, - user=auth_user, - score=-1, - time=datetime(1800, 1, 1)), - model.PostScore( - post=post, - user=user_factory(), - score=1, - time=datetime(1800, 1, 1)), - model.PostScore( - post=post, - user=user_factory(), - score=1, - time=datetime(1800, 1, 1))]) + db.session.add_all( + [ + comment_factory( + user=user_factory(name="commenter1"), + post=post, + time=datetime(1999, 1, 1), + ), + comment_factory( + user=user_factory(name="commenter2"), + post=post, + time=datetime(1999, 1, 2), + ), + model.PostFavorite( + post=post, + user=user_factory(name="fav1"), + time=datetime(1800, 1, 1), + ), + model.PostFeature( + post=post, user=user_factory(), time=datetime(1999, 1, 1) + ), + model.PostScore( + post=post, + user=auth_user, + score=-1, + time=datetime(1800, 1, 1), + ), + model.PostScore( + post=post, + user=user_factory(), + score=1, + time=datetime(1800, 1, 1), + ), + model.PostScore( + post=post, + user=user_factory(), + score=1, + time=datetime(1800, 1, 1), + ), + ] + ) + db.session.flush() + + pool1 = pool_factory( + id=1, + names=["pool1", "pool2"], + description="desc", + category=pool_category_factory("test-cat1"), + ) + pool1.last_edit_time = datetime(1998, 1, 1) + pool1.posts.append(post) + + pool2 = pool_factory( + id=2, + names=["pool3"], + description="desc2", + category=pool_category_factory("test-cat2"), + ) + pool2.last_edit_time = datetime(1998, 1, 1) + pool2.posts.append(post) + + db.session.add_all([pool1, pool2]) db.session.flush() result = posts.serialize_post(post, auth_user) - result['tags'].sort(key=lambda tag: tag['names'][0]) + result["tags"].sort(key=lambda tag: tag["names"][0]) assert result == { - 'id': 1, - 'version': 1, - 'creationTime': datetime(1997, 1, 1), - 'lastEditTime': datetime(1998, 1, 1), - 'safety': 'safe', - 'source': '4gag', - 'type': 'image', - 'checksum': 'deadbeef', - 'fileSize': 100, - 'canvasWidth': 200, - 'canvasHeight': 300, - 'contentUrl': 'http://example.com/posts/1_244c8840887984c4.jpg', - 'thumbnailUrl': - 'http://example.com/' - 'generated-thumbnails/1_244c8840887984c4.jpg', - 'flags': ['loop'], - 'tags': [ + "id": 1, + "version": 1, + "creationTime": datetime(1997, 1, 1), + "lastEditTime": datetime(1998, 1, 1), + "safety": "safe", + "source": "4gag", + "type": "image", + "checksum": "deadbeef", + "fileSize": 100, + "canvasWidth": 200, + "canvasHeight": 300, + "contentUrl": "http://example.com/posts/1_244c8840887984c4.jpg", + "thumbnailUrl": "http://example.com/" + "generated-thumbnails/1_244c8840887984c4.jpg", + "flags": ["loop"], + "tags": [ { - 'names': ['tag1', 'tag2'], - 'category': 'test-cat1', - 'usages': 1, - 'metric': { - 'min': -2.5, - 'max': 2.5 - } + "names": ["tag1", "tag2"], + "category": "test-cat1", + "usages": 1, + "metric": { + "min": -2.5, + "max": 2.5 + }, }, { - 'names': ['tag3'], - 'category': 'test-cat2', - 'usages': 1, - 'metric': None + "names": ["tag3"], + "category": "test-cat2", + "usages": 1, + "metric": None, }, ], - 'relations': [], - 'notes': [], - 'user': 'post author', - 'score': 1, - 'ownFavorite': False, - 'ownScore': -1, - 'tagCount': 2, - 'favoriteCount': 1, - 'commentCount': 2, - 'noteCount': 0, - 'featureCount': 1, - 'relationCount': 0, - 'lastFeatureTime': datetime(1999, 1, 1), - 'favoritedBy': ['fav1'], - 'hasCustomThumbnail': True, - 'mimeType': 'image/jpeg', - 'comments': ['commenter1', 'commenter2'], - 'metrics': [ + "relations": [], + "notes": [], + "pools": [ { - 'tag_name': 'tag1', - 'post_id': 1, - 'value': -1.2 + "id": 1, + "names": ["pool1", "pool2"], + "description": "desc", + "category": "test-cat1", + "postCount": 1, + }, + { + "id": 2, + "names": ["pool3"], + "description": "desc2", + "category": "test-cat2", + "postCount": 1, + }, + ], + "user": "post author", + "score": 1, + "ownFavorite": False, + "ownScore": -1, + "tagCount": 2, + "favoriteCount": 1, + "commentCount": 2, + "noteCount": 0, + "featureCount": 1, + "relationCount": 0, + "lastFeatureTime": datetime(1999, 1, 1), + "favoritedBy": ["fav1"], + "hasCustomThumbnail": True, + "mimeType": "image/jpeg", + "comments": ["commenter1", "commenter2"], + "metrics": [ + { + "tag_name": "tag1", + "post_id": 1, + "value": -1.2 } ], - 'metricRanges': [ + "metricRanges": [ { - 'tag_name': 'tag1', - 'post_id': 1, - 'low': 2, - 'high': 3 + "tag_name": "tag1", + "post_id": 1, + "low": 2, + "high": 3 } - ] + ], } def test_serialize_micro_post(post_factory, user_factory): - with patch('szurubooru.func.posts.get_post_thumbnail_url'): - posts.get_post_thumbnail_url.return_value \ - = 'https://example.com/thumb.png' + with patch("szurubooru.func.posts.get_post_thumbnail_url"): + posts.get_post_thumbnail_url.return_value = ( + "https://example.com/thumb.png" + ) auth_user = user_factory() post = post_factory() db.session.add(post) db.session.flush() assert posts.serialize_micro_post(post, auth_user) == { - 'id': post.post_id, - 'thumbnailUrl': 'https://example.com/thumb.png', + "id": post.post_id, + "thumbnailUrl": "https://example.com/thumb.png", } @@ -273,22 +348,25 @@ def test_get_post_by_id(post_factory): def test_create_post(user_factory, fake_datetime): - with patch('szurubooru.func.posts.update_post_content'), \ - patch('szurubooru.func.posts.update_post_tags'), \ - fake_datetime('1997-01-01'): + with patch("szurubooru.func.posts.update_post_content"), patch( + "szurubooru.func.posts.update_post_tags" + ), fake_datetime("1997-01-01"): auth_user = user_factory() - post, _new_tags = posts.create_post('content', ['tag'], auth_user) + post, _new_tags = posts.create_post("content", ["tag"], auth_user) assert post.creation_time == datetime(1997, 1, 1) assert post.last_edit_time is None - posts.update_post_tags.assert_called_once_with(post, ['tag']) - posts.update_post_content.assert_called_once_with(post, 'content') + posts.update_post_tags.assert_called_once_with(post, ["tag"]) + posts.update_post_content.assert_called_once_with(post, "content") -@pytest.mark.parametrize('input_safety,expected_safety', [ - ('safe', model.Post.SAFETY_SAFE), - ('sketchy', model.Post.SAFETY_SKETCHY), - ('unsafe', model.Post.SAFETY_UNSAFE), -]) +@pytest.mark.parametrize( + "input_safety,expected_safety", + [ + ("safe", model.Post.SAFETY_SAFE), + ("sketchy", model.Post.SAFETY_SKETCHY), + ("unsafe", model.Post.SAFETY_UNSAFE), + ], +) def test_update_post_safety(input_safety, expected_safety): post = model.Post() posts.update_post_safety(post, input_safety) @@ -298,95 +376,107 @@ def test_update_post_safety(input_safety, expected_safety): def test_update_post_safety_with_invalid_string(): post = model.Post() with pytest.raises(posts.InvalidPostSafetyError): - posts.update_post_safety(post, 'bad') + posts.update_post_safety(post, "bad") def test_update_post_source(): post = model.Post() - posts.update_post_source(post, 'x') - assert post.source == 'x' + posts.update_post_source(post, "x") + assert post.source == "x" def test_update_post_source_with_too_long_string(): post = model.Post() with pytest.raises(posts.InvalidPostSourceError): - posts.update_post_source(post, 'x' * 1000) + posts.update_post_source(post, "x" * 3000) @pytest.mark.parametrize( - 'is_existing,input_file,expected_mime_type,expected_type,output_file_name', + "is_existing,input_file,expected_mime_type,expected_type,output_file_name", [ ( True, - 'png.png', - 'image/png', + "png.png", + "image/png", model.Post.TYPE_IMAGE, - '1_244c8840887984c4.png', + "1_244c8840887984c4.png", ), ( False, - 'png.png', - 'image/png', + "png.png", + "image/png", model.Post.TYPE_IMAGE, - '1_244c8840887984c4.png', + "1_244c8840887984c4.png", ), ( False, - 'jpeg.jpg', - 'image/jpeg', + "jpeg.jpg", + "image/jpeg", model.Post.TYPE_IMAGE, - '1_244c8840887984c4.jpg', + "1_244c8840887984c4.jpg", ), ( False, - 'gif.gif', - 'image/gif', + "gif.gif", + "image/gif", model.Post.TYPE_IMAGE, - '1_244c8840887984c4.gif', + "1_244c8840887984c4.gif", ), ( False, - 'gif-animated.gif', - 'image/gif', + "gif-animated.gif", + "image/gif", model.Post.TYPE_ANIMATION, - '1_244c8840887984c4.gif', + "1_244c8840887984c4.gif", ), ( False, - 'webm.webm', - 'video/webm', + "webm.webm", + "video/webm", model.Post.TYPE_VIDEO, - '1_244c8840887984c4.webm', + "1_244c8840887984c4.webm", ), ( False, - 'mp4.mp4', - 'video/mp4', + "mp4.mp4", + "video/mp4", model.Post.TYPE_VIDEO, - '1_244c8840887984c4.mp4', + "1_244c8840887984c4.mp4", ), ( False, - 'flash.swf', - 'application/x-shockwave-flash', + "flash.swf", + "application/x-shockwave-flash", model.Post.TYPE_FLASH, - '1_244c8840887984c4.swf', + "1_244c8840887984c4.swf", ), - ]) + ], +) def test_update_post_content_for_new_post( - tmpdir, config_injector, post_factory, read_asset, is_existing, - input_file, expected_mime_type, expected_type, output_file_name): - with patch('szurubooru.func.util.get_sha1'): - util.get_sha1.return_value = 'crc' - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - }) - output_file_path = '{}/data/posts/{}'.format(tmpdir, output_file_name) + tmpdir, + config_injector, + post_factory, + read_asset, + is_existing, + input_file, + expected_mime_type, + expected_type, + output_file_name, +): + with patch("szurubooru.func.util.get_sha1"): + util.get_sha1.return_value = "crc" + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": { + "post_width": 300, + "post_height": 300, + }, + "secret": "test", + "allow_broken_uploads": False, + } + ) + output_file_path = "{}/data/posts/{}".format(tmpdir, output_file_name) post = post_factory(id=1) db.session.add(post) if is_existing: @@ -399,126 +489,157 @@ def test_update_post_content_for_new_post( db.session.flush() assert post.mime_type == expected_mime_type assert post.type == expected_type - assert post.checksum == 'crc' + assert post.checksum == "crc" assert os.path.exists(output_file_path) if post.type in (model.Post.TYPE_IMAGE, model.Post.TYPE_ANIMATION): - image_hash.delete_image.assert_called_once_with(post.post_id) - image_hash.add_image.assert_called_once_with(post.post_id, content) + assert db.session.query(model.PostSignature).count() == 1 else: - image_hash.delete_image.assert_not_called() - image_hash.add_image.assert_not_called() + assert db.session.query(model.PostSignature).count() == 0 def test_update_post_content_to_existing_content( - tmpdir, config_injector, post_factory, read_asset): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'data_url': 'example.com', - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - }) + tmpdir, config_injector, post_factory, read_asset +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "data_url": "example.com", + "thumbnails": { + "post_width": 300, + "post_height": 300, + }, + "secret": "test", + "allow_broken_uploads": False, + } + ) post = post_factory() another_post = post_factory() db.session.add_all([post, another_post]) - posts.update_post_content(post, read_asset('png.png')) + posts.update_post_content(post, read_asset("png.png")) db.session.flush() with pytest.raises(posts.PostAlreadyUploadedError): - posts.update_post_content(another_post, read_asset('png.png')) + posts.update_post_content(another_post, read_asset("png.png")) +@pytest.mark.parametrize("allow_broken_uploads", [True, False]) def test_update_post_content_with_broken_content( - tmpdir, config_injector, post_factory, read_asset): + tmpdir, config_injector, post_factory, read_asset, allow_broken_uploads +): # the rationale behind this behavior is to salvage user upload even if the # server software thinks it's broken. chances are the server is wrong, # especially about flash movies. - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - }) + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": { + "post_width": 300, + "post_height": 300, + }, + "secret": "test", + "allow_broken_uploads": allow_broken_uploads, + } + ) post = post_factory() another_post = post_factory() db.session.add_all([post, another_post]) - posts.update_post_content(post, read_asset('png-broken.png')) - db.session.flush() - assert post.canvas_width is None - assert post.canvas_height is None + if allow_broken_uploads: + posts.update_post_content(post, read_asset("png-broken.png")) + db.session.flush() + assert post.canvas_width is None + assert post.canvas_height is None + else: + with pytest.raises(posts.InvalidPostContentError): + posts.update_post_content(post, read_asset("png-broken.png")) + db.session.flush() -@pytest.mark.parametrize('input_content', [None, b'not a media file']) -def test_update_post_content_with_invalid_content(input_content): +@pytest.mark.parametrize("input_content", [None, b"not a media file"]) +def test_update_post_content_with_invalid_content( + config_injector, input_content +): + config_injector( + { + "allow_broken_uploads": True, + } + ) post = model.Post() with pytest.raises(posts.InvalidPostContentError): posts.update_post_content(post, input_content) -@pytest.mark.parametrize('is_existing', (True, False)) +@pytest.mark.parametrize("is_existing", (True, False)) def test_update_post_thumbnail_to_new_one( - tmpdir, config_injector, read_asset, post_factory, is_existing): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - }) + tmpdir, config_injector, read_asset, post_factory, is_existing +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": { + "post_width": 300, + "post_height": 300, + }, + "secret": "test", + "allow_broken_uploads": False, + } + ) post = post_factory(id=1) db.session.add(post) if is_existing: db.session.flush() assert post.post_id generated_path = ( - '{}/data/generated-thumbnails/1_244c8840887984c4.jpg' - .format(tmpdir)) + "{}/data/generated-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.jpg" + ) source_path = ( - '{}/data/posts/custom-thumbnails/1_244c8840887984c4.dat' - .format(tmpdir)) + "{}/data/posts/custom-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.dat" + ) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) - posts.update_post_content(post, read_asset('png.png')) - posts.update_post_thumbnail(post, read_asset('jpeg.jpg')) + posts.update_post_content(post, read_asset("png.png")) + posts.update_post_thumbnail(post, read_asset("jpeg.jpg")) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) db.session.flush() assert os.path.exists(generated_path) assert os.path.exists(source_path) - with open(source_path, 'rb') as handle: - assert handle.read() == read_asset('jpeg.jpg') + with open(source_path, "rb") as handle: + assert handle.read() == read_asset("jpeg.jpg") -@pytest.mark.parametrize('is_existing', (True, False)) +@pytest.mark.parametrize("is_existing", (True, False)) def test_update_post_thumbnail_to_default( - tmpdir, config_injector, read_asset, post_factory, is_existing): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - }) + tmpdir, config_injector, read_asset, post_factory, is_existing +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": { + "post_width": 300, + "post_height": 300, + }, + "secret": "test", + "allow_broken_uploads": False, + } + ) post = post_factory(id=1) db.session.add(post) if is_existing: db.session.flush() assert post.post_id generated_path = ( - '{}/data/generated-thumbnails/1_244c8840887984c4.jpg' - .format(tmpdir)) + "{}/data/generated-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.jpg" + ) source_path = ( - '{}/data/posts/custom-thumbnails/1_244c8840887984c4.dat' - .format(tmpdir)) + "{}/data/posts/custom-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.dat" + ) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) - posts.update_post_content(post, read_asset('png.png')) - posts.update_post_thumbnail(post, read_asset('jpeg.jpg')) + posts.update_post_content(post, read_asset("png.png")) + posts.update_post_thumbnail(post, read_asset("jpeg.jpg")) posts.update_post_thumbnail(post, None) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) @@ -527,80 +648,94 @@ def test_update_post_thumbnail_to_default( assert not os.path.exists(source_path) -@pytest.mark.parametrize('is_existing', (True, False)) +@pytest.mark.parametrize("is_existing", (True, False)) def test_update_post_thumbnail_with_broken_thumbnail( - tmpdir, config_injector, read_asset, post_factory, is_existing): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - }) + tmpdir, config_injector, read_asset, post_factory, is_existing +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": { + "post_width": 300, + "post_height": 300, + }, + "secret": "test", + "allow_broken_uploads": False, + } + ) post = post_factory(id=1) db.session.add(post) if is_existing: db.session.flush() assert post.post_id generated_path = ( - '{}/data/generated-thumbnails/1_244c8840887984c4.jpg' - .format(tmpdir)) + "{}/data/generated-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.jpg" + ) source_path = ( - '{}/data/posts/custom-thumbnails/1_244c8840887984c4.dat' - .format(tmpdir)) + "{}/data/posts/custom-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.dat" + ) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) - posts.update_post_content(post, read_asset('png.png')) - posts.update_post_thumbnail(post, read_asset('png-broken.png')) + posts.update_post_content(post, read_asset("png.png")) + posts.update_post_thumbnail(post, read_asset("png-broken.png")) assert not os.path.exists(generated_path) assert not os.path.exists(source_path) db.session.flush() assert os.path.exists(generated_path) assert os.path.exists(source_path) - with open(source_path, 'rb') as handle: - assert handle.read() == read_asset('png-broken.png') - with open(generated_path, 'rb') as handle: + with open(source_path, "rb") as handle: + assert handle.read() == read_asset("png-broken.png") + with open(generated_path, "rb") as handle: image = images.Image(handle.read()) assert image.width == 1 assert image.height == 1 def test_update_post_content_leaving_custom_thumbnail( - tmpdir, config_injector, read_asset, post_factory): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - }) + tmpdir, config_injector, read_asset, post_factory +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": { + "post_width": 300, + "post_height": 300, + }, + "secret": "test", + "allow_broken_uploads": False, + } + ) post = post_factory(id=1) db.session.add(post) - posts.update_post_content(post, read_asset('png.png')) - posts.update_post_thumbnail(post, read_asset('jpeg.jpg')) - posts.update_post_content(post, read_asset('png.png')) + posts.update_post_content(post, read_asset("png.png")) + posts.update_post_thumbnail(post, read_asset("jpeg.jpg")) + posts.update_post_content(post, read_asset("png.png")) db.session.flush() generated_path = ( - '{}/data/generated-thumbnails/1_244c8840887984c4.jpg' - .format(tmpdir)) + "{}/data/generated-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.jpg" + ) source_path = ( - '{}/data/posts/custom-thumbnails/1_244c8840887984c4.dat' - .format(tmpdir)) + "{}/data/posts/custom-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.dat" + ) assert os.path.exists(source_path) assert os.path.exists(generated_path) def test_update_post_tags(tag_factory): post = model.Post() - with patch('szurubooru.func.tags.get_or_create_tags_by_names'): - tags.get_or_create_tags_by_names.side_effect = lambda tag_names: \ - ([tag_factory(names=[name]) for name in tag_names], []) - posts.update_post_tags(post, ['tag1', 'tag2']) + with patch("szurubooru.func.tags.get_or_create_tags_by_names"): + tags.get_or_create_tags_by_names.side_effect = lambda tag_names: ( + [tag_factory(names=[name]) for name in tag_names], + [], + ) + posts.update_post_tags(post, ["tag1", "tag2"]) assert len(post.tags) == 2 - assert post.tags[0].names[0].name == 'tag1' - assert post.tags[1].names[0].name == 'tag2' + assert post.tags[0].names[0].name == "tag1" + assert post.tags[1].names[0].name == "tag2" def test_update_post_relations(post_factory): @@ -612,7 +747,9 @@ def test_update_post_relations(post_factory): posts.update_post_relations(post, [relation1.post_id, relation2.post_id]) assert len(post.relations) == 2 assert sorted(r.post_id for r in post.relations) == [ - relation1.post_id, relation2.post_id] + relation1.post_id, + relation2.post_id, + ] def test_update_post_relations_bidirectionality(post_factory): @@ -647,35 +784,44 @@ def test_update_post_notes(): posts.update_post_notes( post, [ - {'polygon': [[0, 0], [0, 1], [1, 0], [0, 0]], 'text': 'text1'}, - {'polygon': [[0, 0], [0, 1], [1, 0], [0, 0]], 'text': 'text2'}, - ]) + {"polygon": [[0, 0], [0, 1], [1, 0], [0, 0]], "text": "text1"}, + {"polygon": [[0, 0], [0, 1], [1, 0], [0, 0]], "text": "text2"}, + ], + ) assert len(post.notes) == 2 assert post.notes[0].polygon == [[0, 0], [0, 1], [1, 0], [0, 0]] - assert post.notes[0].text == 'text1' + assert post.notes[0].text == "text1" assert post.notes[1].polygon == [[0, 0], [0, 1], [1, 0], [0, 0]] - assert post.notes[1].text == 'text2' + assert post.notes[1].text == "text2" -@pytest.mark.parametrize('input', [ - [{'text': '...'}], - [{'polygon': None, 'text': '...'}], - [{'polygon': 'trash', 'text': '...'}], - [{'polygon': ['trash', 'trash', 'trash'], 'text': '...'}], - [{'polygon': {2: 'trash', 3: 'trash', 4: 'trash'}, 'text': '...'}], - [{'polygon': [[0, 0]], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], None], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], 'surprise'], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], {2: 'trash', 3: 'trash'}], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], 5], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], [0, 2]], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], [0, '...']], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], [0, 0, 0]], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], [0]], 'text': '...'}], - [{'polygon': [[0, 0], [0, 0], [0, 1]], 'text': ''}], - [{'polygon': [[0, 0], [0, 0], [0, 1]], 'text': None}], - [{'polygon': [[0, 0], [0, 0], [0, 1]]}], -]) +@pytest.mark.parametrize( + "input", + [ + [{"text": "..."}], + [{"polygon": None, "text": "..."}], + [{"polygon": "trash", "text": "..."}], + [{"polygon": ["trash", "trash", "trash"], "text": "..."}], + [{"polygon": {2: "trash", 3: "trash", 4: "trash"}, "text": "..."}], + [{"polygon": [[0, 0]], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], None], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], "surprise"], "text": "..."}], + [ + { + "polygon": [[0, 0], [0, 0], {2: "trash", 3: "trash"}], + "text": "...", + } + ], + [{"polygon": [[0, 0], [0, 0], 5], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], [0, 2]], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], [0, "..."]], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], [0, 0, 0]], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], [0]], "text": "..."}], + [{"polygon": [[0, 0], [0, 0], [0, 1]], "text": ""}], + [{"polygon": [[0, 0], [0, 0], [0, 1]], "text": None}], + [{"polygon": [[0, 0], [0, 0], [0, 1]]}], + ], +) def test_update_post_notes_with_invalid_content(input): post = model.Post() with pytest.raises(posts.InvalidPostNoteError): @@ -684,14 +830,14 @@ def test_update_post_notes_with_invalid_content(input): def test_update_post_flags(): post = model.Post() - posts.update_post_flags(post, ['loop']) - assert post.flags == ['loop'] + posts.update_post_flags(post, ["loop"]) + assert post.flags == ["loop"] def test_update_post_flags_with_invalid_content(): post = model.Post() with pytest.raises(posts.InvalidPostFlagError): - posts.update_post_flags(post, ['invalid']) + posts.update_post_flags(post, ["invalid"]) def test_feature_post(post_factory, user_factory): @@ -707,7 +853,7 @@ def test_feature_post(post_factory, user_factory): def test_delete(post_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) post = post_factory() db.session.add(post) db.session.flush() @@ -718,7 +864,7 @@ def test_delete(post_factory, config_injector): def test_merge_posts_deletes_source_post(post_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() db.session.add_all([source_post, target_post]) @@ -731,7 +877,7 @@ def test_merge_posts_deletes_source_post(post_factory, config_injector): def test_merge_posts_with_itself(post_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) source_post = post_factory() db.session.add(source_post) db.session.flush() @@ -740,7 +886,7 @@ def test_merge_posts_with_itself(post_factory, config_injector): def test_merge_posts_moves_tags(post_factory, tag_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() tag = tag_factory() @@ -756,8 +902,9 @@ def test_merge_posts_moves_tags(post_factory, tag_factory, config_injector): def test_merge_posts_doesnt_duplicate_tags( - post_factory, tag_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, tag_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() tag = tag_factory() @@ -773,8 +920,9 @@ def test_merge_posts_doesnt_duplicate_tags( def test_merge_posts_moves_comments( - post_factory, comment_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, comment_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() comment = comment_factory(post=source_post) @@ -789,8 +937,9 @@ def test_merge_posts_moves_comments( def test_merge_posts_moves_scores( - post_factory, post_score_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, post_score_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() score = post_score_factory(post=source_post, score=1) @@ -805,8 +954,9 @@ def test_merge_posts_moves_scores( def test_merge_posts_doesnt_duplicate_scores( - post_factory, user_factory, post_score_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, user_factory, post_score_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() user = user_factory() @@ -823,8 +973,9 @@ def test_merge_posts_doesnt_duplicate_scores( def test_merge_posts_moves_favorites( - post_factory, post_favorite_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, post_favorite_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() favorite = post_favorite_factory(post=source_post) @@ -839,8 +990,9 @@ def test_merge_posts_moves_favorites( def test_merge_posts_doesnt_duplicate_favorites( - post_factory, user_factory, post_favorite_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, user_factory, post_favorite_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() user = user_factory() @@ -857,7 +1009,7 @@ def test_merge_posts_doesnt_duplicate_favorites( def test_merge_posts_moves_child_relations(post_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() related_post = post_factory() @@ -873,8 +1025,9 @@ def test_merge_posts_moves_child_relations(post_factory, config_injector): def test_merge_posts_doesnt_duplicate_child_relations( - post_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() related_post = post_factory() @@ -891,7 +1044,7 @@ def test_merge_posts_doesnt_duplicate_child_relations( def test_merge_posts_moves_parent_relations(post_factory, config_injector): - config_injector({'delete_source_files': False}) + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() related_post = post_factory() @@ -909,8 +1062,9 @@ def test_merge_posts_moves_parent_relations(post_factory, config_injector): def test_merge_posts_doesnt_duplicate_parent_relations( - post_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() related_post = post_factory() @@ -928,8 +1082,9 @@ def test_merge_posts_doesnt_duplicate_parent_relations( def test_merge_posts_doesnt_create_relation_loop_for_children( - post_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() source_post.relations = [target_post] @@ -944,8 +1099,9 @@ def test_merge_posts_doesnt_create_relation_loop_for_children( def test_merge_posts_doesnt_create_relation_loop_for_parents( - post_factory, config_injector): - config_injector({'delete_source_files': False}) + post_factory, config_injector +): + config_injector({"delete_source_files": False}) source_post = post_factory() target_post = post_factory() target_post.relations = [source_post] @@ -960,30 +1116,36 @@ def test_merge_posts_doesnt_create_relation_loop_for_parents( def test_merge_posts_replaces_content( - post_factory, config_injector, tmpdir, read_asset): - config_injector({ - 'data_dir': str(tmpdir.mkdir('data')), - 'data_url': 'example.com', - 'delete_source_files': False, - 'thumbnails': { - 'post_width': 300, - 'post_height': 300, - }, - 'secret': 'test', - }) + post_factory, config_injector, tmpdir, read_asset +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "data_url": "example.com", + "delete_source_files": False, + "thumbnails": { + "post_width": 300, + "post_height": 300, + }, + "secret": "test", + } + ) source_post = post_factory(id=1) target_post = post_factory(id=2) - content = read_asset('png.png') + content = read_asset("png.png") db.session.add_all([source_post, target_post]) db.session.commit() posts.update_post_content(source_post, content) db.session.flush() - source_path = ( - os.path.join('{}/data/posts/1_244c8840887984c4.png'.format(tmpdir))) - target_path1 = ( - os.path.join('{}/data/posts/2_49caeb3ec1643406.png'.format(tmpdir))) - target_path2 = ( - os.path.join('{}/data/posts/2_49caeb3ec1643406.dat'.format(tmpdir))) + source_path = os.path.join( + "{}/data/posts/1_244c8840887984c4.png".format(tmpdir) + ) + target_path1 = os.path.join( + "{}/data/posts/2_49caeb3ec1643406.png".format(tmpdir) + ) + target_path2 = os.path.join( + "{}/data/posts/2_49caeb3ec1643406.dat".format(tmpdir) + ) assert os.path.exists(source_path) assert not os.path.exists(target_path1) assert not os.path.exists(target_path2) @@ -995,3 +1157,19 @@ def test_merge_posts_replaces_content( assert os.path.exists(source_path) assert os.path.exists(target_path1) assert not os.path.exists(target_path2) + + +def test_search_by_image(post_factory, config_injector, read_asset): + config_injector({"allow_broken_uploads": False}) + post = post_factory() + posts.generate_post_signature(post, read_asset("jpeg.jpg")) + db.session.flush() + + result1 = posts.search_by_image(read_asset("jpeg-similar.jpg")) + assert len(result1) == 1 + result1_distance, result1_post = result1[0] + assert abs(result1_distance - 0.19713075553164386) < 1e-8 + assert result1_post.post_id == post.post_id + + result2 = posts.search_by_image(read_asset("png.png")) + assert not result2 diff --git a/server/szurubooru/tests/func/test_snapshots.py b/server/szurubooru/tests/func/test_snapshots.py index 0949199..da93530 100644 --- a/server/szurubooru/tests/func/test_snapshots.py +++ b/server/szurubooru/tests/func/test_snapshots.py @@ -1,55 +1,57 @@ -from unittest.mock import patch from datetime import datetime +from unittest.mock import patch + import pytest + from szurubooru import db, model from szurubooru.func import snapshots, users def test_get_tag_category_snapshot(tag_category_factory): - category = tag_category_factory(name='name', color='color') + category = tag_category_factory(name="name", color="color") assert snapshots.get_tag_category_snapshot(category) == { - 'name': 'name', - 'color': 'color', - 'default': False, + "name": "name", + "color": "color", + "default": False, } category.default = True assert snapshots.get_tag_category_snapshot(category) == { - 'name': 'name', - 'color': 'color', - 'default': True, + "name": "name", + "color": "color", + "default": True, } def test_get_tag_snapshot(tag_factory, tag_category_factory): - category = tag_category_factory(name='dummy') - tag = tag_factory(names=['main_name', 'alias'], category=category) + category = tag_category_factory(name="dummy") + tag = tag_factory(names=["main_name", "alias"], category=category) assert snapshots.get_tag_snapshot(tag) == { - 'names': ['main_name', 'alias'], - 'category': 'dummy', - 'suggestions': [], - 'implications': [], + "names": ["main_name", "alias"], + "category": "dummy", + "suggestions": [], + "implications": [], } - tag = tag_factory(names=['main_name', 'alias'], category=category) - imp1 = tag_factory(names=['imp1_main_name', 'imp1_alias']) - imp2 = tag_factory(names=['imp2_main_name', 'imp2_alias']) - sug1 = tag_factory(names=['sug1_main_name', 'sug1_alias']) - sug2 = tag_factory(names=['sug2_main_name', 'sug2_alias']) + tag = tag_factory(names=["main_name", "alias"], category=category) + imp1 = tag_factory(names=["imp1_main_name", "imp1_alias"]) + imp2 = tag_factory(names=["imp2_main_name", "imp2_alias"]) + sug1 = tag_factory(names=["sug1_main_name", "sug1_alias"]) + sug2 = tag_factory(names=["sug2_main_name", "sug2_alias"]) db.session.add_all([imp1, imp2, sug1, sug2]) tag.implications = [imp1, imp2] tag.suggestions = [sug1, sug2] db.session.flush() assert snapshots.get_tag_snapshot(tag) == { - 'names': ['main_name', 'alias'], - 'category': 'dummy', - 'implications': ['imp1_main_name', 'imp2_main_name'], - 'suggestions': ['sug1_main_name', 'sug2_main_name'], + "names": ["main_name", "alias"], + "category": "dummy", + "implications": ["imp1_main_name", "imp2_main_name"], + "suggestions": ["sug1_main_name", "sug2_main_name"], } def test_get_post_snapshot(post_factory, user_factory, tag_factory): - user = user_factory(name='dummy-user') - tag1 = tag_factory(names=['dummy-tag1']) - tag2 = tag_factory(names=['dummy-tag2']) + user = user_factory(name="dummy-user") + tag1 = tag_factory(names=["dummy-tag1"]) + tag2 = tag_factory(names=["dummy-tag2"]) post = post_factory(id=1) related_post1 = post_factory(id=2) related_post2 = post_factory(id=3) @@ -72,13 +74,13 @@ def test_get_post_snapshot(post_factory, user_factory, tag_factory): note = model.PostNote() note.post = post note.polygon = [(1, 1), (200, 1), (200, 200), (1, 200)] - note.text = 'some text' + note.text = "some text" db.session.add_all([score]) db.session.flush() post.user = user - post.checksum = 'deadbeef' - post.source = 'example.com' + post.checksum = "deadbeef" + post.source = "example.com" post.tags.append(tag1) post.tags.append(tag2) post.relations.append(related_post1) @@ -89,17 +91,19 @@ def test_get_post_snapshot(post_factory, user_factory, tag_factory): post.notes.append(note) assert snapshots.get_post_snapshot(post) == { - 'checksum': 'deadbeef', - 'featured': True, - 'flags': [], - 'notes': [{ - 'polygon': [[1, 1], [200, 1], [200, 200], [1, 200]], - 'text': 'some text', - }], - 'relations': [2, 3], - 'safety': 'safe', - 'source': 'example.com', - 'tags': ['dummy-tag1', 'dummy-tag2'], + "checksum": "deadbeef", + "featured": True, + "flags": [], + "notes": [ + { + "polygon": [[1, 1], [200, 1], [200, 200], [1, 200]], + "text": "some text", + } + ], + "relations": [2, 3], + "safety": "safe", + "source": "example.com", + "tags": ["dummy-tag1", "dummy-tag2"], } @@ -107,75 +111,81 @@ def test_serialize_snapshot(user_factory): auth_user = user_factory() snapshot = model.Snapshot() snapshot.operation = snapshot.OPERATION_CREATED - snapshot.resource_type = 'type' - snapshot.resource_name = 'id' - snapshot.user = user_factory(name='issuer') - snapshot.data = {'complex': list('object')} + snapshot.resource_type = "type" + snapshot.resource_name = "id" + snapshot.user = user_factory(name="issuer") + snapshot.data = {"complex": list("object")} snapshot.creation_time = datetime(1997, 1, 1) - with patch('szurubooru.func.users.serialize_micro_user'): - users.serialize_micro_user.return_value = 'mocked' + with patch("szurubooru.func.users.serialize_micro_user"): + users.serialize_micro_user.return_value = "mocked" assert snapshots.serialize_snapshot(snapshot, auth_user) == { - 'operation': 'created', - 'type': 'type', - 'id': 'id', - 'user': 'mocked', - 'data': {'complex': list('object')}, - 'time': datetime(1997, 1, 1), + "operation": "created", + "type": "type", + "id": "id", + "user": "mocked", + "data": {"complex": list("object")}, + "time": datetime(1997, 1, 1), } def test_create(tag_factory, user_factory): - tag = tag_factory(names=['dummy']) + tag = tag_factory(names=["dummy"]) db.session.add(tag) db.session.flush() - with patch('szurubooru.func.snapshots.get_tag_snapshot'): - snapshots.get_tag_snapshot.return_value = 'mocked' + with patch("szurubooru.func.snapshots.get_tag_snapshot"), patch( + "szurubooru.func.snapshots._post_to_webhooks" + ): + snapshots.get_tag_snapshot.return_value = "mocked" snapshots.create(tag, user_factory()) db.session.flush() results = db.session.query(model.Snapshot).all() assert len(results) == 1 assert results[0].operation == model.Snapshot.OPERATION_CREATED - assert results[0].data == 'mocked' + assert results[0].data == "mocked" def test_modify_saves_non_empty_diffs(post_factory, user_factory): - if 'sqlite' in db.sessionmaker.kw['bind'].driver: + if "sqlite" in db.session.get_bind().driver: pytest.xfail( - 'SQLite doesn\'t support transaction isolation, ' - 'which is required to retrieve original entity') + "SQLite doesn't support transaction isolation, " + "which is required to retrieve original entity" + ) post = post_factory() - post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text='old')] + post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="old")] user = user_factory() db.session.add_all([post, user]) db.session.commit() - post.source = 'new source' - post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text='new')] - db.session.flush() - snapshots.modify(post, user) + post.source = "new source" + post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="new")] db.session.flush() - results = db.session.query(model.Snapshot).all() - assert len(results) == 1 - assert results[0].data == { - 'type': 'object change', - 'value': { - 'source': { - 'type': 'primitive change', - 'old-value': None, - 'new-value': 'new source', - }, - 'notes': { - 'type': 'list change', - 'removed': [ - {'polygon': [[0, 0], [0, 1], [1, 1]], 'text': 'old'}], - 'added': [ - {'polygon': [[0, 0], [0, 1], [1, 1]], 'text': 'new'}], + with patch("szurubooru.func.snapshots._post_to_webhooks"): + snapshots.modify(post, user) + db.session.flush() + results = db.session.query(model.Snapshot).all() + assert len(results) == 1 + assert results[0].data == { + "type": "object change", + "value": { + "source": { + "type": "primitive change", + "old-value": None, + "new-value": "new source", + }, + "notes": { + "type": "list change", + "removed": [ + {"polygon": [[0, 0], [0, 1], [1, 1]], "text": "old"} + ], + "added": [ + {"polygon": [[0, 0], [0, 1], [1, 1]], "text": "new"} + ], + }, }, - }, - } + } def test_modify_doesnt_save_empty_diffs(tag_factory, user_factory): - tag = tag_factory(names=['dummy']) + tag = tag_factory(names=["dummy"]) user = user_factory() db.session.add_all([tag, user]) db.session.commit() @@ -185,26 +195,29 @@ def test_modify_doesnt_save_empty_diffs(tag_factory, user_factory): def test_delete(tag_factory, user_factory): - tag = tag_factory(names=['dummy']) + tag = tag_factory(names=["dummy"]) db.session.add(tag) db.session.flush() - with patch('szurubooru.func.snapshots.get_tag_snapshot'): - snapshots.get_tag_snapshot.return_value = 'mocked' + with patch("szurubooru.func.snapshots.get_tag_snapshot"), patch( + "szurubooru.func.snapshots._post_to_webhooks" + ): + snapshots.get_tag_snapshot.return_value = "mocked" snapshots.delete(tag, user_factory()) db.session.flush() results = db.session.query(model.Snapshot).all() assert len(results) == 1 assert results[0].operation == model.Snapshot.OPERATION_DELETED - assert results[0].data == 'mocked' + assert results[0].data == "mocked" def test_merge(tag_factory, user_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) db.session.add_all([source_tag, target_tag]) db.session.flush() - snapshots.merge(source_tag, target_tag, user_factory()) - db.session.flush() - result = db.session.query(model.Snapshot).one() - assert result.operation == model.Snapshot.OPERATION_MERGED - assert result.data == ['tag', 'target'] + with patch("szurubooru.func.snapshots._post_to_webhooks"): + snapshots.merge(source_tag, target_tag, user_factory()) + db.session.flush() + result = db.session.query(model.Snapshot).one() + assert result.operation == model.Snapshot.OPERATION_MERGED + assert result.data == ["tag", "target"] diff --git a/server/szurubooru/tests/func/test_tag_categories.py b/server/szurubooru/tests/func/test_tag_categories.py index d1e5570..11300cf 100644 --- a/server/szurubooru/tests/func/test_tag_categories.py +++ b/server/szurubooru/tests/func/test_tag_categories.py @@ -1,7 +1,9 @@ from unittest.mock import patch + import pytest + from szurubooru import db, model -from szurubooru.func import tag_categories, cache +from szurubooru.func import cache, tag_categories @pytest.fixture(autouse=True) @@ -14,7 +16,7 @@ def test_serialize_category_when_empty(): def test_serialize_category(tag_category_factory, tag_factory): - category = tag_category_factory(name='name', color='color') + category = tag_category_factory(name="name", color="color") category.category_id = 1 category.default = True tag1 = tag_factory(category=category) @@ -23,36 +25,49 @@ def test_serialize_category(tag_category_factory, tag_factory): db.session.flush() result = tag_categories.serialize_category(category) assert result == { - 'name': 'name', - 'color': 'color', - 'default': True, - 'version': 1, - 'usages': 2, + "name": "name", + "color": "color", + "default": True, + "version": 1, + "order": 1, + "usages": 2, } def test_create_category_when_first(): - with patch('szurubooru.func.tag_categories.update_category_name'), \ - patch('szurubooru.func.tag_categories.update_category_color'): - category = tag_categories.create_category('name', 'color') + with patch("szurubooru.func.tag_categories.update_category_name"), patch( + "szurubooru.func.tag_categories.update_category_color" + ), patch("szurubooru.func.tag_categories.update_category_order"): + category = tag_categories.create_category("name", "color", 7) assert category.default - tag_categories.update_category_name \ - .assert_called_once_with(category, 'name') - tag_categories.update_category_color \ - .assert_called_once_with(category, 'color') + tag_categories.update_category_name.assert_called_once_with( + category, "name" + ) + tag_categories.update_category_color.assert_called_once_with( + category, "color" + ) + tag_categories.update_category_order.assert_called_once_with( + category, 7 + ) def test_create_category_when_subsequent(tag_category_factory): db.session.add(tag_category_factory()) db.session.flush() - with patch('szurubooru.func.tag_categories.update_category_name'), \ - patch('szurubooru.func.tag_categories.update_category_color'): - category = tag_categories.create_category('name', 'color') + with patch("szurubooru.func.tag_categories.update_category_name"), patch( + "szurubooru.func.tag_categories.update_category_color" + ), patch("szurubooru.func.tag_categories.update_category_order"): + category = tag_categories.create_category("name", "color", 7) assert not category.default - tag_categories.update_category_name \ - .assert_called_once_with(category, 'name') - tag_categories.update_category_color \ - .assert_called_once_with(category, 'color') + tag_categories.update_category_name.assert_called_once_with( + category, "name" + ) + tag_categories.update_category_color.assert_called_once_with( + category, "color" + ) + tag_categories.update_category_order.assert_called_once_with( + category, 7 + ) def test_update_category_name_with_empty_string(tag_category_factory): @@ -62,38 +77,42 @@ def test_update_category_name_with_empty_string(tag_category_factory): def test_update_category_name_with_invalid_name( - config_injector, tag_category_factory): - config_injector({'tag_category_name_regex': '^[a-z]+$'}) + config_injector, tag_category_factory +): + config_injector({"tag_category_name_regex": "^[a-z]+$"}) category = tag_category_factory() with pytest.raises(tag_categories.InvalidTagCategoryNameError): - tag_categories.update_category_name(category, '0') + tag_categories.update_category_name(category, "0") def test_update_category_name_with_too_long_string( - config_injector, tag_category_factory): - config_injector({'tag_category_name_regex': '^[a-z]+$'}) + config_injector, tag_category_factory +): + config_injector({"tag_category_name_regex": "^[a-z]+$"}) category = tag_category_factory() with pytest.raises(tag_categories.InvalidTagCategoryNameError): - tag_categories.update_category_name(category, 'a' * 3000) + tag_categories.update_category_name(category, "a" * 3000) def test_update_category_name_reusing_other_name( - config_injector, tag_category_factory): - config_injector({'tag_category_name_regex': '.*'}) - db.session.add(tag_category_factory(name='name')) + config_injector, tag_category_factory +): + config_injector({"tag_category_name_regex": ".*"}) + db.session.add(tag_category_factory(name="name")) db.session.flush() category = tag_category_factory() with pytest.raises(tag_categories.TagCategoryAlreadyExistsError): - tag_categories.update_category_name(category, 'name') + tag_categories.update_category_name(category, "name") with pytest.raises(tag_categories.TagCategoryAlreadyExistsError): - tag_categories.update_category_name(category, 'NAME') + tag_categories.update_category_name(category, "NAME") def test_update_category_name_reusing_own_name( - config_injector, tag_category_factory): - config_injector({'tag_category_name_regex': '.*'}) - for name in ['name', 'NAME']: - category = tag_category_factory(name='name') + config_injector, tag_category_factory +): + config_injector({"tag_category_name_regex": ".*"}) + for name in ["name", "NAME"]: + category = tag_category_factory(name="name") db.session.add(category) db.session.flush() tag_categories.update_category_name(category, name) @@ -110,16 +129,16 @@ def test_update_category_color_with_empty_string(tag_category_factory): def test_update_category_color_with_too_long_string(tag_category_factory): category = tag_category_factory() with pytest.raises(tag_categories.InvalidTagCategoryColorError): - tag_categories.update_category_color(category, 'a' * 3000) + tag_categories.update_category_color(category, "a" * 3000) def test_update_category_color_with_invalid_string(tag_category_factory): category = tag_category_factory() with pytest.raises(tag_categories.InvalidTagCategoryColorError): - tag_categories.update_category_color(category, 'NOPE') + tag_categories.update_category_color(category, "NOPE") -@pytest.mark.parametrize('attempt', ['#aaaaaa', '#012345', '012345', 'red']) +@pytest.mark.parametrize("attempt", ["#aaaaaa", "#012345", "012345", "red"]) def test_update_category_color(attempt, tag_category_factory): category = tag_category_factory() tag_categories.update_category_color(category, attempt) @@ -127,36 +146,36 @@ def test_update_category_color(attempt, tag_category_factory): def test_try_get_category_by_name(tag_category_factory): - category = tag_category_factory(name='test') + category = tag_category_factory(name="test") db.session.add(category) db.session.flush() - assert tag_categories.try_get_category_by_name('test') == category - assert tag_categories.try_get_category_by_name('TEST') == category - assert tag_categories.try_get_category_by_name('-') is None + assert tag_categories.try_get_category_by_name("test") == category + assert tag_categories.try_get_category_by_name("TEST") == category + assert tag_categories.try_get_category_by_name("-") is None def test_get_category_by_name(tag_category_factory): - category = tag_category_factory(name='test') + category = tag_category_factory(name="test") db.session.add(category) db.session.flush() - assert tag_categories.get_category_by_name('test') == category - assert tag_categories.get_category_by_name('TEST') == category + assert tag_categories.get_category_by_name("test") == category + assert tag_categories.get_category_by_name("TEST") == category with pytest.raises(tag_categories.TagCategoryNotFoundError): - tag_categories.get_category_by_name('-') + tag_categories.get_category_by_name("-") def test_get_all_category_names(tag_category_factory): - category1 = tag_category_factory(name='cat1') - category2 = tag_category_factory(name='cat2') - db.session.add_all([category1, category2]) + category1 = tag_category_factory(name="cat1") + category2 = tag_category_factory(name="cat2") + db.session.add_all([category2, category1]) db.session.flush() - assert tag_categories.get_all_category_names() == ['cat1', 'cat2'] + assert tag_categories.get_all_category_names() == ["cat1", "cat2"] def test_get_all_categories(tag_category_factory): - category1 = tag_category_factory(name='cat1') - category2 = tag_category_factory(name='cat2') - db.session.add_all([category1, category2]) + category1 = tag_category_factory(name="cat1") + category2 = tag_category_factory(name="cat2") + db.session.add_all([category2, category1]) db.session.flush() assert tag_categories.get_all_categories() == [category1, category2] @@ -211,12 +230,12 @@ def test_get_default_category_name_caching(tag_category_factory): def test_get_default_category(): - with patch('szurubooru.func.tag_categories.try_get_default_category'): + with patch("szurubooru.func.tag_categories.try_get_default_category"): tag_categories.try_get_default_category.return_value = None with pytest.raises(tag_categories.TagCategoryNotFoundError): tag_categories.get_default_category() - tag_categories.try_get_default_category.return_value = 'mocked' - assert tag_categories.get_default_category() == 'mocked' + tag_categories.try_get_default_category.return_value = "mocked" + assert tag_categories.get_default_category() == "mocked" def test_set_default_category_with_previous_default(tag_category_factory): @@ -257,9 +276,9 @@ def test_delete_category_with_usages(tag_category_factory, tag_factory): def test_delete_category(tag_category_factory): db.session.add(tag_category_factory()) - category = tag_category_factory(name='target') + category = tag_category_factory(name="target") db.session.add(category) db.session.flush() tag_categories.delete_category(category) db.session.flush() - assert tag_categories.try_get_category_by_name('target') is None + assert tag_categories.try_get_category_by_name("target") is None diff --git a/server/szurubooru/tests/func/test_tags.py b/server/szurubooru/tests/func/test_tags.py index 1ecc142..c938e68 100644 --- a/server/szurubooru/tests/func/test_tags.py +++ b/server/szurubooru/tests/func/test_tags.py @@ -1,10 +1,12 @@ -import os import json -from unittest.mock import patch +import os from datetime import datetime +from unittest.mock import patch + import pytest + from szurubooru import db, model -from szurubooru.func import tags, tag_categories, cache +from szurubooru.func import cache, tag_categories, tags @pytest.fixture(autouse=True) @@ -14,18 +16,35 @@ def purge_cache(): def _assert_tag_siblings(result, expected_names_and_occurrences): actual_names_and_occurences = [ - (tag.names[0].name, occurrences) for tag, occurrences in result] + (tag.names[0].name, occurrences) for tag, occurrences in result + ] assert actual_names_and_occurences == expected_names_and_occurrences -@pytest.mark.parametrize('input,expected_tag_names', [ - ([('a', 'a', True), ('b', 'b', False), ('c', 'c', False)], list('bca')), - ([('c', 'a', True), ('b', 'b', False), ('a', 'c', False)], list('bac')), - ([('a', 'c', True), ('b', 'b', False), ('c', 'a', False)], list('cba')), - ([('a', 'c', False), ('b', 'b', False), ('c', 'a', True)], list('bac')), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ( + [("a", "a", True), ("b", "b", False), ("c", "c", False)], + list("bca"), + ), + ( + [("c", "a", True), ("b", "b", False), ("a", "c", False)], + list("bac"), + ), + ( + [("a", "c", True), ("b", "b", False), ("c", "a", False)], + list("cba"), + ), + ( + [("a", "c", False), ("b", "b", False), ("c", "a", True)], + list("bac"), + ), + ], +) def test_sort_tags( - input, expected_tag_names, tag_factory, tag_category_factory): + input, expected_tag_names, tag_factory, tag_category_factory +): db_tags = [] for tag in input: tag_name, category_name, category_is_default = tag @@ -33,7 +52,10 @@ def test_sort_tags( tag_factory( names=[tag_name], category=tag_category_factory( - name=category_name, default=category_is_default))) + name=category_name, default=category_is_default + ), + ) + ) db.session.add_all(db_tags) db.session.flush() actual_tag_names = [tag.names[0].name for tag in tags.sort_tags(db_tags)] @@ -45,63 +67,73 @@ def test_serialize_tag_when_empty(): def test_serialize_tag( - post_factory, tag_factory, tag_category_factory, metric_factory): - cat = tag_category_factory(name='cat') - tag = tag_factory(names=['tag1', 'tag2'], category=cat) - tag.tag_id = 1 - tag.description = 'description' + post_factory, + tag_factory, + tag_category_factory, + metric_factory, +): + cat = tag_category_factory(name="cat") + tag = tag_factory(names=["tag1", "tag2"], category=cat) + # tag.tag_id = 1 + tag.description = "description" tag.suggestions = [ - tag_factory(names=['sug1'], category=cat), - tag_factory(names=['sug2'], category=cat), + tag_factory(names=["sug1"], category=cat), + tag_factory(names=["sug2"], category=cat), ] tag.implications = [ - tag_factory(names=['impl1'], category=cat), - tag_factory(names=['impl2'], category=cat), + tag_factory(names=["impl1"], category=cat), + tag_factory(names=["impl2"], category=cat), ] tag.last_edit_time = datetime(1998, 1, 1) + tag.metric = metric_factory(tag, min=1.5, max=10) + post1 = post_factory() - post2 = post_factory() post1.tags = [tag] + post2 = post_factory() post2.tags = [tag] db.session.add_all([tag, post1, post2]) db.session.flush() + result = tags.serialize_tag(tag) - result['suggestions'].sort(key=lambda relation: relation['names'][0]) - result['implications'].sort(key=lambda relation: relation['names'][0]) + result["suggestions"].sort(key=lambda relation: relation["names"][0]) + result["implications"].sort(key=lambda relation: relation["names"][0]) assert result == { - 'names': ['tag1', 'tag2'], - 'version': 1, - 'category': 'cat', - 'creationTime': datetime(1996, 1, 1, 0, 0), - 'lastEditTime': datetime(1998, 1, 1, 0, 0), - 'description': 'description', - 'suggestions': [ - {'names': ['sug1'], 'category': 'cat', 'usages': 0}, - {'names': ['sug2'], 'category': 'cat', 'usages': 0}, + "names": ["tag1", "tag2"], + "version": 1, + "category": "cat", + "creationTime": datetime(1996, 1, 1, 0, 0), + "lastEditTime": datetime(1998, 1, 1, 0, 0), + "description": "description", + "suggestions": [ + {"names": ["sug1"], "category": "cat", "usages": 0}, + {"names": ["sug2"], "category": "cat", "usages": 0}, ], - 'implications': [ - {'names': ['impl1'], 'category': 'cat', 'usages': 0}, - {'names': ['impl2'], 'category': 'cat', 'usages': 0}, + "implications": [ + {"names": ["impl1"], "category": "cat", "usages": 0}, + {"names": ["impl2"], "category": "cat", "usages": 0}, ], - 'metric': { - 'version': 1, - 'min': 1.5, - 'max': 10 + "metric": { + "version": 1, + "min": 1.5, + "max": 10 }, - 'usages': 2, + "usages": 2, } -@pytest.mark.parametrize('name_to_search,expected_to_find', [ - ('name', True), - ('NAME', True), - ('alias', True), - ('ALIAS', True), - ('-', False), -]) +@pytest.mark.parametrize( + "name_to_search,expected_to_find", + [ + ("name", True), + ("NAME", True), + ("alias", True), + ("ALIAS", True), + ("-", False), + ], +) def test_try_get_tag_by_name(name_to_search, expected_to_find, tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) + tag = tag_factory(names=["name", "ALIAS"]) db.session.add(tag) db.session.flush() if expected_to_find: @@ -110,15 +142,18 @@ def test_try_get_tag_by_name(name_to_search, expected_to_find, tag_factory): assert tags.try_get_tag_by_name(name_to_search) is None -@pytest.mark.parametrize('name_to_search,expected_to_find', [ - ('name', True), - ('NAME', True), - ('alias', True), - ('ALIAS', True), - ('-', False), -]) +@pytest.mark.parametrize( + "name_to_search,expected_to_find", + [ + ("name", True), + ("NAME", True), + ("alias", True), + ("ALIAS", True), + ("-", False), + ], +) def test_get_tag_by_name(name_to_search, expected_to_find, tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) + tag = tag_factory(names=["name", "ALIAS"]) db.session.add(tag) db.session.flush() if expected_to_find: @@ -128,25 +163,28 @@ def test_get_tag_by_name(name_to_search, expected_to_find, tag_factory): tags.get_tag_by_name(name_to_search) -@pytest.mark.parametrize('names,expected_indexes', [ - ([], []), - (['name1'], [0]), - (['NAME1'], [0]), - (['alias1'], [0]), - (['ALIAS1'], [0]), - (['name2'], [1]), - (['name1', 'name1'], [0]), - (['name1', 'NAME1'], [0]), - (['name1', 'alias1'], [0]), - (['name1', 'alias2'], [0, 1]), - (['NAME1', 'alias2'], [0, 1]), - (['name1', 'ALIAS2'], [0, 1]), - (['name2', 'alias1'], [0, 1]), -]) +@pytest.mark.parametrize( + "names,expected_indexes", + [ + ([], []), + (["name1"], [0]), + (["NAME1"], [0]), + (["alias1"], [0]), + (["ALIAS1"], [0]), + (["name2"], [1]), + (["name1", "name1"], [0]), + (["name1", "NAME1"], [0]), + (["name1", "alias1"], [0]), + (["name1", "alias2"], [0, 1]), + (["NAME1", "alias2"], [0, 1]), + (["name1", "ALIAS2"], [0, 1]), + (["name2", "alias1"], [0, 1]), + ], +) def test_get_tag_by_names(names, expected_indexes, tag_factory): input_tags = [ - tag_factory(names=['name1', 'ALIAS1']), - tag_factory(names=['name2', 'ALIAS2']), + tag_factory(names=["name1", "ALIAS1"]), + tag_factory(names=["name2", "ALIAS2"]), ] db.session.add_all(input_tags) db.session.flush() @@ -156,49 +194,52 @@ def test_get_tag_by_names(names, expected_indexes, tag_factory): @pytest.mark.parametrize( - 'names,expected_indexes,expected_created_names', [ + "names,expected_indexes,expected_created_names", + [ ([], [], []), - (['name1'], [0], []), - (['NAME1'], [0], []), - (['alias1'], [0], []), - (['ALIAS1'], [0], []), - (['name2'], [1], []), - (['name1', 'name1'], [0], []), - (['name1', 'NAME1'], [0], []), - (['name1', 'alias1'], [0], []), - (['name1', 'alias2'], [0, 1], []), - (['NAME1', 'alias2'], [0, 1], []), - (['name1', 'ALIAS2'], [0, 1], []), - (['name2', 'alias1'], [0, 1], []), - (['new'], [], ['new']), - (['new', 'name1'], [0], ['new']), - (['new', 'NAME1'], [0], ['new']), - (['new', 'alias1'], [0], ['new']), - (['new', 'ALIAS1'], [0], ['new']), - (['new', 'name2'], [1], ['new']), - (['new', 'name1', 'name1'], [0], ['new']), - (['new', 'name1', 'NAME1'], [0], ['new']), - (['new', 'name1', 'alias1'], [0], ['new']), - (['new', 'name1', 'alias2'], [0, 1], ['new']), - (['new', 'NAME1', 'alias2'], [0, 1], ['new']), - (['new', 'name1', 'ALIAS2'], [0, 1], ['new']), - (['new', 'name2', 'alias1'], [0, 1], ['new']), - (['new', 'new'], [], ['new']), - (['new', 'NEW'], [], ['new']), - (['new', 'new2'], [], ['new', 'new2']), - ]) + (["name1"], [0], []), + (["NAME1"], [0], []), + (["alias1"], [0], []), + (["ALIAS1"], [0], []), + (["name2"], [1], []), + (["name1", "name1"], [0], []), + (["name1", "NAME1"], [0], []), + (["name1", "alias1"], [0], []), + (["name1", "alias2"], [0, 1], []), + (["NAME1", "alias2"], [0, 1], []), + (["name1", "ALIAS2"], [0, 1], []), + (["name2", "alias1"], [0, 1], []), + (["new"], [], ["new"]), + (["new", "name1"], [0], ["new"]), + (["new", "NAME1"], [0], ["new"]), + (["new", "alias1"], [0], ["new"]), + (["new", "ALIAS1"], [0], ["new"]), + (["new", "name2"], [1], ["new"]), + (["new", "name1", "name1"], [0], ["new"]), + (["new", "name1", "NAME1"], [0], ["new"]), + (["new", "name1", "alias1"], [0], ["new"]), + (["new", "name1", "alias2"], [0, 1], ["new"]), + (["new", "NAME1", "alias2"], [0, 1], ["new"]), + (["new", "name1", "ALIAS2"], [0, 1], ["new"]), + (["new", "name2", "alias1"], [0, 1], ["new"]), + (["new", "new"], [], ["new"]), + (["new", "NEW"], [], ["new"]), + (["new", "new2"], [], ["new", "new2"]), + ], +) def test_get_or_create_tags_by_names( - names, - expected_indexes, - expected_created_names, - tag_factory, - tag_category_factory, - config_injector): - config_injector({'tag_name_regex': '.*'}) + names, + expected_indexes, + expected_created_names, + tag_factory, + tag_category_factory, + config_injector, +): + config_injector({"tag_name_regex": ".*"}) category = tag_category_factory() input_tags = [ - tag_factory(names=['name1', 'ALIAS1'], category=category), - tag_factory(names=['name2', 'ALIAS2'], category=category), + tag_factory(names=["name1", "ALIAS1"], category=category), + tag_factory(names=["name2", "ALIAS2"], category=category), ] db.session.add_all(input_tags) db.session.flush() @@ -211,14 +252,14 @@ def test_get_or_create_tags_by_names( def test_get_tag_siblings_for_unused(tag_factory): - tag = tag_factory(names=['tag']) + tag = tag_factory(names=["tag"]) db.session.add(tag) db.session.flush() _assert_tag_siblings(tags.get_tag_siblings(tag), []) def test_get_tag_siblings_for_used_alone(tag_factory, post_factory): - tag = tag_factory(names=['tag']) + tag = tag_factory(names=["tag"]) post = post_factory() post.tags = [tag] db.session.add_all([post, tag]) @@ -227,20 +268,20 @@ def test_get_tag_siblings_for_used_alone(tag_factory, post_factory): def test_get_tag_siblings_for_used_with_others(tag_factory, post_factory): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) post = post_factory() post.tags = [tag1, tag2] db.session.add_all([post, tag1, tag2]) db.session.flush() - _assert_tag_siblings(tags.get_tag_siblings(tag1), [('t2', 1)]) - _assert_tag_siblings(tags.get_tag_siblings(tag2), [('t1', 1)]) + _assert_tag_siblings(tags.get_tag_siblings(tag1), [("t2", 1)]) + _assert_tag_siblings(tags.get_tag_siblings(tag2), [("t1", 1)]) def test_get_tag_siblings_used_for_multiple_others(tag_factory, post_factory): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) - tag3 = tag_factory(names=['t3']) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) + tag3 = tag_factory(names=["t3"]) post1 = post_factory() post2 = post_factory() post3 = post_factory() @@ -251,16 +292,16 @@ def test_get_tag_siblings_used_for_multiple_others(tag_factory, post_factory): post4.tags = [tag2] db.session.add_all([post1, post2, post3, post4, tag1, tag2, tag3]) db.session.flush() - _assert_tag_siblings(tags.get_tag_siblings(tag1), [('t3', 2), ('t2', 1)]) - _assert_tag_siblings(tags.get_tag_siblings(tag2), [('t1', 1), ('t3', 1)]) + _assert_tag_siblings(tags.get_tag_siblings(tag1), [("t3", 2), ("t2", 1)]) + _assert_tag_siblings(tags.get_tag_siblings(tag2), [("t1", 1), ("t3", 1)]) # even though tag2 is used more widely, tag1 is more relevant to tag3 - _assert_tag_siblings(tags.get_tag_siblings(tag3), [('t1', 2), ('t2', 1)]) + _assert_tag_siblings(tags.get_tag_siblings(tag3), [("t1", 2), ("t2", 1)]) def test_delete(tag_factory): - tag = tag_factory(names=['tag']) - tag.suggestions = [tag_factory(names=['sug'])] - tag.implications = [tag_factory(names=['imp'])] + tag = tag_factory(names=["tag"]) + tag.suggestions = [tag_factory(names=["sug"])] + tag.implications = [tag_factory(names=["imp"])] db.session.add(tag) db.session.flush() assert db.session.query(model.Tag).count() == 3 @@ -270,19 +311,19 @@ def test_delete(tag_factory): def test_merge_tags_deletes_source_tag(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) db.session.add_all([source_tag, target_tag]) db.session.flush() tags.merge_tags(source_tag, target_tag) db.session.flush() - assert tags.try_get_tag_by_name('source') is None - tag = tags.get_tag_by_name('target') + assert tags.try_get_tag_by_name("source") is None + tag = tags.get_tag_by_name("target") assert tag is not None def test_merge_tags_with_itself(tag_factory): - source_tag = tag_factory(names=['source']) + source_tag = tag_factory(names=["source"]) db.session.add(source_tag) db.session.flush() with pytest.raises(tags.InvalidTagRelationError): @@ -306,8 +347,8 @@ def test_merge_tags_with_metrics(tag_factory, metric_factory): def test_merge_tags_moves_usages(tag_factory, post_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) post = post_factory() post.tags = [source_tag] db.session.add_all([source_tag, target_tag, post]) @@ -316,13 +357,13 @@ def test_merge_tags_moves_usages(tag_factory, post_factory): assert target_tag.post_count == 0 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').post_count == 1 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").post_count == 1 def test_merge_tags_doesnt_duplicate_usages(tag_factory, post_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) post = post_factory() post.tags = [source_tag, target_tag] db.session.add_all([source_tag, target_tag, post]) @@ -331,13 +372,13 @@ def test_merge_tags_doesnt_duplicate_usages(tag_factory, post_factory): assert target_tag.post_count == 1 tags.merge_tags(source_tag, target_tag) db.session.flush() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').post_count == 1 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").post_count == 1 def test_merge_tags_moves_child_relations(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) related_tag = tag_factory() source_tag.suggestions = [related_tag] source_tag.implications = [related_tag] @@ -349,14 +390,14 @@ def test_merge_tags_moves_child_relations(tag_factory): assert target_tag.implication_count == 0 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').suggestion_count == 1 - assert tags.get_tag_by_name('target').implication_count == 1 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").suggestion_count == 1 + assert tags.get_tag_by_name("target").implication_count == 1 def test_merge_tags_doesnt_duplicate_child_relations(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) related_tag = tag_factory() source_tag.suggestions = [related_tag] source_tag.implications = [related_tag] @@ -370,15 +411,15 @@ def test_merge_tags_doesnt_duplicate_child_relations(tag_factory): assert target_tag.implication_count == 1 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').suggestion_count == 1 - assert tags.get_tag_by_name('target').implication_count == 1 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").suggestion_count == 1 + assert tags.get_tag_by_name("target").implication_count == 1 def test_merge_tags_moves_parent_relations(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) - related_tag = tag_factory(names=['related']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) + related_tag = tag_factory(names=["related"]) related_tag.suggestions = [related_tag] related_tag.implications = [related_tag] db.session.add_all([source_tag, target_tag, related_tag]) @@ -389,16 +430,16 @@ def test_merge_tags_moves_parent_relations(tag_factory): assert target_tag.implication_count == 0 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('related').suggestion_count == 1 - assert tags.get_tag_by_name('related').suggestion_count == 1 - assert tags.get_tag_by_name('target').suggestion_count == 0 - assert tags.get_tag_by_name('target').implication_count == 0 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("related").suggestion_count == 1 + assert tags.get_tag_by_name("related").suggestion_count == 1 + assert tags.get_tag_by_name("target").suggestion_count == 0 + assert tags.get_tag_by_name("target").implication_count == 0 def test_merge_tags_doesnt_create_relation_loop_for_children(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) source_tag.suggestions = [target_tag] source_tag.implications = [target_tag] db.session.add_all([source_tag, target_tag]) @@ -409,14 +450,14 @@ def test_merge_tags_doesnt_create_relation_loop_for_children(tag_factory): assert target_tag.implication_count == 0 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').suggestion_count == 0 - assert tags.get_tag_by_name('target').implication_count == 0 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").suggestion_count == 0 + assert tags.get_tag_by_name("target").implication_count == 0 def test_merge_tags_doesnt_create_relation_loop_for_parents(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) + source_tag = tag_factory(names=["source"]) + target_tag = tag_factory(names=["target"]) target_tag.suggestions = [source_tag] target_tag.implications = [source_tag] db.session.add_all([source_tag, target_tag]) @@ -427,33 +468,35 @@ def test_merge_tags_doesnt_create_relation_loop_for_parents(tag_factory): assert target_tag.implication_count == 1 tags.merge_tags(source_tag, target_tag) db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').suggestion_count == 0 - assert tags.get_tag_by_name('target').implication_count == 0 + assert tags.try_get_tag_by_name("source") is None + assert tags.get_tag_by_name("target").suggestion_count == 0 + assert tags.get_tag_by_name("target").implication_count == 0 def test_create_tag(fake_datetime): - with patch('szurubooru.func.tags.update_tag_names'), \ - patch('szurubooru.func.tags.update_tag_category_name'), \ - patch('szurubooru.func.tags.update_tag_suggestions'), \ - patch('szurubooru.func.tags.update_tag_implications'), \ - fake_datetime('1997-01-01'): - tag = tags.create_tag(['name'], 'cat', ['sug'], ['imp']) + with patch("szurubooru.func.tags.update_tag_names"), patch( + "szurubooru.func.tags.update_tag_category_name" + ), patch("szurubooru.func.tags.update_tag_suggestions"), patch( + "szurubooru.func.tags.update_tag_implications" + ), fake_datetime( + "1997-01-01" + ): + tag = tags.create_tag(["name"], "cat", ["sug"], ["imp"]) assert tag.creation_time == datetime(1997, 1, 1) assert tag.last_edit_time is None - tags.update_tag_names.assert_called_once_with(tag, ['name']) - tags.update_tag_category_name.assert_called_once_with(tag, 'cat') - tags.update_tag_suggestions.assert_called_once_with(tag, ['sug']) - tags.update_tag_implications.assert_called_once_with(tag, ['imp']) + tags.update_tag_names.assert_called_once_with(tag, ["name"]) + tags.update_tag_category_name.assert_called_once_with(tag, "cat") + tags.update_tag_suggestions.assert_called_once_with(tag, ["sug"]) + tags.update_tag_implications.assert_called_once_with(tag, ["imp"]) def test_update_tag_category_name(tag_factory): - with patch('szurubooru.func.tag_categories.get_category_by_name'): - tag_categories.get_category_by_name.return_value = 'mocked' + with patch("szurubooru.func.tag_categories.get_category_by_name"): + tag_categories.get_category_by_name.return_value = "mocked" tag = tag_factory() - tags.update_tag_category_name(tag, 'cat') - assert tag_categories.get_category_by_name.called_once_with('cat') - assert tag.category == 'mocked' + tags.update_tag_category_name(tag, "cat") + assert tag_categories.get_category_by_name.called_once_with("cat") + assert tag.category == "mocked" def test_update_tag_names_to_empty(tag_factory): @@ -463,44 +506,45 @@ def test_update_tag_names_to_empty(tag_factory): def test_update_tag_names_with_invalid_name(config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-z]*$'}) + config_injector({"tag_name_regex": "^[a-z]*$"}) tag = tag_factory() with pytest.raises(tags.InvalidTagNameError): - tags.update_tag_names(tag, ['0']) + tags.update_tag_names(tag, ["0"]) def test_update_tag_names_with_too_long_string(config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-z]*$'}) + config_injector({"tag_name_regex": "^[a-z]*$"}) tag = tag_factory() with pytest.raises(tags.InvalidTagNameError): - tags.update_tag_names(tag, ['a' * 300]) + tags.update_tag_names(tag, ["a" * 300]) def test_update_tag_names_with_duplicate_names(config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-z]*$'}) + config_injector({"tag_name_regex": "^[a-z]*$"}) tag = tag_factory() - tags.update_tag_names(tag, ['a', 'A']) - assert [tag_name.name for tag_name in tag.names] == ['a'] + tags.update_tag_names(tag, ["a", "A"]) + assert [tag_name.name for tag_name in tag.names] == ["a"] def test_update_tag_names_trying_to_use_taken_name( - config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-zA-Z]*$'}) - existing_tag = tag_factory(names=['a']) + config_injector, tag_factory +): + config_injector({"tag_name_regex": "^[a-zA-Z]*$"}) + existing_tag = tag_factory(names=["a"]) db.session.add(existing_tag) tag = tag_factory() db.session.add(tag) db.session.flush() with pytest.raises(tags.TagAlreadyExistsError): - tags.update_tag_names(tag, ['a']) + tags.update_tag_names(tag, ["a"]) with pytest.raises(tags.TagAlreadyExistsError): - tags.update_tag_names(tag, ['A']) + tags.update_tag_names(tag, ["A"]) def test_update_tag_names_reusing_own_name(config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-zA-Z]*$'}) - for name in list('aA'): - tag = tag_factory(names=['a']) + config_injector({"tag_name_regex": "^[a-zA-Z]*$"}) + for name in list("aA"): + tag = tag_factory(names=["a"]) db.session.add(tag) db.session.flush() tags.update_tag_names(tag, [name]) @@ -509,48 +553,48 @@ def test_update_tag_names_reusing_own_name(config_injector, tag_factory): def test_update_tag_names_changing_primary_name(config_injector, tag_factory): - config_injector({'tag_name_regex': '^[a-zA-Z]*$'}) - tag = tag_factory(names=['a', 'b']) + config_injector({"tag_name_regex": "^[a-zA-Z]*$"}) + tag = tag_factory(names=["a", "b"]) db.session.add(tag) db.session.flush() - tags.update_tag_names(tag, ['b', 'a']) + tags.update_tag_names(tag, ["b", "a"]) db.session.flush() db.session.refresh(tag) - assert [tag_name.name for tag_name in tag.names] == ['b', 'a'] + assert [tag_name.name for tag_name in tag.names] == ["b", "a"] db.session.rollback() -@pytest.mark.parametrize('attempt', ['name', 'NAME', 'alias', 'ALIAS']) +@pytest.mark.parametrize("attempt", ["name", "NAME", "alias", "ALIAS"]) def test_update_tag_suggestions_with_itself(attempt, tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) + tag = tag_factory(names=["name", "ALIAS"]) with pytest.raises(tags.InvalidTagRelationError): tags.update_tag_suggestions(tag, [attempt]) def test_update_tag_suggestions(tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) - with patch('szurubooru.func.tags.get_tags_by_names'): - tags.get_tags_by_names.return_value = ['returned tags'] - tags.update_tag_suggestions(tag, ['test']) - assert tag.suggestions == ['returned tags'] + tag = tag_factory(names=["name", "ALIAS"]) + with patch("szurubooru.func.tags.get_tags_by_names"): + tags.get_tags_by_names.return_value = ["returned tags"] + tags.update_tag_suggestions(tag, ["test"]) + assert tag.suggestions == ["returned tags"] -@pytest.mark.parametrize('attempt', ['name', 'NAME', 'alias', 'ALIAS']) +@pytest.mark.parametrize("attempt", ["name", "NAME", "alias", "ALIAS"]) def test_update_tag_implications_with_itself(attempt, tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) + tag = tag_factory(names=["name", "ALIAS"]) with pytest.raises(tags.InvalidTagRelationError): tags.update_tag_implications(tag, [attempt]) def test_update_tag_implications(tag_factory): - tag = tag_factory(names=['name', 'ALIAS']) - with patch('szurubooru.func.tags.get_tags_by_names'): - tags.get_tags_by_names.return_value = ['returned tags'] - tags.update_tag_implications(tag, ['test']) - assert tag.implications == ['returned tags'] + tag = tag_factory(names=["name", "ALIAS"]) + with patch("szurubooru.func.tags.get_tags_by_names"): + tags.get_tags_by_names.return_value = ["returned tags"] + tags.update_tag_implications(tag, ["test"]) + assert tag.implications == ["returned tags"] def test_update_tag_description(tag_factory): tag = tag_factory() - tags.update_tag_description(tag, 'test') - assert tag.description == 'test' + tags.update_tag_description(tag, "test") + assert tag.description == "test" diff --git a/server/szurubooru/tests/func/test_user_tokens.py b/server/szurubooru/tests/func/test_user_tokens.py index 8c3577c..0422f4d 100644 --- a/server/szurubooru/tests/func/test_user_tokens.py +++ b/server/szurubooru/tests/func/test_user_tokens.py @@ -1,32 +1,35 @@ +import random +import string from datetime import datetime, timedelta from unittest.mock import patch + import pytest import pytz -import random -import string + from szurubooru import db, model -from szurubooru.func import user_tokens, users, auth, util +from szurubooru.func import auth, user_tokens, users, util def test_serialize_user_token(user_token_factory): user_token = user_token_factory() db.session.add(user_token) db.session.flush() - with patch('szurubooru.func.users.get_avatar_url'): - users.get_avatar_url.return_value = 'https://example.com/avatar.png' + with patch("szurubooru.func.users.get_avatar_url"): + users.get_avatar_url.return_value = "https://example.com/avatar.png" result = user_tokens.serialize_user_token(user_token, user_token.user) assert result == { - 'creationTime': datetime(1997, 1, 1, 0, 0), - 'enabled': True, - 'expirationTime': None, - 'lastEditTime': None, - 'lastUsageTime': None, - 'note': None, - 'token': 'dummy', - 'user': { - 'avatarUrl': 'https://example.com/avatar.png', - 'name': user_token.user.name}, - 'version': 1 + "creationTime": datetime(1997, 1, 1, 0, 0), + "enabled": True, + "expirationTime": None, + "lastEditTime": None, + "lastUsageTime": None, + "note": None, + "token": "dummy", + "user": { + "avatarUrl": "https://example.com/avatar.png", + "name": user_token.user.name, + }, + "version": 1, } @@ -41,7 +44,8 @@ def test_get_by_user_and_token(user_token_factory): db.session.flush() db.session.commit() result = user_tokens.get_by_user_and_token( - user_token.user, user_token.token) + user_token.user, user_token.token + ) assert result == user_token @@ -61,10 +65,10 @@ def test_create_user_token(user_factory): db.session.add(user) db.session.flush() db.session.commit() - with patch('szurubooru.func.auth.generate_authorization_token'): - auth.generate_authorization_token.return_value = 'test' + with patch("szurubooru.func.auth.generate_authorization_token"): + auth.generate_authorization_token.return_value = "test" result = user_tokens.create_user_token(user, True) - assert result.token == 'test' + assert result.token == "test" assert result.user == user @@ -85,8 +89,8 @@ def test_update_user_token_edit_time(user_token_factory): def test_update_user_token_note(user_token_factory): user_token = user_token_factory() assert user_token.note is None - user_tokens.update_user_token_note(user_token, ' Test Note ') - assert user_token.note == 'Test Note' + user_tokens.update_user_token_note(user_token, " Test Note ") + assert user_token.note == "Test Note" assert user_token.last_edit_time is not None @@ -94,8 +98,9 @@ def test_update_user_token_note_input_too_long(user_token_factory): user_token = user_token_factory() assert user_token.note is None note_max_length = util.get_column_size(model.UserToken.note) + 1 - note = ''.join( - random.choice(string.ascii_letters) for _ in range(note_max_length)) + note = "".join( + random.choice(string.ascii_letters) for _ in range(note_max_length) + ) with pytest.raises(user_tokens.InvalidNoteError): user_tokens.update_user_token_note(user_token, note) @@ -104,11 +109,11 @@ def test_update_user_token_expiration_time(user_token_factory): user_token = user_token_factory() assert user_token.expiration_time is None expiration_time_str = ( - (datetime.utcnow() + timedelta(days=1)) - .replace(tzinfo=pytz.utc) + (datetime.utcnow() + timedelta(days=1)).replace(tzinfo=pytz.utc) ).isoformat() user_tokens.update_user_token_expiration_time( - user_token, expiration_time_str) + user_token, expiration_time_str + ) assert user_token.expiration_time.isoformat() == expiration_time_str assert user_token.last_edit_time is not None @@ -117,39 +122,45 @@ def test_update_user_token_expiration_time_in_past(user_token_factory): user_token = user_token_factory() assert user_token.expiration_time is None expiration_time_str = ( - (datetime.utcnow() - timedelta(days=1)) - .replace(tzinfo=pytz.utc) + (datetime.utcnow() - timedelta(days=1)).replace(tzinfo=pytz.utc) ).isoformat() with pytest.raises( - user_tokens.InvalidExpirationError, - match='Expiration cannot happen in the past'): + user_tokens.InvalidExpirationError, + match="Expiration cannot happen in the past", + ): user_tokens.update_user_token_expiration_time( - user_token, expiration_time_str) + user_token, expiration_time_str + ) -@pytest.mark.parametrize('expiration_time_str', [ - datetime.utcnow().isoformat(), - (datetime.utcnow() - timedelta(days=1)).ctime(), - '1970/01/01 00:00:01.0000Z', - '70/01/01 00:00:01.0000Z', - ''.join(random.choice(string.ascii_letters) for _ in range(15)), - ''.join(random.choice(string.digits) for _ in range(8)) -]) +@pytest.mark.parametrize( + "expiration_time_str", + [ + datetime.utcnow().isoformat(), + (datetime.utcnow() - timedelta(days=1)).ctime(), + "1970/01/01 00:00:01.0000Z", + "70/01/01 00:00:01.0000Z", + "".join(random.choice(string.ascii_letters) for _ in range(15)), + "".join(random.choice(string.digits) for _ in range(8)), + ], +) def test_update_user_token_expiration_time_invalid_format( - expiration_time_str, user_token_factory): + expiration_time_str, user_token_factory +): user_token = user_token_factory() assert user_token.expiration_time is None with pytest.raises( - user_tokens.InvalidExpirationError, - match='Expiration is in an invalid format %s' - % expiration_time_str): + user_tokens.InvalidExpirationError, + match="Expiration is in an invalid format %s" % expiration_time_str, + ): user_tokens.update_user_token_expiration_time( - user_token, expiration_time_str) + user_token, expiration_time_str + ) def test_bump_usage_time(user_token_factory, fake_datetime): user_token = user_token_factory() - with fake_datetime('1997-01-01'): + with fake_datetime("1997-01-01"): user_tokens.bump_usage_time(user_token) assert user_token.last_usage_time == datetime(1997, 1, 1) diff --git a/server/szurubooru/tests/func/test_users.py b/server/szurubooru/tests/func/test_users.py index 5506127..94e9c7c 100644 --- a/server/szurubooru/tests/func/test_users.py +++ b/server/szurubooru/tests/func/test_users.py @@ -1,56 +1,70 @@ -from unittest.mock import patch from datetime import datetime +from unittest.mock import patch + import pytest -from szurubooru import db, model, errors -from szurubooru.func import auth, users, files, util +from szurubooru import db, errors, model +from szurubooru.func import auth, files, users, util EMPTY_PIXEL = ( - b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00' - b'\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00' - b'\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b') + b"\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00" + b"\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00" + b"\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b" +) -@pytest.mark.parametrize('user_name', ['test', 'TEST']) +@pytest.mark.parametrize("user_name", ["test", "TEST"]) def test_get_avatar_path(user_name): - assert users.get_avatar_path(user_name) == 'avatars/test.png' + assert users.get_avatar_path(user_name) == "avatars/test.png" -@pytest.mark.parametrize('user_name,user_email,avatar_style,expected_url', [ - ( - 'user', - None, - model.User.AVATAR_GRAVATAR, - ('https://gravatar.com/avatar/' + - 'ee11cbb19052e40b07aac0ca060c23ee?d=retro&s=100'), - ), - ( - None, - 'user@example.com', - model.User.AVATAR_GRAVATAR, - ('https://gravatar.com/avatar/' + - 'b58996c504c5638798eb6b511e6f49af?d=retro&s=100'), - ), - ( - 'user', - 'user@example.com', - model.User.AVATAR_GRAVATAR, - ('https://gravatar.com/avatar/' + - 'b58996c504c5638798eb6b511e6f49af?d=retro&s=100'), - ), - ( - 'user', - None, - model.User.AVATAR_MANUAL, - 'http://example.com/avatars/user.png', - ), -]) +@pytest.mark.parametrize( + "user_name,user_email,avatar_style,expected_url", + [ + ( + "user", + None, + model.User.AVATAR_GRAVATAR, + ( + "https://gravatar.com/avatar/" + + "ee11cbb19052e40b07aac0ca060c23ee?d=retro&s=100" + ), + ), + ( + None, + "user@example.com", + model.User.AVATAR_GRAVATAR, + ( + "https://gravatar.com/avatar/" + + "b58996c504c5638798eb6b511e6f49af?d=retro&s=100" + ), + ), + ( + "user", + "user@example.com", + model.User.AVATAR_GRAVATAR, + ( + "https://gravatar.com/avatar/" + + "b58996c504c5638798eb6b511e6f49af?d=retro&s=100" + ), + ), + ( + "user", + None, + model.User.AVATAR_MANUAL, + "http://example.com/avatars/user.png", + ), + ], +) def test_get_avatar_url( - user_name, user_email, avatar_style, expected_url, config_injector): - config_injector({ - 'data_url': 'http://example.com/', - 'thumbnails': {'avatar_width': 100}, - }) + user_name, user_email, avatar_style, expected_url, config_injector +): + config_injector( + { + "data_url": "http://example.com/", + "thumbnails": {"avatar_width": 100}, + } + ) user = model.User() user.name = user_name user.email = user_email @@ -59,23 +73,21 @@ def test_get_avatar_url( @pytest.mark.parametrize( - 'same_user,can_edit_any_email,force_show,expected_email', + "same_user,can_edit_any_email,force_show,expected_email", [ (False, False, False, False), - (True, False, False, 'test@example.com'), - (False, True, False, 'test@example.com'), - (False, False, True, 'test@example.com'), - ]) + (True, False, False, "test@example.com"), + (False, True, False, "test@example.com"), + (False, False, True, "test@example.com"), + ], +) def test_get_email( - same_user, - can_edit_any_email, - force_show, - expected_email, - user_factory): - with patch('szurubooru.func.auth.has_privilege'): + same_user, can_edit_any_email, force_show, expected_email, user_factory +): + with patch("szurubooru.func.auth.has_privilege"): auth.has_privilege = lambda user, name: can_edit_any_email user = user_factory() - user.email = 'test@example.com' + user.email = "test@example.com" auth_user = user if same_user else user_factory() db.session.add_all([user, auth_user]) db.session.flush() @@ -83,25 +95,28 @@ def test_get_email( @pytest.mark.parametrize( - 'same_user,score,expected_liked_post_count,expected_disliked_post_count', + "same_user,score,expected_liked_post_count,expected_disliked_post_count", [ (False, 1, False, False), (False, -1, False, False), (True, 1, 1, 0), (True, -1, 0, 1), - ]) + ], +) def test_get_liked_post_count( - same_user, - score, - expected_liked_post_count, - expected_disliked_post_count, - user_factory, - post_factory): + same_user, + score, + expected_liked_post_count, + expected_disliked_post_count, + user_factory, + post_factory, +): user = user_factory() post = post_factory() auth_user = user if same_user else user_factory() score = model.PostScore( - post=post, user=user, score=score, time=datetime.now()) + post=post, user=user, score=score, time=datetime.now() + ) db.session.add_all([post, user, score]) db.session.flush() actual_liked_post_count = users.get_liked_post_count(user, auth_user) @@ -115,16 +130,17 @@ def test_serialize_user_when_empty(): def test_serialize_user(user_factory): - with patch('szurubooru.func.users.get_email'), \ - patch('szurubooru.func.users.get_avatar_url'), \ - patch('szurubooru.func.users.get_liked_post_count'), \ - patch('szurubooru.func.users.get_disliked_post_count'): - users.get_email.return_value = 'test@example.com' - users.get_avatar_url.return_value = 'https://example.com/avatar.png' + with patch("szurubooru.func.users.get_email"), patch( + "szurubooru.func.users.get_avatar_url" + ), patch("szurubooru.func.users.get_liked_post_count"), patch( + "szurubooru.func.users.get_disliked_post_count" + ): + users.get_email.return_value = "test@example.com" + users.get_avatar_url.return_value = "https://example.com/avatar.png" users.get_liked_post_count.return_value = 66 users.get_disliked_post_count.return_value = 33 auth_user = user_factory() - user = user_factory(name='dummy user') + user = user_factory(name="dummy user") user.creation_time = datetime(1997, 1, 1) user.last_edit_time = datetime(1998, 1, 1) user.avatar_style = model.User.AVATAR_MANUAL @@ -132,36 +148,36 @@ def test_serialize_user(user_factory): db.session.add(user) db.session.flush() assert users.serialize_user(user, auth_user) == { - 'version': 1, - 'name': 'dummy user', - 'email': 'test@example.com', - 'rank': 'administrator', - 'creationTime': datetime(1997, 1, 1, 0, 0), - 'lastLoginTime': None, - 'avatarStyle': 'manual', - 'avatarUrl': 'https://example.com/avatar.png', - 'likedPostCount': 66, - 'dislikedPostCount': 33, - 'commentCount': 0, - 'favoritePostCount': 0, - 'uploadedPostCount': 0, + "version": 1, + "name": "dummy user", + "email": "test@example.com", + "rank": "administrator", + "creationTime": datetime(1997, 1, 1, 0, 0), + "lastLoginTime": None, + "avatarStyle": "manual", + "avatarUrl": "https://example.com/avatar.png", + "likedPostCount": 66, + "dislikedPostCount": 33, + "commentCount": 0, + "favoritePostCount": 0, + "uploadedPostCount": 0, } def test_serialize_micro_user(user_factory): - with patch('szurubooru.func.users.get_avatar_url'): - users.get_avatar_url.return_value = 'https://example.com/avatar.png' + with patch("szurubooru.func.users.get_avatar_url"): + users.get_avatar_url.return_value = "https://example.com/avatar.png" auth_user = user_factory() - user = user_factory(name='dummy user') + user = user_factory(name="dummy user") db.session.add(user) db.session.flush() assert users.serialize_micro_user(user, auth_user) == { - 'name': 'dummy user', - 'avatarUrl': 'https://example.com/avatar.png', + "name": "dummy user", + "avatarUrl": "https://example.com/avatar.png", } -@pytest.mark.parametrize('count', [0, 1, 2]) +@pytest.mark.parametrize("count", [0, 1, 2]) def test_get_user_count(count, user_factory): for _ in range(count): db.session.add(user_factory()) @@ -170,72 +186,73 @@ def test_get_user_count(count, user_factory): def test_try_get_user_by_name(user_factory): - user = user_factory(name='name', email='email') + user = user_factory(name="name", email="email") db.session.add(user) db.session.flush() - assert users.try_get_user_by_name('non-existing') is None - assert users.try_get_user_by_name('email') is None - assert users.try_get_user_by_name('name') is user - assert users.try_get_user_by_name('NAME') is user + assert users.try_get_user_by_name("non-existing") is None + assert users.try_get_user_by_name("email") is None + assert users.try_get_user_by_name("name") is user + assert users.try_get_user_by_name("NAME") is user def test_get_user_by_name(user_factory): - user = user_factory(name='name', email='email') + user = user_factory(name="name", email="email") db.session.add(user) db.session.flush() with pytest.raises(users.UserNotFoundError): - assert users.get_user_by_name('non-existing') + assert users.get_user_by_name("non-existing") with pytest.raises(users.UserNotFoundError): - assert users.get_user_by_name('email') - assert users.get_user_by_name('name') is user - assert users.get_user_by_name('NAME') is user + assert users.get_user_by_name("email") + assert users.get_user_by_name("name") is user + assert users.get_user_by_name("NAME") is user def test_try_get_user_by_name_or_email(user_factory): - user = user_factory(name='name', email='email') + user = user_factory(name="name", email="email") db.session.add(user) db.session.flush() - assert users.try_get_user_by_name_or_email('non-existing') is None - assert users.try_get_user_by_name_or_email('email') is user - assert users.try_get_user_by_name_or_email('EMAIL') is user - assert users.try_get_user_by_name_or_email('name') is user - assert users.try_get_user_by_name_or_email('NAME') is user + assert users.try_get_user_by_name_or_email("non-existing") is None + assert users.try_get_user_by_name_or_email("email") is user + assert users.try_get_user_by_name_or_email("EMAIL") is user + assert users.try_get_user_by_name_or_email("name") is user + assert users.try_get_user_by_name_or_email("NAME") is user def test_get_user_by_name_or_email(user_factory): - user = user_factory(name='name', email='email') + user = user_factory(name="name", email="email") db.session.add(user) db.session.flush() with pytest.raises(users.UserNotFoundError): - assert users.get_user_by_name_or_email('non-existing') - assert users.get_user_by_name_or_email('email') is user - assert users.get_user_by_name_or_email('EMAIL') is user - assert users.get_user_by_name_or_email('name') is user - assert users.get_user_by_name_or_email('NAME') is user + assert users.get_user_by_name_or_email("non-existing") + assert users.get_user_by_name_or_email("email") is user + assert users.get_user_by_name_or_email("EMAIL") is user + assert users.get_user_by_name_or_email("name") is user + assert users.get_user_by_name_or_email("NAME") is user def test_create_user_for_first_user(fake_datetime): - with patch('szurubooru.func.users.update_user_name'), \ - patch('szurubooru.func.users.update_user_password'), \ - patch('szurubooru.func.users.update_user_email'), \ - fake_datetime('1997-01-01'): - user = users.create_user('name', 'password', 'email') + with patch("szurubooru.func.users.update_user_name"), patch( + "szurubooru.func.users.update_user_password" + ), patch("szurubooru.func.users.update_user_email"), fake_datetime( + "1997-01-01" + ): + user = users.create_user("name", "password", "email") assert user.creation_time == datetime(1997, 1, 1) assert user.last_login_time is None assert user.rank == model.User.RANK_ADMINISTRATOR - users.update_user_name.assert_called_once_with(user, 'name') - users.update_user_password.assert_called_once_with(user, 'password') - users.update_user_email.assert_called_once_with(user, 'email') + users.update_user_name.assert_called_once_with(user, "name") + users.update_user_password.assert_called_once_with(user, "password") + users.update_user_email.assert_called_once_with(user, "email") def test_create_user_for_subsequent_users(user_factory, config_injector): - config_injector({'default_rank': 'regular'}) + config_injector({"default_rank": "regular"}) db.session.add(user_factory()) db.session.flush() - with patch('szurubooru.func.users.update_user_name'), \ - patch('szurubooru.func.users.update_user_email'), \ - patch('szurubooru.func.users.update_user_password'): - user = users.create_user('name', 'password', 'email') + with patch("szurubooru.func.users.update_user_name"), patch( + "szurubooru.func.users.update_user_email" + ), patch("szurubooru.func.users.update_user_password"): + user = users.create_user("name", "password", "email") assert user.rank == model.User.RANK_REGULAR @@ -248,56 +265,58 @@ def test_update_user_name_with_empty_string(user_factory): def test_update_user_name_with_too_long_string(user_factory): user = user_factory() with pytest.raises(users.InvalidUserNameError): - users.update_user_name(user, 'a' * 300) + users.update_user_name(user, "a" * 300) def test_update_user_name_with_invalid_name(user_factory, config_injector): - config_injector({'user_name_regex': '^[a-z]+$'}) + config_injector({"user_name_regex": "^[a-z]+$"}) user = user_factory() with pytest.raises(users.InvalidUserNameError): - users.update_user_name(user, '0') + users.update_user_name(user, "0") def test_update_user_name_with_duplicate_name(user_factory, config_injector): - config_injector({'user_name_regex': '^[a-z]+$'}) + config_injector({"user_name_regex": "^[a-z]+$"}) user = user_factory() - existing_user = user_factory(name='dummy') + existing_user = user_factory(name="dummy") db.session.add(existing_user) db.session.flush() with pytest.raises(users.UserAlreadyExistsError): - users.update_user_name(user, 'dummy') + users.update_user_name(user, "dummy") def test_update_user_name_reusing_own_name(user_factory, config_injector): - config_injector({'user_name_regex': '^[a-z]+$'}) - user = user_factory(name='dummy') + config_injector({"user_name_regex": "^[a-z]+$"}) + user = user_factory(name="dummy") db.session.add(user) db.session.flush() - with patch('szurubooru.func.files.has'): + with patch("szurubooru.func.files.has"): files.has.return_value = False - users.update_user_name(user, 'dummy') + users.update_user_name(user, "dummy") db.session.flush() - assert users.try_get_user_by_name('dummy') is user + assert users.try_get_user_by_name("dummy") is user def test_update_user_name_for_new_user(user_factory, config_injector): - config_injector({'user_name_regex': '^[a-z]+$'}) + config_injector({"user_name_regex": "^[a-z]+$"}) user = user_factory() - with patch('szurubooru.func.files.has'): + with patch("szurubooru.func.files.has"): files.has.return_value = False - users.update_user_name(user, 'dummy') - assert user.name == 'dummy' + users.update_user_name(user, "dummy") + assert user.name == "dummy" def test_update_user_name_moves_avatar(user_factory, config_injector): - config_injector({'user_name_regex': '^[a-z]+$'}) - user = user_factory(name='old') - with patch('szurubooru.func.files.has'), \ - patch('szurubooru.func.files.move'): + config_injector({"user_name_regex": "^[a-z]+$"}) + user = user_factory(name="old") + with patch("szurubooru.func.files.has"), patch( + "szurubooru.func.files.move" + ): files.has.return_value = True - users.update_user_name(user, 'new') + users.update_user_name(user, "new") files.move.assert_called_once_with( - 'avatars/old.png', 'avatars/new.png') + "avatars/old.png", "avatars/new.png" + ) def test_update_user_password_with_empty_string(user_factory): @@ -307,72 +326,74 @@ def test_update_user_password_with_empty_string(user_factory): def test_update_user_password_with_invalid_string( - user_factory, config_injector): - config_injector({'password_regex': '^[a-z]+$'}) + user_factory, config_injector +): + config_injector({"password_regex": "^[a-z]+$"}) user = user_factory() with pytest.raises(users.InvalidPasswordError): - users.update_user_password(user, '0') + users.update_user_password(user, "0") def test_update_user_password(user_factory, config_injector): - config_injector({'password_regex': '^[a-z]+$'}) + config_injector({"password_regex": "^[a-z]+$"}) user = user_factory() - with patch('szurubooru.func.auth.create_password'), \ - patch('szurubooru.func.auth.get_password_hash'): - auth.create_password.return_value = 'salt' - auth.get_password_hash.return_value = ('hash', 3) - users.update_user_password(user, 'a') - assert user.password_salt == 'salt' - assert user.password_hash == 'hash' + with patch("szurubooru.func.auth.create_password"), patch( + "szurubooru.func.auth.get_password_hash" + ): + auth.create_password.return_value = "salt" + auth.get_password_hash.return_value = ("hash", 3) + users.update_user_password(user, "a") + assert user.password_salt == "salt" + assert user.password_hash == "hash" assert user.password_revision == 3 def test_update_user_email_with_too_long_string(user_factory): user = user_factory() with pytest.raises(users.InvalidEmailError): - users.update_user_email(user, 'a' * 300) + users.update_user_email(user, "a" * 300) def test_update_user_email_with_invalid_email(user_factory): user = user_factory() - with patch('szurubooru.func.util.is_valid_email'): + with patch("szurubooru.func.util.is_valid_email"): util.is_valid_email.return_value = False with pytest.raises(users.InvalidEmailError): - users.update_user_email(user, 'a') + users.update_user_email(user, "a") def test_update_user_email_with_empty_string(user_factory): user = user_factory() - with patch('szurubooru.func.util.is_valid_email'): + with patch("szurubooru.func.util.is_valid_email"): util.is_valid_email.return_value = True - users.update_user_email(user, '') + users.update_user_email(user, "") assert user.email is None def test_update_user_email(user_factory): user = user_factory() - with patch('szurubooru.func.util.is_valid_email'): + with patch("szurubooru.func.util.is_valid_email"): util.is_valid_email.return_value = True - users.update_user_email(user, 'a') - assert user.email == 'a' + users.update_user_email(user, "a") + assert user.email == "a" def test_update_user_rank_with_empty_string(user_factory): user = user_factory() auth_user = user_factory() with pytest.raises(users.InvalidRankError): - users.update_user_rank(user, '', auth_user) + users.update_user_rank(user, "", auth_user) def test_update_user_rank_with_invalid_string(user_factory): user = user_factory() auth_user = user_factory() with pytest.raises(users.InvalidRankError): - users.update_user_rank(user, 'invalid', auth_user) + users.update_user_rank(user, "invalid", auth_user) with pytest.raises(users.InvalidRankError): - users.update_user_rank(user, 'anonymous', auth_user) + users.update_user_rank(user, "anonymous", auth_user) with pytest.raises(users.InvalidRankError): - users.update_user_rank(user, 'nobody', auth_user) + users.update_user_rank(user, "nobody", auth_user) def test_update_user_rank_with_higher_rank_than_possible(user_factory): @@ -382,9 +403,9 @@ def test_update_user_rank_with_higher_rank_than_possible(user_factory): auth_user = user_factory() auth_user.rank = model.User.RANK_ANONYMOUS with pytest.raises(errors.AuthError): - users.update_user_rank(user, 'regular', auth_user) + users.update_user_rank(user, "regular", auth_user) with pytest.raises(errors.AuthError): - users.update_user_rank(auth_user, 'regular', auth_user) + users.update_user_rank(auth_user, "regular", auth_user) def test_update_user_rank(user_factory): @@ -393,8 +414,8 @@ def test_update_user_rank(user_factory): user = user_factory() auth_user = user_factory() auth_user.rank = model.User.RANK_ADMINISTRATOR - users.update_user_rank(user, 'regular', auth_user) - users.update_user_rank(auth_user, 'regular', auth_user) + users.update_user_rank(user, "regular", auth_user) + users.update_user_rank(auth_user, "regular", auth_user) assert user.rank == model.User.RANK_REGULAR assert auth_user.rank == model.User.RANK_REGULAR @@ -402,54 +423,57 @@ def test_update_user_rank(user_factory): def test_update_user_avatar_with_invalid_style(user_factory): user = user_factory() with pytest.raises(users.InvalidAvatarError): - users.update_user_avatar(user, 'invalid', b'') + users.update_user_avatar(user, "invalid", b"") def test_update_user_avatar_to_gravatar(user_factory): user = user_factory() - users.update_user_avatar(user, 'gravatar') + users.update_user_avatar(user, "gravatar") assert user.avatar_style == model.User.AVATAR_GRAVATAR def test_update_user_avatar_to_empty_manual(user_factory): user = user_factory() - with patch('szurubooru.func.files.has'), \ - pytest.raises(users.InvalidAvatarError): + with patch("szurubooru.func.files.has"), pytest.raises( + users.InvalidAvatarError + ): files.has.return_value = False - users.update_user_avatar(user, 'manual', b'') + users.update_user_avatar(user, "manual", b"") def test_update_user_avatar_to_previous_manual(user_factory): user = user_factory() - with patch('szurubooru.func.files.has'): + with patch("szurubooru.func.files.has"): files.has.return_value = True - users.update_user_avatar(user, 'manual', b'') + users.update_user_avatar(user, "manual", b"") def test_update_user_avatar_to_new_manual(user_factory, config_injector): config_injector( - {'thumbnails': {'avatar_width': 500, 'avatar_height': 500}}) + {"thumbnails": {"avatar_width": 500, "avatar_height": 500}} + ) user = user_factory() - with patch('szurubooru.func.files.save'): - users.update_user_avatar(user, 'manual', EMPTY_PIXEL) + with patch("szurubooru.func.files.save"): + users.update_user_avatar(user, "manual", EMPTY_PIXEL) assert user.avatar_style == model.User.AVATAR_MANUAL assert files.save.called def test_bump_user_login_time(user_factory, fake_datetime): user = user_factory() - with fake_datetime('1997-01-01'): + with fake_datetime("1997-01-01"): users.bump_user_login_time(user) assert user.last_login_time == datetime(1997, 1, 1) def test_reset_user_password(user_factory): - with patch('szurubooru.func.auth.create_password'), \ - patch('szurubooru.func.auth.get_password_hash'): + with patch("szurubooru.func.auth.create_password"), patch( + "szurubooru.func.auth.get_password_hash" + ): user = user_factory() - auth.create_password.return_value = 'salt' - auth.get_password_hash.return_value = ('hash', 3) + auth.create_password.return_value = "salt" + auth.get_password_hash.return_value = ("hash", 3) users.reset_user_password(user) - assert user.password_salt == 'salt' - assert user.password_hash == 'hash' + assert user.password_salt == "salt" + assert user.password_hash == "hash" assert user.password_revision == 3 diff --git a/server/szurubooru/tests/func/test_util.py b/server/szurubooru/tests/func/test_util.py index 24fe4e4..f42ba29 100644 --- a/server/szurubooru/tests/func/test_util.py +++ b/server/szurubooru/tests/func/test_util.py @@ -1,40 +1,47 @@ from datetime import datetime + import pytest + from szurubooru import errors from szurubooru.func import util - -dt = datetime # pylint: disable=invalid-name +dt = datetime def test_parsing_empty_date_time(): with pytest.raises(errors.ValidationError): - util.parse_time_range('') + util.parse_time_range("") -@pytest.mark.parametrize('output,input', [ - ((dt(1997, 1, 2, 0, 0, 0), dt(1997, 1, 2, 23, 59, 59)), 'today'), - ((dt(1997, 1, 1, 0, 0, 0), dt(1997, 1, 1, 23, 59, 59)), 'yesterday'), - ((dt(1999, 1, 1, 0, 0, 0), dt(1999, 12, 31, 23, 59, 59)), '1999'), - ((dt(1999, 2, 1, 0, 0, 0), dt(1999, 2, 28, 23, 59, 59)), '1999-2'), - ((dt(1999, 2, 1, 0, 0, 0), dt(1999, 2, 28, 23, 59, 59)), '1999-02'), - ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), '1999-2-6'), - ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), '1999-02-6'), - ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), '1999-2-06'), - ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), '1999-02-06'), -]) +@pytest.mark.parametrize( + "output,input", + [ + ((dt(1997, 1, 2, 0, 0, 0), dt(1997, 1, 2, 23, 59, 59)), "today"), + ((dt(1997, 1, 1, 0, 0, 0), dt(1997, 1, 1, 23, 59, 59)), "yesterday"), + ((dt(1999, 1, 1, 0, 0, 0), dt(1999, 12, 31, 23, 59, 59)), "1999"), + ((dt(1999, 2, 1, 0, 0, 0), dt(1999, 2, 28, 23, 59, 59)), "1999-2"), + ((dt(1999, 2, 1, 0, 0, 0), dt(1999, 2, 28, 23, 59, 59)), "1999-02"), + ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), "1999-2-6"), + ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), "1999-02-6"), + ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), "1999-2-06"), + ((dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59)), "1999-02-06"), + ], +) def test_parsing_date_time(fake_datetime, input, output): - with fake_datetime('1997-01-02 03:04:05'): + with fake_datetime("1997-01-02 03:04:05"): assert util.parse_time_range(input) == output -@pytest.mark.parametrize('input,output', [ - ([], []), - (['a', 'b', 'c'], ['a', 'b', 'c']), - (['a', 'b', 'a'], ['a', 'b']), - (['a', 'a', 'b'], ['a', 'b']), - (['a', 'A', 'b'], ['a', 'b']), - (['a', 'A', 'b', 'B'], ['a', 'b']), -]) +@pytest.mark.parametrize( + "input,output", + [ + ([], []), + (["a", "b", "c"], ["a", "b", "c"]), + (["a", "b", "a"], ["a", "b"]), + (["a", "a", "b"], ["a", "b"]), + (["a", "A", "b"], ["a", "b"]), + (["a", "A", "b", "B"], ["a", "b"]), + ], +) def test_icase_unique(input, output): assert util.icase_unique(input) == output diff --git a/server/szurubooru/tests/middleware/test_authenticator.py b/server/szurubooru/tests/middleware/test_authenticator.py index be21a93..9a4a3cc 100644 --- a/server/szurubooru/tests/middleware/test_authenticator.py +++ b/server/szurubooru/tests/middleware/test_authenticator.py @@ -1,7 +1,9 @@ from unittest.mock import patch + import pytest + from szurubooru import db -from szurubooru.func import auth, users, user_tokens +from szurubooru.func import auth, user_tokens, users from szurubooru.middleware import authenticator from szurubooru.rest import errors @@ -17,14 +19,12 @@ def test_process_request_bump_login(context_factory, user_factory): db.session.add(user) db.session.flush() ctx = context_factory( - headers={ - 'Authorization': 'Basic dGVzdFVzZXI6dGVzdFRva2Vu' - }, - params={ - 'bump-login': 'true' - }) - with patch('szurubooru.func.auth.is_valid_password'), \ - patch('szurubooru.func.users.get_user_by_name'): + headers={"Authorization": "Basic dGVzdFVzZXI6dGVzdFRva2Vu"}, + params={"bump-login": "true"}, + ) + with patch("szurubooru.func.auth.is_valid_password"), patch( + "szurubooru.func.users.get_user_by_name" + ): users.get_user_by_name.return_value = user auth.is_valid_password.return_value = True authenticator.process_request(ctx) @@ -32,20 +32,18 @@ def test_process_request_bump_login(context_factory, user_factory): def test_process_request_bump_login_with_token( - context_factory, user_token_factory): + context_factory, user_token_factory +): user_token = user_token_factory() db.session.add(user_token) db.session.flush() ctx = context_factory( - headers={ - 'Authorization': 'Token dGVzdFVzZXI6dGVzdFRva2Vu' - }, - params={ - 'bump-login': 'true' - }) - with patch('szurubooru.func.auth.is_valid_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - patch('szurubooru.func.user_tokens.get_by_user_and_token'): + headers={"Authorization": "Token dGVzdFVzZXI6dGVzdFRva2Vu"}, + params={"bump-login": "true"}, + ) + with patch("szurubooru.func.auth.is_valid_token"), patch( + "szurubooru.func.users.get_user_by_name" + ), patch("szurubooru.func.user_tokens.get_by_user_and_token"): users.get_user_by_name.return_value = user_token.user user_tokens.get_by_user_and_token.return_value = user_token auth.is_valid_token.return_value = True @@ -57,11 +55,11 @@ def test_process_request_bump_login_with_token( def test_process_request_basic_auth_valid(context_factory, user_factory): user = user_factory() ctx = context_factory( - headers={ - 'Authorization': 'Basic dGVzdFVzZXI6dGVzdFBhc3N3b3Jk' - }) - with patch('szurubooru.func.auth.is_valid_password'), \ - patch('szurubooru.func.users.get_user_by_name'): + headers={"Authorization": "Basic dGVzdFVzZXI6dGVzdFBhc3N3b3Jk"} + ) + with patch("szurubooru.func.auth.is_valid_password"), patch( + "szurubooru.func.users.get_user_by_name" + ): users.get_user_by_name.return_value = user auth.is_valid_password.return_value = True authenticator.process_request(ctx) @@ -71,12 +69,11 @@ def test_process_request_basic_auth_valid(context_factory, user_factory): def test_process_request_token_auth_valid(context_factory, user_token_factory): user_token = user_token_factory() ctx = context_factory( - headers={ - 'Authorization': 'Token dGVzdFVzZXI6dGVzdFRva2Vu' - }) - with patch('szurubooru.func.auth.is_valid_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - patch('szurubooru.func.user_tokens.get_by_user_and_token'): + headers={"Authorization": "Token dGVzdFVzZXI6dGVzdFRva2Vu"} + ) + with patch("szurubooru.func.auth.is_valid_token"), patch( + "szurubooru.func.users.get_user_by_name" + ), patch("szurubooru.func.user_tokens.get_by_user_and_token"): users.get_user_by_name.return_value = user_token.user user_tokens.get_by_user_and_token.return_value = user_token auth.is_valid_token.return_value = True @@ -85,9 +82,6 @@ def test_process_request_token_auth_valid(context_factory, user_token_factory): def test_process_request_bad_header(context_factory): - ctx = context_factory( - headers={ - 'Authorization': 'Secret SuperSecretValue' - }) + ctx = context_factory(headers={"Authorization": "Secret SuperSecretValue"}) with pytest.raises(errors.HttpBadRequest): authenticator.process_request(ctx) diff --git a/server/szurubooru/tests/model/test_comment.py b/server/szurubooru/tests/model/test_comment.py index ffd5189..fcbf176 100644 --- a/server/szurubooru/tests/model/test_comment.py +++ b/server/szurubooru/tests/model/test_comment.py @@ -1,4 +1,5 @@ from datetime import datetime + from szurubooru import db, model @@ -6,7 +7,7 @@ def test_saving_comment(user_factory, post_factory): user = user_factory() post = post_factory() comment = model.Comment() - comment.text = 'long text' * 1000 + comment.text = "long text" * 1000 comment.user = user comment.post = post comment.creation_time = datetime(1997, 1, 1) @@ -17,7 +18,7 @@ def test_saving_comment(user_factory, post_factory): db.session.refresh(comment) assert not db.session.dirty assert comment.user is not None and comment.user.user_id is not None - assert comment.text == 'long text' * 1000 + assert comment.text == "long text" * 1000 assert comment.creation_time == datetime(1997, 1, 1) assert comment.last_edit_time == datetime(1998, 1, 1) diff --git a/server/szurubooru/tests/model/test_pool.py b/server/szurubooru/tests/model/test_pool.py new file mode 100644 index 0000000..bec9560 --- /dev/null +++ b/server/szurubooru/tests/model/test_pool.py @@ -0,0 +1,97 @@ +from datetime import datetime + +import pytest + +from szurubooru import db, model + + +@pytest.fixture(autouse=True) +def inject_config(config_injector): + config_injector( + {"delete_source_files": False, "secret": "secret", "data_dir": ""} + ) + + +def test_saving_pool(pool_factory, post_factory): + post1 = post_factory() + post2 = post_factory() + pool = model.Pool() + pool.names = [model.PoolName("alias1", 0), model.PoolName("alias2", 1)] + pool.posts = [] + pool.category = model.PoolCategory("category") + pool.creation_time = datetime(1997, 1, 1) + pool.last_edit_time = datetime(1998, 1, 1) + db.session.add_all([pool, post1, post2]) + db.session.commit() + + assert pool.pool_id is not None + pool.posts.append(post1) + pool.posts.append(post2) + db.session.commit() + + pool = ( + db.session.query(model.Pool) + .join(model.PoolName) + .filter(model.PoolName.name == "alias1") + .one() + ) + assert [pool_name.name for pool_name in pool.names] == ["alias1", "alias2"] + assert pool.category.name == "category" + assert pool.creation_time == datetime(1997, 1, 1) + assert pool.last_edit_time == datetime(1998, 1, 1) + assert [post.post_id for post in pool.posts] == [1, 2] + + +def test_cascade_deletions(pool_factory, post_factory): + post1 = post_factory() + post2 = post_factory() + pool = model.Pool() + pool.names = [model.PoolName("alias1", 0), model.PoolName("alias2", 1)] + pool.posts = [] + pool.category = model.PoolCategory("category") + pool.creation_time = datetime(1997, 1, 1) + pool.last_edit_time = datetime(1998, 1, 1) + db.session.add_all([pool, post1, post2]) + db.session.commit() + + assert pool.pool_id is not None + pool.posts.append(post1) + pool.posts.append(post2) + db.session.commit() + + db.session.delete(pool) + db.session.commit() + assert db.session.query(model.Pool).count() == 0 + assert db.session.query(model.PoolName).count() == 0 + assert db.session.query(model.PoolPost).count() == 0 + assert db.session.query(model.PoolCategory).count() == 1 + assert db.session.query(model.Post).count() == 2 + + +def test_tracking_post_count(post_factory, pool_factory): + pool1 = pool_factory() + pool2 = pool_factory() + post1 = post_factory() + post2 = post_factory() + db.session.add_all([pool1, pool2, post1, post2]) + db.session.flush() + assert pool1.pool_id is not None + assert pool2.pool_id is not None + pool1.posts.append(post1) + pool2.posts.append(post1) + pool2.posts.append(post2) + db.session.commit() + assert len(post1.pools) == 2 + assert len(post2.pools) == 1 + assert pool1.post_count == 1 + assert pool2.post_count == 2 + db.session.delete(post1) + db.session.commit() + db.session.refresh(pool1) + db.session.refresh(pool2) + assert pool1.post_count == 0 + assert pool2.post_count == 1 + db.session.delete(post2) + db.session.commit() + db.session.refresh(pool2) + assert pool2.post_count == 0 diff --git a/server/szurubooru/tests/model/test_post.py b/server/szurubooru/tests/model/test_post.py index ee69146..f01455d 100644 --- a/server/szurubooru/tests/model/test_post.py +++ b/server/szurubooru/tests/model/test_post.py @@ -1,15 +1,15 @@ from datetime import datetime + import pytest + from szurubooru import db, model @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'secret': 'secret', - 'data_dir': '', - 'delete_source_files': False - }) + config_injector( + {"secret": "secret", "data_dir": "", "delete_source_files": False} + ) def test_saving_post(post_factory, user_factory, tag_factory): @@ -19,12 +19,12 @@ def test_saving_post(post_factory, user_factory, tag_factory): related_post1 = post_factory() related_post2 = post_factory() post = model.Post() - post.safety = 'safety' - post.type = 'type' - post.checksum = 'deadbeef' + post.safety = "safety" + post.type = "type" + post.checksum = "deadbeef" post.creation_time = datetime(1997, 1, 1) post.last_edit_time = datetime(1998, 1, 1) - post.mime_type = 'application/whatever' + post.mime_type = "application/whatever" db.session.add_all([user, tag1, tag2, related_post1, related_post2, post]) post.user = user @@ -39,9 +39,9 @@ def test_saving_post(post_factory, user_factory, tag_factory): db.session.refresh(related_post2) assert not db.session.dirty assert post.user.user_id is not None - assert post.safety == 'safety' - assert post.type == 'type' - assert post.checksum == 'deadbeef' + assert post.safety == "safety" + assert post.type == "type" + assert post.checksum == "deadbeef" assert post.creation_time == datetime(1997, 1, 1) assert post.last_edit_time == datetime(1998, 1, 1) assert len(post.relations) == 2 @@ -50,9 +50,9 @@ def test_saving_post(post_factory, user_factory, tag_factory): assert len(related_post2.relations) == 0 -# pylint: disable=too-many-statements def test_cascade_deletions( - post_factory, user_factory, tag_factory, comment_factory): + post_factory, user_factory, tag_factory, comment_factory +): user = user_factory() tag1 = tag_factory() tag2 = tag_factory() @@ -60,8 +60,9 @@ def test_cascade_deletions( related_post2 = post_factory() post = post_factory() comment = comment_factory(post=post, user=user) - db.session.add_all([ - user, tag1, tag2, post, related_post1, related_post2, comment]) + db.session.add_all( + [user, tag1, tag2, post, related_post1, related_post2, comment] + ) db.session.flush() score = model.PostScore() @@ -79,9 +80,13 @@ def test_cascade_deletions( feature.time = datetime(1997, 1, 1) note = model.PostNote() note.post = post - note.polygon = '' - note.text = '' - db.session.add_all([score, favorite, feature, note]) + note.polygon = "" + note.text = "" + signature = model.PostSignature() + signature.post = post + signature.signature = b"testvalue" + signature.words = list(range(50)) + db.session.add_all([score, favorite, feature, note, signature]) db.session.flush() post.user = user @@ -107,6 +112,7 @@ def test_cascade_deletions( assert db.session.query(model.PostNote).count() == 1 assert db.session.query(model.PostFeature).count() == 1 assert db.session.query(model.PostFavorite).count() == 1 + assert db.session.query(model.PostSignature).count() == 1 assert db.session.query(model.Comment).count() == 1 db.session.delete(post) @@ -122,6 +128,7 @@ def test_cascade_deletions( assert db.session.query(model.PostNote).count() == 0 assert db.session.query(model.PostFeature).count() == 0 assert db.session.query(model.PostFavorite).count() == 0 + assert db.session.query(model.PostSignature).count() == 0 assert db.session.query(model.Comment).count() == 0 diff --git a/server/szurubooru/tests/model/test_tag.py b/server/szurubooru/tests/model/test_tag.py index b677eef..9332f0d 100644 --- a/server/szurubooru/tests/model/test_tag.py +++ b/server/szurubooru/tests/model/test_tag.py @@ -1,27 +1,27 @@ from datetime import datetime + import pytest + from szurubooru import db, model @pytest.fixture(autouse=True) def inject_config(config_injector): - config_injector({ - 'delete_source_files': False, - 'secret': 'secret', - 'data_dir': '' - }) + config_injector( + {"delete_source_files": False, "secret": "secret", "data_dir": ""} + ) def test_saving_tag(tag_factory): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - imp1 = tag_factory(names=['imp1']) - imp2 = tag_factory(names=['imp2']) + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + imp1 = tag_factory(names=["imp1"]) + imp2 = tag_factory(names=["imp2"]) tag = model.Tag() - tag.names = [model.TagName('alias1', 0), model.TagName('alias2', 1)] + tag.names = [model.TagName("alias1", 0), model.TagName("alias2", 1)] tag.suggestions = [] tag.implications = [] - tag.category = model.TagCategory('category') + tag.category = model.TagCategory("category") tag.creation_time = datetime(1997, 1, 1) tag.last_edit_time = datetime(1998, 1, 1) db.session.add_all([tag, sug1, sug2, imp1, imp2]) @@ -39,31 +39,35 @@ def test_saving_tag(tag_factory): db.session.commit() tag = ( - db.session - .query(model.Tag) + db.session.query(model.Tag) .join(model.TagName) - .filter(model.TagName.name == 'alias1') - .one()) - assert [tag_name.name for tag_name in tag.names] == ['alias1', 'alias2'] - assert tag.category.name == 'category' + .filter(model.TagName.name == "alias1") + .one() + ) + assert [tag_name.name for tag_name in tag.names] == ["alias1", "alias2"] + assert tag.category.name == "category" assert tag.creation_time == datetime(1997, 1, 1) assert tag.last_edit_time == datetime(1998, 1, 1) - assert [relation.names[0].name for relation in tag.suggestions] \ - == ['sug1', 'sug2'] - assert [relation.names[0].name for relation in tag.implications] \ - == ['imp1', 'imp2'] + assert [relation.names[0].name for relation in tag.suggestions] == [ + "sug1", + "sug2", + ] + assert [relation.names[0].name for relation in tag.implications] == [ + "imp1", + "imp2", + ] def test_cascade_deletions(tag_factory): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - imp1 = tag_factory(names=['imp1']) - imp2 = tag_factory(names=['imp2']) + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + imp1 = tag_factory(names=["imp1"]) + imp2 = tag_factory(names=["imp2"]) tag = model.Tag() - tag.names = [model.TagName('alias1', 0), model.TagName('alias2', 1)] + tag.names = [model.TagName("alias1", 0), model.TagName("alias2", 1)] tag.suggestions = [] tag.implications = [] - tag.category = model.TagCategory('category') + tag.category = model.TagCategory("category") tag.creation_time = datetime(1997, 1, 1) tag.last_edit_time = datetime(1998, 1, 1) tag.post_count = 1 diff --git a/server/szurubooru/tests/model/test_user.py b/server/szurubooru/tests/model/test_user.py index 08875fa..adc9dd7 100644 --- a/server/szurubooru/tests/model/test_user.py +++ b/server/szurubooru/tests/model/test_user.py @@ -1,25 +1,26 @@ from datetime import datetime + from szurubooru import db, model def test_saving_user(): user = model.User() - user.name = 'name' - user.password_salt = 'salt' - user.password_hash = 'hash' - user.email = 'email' - user.rank = 'rank' + user.name = "name" + user.password_salt = "salt" + user.password_hash = "hash" + user.email = "email" + user.rank = "rank" user.creation_time = datetime(1997, 1, 1) user.avatar_style = model.User.AVATAR_GRAVATAR db.session.add(user) db.session.flush() db.session.refresh(user) assert not db.session.dirty - assert user.name == 'name' - assert user.password_salt == 'salt' - assert user.password_hash == 'hash' - assert user.email == 'email' - assert user.rank == 'rank' + assert user.name == "name" + assert user.password_salt == "salt" + assert user.password_hash == "hash" + assert user.email == "email" + assert user.rank == "rank" assert user.creation_time == datetime(1997, 1, 1) assert user.avatar_style == model.User.AVATAR_GRAVATAR @@ -43,10 +44,12 @@ def test_comment_count(user_factory, comment_factory): db.session.add(user) db.session.flush() assert user.comment_count == 0 - db.session.add_all([ - comment_factory(user=user), - comment_factory(), - ]) + db.session.add_all( + [ + comment_factory(user=user), + comment_factory(), + ] + ) db.session.flush() db.session.refresh(user) assert user.comment_count == 1 @@ -60,10 +63,12 @@ def test_favorite_count(user_factory, post_factory): assert user1.comment_count == 0 post1 = post_factory() post2 = post_factory() - db.session.add_all([ - model.PostFavorite(post=post1, time=datetime.utcnow(), user=user1), - model.PostFavorite(post=post2, time=datetime.utcnow(), user=user2), - ]) + db.session.add_all( + [ + model.PostFavorite(post=post1, time=datetime.utcnow(), user=user1), + model.PostFavorite(post=post2, time=datetime.utcnow(), user=user2), + ] + ) db.session.flush() db.session.refresh(user1) assert user1.favorite_post_count == 1 @@ -78,12 +83,16 @@ def test_liked_post_count(user_factory, post_factory): assert user1.disliked_post_count == 0 post1 = post_factory() post2 = post_factory() - db.session.add_all([ - model.PostScore( - post=post1, time=datetime.utcnow(), user=user1, score=1), - model.PostScore( - post=post2, time=datetime.utcnow(), user=user2, score=1), - ]) + db.session.add_all( + [ + model.PostScore( + post=post1, time=datetime.utcnow(), user=user1, score=1 + ), + model.PostScore( + post=post2, time=datetime.utcnow(), user=user2, score=1 + ), + ] + ) db.session.flush() db.session.refresh(user1) assert user1.liked_post_count == 1 @@ -99,19 +108,22 @@ def test_disliked_post_count(user_factory, post_factory): assert user1.disliked_post_count == 0 post1 = post_factory() post2 = post_factory() - db.session.add_all([ - model.PostScore( - post=post1, time=datetime.utcnow(), user=user1, score=-1), - model.PostScore( - post=post2, time=datetime.utcnow(), user=user2, score=1), - ]) + db.session.add_all( + [ + model.PostScore( + post=post1, time=datetime.utcnow(), user=user1, score=-1 + ), + model.PostScore( + post=post2, time=datetime.utcnow(), user=user2, score=1 + ), + ] + ) db.session.flush() db.session.refresh(user1) assert user1.liked_post_count == 0 assert user1.disliked_post_count == 1 -# pylint: disable=too-many-statements def test_cascade_deletions(post_factory, user_factory, comment_factory): user = user_factory() @@ -148,10 +160,10 @@ def test_cascade_deletions(post_factory, user_factory, comment_factory): snapshot = model.Snapshot() snapshot.user = user snapshot.creation_time = datetime(1997, 1, 1) - snapshot.resource_type = '-' + snapshot.resource_type = "-" snapshot.resource_pkey = 1 - snapshot.resource_name = '-' - snapshot.operation = '-' + snapshot.resource_name = "-" + snapshot.operation = "-" db.session.add_all([user, post, comment, snapshot]) db.session.commit() diff --git a/server/szurubooru/tests/model/test_user_token.py b/server/szurubooru/tests/model/test_user_token.py index 0280082..cddb5cf 100644 --- a/server/szurubooru/tests/model/test_user_token.py +++ b/server/szurubooru/tests/model/test_user_token.py @@ -1,4 +1,5 @@ from datetime import datetime + from szurubooru import db @@ -9,6 +10,6 @@ def test_saving_user_token(user_token_factory): db.session.refresh(user_token) assert not db.session.dirty assert user_token.user is not None - assert user_token.token == 'dummy' + assert user_token.token == "dummy" assert user_token.enabled is True assert user_token.creation_time == datetime(1997, 1, 1) diff --git a/server/szurubooru/tests/rest/test_context.py b/server/szurubooru/tests/rest/test_context.py index 681d9d7..ec652b1 100644 --- a/server/szurubooru/tests/rest/test_context.py +++ b/server/szurubooru/tests/rest/test_context.py @@ -1,33 +1,38 @@ -# pylint: disable=unexpected-keyword-arg import unittest.mock + import pytest -from szurubooru import rest, errors + +from szurubooru import errors, rest from szurubooru.func import net def test_has_param(): - ctx = rest.Context(env={}, method=None, url=None, params={'key': 'value'}) - assert ctx.has_param('key') - assert not ctx.has_param('non-existing') + ctx = rest.Context(env={}, method=None, url=None, params={"key": "value"}) + assert ctx.has_param("key") + assert not ctx.has_param("non-existing") def test_get_file(): ctx = rest.Context( - env={}, method=None, url=None, files={'key': b'content'}) - assert ctx.get_file('key') == b'content' + env={}, method=None, url=None, files={"key": b"content"} + ) + assert ctx.get_file("key") == b"content" with pytest.raises(errors.ValidationError): - ctx.get_file('non-existing') + ctx.get_file("non-existing") def test_get_file_from_url(): - with unittest.mock.patch('szurubooru.func.net.download'): - net.download.return_value = b'content' + with unittest.mock.patch("szurubooru.func.net.download"): + net.download.return_value = b"content" ctx = rest.Context( - env={}, method=None, url=None, params={'keyUrl': 'example.com'}) - assert ctx.get_file('key') == b'content' - net.download.assert_called_once_with('example.com') + env={}, method=None, url=None, params={"keyUrl": "example.com"} + ) + assert ctx.get_file("key") == b"content" + net.download.assert_called_once_with( + "example.com", use_video_downloader=False + ) with pytest.raises(errors.ValidationError): - assert ctx.get_file('non-existing') + assert ctx.get_file("non-existing") def test_getting_list_parameter(): @@ -35,12 +40,13 @@ def test_getting_list_parameter(): env={}, method=None, url=None, - params={'key': 'value', 'list': ['1', '2', '3']}) - assert ctx.get_param_as_list('key') == ['value'] - assert ctx.get_param_as_list('list') == ['1', '2', '3'] + params={"key": "value", "list": ["1", "2", "3"]}, + ) + assert ctx.get_param_as_list("key") == ["value"] + assert ctx.get_param_as_list("list") == ["1", "2", "3"] with pytest.raises(errors.ValidationError): - ctx.get_param_as_list('non-existing') - assert ctx.get_param_as_list('non-existing', default=['def']) == ['def'] + ctx.get_param_as_list("non-existing") + assert ctx.get_param_as_list("non-existing", default=["def"]) == ["def"] def test_getting_string_parameter(): @@ -48,12 +54,13 @@ def test_getting_string_parameter(): env={}, method=None, url=None, - params={'key': 'value', 'list': ['1', '2', '3']}) - assert ctx.get_param_as_string('key') == 'value' - assert ctx.get_param_as_string('list') == '1,2,3' + params={"key": "value", "list": ["1", "2", "3"]}, + ) + assert ctx.get_param_as_string("key") == "value" + assert ctx.get_param_as_string("list") == "1,2,3" with pytest.raises(errors.ValidationError): - ctx.get_param_as_string('non-existing') - assert ctx.get_param_as_string('non-existing', default='x') == 'x' + ctx.get_param_as_string("non-existing") + assert ctx.get_param_as_string("non-existing", default="x") == "x" def test_getting_int_parameter(): @@ -61,55 +68,57 @@ def test_getting_int_parameter(): env={}, method=None, url=None, - params={'key': '50', 'err': 'invalid', 'list': [1, 2, 3]}) - assert ctx.get_param_as_int('key') == 50 + params={"key": "50", "err": "invalid", "list": [1, 2, 3]}, + ) + assert ctx.get_param_as_int("key") == 50 with pytest.raises(errors.ValidationError): - ctx.get_param_as_int('list') + ctx.get_param_as_int("list") with pytest.raises(errors.ValidationError): - ctx.get_param_as_int('non-existing') - assert ctx.get_param_as_int('non-existing', default=5) == 5 + ctx.get_param_as_int("non-existing") + assert ctx.get_param_as_int("non-existing", default=5) == 5 with pytest.raises(errors.ValidationError): - ctx.get_param_as_int('err') + ctx.get_param_as_int("err") with pytest.raises(errors.ValidationError): - assert ctx.get_param_as_int('key', min=50) == 50 - ctx.get_param_as_int('key', min=51) + assert ctx.get_param_as_int("key", min=50) == 50 + ctx.get_param_as_int("key", min=51) with pytest.raises(errors.ValidationError): - assert ctx.get_param_as_int('key', max=50) == 50 - ctx.get_param_as_int('key', max=49) + assert ctx.get_param_as_int("key", max=50) == 50 + ctx.get_param_as_int("key", max=49) def test_getting_bool_parameter(): def test(value): ctx = rest.Context( - env={}, method=None, url=None, params={'key': value}) - return ctx.get_param_as_bool('key') + env={}, method=None, url=None, params={"key": value} + ) + return ctx.get_param_as_bool("key") - assert test('1') is True - assert test('y') is True - assert test('yes') is True - assert test('yep') is True - assert test('yup') is True - assert test('yeah') is True - assert test('t') is True - assert test('true') is True - assert test('TRUE') is True + assert test("1") is True + assert test("y") is True + assert test("yes") is True + assert test("yep") is True + assert test("yup") is True + assert test("yeah") is True + assert test("t") is True + assert test("true") is True + assert test("TRUE") is True - assert test('0') is False - assert test('n') is False - assert test('no') is False - assert test('nope') is False - assert test('f') is False - assert test('false') is False - assert test('FALSE') is False + assert test("0") is False + assert test("n") is False + assert test("no") is False + assert test("nope") is False + assert test("f") is False + assert test("false") is False + assert test("FALSE") is False with pytest.raises(errors.ValidationError): - test('herp') + test("herp") with pytest.raises(errors.ValidationError): - test('2') + test("2") with pytest.raises(errors.ValidationError): - test(['1', '2']) + test(["1", "2"]) ctx = rest.Context(env={}, method=None, url=None) with pytest.raises(errors.ValidationError): - ctx.get_param_as_bool('non-existing') - assert ctx.get_param_as_bool('non-existing', default=True) is True + ctx.get_param_as_bool("non-existing") + assert ctx.get_param_as_bool("non-existing", default=True) is True diff --git a/server/szurubooru/tests/search/configs/test_comment_search_config.py b/server/szurubooru/tests/search/configs/test_comment_search_config.py index 109629b..d986c22 100644 --- a/server/szurubooru/tests/search/configs/test_comment_search_config.py +++ b/server/szurubooru/tests/search/configs/test_comment_search_config.py @@ -1,6 +1,7 @@ -# pylint: disable=redefined-outer-name from datetime import datetime + import pytest + from szurubooru import db, search @@ -13,22 +14,28 @@ def executor(): def verify_unpaged(executor): def verify(input, expected_comment_text): actual_count, actual_comments = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_comment_text = [c.text for c in actual_comments] assert actual_count == len(expected_comment_text) assert actual_comment_text == expected_comment_text + return verify -@pytest.mark.parametrize('input,expected_comment_text', [ - ('creation-time:2014', ['t2', 't1']), - ('creation-date:2014', ['t2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("creation-time:2014", ["t2", "t1"]), + ("creation-date:2014", ["t2", "t1"]), + ], +) def test_filter_by_creation_time( - verify_unpaged, comment_factory, input, expected_comment_text): - comment1 = comment_factory(text='t1') - comment2 = comment_factory(text='t2') - comment3 = comment_factory(text='t3') + verify_unpaged, comment_factory, input, expected_comment_text +): + comment1 = comment_factory(text="t1") + comment2 = comment_factory(text="t2") + comment3 = comment_factory(text="t3") comment1.creation_time = datetime(2014, 1, 1) comment2.creation_time = datetime(2014, 6, 1) comment3.creation_time = datetime(2015, 1, 1) @@ -37,109 +44,121 @@ def test_filter_by_creation_time( verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('text:t1', ['t1']), - ('text:t2', ['t2']), - ('text:t1,t2', ['t1', 't2']), - ('text:t*', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("text:t1", ["t1"]), + ("text:t2", ["t2"]), + ("text:t1,t2", ["t1", "t2"]), + ("text:t*", ["t1", "t2"]), + ], +) def test_filter_by_text( - verify_unpaged, comment_factory, input, expected_comment_text): - comment1 = comment_factory(text='t1') - comment2 = comment_factory(text='t2') + verify_unpaged, comment_factory, input, expected_comment_text +): + comment1 = comment_factory(text="t1") + comment2 = comment_factory(text="t2") db.session.add_all([comment1, comment2]) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('user:u1', ['t1']), - ('user:u2', ['t2']), - ('user:u1,u2', ['t2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("user:u1", ["t1"]), + ("user:u2", ["t2"]), + ("user:u1,u2", ["t2", "t1"]), + ], +) def test_filter_by_user( - verify_unpaged, - comment_factory, - user_factory, - input, - expected_comment_text): - db.session.add(comment_factory(text='t2', user=user_factory(name='u2'))) - db.session.add(comment_factory(text='t1', user=user_factory(name='u1'))) + verify_unpaged, comment_factory, user_factory, input, expected_comment_text +): + db.session.add(comment_factory(text="t2", user=user_factory(name="u2"))) + db.session.add(comment_factory(text="t1", user=user_factory(name="u1"))) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('post:1', ['t1']), - ('post:2', ['t2']), - ('post:1,2', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("post:1", ["t1"]), + ("post:2", ["t2"]), + ("post:1,2", ["t1", "t2"]), + ], +) def test_filter_by_post( - verify_unpaged, - comment_factory, - post_factory, - input, - expected_comment_text): - db.session.add(comment_factory(text='t1', post=post_factory(id=1))) - db.session.add(comment_factory(text='t2', post=post_factory(id=2))) + verify_unpaged, comment_factory, post_factory, input, expected_comment_text +): + db.session.add(comment_factory(text="t1", post=post_factory(id=1))) + db.session.add(comment_factory(text="t2", post=post_factory(id=2))) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('', ['t1', 't2']), - ('t1', ['t1']), - ('t2', ['t2']), - ('t1,t2', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("", ["t1", "t2"]), + ("t1", ["t1"]), + ("t2", ["t2"]), + ("t1,t2", ["t1", "t2"]), + ], +) def test_anonymous( - verify_unpaged, comment_factory, input, expected_comment_text): - db.session.add(comment_factory(text='t1')) - db.session.add(comment_factory(text='t2')) + verify_unpaged, comment_factory, input, expected_comment_text +): + db.session.add(comment_factory(text="t1")) + db.session.add(comment_factory(text="t2")) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('sort:user', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("sort:user", ["t1", "t2"]), + ], +) def test_sort_by_user( - verify_unpaged, - comment_factory, - user_factory, - input, - expected_comment_text): - db.session.add(comment_factory(text='t2', user=user_factory(name='u2'))) - db.session.add(comment_factory(text='t1', user=user_factory(name='u1'))) + verify_unpaged, comment_factory, user_factory, input, expected_comment_text +): + db.session.add(comment_factory(text="t2", user=user_factory(name="u2"))) + db.session.add(comment_factory(text="t1", user=user_factory(name="u1"))) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('sort:post', ['t2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("sort:post", ["t2", "t1"]), + ], +) def test_sort_by_post( - verify_unpaged, - comment_factory, - post_factory, - input, - expected_comment_text): - db.session.add(comment_factory(text='t1', post=post_factory(id=1))) - db.session.add(comment_factory(text='t2', post=post_factory(id=2))) + verify_unpaged, comment_factory, post_factory, input, expected_comment_text +): + db.session.add(comment_factory(text="t1", post=post_factory(id=1))) + db.session.add(comment_factory(text="t2", post=post_factory(id=2))) db.session.flush() verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('', ['t3', 't2', 't1']), - ('sort:creation-date', ['t3', 't2', 't1']), - ('sort:creation-time', ['t3', 't2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("", ["t3", "t2", "t1"]), + ("sort:creation-date", ["t3", "t2", "t1"]), + ("sort:creation-time", ["t3", "t2", "t1"]), + ], +) def test_sort_by_creation_time( - verify_unpaged, comment_factory, input, expected_comment_text): - comment1 = comment_factory(text='t1') - comment2 = comment_factory(text='t2') - comment3 = comment_factory(text='t3') + verify_unpaged, comment_factory, input, expected_comment_text +): + comment1 = comment_factory(text="t1") + comment2 = comment_factory(text="t2") + comment3 = comment_factory(text="t3") comment1.creation_time = datetime(1991, 1, 1) comment2.creation_time = datetime(1991, 1, 2) comment3.creation_time = datetime(1991, 1, 3) @@ -148,17 +167,21 @@ def test_sort_by_creation_time( verify_unpaged(input, expected_comment_text) -@pytest.mark.parametrize('input,expected_comment_text', [ - ('sort:last-edit-date', ['t3', 't2', 't1']), - ('sort:last-edit-time', ['t3', 't2', 't1']), - ('sort:edit-date', ['t3', 't2', 't1']), - ('sort:edit-time', ['t3', 't2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_comment_text", + [ + ("sort:last-edit-date", ["t3", "t2", "t1"]), + ("sort:last-edit-time", ["t3", "t2", "t1"]), + ("sort:edit-date", ["t3", "t2", "t1"]), + ("sort:edit-time", ["t3", "t2", "t1"]), + ], +) def test_sort_by_last_edit_time( - verify_unpaged, comment_factory, input, expected_comment_text): - comment1 = comment_factory(text='t1') - comment2 = comment_factory(text='t2') - comment3 = comment_factory(text='t3') + verify_unpaged, comment_factory, input, expected_comment_text +): + comment1 = comment_factory(text="t1") + comment2 = comment_factory(text="t2") + comment3 = comment_factory(text="t3") comment1.last_edit_time = datetime(1991, 1, 1) comment2.last_edit_time = datetime(1991, 1, 2) comment3.last_edit_time = datetime(1991, 1, 3) diff --git a/server/szurubooru/tests/search/configs/test_pool_search_config.py b/server/szurubooru/tests/search/configs/test_pool_search_config.py new file mode 100644 index 0000000..202635c --- /dev/null +++ b/server/szurubooru/tests/search/configs/test_pool_search_config.py @@ -0,0 +1,433 @@ +from datetime import datetime + +import pytest + +from szurubooru import db, errors, search + + +@pytest.fixture +def executor(): + return search.Executor(search.configs.PoolSearchConfig()) + + +@pytest.fixture +def verify_unpaged(executor): + def verify(input, expected_pool_names): + actual_count, actual_pools = executor.execute( + input, offset=0, limit=100 + ) + actual_pool_names = [u.names[0].name for u in actual_pools] + assert actual_count == len(expected_pool_names) + assert actual_pool_names == expected_pool_names + + return verify + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("", ["t1", "t2"]), + ("t1", ["t1"]), + ("t2", ["t2"]), + ("t1,t2", ["t1", "t2"]), + ("T1,T2", ["t1", "t2"]), + ], +) +def test_filter_anonymous( + verify_unpaged, pool_factory, input, expected_pool_names +): + db.session.add(pool_factory(id=1, names=["t1"])) + db.session.add(pool_factory(id=2, names=["t2"])) + db.session.flush() + verify_unpaged(input, expected_pool_names) + + +@pytest.mark.parametrize( + "db_driver,input,expected_pool_names", + [ + (None, ",", None), + (None, "t1,", None), + (None, "t1,t2", ["t1", "t2"]), + (None, "t1\\,", []), + (None, "asd..asd", None), + (None, "asd\\..asd", []), + (None, "asd.\\.asd", []), + (None, "asd\\.\\.asd", []), + (None, "-", None), + (None, "\\-", ["-"]), + ( + None, + "--", + [ + "t1", + "t2", + "*", + "*asd*", + ":", + "asd:asd", + "\\", + "\\asd", + "-asd", + ], + ), + (None, "\\--", []), + ( + None, + "-\\-", + [ + "t1", + "t2", + "*", + "*asd*", + ":", + "asd:asd", + "\\", + "\\asd", + "-asd", + ], + ), + (None, "-*", []), + (None, "\\-*", ["-", "-asd"]), + (None, ":", None), + (None, "\\:", [":"]), + (None, "\\:asd", []), + (None, "*\\:*", [":", "asd:asd"]), + (None, "asd:asd", None), + (None, "asd\\:asd", ["asd:asd"]), + ( + None, + "*", + [ + "t1", + "t2", + "*", + "*asd*", + ":", + "asd:asd", + "\\", + "\\asd", + "-", + "-asd", + ], + ), + (None, "\\*", ["*"]), + (None, "\\", None), + (None, "\\asd", None), + ("psycopg2", "\\\\", ["\\"]), + ("psycopg2", "\\\\asd", ["\\asd"]), + ], +) +def test_escaping( + executor, pool_factory, input, expected_pool_names, db_driver +): + db.session.add_all( + [ + pool_factory(id=1, names=["t1"]), + pool_factory(id=2, names=["t2"]), + pool_factory(id=3, names=["*"]), + pool_factory(id=4, names=["*asd*"]), + pool_factory(id=5, names=[":"]), + pool_factory(id=6, names=["asd:asd"]), + pool_factory(id=7, names=["\\"]), + pool_factory(id=8, names=["\\asd"]), + pool_factory(id=9, names=["-"]), + pool_factory(id=10, names=["-asd"]), + ] + ) + db.session.flush() + + if db_driver and db.session.get_bind().driver != db_driver: + pytest.xfail() + if expected_pool_names is None: + with pytest.raises(errors.SearchError): + executor.execute(input, offset=0, limit=100) + else: + actual_count, actual_pools = executor.execute( + input, offset=0, limit=100 + ) + actual_pool_names = [u.names[0].name for u in actual_pools] + assert actual_count == len(expected_pool_names) + assert sorted(actual_pool_names) == sorted(expected_pool_names) + + +def test_filter_anonymous_starting_with_colon(verify_unpaged, pool_factory): + db.session.add(pool_factory(id=1, names=[":t"])) + db.session.flush() + with pytest.raises(errors.SearchError): + verify_unpaged(":t", [":t"]) + verify_unpaged("\\:t", [":t"]) + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("name:pool1", ["pool1"]), + ("name:pool2", ["pool2"]), + ("name:none", []), + ("name:", []), + ("name:*1", ["pool1"]), + ("name:*2", ["pool2"]), + ("name:*", ["pool1", "pool2", "pool3", "pool4"]), + ("name:p*", ["pool1", "pool2", "pool3", "pool4"]), + ("name:*o*", ["pool1", "pool2", "pool3", "pool4"]), + ("name:*!*", []), + ("name:!*", []), + ("name:*!", []), + ("-name:pool1", ["pool2", "pool3", "pool4"]), + ("-name:pool2", ["pool1", "pool3", "pool4"]), + ("name:pool1,pool2", ["pool1", "pool2"]), + ("-name:pool1,pool3", ["pool2", "pool4"]), + ("name:pool4", ["pool4"]), + ("name:pool5", ["pool4"]), + ("name:pool4,pool5", ["pool4"]), + ], +) +def test_filter_by_name( + verify_unpaged, pool_factory, input, expected_pool_names +): + db.session.add(pool_factory(id=1, names=["pool1"])) + db.session.add(pool_factory(id=2, names=["pool2"])) + db.session.add(pool_factory(id=3, names=["pool3"])) + db.session.add(pool_factory(id=4, names=["pool4", "pool5", "pool6"])) + db.session.flush() + verify_unpaged(input, expected_pool_names) + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("category:cat1", ["t1", "t2"]), + ("category:cat2", ["t3"]), + ("category:cat1,cat2", ["t1", "t2", "t3"]), + ], +) +def test_filter_by_category( + verify_unpaged, + pool_factory, + pool_category_factory, + input, + expected_pool_names, +): + cat1 = pool_category_factory(name="cat1") + cat2 = pool_category_factory(name="cat2") + pool1 = pool_factory(id=1, names=["t1"], category=cat1) + pool2 = pool_factory(id=2, names=["t2"], category=cat1) + pool3 = pool_factory(id=3, names=["t3"], category=cat2) + db.session.add_all([pool1, pool2, pool3]) + db.session.flush() + verify_unpaged(input, expected_pool_names) + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("creation-time:2014", ["t1", "t2"]), + ("creation-date:2014", ["t1", "t2"]), + ("-creation-time:2014", ["t3"]), + ("-creation-date:2014", ["t3"]), + ("creation-time:2014..2014-06", ["t1", "t2"]), + ("creation-time:2014-06..2015-01-01", ["t2", "t3"]), + ("creation-time:2014-06..", ["t2", "t3"]), + ("creation-time:..2014-06", ["t1", "t2"]), + ("-creation-time:2014..2014-06", ["t3"]), + ("-creation-time:2014-06..2015-01-01", ["t1"]), + ("creation-date:2014..2014-06", ["t1", "t2"]), + ("creation-date:2014-06..2015-01-01", ["t2", "t3"]), + ("creation-date:2014-06..", ["t2", "t3"]), + ("creation-date:..2014-06", ["t1", "t2"]), + ("-creation-date:2014..2014-06", ["t3"]), + ("-creation-date:2014-06..2015-01-01", ["t1"]), + ("creation-time:2014-01,2015", ["t1", "t3"]), + ("creation-date:2014-01,2015", ["t1", "t3"]), + ("-creation-time:2014-01,2015", ["t2"]), + ("-creation-date:2014-01,2015", ["t2"]), + ], +) +def test_filter_by_creation_time( + verify_unpaged, pool_factory, input, expected_pool_names +): + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) + pool3 = pool_factory(id=3, names=["t3"]) + pool1.creation_time = datetime(2014, 1, 1) + pool2.creation_time = datetime(2014, 6, 1) + pool3.creation_time = datetime(2015, 1, 1) + db.session.add_all([pool1, pool2, pool3]) + db.session.flush() + verify_unpaged(input, expected_pool_names) + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("last-edit-date:2014", ["t1", "t3"]), + ("last-edit-time:2014", ["t1", "t3"]), + ("edit-date:2014", ["t1", "t3"]), + ("edit-time:2014", ["t1", "t3"]), + ], +) +def test_filter_by_edit_time( + verify_unpaged, pool_factory, input, expected_pool_names +): + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) + pool3 = pool_factory(id=3, names=["t3"]) + pool1.last_edit_time = datetime(2014, 1, 1) + pool2.last_edit_time = datetime(2015, 1, 1) + pool3.last_edit_time = datetime(2014, 1, 1) + db.session.add_all([pool1, pool2, pool3]) + db.session.flush() + verify_unpaged(input, expected_pool_names) + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("post-count:2", ["t1"]), + ("post-count:1", ["t2"]), + ("post-count:1..", ["t1", "t2"]), + ("post-count-min:1", ["t1", "t2"]), + ("post-count:..1", ["t2"]), + ("post-count-max:1", ["t2"]), + ], +) +def test_filter_by_post_count( + verify_unpaged, pool_factory, post_factory, input, expected_pool_names +): + post1 = post_factory(id=1) + post2 = post_factory(id=2) + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) + db.session.add_all([post1, post2, pool1, pool2]) + pool1.posts.append(post1) + pool1.posts.append(post2) + pool2.posts.append(post1) + db.session.flush() + verify_unpaged(input, expected_pool_names) + + +@pytest.mark.parametrize( + "input", + [ + "post-count:..", + "post-count:asd", + "post-count:asd,1", + "post-count:1,asd", + "post-count:asd..1", + "post-count:1..asd", + ], +) +def test_filter_by_invalid_input(executor, input): + with pytest.raises(errors.SearchError): + executor.execute(input, offset=0, limit=100) + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("", ["t1", "t2"]), + ("sort:name", ["t1", "t2"]), + ("-sort:name", ["t2", "t1"]), + ("sort:name,asc", ["t1", "t2"]), + ("sort:name,desc", ["t2", "t1"]), + ("-sort:name,asc", ["t2", "t1"]), + ("-sort:name,desc", ["t1", "t2"]), + ], +) +def test_sort_by_name( + verify_unpaged, pool_factory, input, expected_pool_names +): + db.session.add(pool_factory(id=2, names=["t2"])) + db.session.add(pool_factory(id=1, names=["t1"])) + db.session.flush() + verify_unpaged(input, expected_pool_names) + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("", ["t1", "t2", "t3"]), + ("sort:creation-date", ["t3", "t2", "t1"]), + ("sort:creation-time", ["t3", "t2", "t1"]), + ], +) +def test_sort_by_creation_time( + verify_unpaged, pool_factory, input, expected_pool_names +): + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) + pool3 = pool_factory(id=3, names=["t3"]) + pool1.creation_time = datetime(1991, 1, 1) + pool2.creation_time = datetime(1991, 1, 2) + pool3.creation_time = datetime(1991, 1, 3) + db.session.add_all([pool3, pool1, pool2]) + db.session.flush() + verify_unpaged(input, expected_pool_names) + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("", ["t1", "t2", "t3"]), + ("sort:last-edit-date", ["t3", "t2", "t1"]), + ("sort:last-edit-time", ["t3", "t2", "t1"]), + ("sort:edit-date", ["t3", "t2", "t1"]), + ("sort:edit-time", ["t3", "t2", "t1"]), + ], +) +def test_sort_by_last_edit_time( + verify_unpaged, pool_factory, input, expected_pool_names +): + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) + pool3 = pool_factory(id=3, names=["t3"]) + pool1.last_edit_time = datetime(1991, 1, 1) + pool2.last_edit_time = datetime(1991, 1, 2) + pool3.last_edit_time = datetime(1991, 1, 3) + db.session.add_all([pool3, pool1, pool2]) + db.session.flush() + verify_unpaged(input, expected_pool_names) + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("sort:post-count", ["t2", "t1"]), + ], +) +def test_sort_by_post_count( + verify_unpaged, pool_factory, post_factory, input, expected_pool_names +): + post1 = post_factory(id=1) + post2 = post_factory(id=2) + pool1 = pool_factory(id=1, names=["t1"]) + pool2 = pool_factory(id=2, names=["t2"]) + db.session.add_all([post1, post2, pool1, pool2]) + pool1.posts.append(post1) + pool2.posts.append(post1) + pool2.posts.append(post2) + db.session.flush() + verify_unpaged(input, expected_pool_names) + + +@pytest.mark.parametrize( + "input,expected_pool_names", + [ + ("sort:category", ["t3", "t1", "t2"]), + ], +) +def test_sort_by_category( + verify_unpaged, + pool_factory, + pool_category_factory, + input, + expected_pool_names, +): + cat1 = pool_category_factory(name="cat1") + cat2 = pool_category_factory(name="cat2") + pool1 = pool_factory(id=1, names=["t1"], category=cat2) + pool2 = pool_factory(id=2, names=["t2"], category=cat2) + pool3 = pool_factory(id=3, names=["t3"], category=cat1) + db.session.add_all([pool1, pool2, pool3]) + db.session.flush() + verify_unpaged(input, expected_pool_names) diff --git a/server/szurubooru/tests/search/configs/test_post_search_config.py b/server/szurubooru/tests/search/configs/test_post_search_config.py index dd9b98f..84e1601 100644 --- a/server/szurubooru/tests/search/configs/test_post_search_config.py +++ b/server/szurubooru/tests/search/configs/test_post_search_config.py @@ -1,16 +1,17 @@ -# pylint: disable=redefined-outer-name from datetime import datetime + import pytest -from szurubooru import db, model, errors, search + +from szurubooru import db, errors, model, search @pytest.fixture def fav_factory(user_factory): def factory(post, user=None): return model.PostFavorite( - post=post, - user=user or user_factory(), - time=datetime.utcnow()) + post=post, user=user or user_factory(), time=datetime.utcnow() + ) + return factory @@ -21,14 +22,17 @@ def score_factory(user_factory): post=post, user=user or user_factory(), time=datetime.utcnow(), - score=score) + score=score, + ) + return factory @pytest.fixture def note_factory(): - def factory(text='...'): - return model.PostNote(polygon='...', text=text) + def factory(text="..."): + return model.PostNote(polygon="...", text=text) + return factory @@ -37,11 +41,10 @@ def feature_factory(user_factory): def factory(post=None): if post: return model.PostFeature( - time=datetime.utcnow(), - user=user_factory(), - post=post) - return model.PostFeature( - time=datetime.utcnow(), user=user_factory()) + time=datetime.utcnow(), user=user_factory(), post=post + ) + return model.PostFeature(time=datetime.utcnow(), user=user_factory()) + return factory @@ -58,6 +61,7 @@ def auth_executor(executor, user_factory): db.session.flush() executor.config.user = auth_user return auth_user + return wrapper @@ -65,15 +69,16 @@ def auth_executor(executor, user_factory): def verify_unpaged(executor): def verify(input, expected_post_ids, test_order=False): actual_count, actual_posts = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_post_ids = list([p.post_id for p in actual_posts]) if not test_order: actual_post_ids = sorted(actual_post_ids) expected_post_ids = sorted(expected_post_ids) assert actual_post_ids == expected_post_ids assert actual_count == len(expected_post_ids) - return verify + return verify @pytest.fixture def verify_around(executor): @@ -84,11 +89,14 @@ def verify_around(executor): return verify -@pytest.mark.parametrize('input,expected_post_ids', [ - ('id:1', [1]), - ('id:3', [3]), - ('id:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("id:1", [1]), + ("id:3", [3]), + ("id:1,3", [1, 3]), + ], +) def test_filter_by_id(verify_unpaged, post_factory, input, expected_post_ids): post1 = post_factory(id=1) post2 = post_factory(id=2) @@ -98,35 +106,43 @@ def test_filter_by_id(verify_unpaged, post_factory, input, expected_post_ids): verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('tag:t1', [1]), - ('tag:t2', [2]), - ('tag:t1,t2', [1, 2]), - ('tag:t4a', [4]), - ('tag:t4b', [4]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("tag:t1", [1]), + ("tag:t2", [2]), + ("tag:t1,t2", [1, 2]), + ("tag:t4a", [4]), + ("tag:t4b", [4]), + ], +) def test_filter_by_tag( - verify_unpaged, post_factory, tag_factory, input, expected_post_ids): + verify_unpaged, post_factory, tag_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) post4 = post_factory(id=4) - post1.tags = [tag_factory(names=['t1'])] - post2.tags = [tag_factory(names=['t2'])] - post3.tags = [tag_factory(names=['t3'])] - post4.tags = [tag_factory(names=['t4a', 't4b'])] + post1.tags = [tag_factory(names=["t1"])] + post2.tags = [tag_factory(names=["t2"])] + post3.tags = [tag_factory(names=["t3"])] + post4.tags = [tag_factory(names=["t4a", "t4b"])] db.session.add_all([post1, post2, post3, post4]) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('score:1', [1]), - ('score:3', [3]), - ('score:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("score:1", [1]), + ("score:3", [3]), + ("score:1,3", [1, 3]), + ], +) def test_filter_by_score( - verify_unpaged, post_factory, user_factory, input, expected_post_ids): + verify_unpaged, post_factory, user_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -136,96 +152,123 @@ def test_filter_by_score( score=post.post_id, time=datetime.utcnow(), post=post, - user=user_factory())) + user=user_factory(), + ) + ) db.session.add_all([post1, post2, post3]) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('uploader:', [4]), - ('uploader:u1', [1]), - ('uploader:u3', [3]), - ('uploader:u1,u3', [1, 3]), - ('upload:', [4]), - ('upload:u1', [1]), - ('upload:u3', [3]), - ('upload:u1,u3', [1, 3]), - ('submit:', [4]), - ('submit:u1', [1]), - ('submit:u3', [3]), - ('submit:u1,u3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("uploader:", [4]), + ("uploader:u1", [1]), + ("uploader:u3", [3]), + ("uploader:u1,u3", [1, 3]), + ("upload:", [4]), + ("upload:u1", [1]), + ("upload:u3", [3]), + ("upload:u1,u3", [1, 3]), + ("submit:", [4]), + ("submit:u1", [1]), + ("submit:u3", [3]), + ("submit:u1,u3", [1, 3]), + ], +) def test_filter_by_uploader( - verify_unpaged, post_factory, user_factory, input, expected_post_ids): + verify_unpaged, post_factory, user_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) post4 = post_factory(id=4) - post1.user = user_factory(name='u1') - post2.user = user_factory(name='u2') - post3.user = user_factory(name='u3') + post1.user = user_factory(name="u1") + post2.user = user_factory(name="u2") + post3.user = user_factory(name="u3") db.session.add_all([post1, post2, post3, post4]) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('comment:u1', [1]), - ('comment:u3', [3]), - ('comment:u1,u3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("comment:u1", [1]), + ("comment:u3", [3]), + ("comment:u1,u3", [1, 3]), + ], +) def test_filter_by_commenter( - verify_unpaged, - post_factory, - user_factory, - comment_factory, - input, - expected_post_ids): + verify_unpaged, + post_factory, + user_factory, + comment_factory, + input, + expected_post_ids, +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - comment_factory(post=post1, user=user_factory(name='u1')), - comment_factory(post=post2, user=user_factory(name='u2')), - comment_factory(post=post3, user=user_factory(name='u3')), - post1, post2, post3, - ]) + db.session.add_all( + [ + comment_factory(post=post1, user=user_factory(name="u1")), + comment_factory(post=post2, user=user_factory(name="u2")), + comment_factory(post=post3, user=user_factory(name="u3")), + post1, + post2, + post3, + ] + ) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('fav:u1', [1]), - ('fav:u3', [3]), - ('fav:u1,u3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("fav:u1", [1]), + ("fav:u3", [3]), + ("fav:u1,u3", [1, 3]), + ], +) def test_filter_by_favorite( - verify_unpaged, - post_factory, - user_factory, - fav_factory, - input, - expected_post_ids): + verify_unpaged, + post_factory, + user_factory, + fav_factory, + input, + expected_post_ids, +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - fav_factory(post=post1, user=user_factory(name='u1')), - fav_factory(post=post2, user=user_factory(name='u2')), - fav_factory(post=post3, user=user_factory(name='u3')), - post1, post2, post3]) + db.session.add_all( + [ + fav_factory(post=post1, user=user_factory(name="u1")), + fav_factory(post=post2, user=user_factory(name="u2")), + fav_factory(post=post3, user=user_factory(name="u3")), + post1, + post2, + post3, + ] + ) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('tag-count:1', [1]), - ('tag-count:3', [3]), - ('tag-count:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("tag-count:1", [1]), + ("tag-count:3", [3]), + ("tag-count:1,3", [1, 3]), + ], +) def test_filter_by_tag_count( - verify_unpaged, post_factory, tag_factory, input, expected_post_ids): + verify_unpaged, post_factory, tag_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -237,61 +280,79 @@ def test_filter_by_tag_count( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('comment-count:1', [1]), - ('comment-count:3', [3]), - ('comment-count:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("comment-count:1", [1]), + ("comment-count:3", [3]), + ("comment-count:1,3", [1, 3]), + ], +) def test_filter_by_comment_count( - verify_unpaged, - post_factory, - comment_factory, - input, - expected_post_ids): + verify_unpaged, post_factory, comment_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - comment_factory(post=post1), - comment_factory(post=post2), - comment_factory(post=post2), - comment_factory(post=post3), - comment_factory(post=post3), - comment_factory(post=post3), - post1, post2, post3]) + db.session.add_all( + [ + comment_factory(post=post1), + comment_factory(post=post2), + comment_factory(post=post2), + comment_factory(post=post3), + comment_factory(post=post3), + comment_factory(post=post3), + post1, + post2, + post3, + ] + ) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('fav-count:1', [1]), - ('fav-count:3', [3]), - ('fav-count:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("fav-count:1", [1]), + ("fav-count:3", [3]), + ("fav-count:1,3", [1, 3]), + ], +) def test_filter_by_favorite_count( - verify_unpaged, post_factory, fav_factory, input, expected_post_ids): + verify_unpaged, post_factory, fav_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - fav_factory(post=post1), - fav_factory(post=post2), - fav_factory(post=post2), - fav_factory(post=post3), - fav_factory(post=post3), - fav_factory(post=post3), - post1, post2, post3]) + db.session.add_all( + [ + fav_factory(post=post1), + fav_factory(post=post2), + fav_factory(post=post2), + fav_factory(post=post3), + fav_factory(post=post3), + fav_factory(post=post3), + post1, + post2, + post3, + ] + ) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('note-count:1', [1]), - ('note-count:3', [3]), - ('note-count:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("note-count:1", [1]), + ("note-count:3", [3]), + ("note-count:1,3", [1, 3]), + ], +) def test_filter_by_note_count( - verify_unpaged, post_factory, note_factory, input, expected_post_ids): + verify_unpaged, post_factory, note_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -303,36 +364,40 @@ def test_filter_by_note_count( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('note-text:*', [1, 2, 3]), - ('note-text:text2', [2]), - ('note-text:text3*', [3]), - ('note-text:text3a,text2', [2, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("note-text:*", [1, 2, 3]), + ("note-text:text2", [2]), + ("note-text:text3*", [3]), + ("note-text:text3a,text2", [2, 3]), + ], +) def test_filter_by_note_text( - verify_unpaged, post_factory, note_factory, input, expected_post_ids): + verify_unpaged, post_factory, note_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - post1.notes = [note_factory(text='text1')] - post2.notes = [note_factory(text='text2'), note_factory(text='text2')] - post3.notes = [note_factory(text='text3a'), note_factory(text='text3b')] + post1.notes = [note_factory(text="text1")] + post2.notes = [note_factory(text="text2"), note_factory(text="text2")] + post3.notes = [note_factory(text="text3a"), note_factory(text="text3b")] db.session.add_all([post1, post2, post3]) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('feature-count:1', [1]), - ('feature-count:3', [3]), - ('feature-count:1,3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("feature-count:1", [1]), + ("feature-count:3", [3]), + ("feature-count:1,3", [1, 3]), + ], +) def test_filter_by_feature_count( - verify_unpaged, - post_factory, - feature_factory, - input, - expected_post_ids): + verify_unpaged, post_factory, feature_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -344,18 +409,22 @@ def test_filter_by_feature_count( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('type:image', [1]), - ('type:anim', [2]), - ('type:animation', [2]), - ('type:gif', [2]), - ('type:video', [3]), - ('type:webm', [3]), - ('type:flash', [4]), - ('type:swf', [4]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("type:image", [1]), + ("type:anim", [2]), + ("type:animation", [2]), + ("type:gif", [2]), + ("type:video", [3]), + ("type:webm", [3]), + ("type:flash", [4]), + ("type:swf", [4]), + ], +) def test_filter_by_type( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -369,14 +438,18 @@ def test_filter_by_type( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('safety:safe', [1]), - ('safety:sketchy', [2]), - ('safety:questionable', [2]), - ('safety:unsafe', [3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("safety:safe", [1]), + ("safety:sketchy", [2]), + ("safety:questionable", [2]), + ("safety:unsafe", [3]), + ], +) def test_filter_by_safety( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -390,34 +463,42 @@ def test_filter_by_safety( def test_filter_by_invalid_type(executor): with pytest.raises(errors.SearchError): - executor.execute('type:invalid', offset=0, limit=100) + executor.execute("type:invalid", offset=0, limit=100) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('content-checksum:checksum1', [1]), - ('content-checksum:checksum3', [3]), - ('content-checksum:checksum1,checksum3', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("content-checksum:checksum1", [1]), + ("content-checksum:checksum3", [3]), + ("content-checksum:checksum1,checksum3", [1, 3]), + ], +) def test_filter_by_content_checksum( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - post1.checksum = 'checksum1' - post2.checksum = 'checksum2' - post3.checksum = 'checksum3' + post1.checksum = "checksum1" + post2.checksum = "checksum2" + post3.checksum = "checksum3" db.session.add_all([post1, post2, post3]) db.session.flush() verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('file-size:100', [1]), - ('file-size:102', [3]), - ('file-size:100,102', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("file-size:100", [1]), + ("file-size:102", [3]), + ("file-size:100,102", [1, 3]), + ], +) def test_filter_by_file_size( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -429,25 +510,29 @@ def test_filter_by_file_size( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('image-width:100', [1]), - ('image-width:200', [2]), - ('image-width:100,300', [1, 3]), - ('image-height:200', [1]), - ('image-height:100', [2]), - ('image-height:200,300', [1, 3]), - ('image-area:20000', [1, 2]), - ('image-area:90000', [3]), - ('image-area:20000,90000', [1, 2, 3]), - ('image-ar:1', [3]), - ('image-ar:..0.9', [1, 4]), - ('image-ar:1.1..', [2]), - ('image-ar:1/1..1/1', [3]), - ('image-ar:1:1..1:1', [3]), - ('image-ar:0.62..0.63', [4]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("image-width:100", [1]), + ("image-width:200", [2]), + ("image-width:100,300", [1, 3]), + ("image-height:200", [1]), + ("image-height:100", [2]), + ("image-height:200,300", [1, 3]), + ("image-area:20000", [1, 2]), + ("image-area:90000", [3]), + ("image-area:20000,90000", [1, 2, 3]), + ("image-ar:1", [3]), + ("image-ar:..0.9", [1, 4]), + ("image-ar:1.1..", [2]), + ("image-ar:1/1..1/1", [3]), + ("image-ar:1:1..1:1", [3]), + ("image-ar:0.62..0.63", [4]), + ], +) def test_filter_by_image_size( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -467,25 +552,29 @@ def test_filter_by_image_size( def test_filter_by_invalid_aspect_ratio(executor): with pytest.raises(errors.SearchError): - executor.execute('image-ar:1:1:1', offset=0, limit=100) + executor.execute("image-ar:1:1:1", offset=0, limit=100) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('creation-date:2014', [1]), - ('creation-date:2016', [3]), - ('creation-date:2014,2016', [1, 3]), - ('creation-time:2014', [1]), - ('creation-time:2016', [3]), - ('creation-time:2014,2016', [1, 3]), - ('date:2014', [1]), - ('date:2016', [3]), - ('date:2014,2016', [1, 3]), - ('time:2014', [1]), - ('time:2016', [3]), - ('time:2014,2016', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("creation-date:2014", [1]), + ("creation-date:2016", [3]), + ("creation-date:2014,2016", [1, 3]), + ("creation-time:2014", [1]), + ("creation-time:2016", [3]), + ("creation-time:2014,2016", [1, 3]), + ("date:2014", [1]), + ("date:2016", [3]), + ("date:2014,2016", [1, 3]), + ("time:2014", [1]), + ("time:2016", [3]), + ("time:2014,2016", [1, 3]), + ], +) def test_filter_by_creation_time( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -497,22 +586,26 @@ def test_filter_by_creation_time( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('last-edit-date:2014', [1]), - ('last-edit-date:2016', [3]), - ('last-edit-date:2014,2016', [1, 3]), - ('last-edit-time:2014', [1]), - ('last-edit-time:2016', [3]), - ('last-edit-time:2014,2016', [1, 3]), - ('edit-date:2014', [1]), - ('edit-date:2016', [3]), - ('edit-date:2014,2016', [1, 3]), - ('edit-time:2014', [1]), - ('edit-time:2016', [3]), - ('edit-time:2014,2016', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("last-edit-date:2014", [1]), + ("last-edit-date:2016", [3]), + ("last-edit-date:2014,2016", [1, 3]), + ("last-edit-time:2014", [1]), + ("last-edit-time:2016", [3]), + ("last-edit-time:2014,2016", [1, 3]), + ("edit-date:2014", [1]), + ("edit-date:2016", [3]), + ("edit-date:2014,2016", [1, 3]), + ("edit-time:2014", [1]), + ("edit-time:2016", [3]), + ("edit-time:2014,2016", [1, 3]), + ], +) def test_filter_by_last_edit_time( - verify_unpaged, post_factory, input, expected_post_ids): + verify_unpaged, post_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -524,20 +617,20 @@ def test_filter_by_last_edit_time( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('comment-date:2014', [1]), - ('comment-date:2016', [3]), - ('comment-date:2014,2016', [1, 3]), - ('comment-time:2014', [1]), - ('comment-time:2016', [3]), - ('comment-time:2014,2016', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("comment-date:2014", [1]), + ("comment-date:2016", [3]), + ("comment-date:2014,2016", [1, 3]), + ("comment-time:2014", [1]), + ("comment-time:2016", [3]), + ("comment-time:2014,2016", [1, 3]), + ], +) def test_filter_by_comment_date( - verify_unpaged, - post_factory, - comment_factory, - input, - expected_post_ids): + verify_unpaged, post_factory, comment_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -552,16 +645,20 @@ def test_filter_by_comment_date( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('fav-date:2014', [1]), - ('fav-date:2016', [3]), - ('fav-date:2014,2016', [1, 3]), - ('fav-time:2014', [1]), - ('fav-time:2016', [3]), - ('fav-time:2014,2016', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("fav-date:2014", [1]), + ("fav-date:2016", [3]), + ("fav-date:2014,2016", [1, 3]), + ("fav-time:2014", [1]), + ("fav-time:2016", [3]), + ("fav-time:2014,2016", [1, 3]), + ], +) def test_filter_by_fav_date( - verify_unpaged, post_factory, fav_factory, input, expected_post_ids): + verify_unpaged, post_factory, fav_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -576,20 +673,20 @@ def test_filter_by_fav_date( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('feature-date:2014', [1]), - ('feature-date:2016', [3]), - ('feature-date:2014,2016', [1, 3]), - ('feature-time:2014', [1]), - ('feature-time:2016', [3]), - ('feature-time:2014,2016', [1, 3]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("feature-date:2014", [1]), + ("feature-date:2016", [3]), + ("feature-date:2014,2016", [1, 3]), + ("feature-time:2014", [1]), + ("feature-time:2016", [3]), + ("feature-time:2014,2016", [1, 3]), + ], +) def test_filter_by_feature_date( - verify_unpaged, - post_factory, - feature_factory, - input, - expected_post_ids): + verify_unpaged, post_factory, feature_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) @@ -604,37 +701,40 @@ def test_filter_by_feature_date( verify_unpaged(input, expected_post_ids) -@pytest.mark.parametrize('input', [ - 'sort:random', - 'sort:id', - 'sort:score', - 'sort:tag-count', - 'sort:comment-count', - 'sort:fav-count', - 'sort:note-count', - 'sort:feature-count', - 'sort:file-size', - 'sort:image-width', - 'sort:width', - 'sort:image-height', - 'sort:height', - 'sort:image-area', - 'sort:area', - 'sort:creation-date', - 'sort:creation-time', - 'sort:date', - 'sort:time', - 'sort:last-edit-date', - 'sort:last-edit-time', - 'sort:edit-date', - 'sort:edit-time', - 'sort:comment-date', - 'sort:comment-time', - 'sort:fav-date', - 'sort:fav-time', - 'sort:feature-date', - 'sort:feature-time', -]) +@pytest.mark.parametrize( + "input", + [ + "sort:random", + "sort:id", + "sort:score", + "sort:tag-count", + "sort:comment-count", + "sort:fav-count", + "sort:note-count", + "sort:feature-count", + "sort:file-size", + "sort:image-width", + "sort:width", + "sort:image-height", + "sort:height", + "sort:image-area", + "sort:area", + "sort:creation-date", + "sort:creation-time", + "sort:date", + "sort:time", + "sort:last-edit-date", + "sort:last-edit-time", + "sort:edit-date", + "sort:edit-time", + "sort:comment-date", + "sort:comment-time", + "sort:fav-date", + "sort:fav-time", + "sort:feature-date", + "sort:feature-time", + ], +) def test_sort_tokens(verify_unpaged, post_factory, input): post1 = post_factory(id=1) post2 = post_factory(id=2) @@ -644,137 +744,152 @@ def test_sort_tokens(verify_unpaged, post_factory, input): verify_unpaged(input, [1, 2, 3]) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('', [1, 2, 3, 4]), - ('t1', [1]), - ('t2', [2]), - ('t1,t2', [1, 2]), - ('t4a', [4]), - ('t4b', [4]), -]) +@pytest.mark.parametrize( + "input,expected_post_ids", + [ + ("", [1, 2, 3, 4]), + ("t1", [1]), + ("t2", [2]), + ("t1,t2", [1, 2]), + ("t4a", [4]), + ("t4b", [4]), + ], +) def test_anonymous( - verify_unpaged, post_factory, tag_factory, input, expected_post_ids): + verify_unpaged, post_factory, tag_factory, input, expected_post_ids +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) post4 = post_factory(id=4) - post1.tags = [tag_factory(names=['t1'])] - post2.tags = [tag_factory(names=['t2'])] - post3.tags = [tag_factory(names=['t3'])] - post4.tags = [tag_factory(names=['t4a', 't4b'])] + post1.tags = [tag_factory(names=["t1"])] + post2.tags = [tag_factory(names=["t2"])] + post3.tags = [tag_factory(names=["t3"])] + post4.tags = [tag_factory(names=["t4a", "t4b"])] db.session.add_all([post1, post2, post3, post4]) db.session.flush() verify_unpaged(input, expected_post_ids) def test_own_liked( - auth_executor, - post_factory, - score_factory, - user_factory, - verify_unpaged): + auth_executor, post_factory, score_factory, user_factory, verify_unpaged +): auth_user = auth_executor() post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - score_factory(post=post1, user=auth_user, score=1), - score_factory(post=post2, user=user_factory(name='dummy'), score=1), - score_factory(post=post3, user=auth_user, score=-1), - post1, post2, post3, - ]) + db.session.add_all( + [ + score_factory(post=post1, user=auth_user, score=1), + score_factory( + post=post2, user=user_factory(name="dummy"), score=1 + ), + score_factory(post=post3, user=auth_user, score=-1), + post1, + post2, + post3, + ] + ) db.session.flush() - verify_unpaged('special:liked', [1]) - verify_unpaged('-special:liked', [2, 3]) + verify_unpaged("special:liked", [1]) + verify_unpaged("-special:liked", [2, 3]) def test_own_disliked( - auth_executor, - post_factory, - score_factory, - user_factory, - verify_unpaged): + auth_executor, post_factory, score_factory, user_factory, verify_unpaged +): auth_user = auth_executor() post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) - db.session.add_all([ - score_factory(post=post1, user=auth_user, score=-1), - score_factory(post=post2, user=user_factory(name='dummy'), score=-1), - score_factory(post=post3, user=auth_user, score=1), - post1, post2, post3, - ]) + db.session.add_all( + [ + score_factory(post=post1, user=auth_user, score=-1), + score_factory( + post=post2, user=user_factory(name="dummy"), score=-1 + ), + score_factory(post=post3, user=auth_user, score=1), + post1, + post2, + post3, + ] + ) db.session.flush() - verify_unpaged('special:disliked', [1]) - verify_unpaged('-special:disliked', [2, 3]) + verify_unpaged("special:disliked", [1]) + verify_unpaged("-special:disliked", [2, 3]) -@pytest.mark.parametrize('input', [ - 'liked:x', - 'disliked:x', -]) +@pytest.mark.parametrize( + "input", + [ + "liked:x", + "disliked:x", + ], +) def test_someones_score(executor, input): with pytest.raises(errors.SearchError): executor.execute(input, offset=0, limit=100) def test_own_fav( - auth_executor, - post_factory, - fav_factory, - user_factory, - verify_unpaged): + auth_executor, post_factory, fav_factory, user_factory, verify_unpaged +): auth_user = auth_executor() post1 = post_factory(id=1) post2 = post_factory(id=2) - db.session.add_all([ - fav_factory(post=post1, user=auth_user), - fav_factory(post=post2, user=user_factory(name='unrelated')), - post1, post2, - ]) + db.session.add_all( + [ + fav_factory(post=post1, user=auth_user), + fav_factory(post=post2, user=user_factory(name="unrelated")), + post1, + post2, + ] + ) db.session.flush() - verify_unpaged('special:fav', [1]) - verify_unpaged('-special:fav', [2]) + verify_unpaged("special:fav", [1]) + verify_unpaged("-special:fav", [2]) def test_tumbleweed( - post_factory, - fav_factory, - comment_factory, - score_factory, - verify_unpaged): + post_factory, fav_factory, comment_factory, score_factory, verify_unpaged +): post1 = post_factory(id=1) post2 = post_factory(id=2) post3 = post_factory(id=3) post4 = post_factory(id=4) - db.session.add_all([ - comment_factory(post=post1), - score_factory(post=post2), - fav_factory(post=post3), - post1, post2, post3, post4, - ]) + db.session.add_all( + [ + comment_factory(post=post1), + score_factory(post=post2), + fav_factory(post=post3), + post1, + post2, + post3, + post4, + ] + ) db.session.flush() - verify_unpaged('special:tumbleweed', [4]) - verify_unpaged('-special:tumbleweed', [1, 2, 3]) + verify_unpaged("special:tumbleweed", [4]) + verify_unpaged("-special:tumbleweed", [1, 2, 3]) -@pytest.mark.parametrize('input,expected_post_ids', [ - ('sort:id,asc metric-a:1..3', [1, 2, 3]), - ('sort:id,asc metric-a-min:2', [2, 3]), - ('sort:id,asc metric-a:1.5..', [2, 3]), - ('sort:id,asc metric-a:1..3 metric-b:2..', [2]), - ('sort:id,asc c metric-a:3..', [3]), - ('sort:id,asc metric-b:..2', [1, 2]), - ('sort:id,asc metric-b:..1.9', [1]), - ('sort:metric-a', [1, 2, 3, 5, 4]), - ('sort:metric-a,desc', [3, 2, 1, 5, 4]), - ('metric-a:1..3 metric-b:1..3 sort:metric-b,desc', [2, 1]), - ('metric-a:1..3 sort:metric-b,desc', [2, 1, 3]), - ('metric-a:2..3 metric-b:1..3 sort:metric-b,desc', [2]), - ('metric-a:2..3 sort:metric-b,desc', [2, 3]), - ('sort:id,asc metric:a', [1, 2, 3]), - ('sort:id,asc -metric:a', [4, 5]), - ('sort:id,asc metric:a -metric:b', [3]), +@pytest.mark.parametrize("input,expected_post_ids", [ + ("sort:id,asc metric-a:1..3", [1, 2, 3]), + ("sort:id,asc metric-a-min:2", [2, 3]), + ("sort:id,asc metric-a:1.5..", [2, 3]), + ("sort:id,asc metric-a:1..3 metric-b:2..", [2]), + ("sort:id,asc c metric-a:3..", [3]), + ("sort:id,asc metric-b:..2", [1, 2]), + ("sort:id,asc metric-b:..1.9", [1]), + ("sort:metric-a", [1, 2, 3, 5, 4]), + ("sort:metric-a,desc", [3, 2, 1, 5, 4]), + ("metric-a:1..3 metric-b:1..3 sort:metric-b,desc", [2, 1]), + ("metric-a:1..3 sort:metric-b,desc", [2, 1, 3]), + ("metric-a:2..3 metric-b:1..3 sort:metric-b,desc", [2]), + ("metric-a:2..3 sort:metric-b,desc", [2, 3]), + ("sort:id,asc metric:a", [1, 2, 3]), + ("sort:id,asc -metric:a", [4, 5]), + ("sort:id,asc metric:a -metric:b", [3]), ]) def test_metrics( input, @@ -785,9 +900,9 @@ def test_metrics( post_metric_factory, post_metric_range_factory, verify_unpaged): - tag_a = tag_factory(names=['a']) - tag_b = tag_factory(names=['b']) - tag_c = tag_factory(names=['c']) + tag_a = tag_factory(names=["a"]) + tag_b = tag_factory(names=["b"]) + tag_c = tag_factory(names=["c"]) post1 = post_factory(id=1, tags=[tag_a, tag_b, tag_c]) post2 = post_factory(id=2, tags=[tag_a, tag_b, tag_c]) post3 = post_factory(id=3, tags=[tag_a, tag_b, tag_c]) @@ -813,15 +928,15 @@ def test_metrics( verify_unpaged(input, expected_post_ids, True) -@pytest.mark.parametrize('input,expected_prev_id,expected_next_id', [ - ('', 3, 1), # default order is actually descending - ('sort:id,asc', 1, 3), - ('sort:id,desc', 3, 1), - ('sort:tag-count,asc', 1, 3), - ('sort:tag-count,desc', 3, 1), - ('metric-a:0..2 sort:metric-a', 3, 1), - ('metric-a:0..2 sort:metric-a,desc', 1, 3), - ('sort:metric-b', 3, 1), +@pytest.mark.parametrize("input,expected_prev_id,expected_next_id", [ + ("", 3, 1), # default order is actually descending + ("sort:id,asc", 1, 3), + ("sort:id,desc", 3, 1), + ("sort:tag-count,asc", 1, 3), + ("sort:tag-count,desc", 3, 1), + ("metric-a:0..2 sort:metric-a", 3, 1), + ("metric-a:0..2 sort:metric-a,desc", 1, 3), + ("sort:metric-b", 3, 1), ]) def test_around_query( input, @@ -832,10 +947,10 @@ def test_around_query( metric_factory, post_metric_factory, verify_around): - tag_a = tag_factory(names=['a']) - tag_b = tag_factory(names=['b']) - tag_c = tag_factory(names=['c']) - tag_d = tag_factory(names=['d']) + tag_a = tag_factory(names=["a"]) + tag_b = tag_factory(names=["b"]) + tag_c = tag_factory(names=["c"]) + tag_d = tag_factory(names=["d"]) post1 = post_factory(id=1, tags=[tag_a]) post2 = post_factory(id=2, tags=[tag_a, tag_b]) post3 = post_factory(id=3, tags=[tag_a, tag_b, tag_c]) diff --git a/server/szurubooru/tests/search/configs/test_tag_search_config.py b/server/szurubooru/tests/search/configs/test_tag_search_config.py index 8ea107f..8175b73 100644 --- a/server/szurubooru/tests/search/configs/test_tag_search_config.py +++ b/server/szurubooru/tests/search/configs/test_tag_search_config.py @@ -1,6 +1,7 @@ -# pylint: disable=redefined-outer-name from datetime import datetime + import pytest + from szurubooru import db, errors, search @@ -13,180 +14,239 @@ def executor(): def verify_unpaged(executor): def verify(input, expected_tag_names): actual_count, actual_tags = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_tag_names = [u.names[0].name for u in actual_tags] assert actual_count == len(expected_tag_names) assert actual_tag_names == expected_tag_names + return verify -@pytest.mark.parametrize('input,expected_tag_names', [ - ('', ['t1', 't2']), - ('t1', ['t1']), - ('t2', ['t2']), - ('t1,t2', ['t1', 't2']), - ('T1,T2', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("", ["t1", "t2"]), + ("t1", ["t1"]), + ("t2", ["t2"]), + ("t1,t2", ["t1", "t2"]), + ("T1,T2", ["t1", "t2"]), + ], +) def test_filter_anonymous( - verify_unpaged, tag_factory, input, expected_tag_names): - db.session.add(tag_factory(names=['t1'])) - db.session.add(tag_factory(names=['t2'])) + verify_unpaged, tag_factory, input, expected_tag_names +): + db.session.add(tag_factory(names=["t1"])) + db.session.add(tag_factory(names=["t2"])) db.session.flush() verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('db_driver,input,expected_tag_names', [ - (None, ',', None), - (None, 't1,', None), - (None, 't1,t2', ['t1', 't2']), - (None, 't1\\,', []), - (None, 'asd..asd', None), - (None, 'asd\\..asd', []), - (None, 'asd.\\.asd', []), - (None, 'asd\\.\\.asd', []), - (None, '-', None), - (None, '\\-', ['-']), - (None, '--', [ - 't1', 't2', '*', '*asd*', ':', 'asd:asd', '\\', '\\asd', '-asd', - ]), - (None, '\\--', []), - (None, '-\\-', [ - 't1', 't2', '*', '*asd*', ':', 'asd:asd', '\\', '\\asd', '-asd', - ]), - (None, '-*', []), - (None, '\\-*', ['-', '-asd']), - (None, ':', None), - (None, '\\:', [':']), - (None, '\\:asd', []), - (None, '*\\:*', [':', 'asd:asd']), - (None, 'asd:asd', None), - (None, 'asd\\:asd', ['asd:asd']), - (None, '*', [ - 't1', 't2', '*', '*asd*', ':', 'asd:asd', '\\', '\\asd', '-', '-asd' - ]), - (None, '\\*', ['*']), - (None, '\\', None), - (None, '\\asd', None), - ('psycopg2', '\\\\', ['\\']), - ('psycopg2', '\\\\asd', ['\\asd']), -]) -def test_escaping( - executor, tag_factory, input, expected_tag_names, db_driver): - db.session.add_all([ - tag_factory(names=['t1']), - tag_factory(names=['t2']), - tag_factory(names=['*']), - tag_factory(names=['*asd*']), - tag_factory(names=[':']), - tag_factory(names=['asd:asd']), - tag_factory(names=['\\']), - tag_factory(names=['\\asd']), - tag_factory(names=['-']), - tag_factory(names=['-asd']) - ]) +@pytest.mark.parametrize( + "db_driver,input,expected_tag_names", + [ + (None, ",", None), + (None, "t1,", None), + (None, "t1,t2", ["t1", "t2"]), + (None, "t1\\,", []), + (None, "asd..asd", None), + (None, "asd\\..asd", []), + (None, "asd.\\.asd", []), + (None, "asd\\.\\.asd", []), + (None, "-", None), + (None, "\\-", ["-"]), + ( + None, + "--", + [ + "t1", + "t2", + "*", + "*asd*", + ":", + "asd:asd", + "\\", + "\\asd", + "-asd", + ], + ), + (None, "\\--", []), + ( + None, + "-\\-", + [ + "t1", + "t2", + "*", + "*asd*", + ":", + "asd:asd", + "\\", + "\\asd", + "-asd", + ], + ), + (None, "-*", []), + (None, "\\-*", ["-", "-asd"]), + (None, ":", None), + (None, "\\:", [":"]), + (None, "\\:asd", []), + (None, "*\\:*", [":", "asd:asd"]), + (None, "asd:asd", None), + (None, "asd\\:asd", ["asd:asd"]), + ( + None, + "*", + [ + "t1", + "t2", + "*", + "*asd*", + ":", + "asd:asd", + "\\", + "\\asd", + "-", + "-asd", + ], + ), + (None, "\\*", ["*"]), + (None, "\\", None), + (None, "\\asd", None), + ("psycopg2", "\\\\", ["\\"]), + ("psycopg2", "\\\\asd", ["\\asd"]), + ], +) +def test_escaping(executor, tag_factory, input, expected_tag_names, db_driver): + db.session.add_all( + [ + tag_factory(names=["t1"]), + tag_factory(names=["t2"]), + tag_factory(names=["*"]), + tag_factory(names=["*asd*"]), + tag_factory(names=[":"]), + tag_factory(names=["asd:asd"]), + tag_factory(names=["\\"]), + tag_factory(names=["\\asd"]), + tag_factory(names=["-"]), + tag_factory(names=["-asd"]), + ] + ) db.session.flush() - if db_driver: - if db.sessionmaker.kw['bind'].driver != db_driver: - pytest.xfail() + if db_driver and db.session.get_bind().driver != db_driver: + pytest.xfail() if expected_tag_names is None: with pytest.raises(errors.SearchError): executor.execute(input, offset=0, limit=100) else: actual_count, actual_tags = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_tag_names = [u.names[0].name for u in actual_tags] assert actual_count == len(expected_tag_names) assert sorted(actual_tag_names) == sorted(expected_tag_names) def test_filter_anonymous_starting_with_colon(verify_unpaged, tag_factory): - db.session.add(tag_factory(names=[':t'])) + db.session.add(tag_factory(names=[":t"])) db.session.flush() with pytest.raises(errors.SearchError): - verify_unpaged(':t', [':t']) - verify_unpaged('\\:t', [':t']) + verify_unpaged(":t", [":t"]) + verify_unpaged("\\:t", [":t"]) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('name:tag1', ['tag1']), - ('name:tag2', ['tag2']), - ('name:none', []), - ('name:', []), - ('name:*1', ['tag1']), - ('name:*2', ['tag2']), - ('name:*', ['tag1', 'tag2', 'tag3', 'tag4']), - ('name:t*', ['tag1', 'tag2', 'tag3', 'tag4']), - ('name:*a*', ['tag1', 'tag2', 'tag3', 'tag4']), - ('name:*!*', []), - ('name:!*', []), - ('name:*!', []), - ('-name:tag1', ['tag2', 'tag3', 'tag4']), - ('-name:tag2', ['tag1', 'tag3', 'tag4']), - ('name:tag1,tag2', ['tag1', 'tag2']), - ('-name:tag1,tag3', ['tag2', 'tag4']), - ('name:tag4', ['tag4']), - ('name:tag5', ['tag4']), - ('name:tag4,tag5', ['tag4']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("name:tag1", ["tag1"]), + ("name:tag2", ["tag2"]), + ("name:none", []), + ("name:", []), + ("name:*1", ["tag1"]), + ("name:*2", ["tag2"]), + ("name:*", ["tag1", "tag2", "tag3", "tag4"]), + ("name:t*", ["tag1", "tag2", "tag3", "tag4"]), + ("name:*a*", ["tag1", "tag2", "tag3", "tag4"]), + ("name:*!*", []), + ("name:!*", []), + ("name:*!", []), + ("-name:tag1", ["tag2", "tag3", "tag4"]), + ("-name:tag2", ["tag1", "tag3", "tag4"]), + ("name:tag1,tag2", ["tag1", "tag2"]), + ("-name:tag1,tag3", ["tag2", "tag4"]), + ("name:tag4", ["tag4"]), + ("name:tag5", ["tag4"]), + ("name:tag4,tag5", ["tag4"]), + ], +) def test_filter_by_name( - verify_unpaged, tag_factory, input, expected_tag_names): - db.session.add(tag_factory(names=['tag1'])) - db.session.add(tag_factory(names=['tag2'])) - db.session.add(tag_factory(names=['tag3'])) - db.session.add(tag_factory(names=['tag4', 'tag5', 'tag6'])) + verify_unpaged, tag_factory, input, expected_tag_names +): + db.session.add(tag_factory(names=["tag1"])) + db.session.add(tag_factory(names=["tag2"])) + db.session.add(tag_factory(names=["tag3"])) + db.session.add(tag_factory(names=["tag4", "tag5", "tag6"])) db.session.flush() verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('category:cat1', ['t1', 't2']), - ('category:cat2', ['t3']), - ('category:cat1,cat2', ['t1', 't2', 't3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("category:cat1", ["t1", "t2"]), + ("category:cat2", ["t3"]), + ("category:cat1,cat2", ["t1", "t2", "t3"]), + ], +) def test_filter_by_category( - verify_unpaged, - tag_factory, - tag_category_factory, - input, - expected_tag_names): - cat1 = tag_category_factory(name='cat1') - cat2 = tag_category_factory(name='cat2') - tag1 = tag_factory(names=['t1'], category=cat1) - tag2 = tag_factory(names=['t2'], category=cat1) - tag3 = tag_factory(names=['t3'], category=cat2) + verify_unpaged, + tag_factory, + tag_category_factory, + input, + expected_tag_names, +): + cat1 = tag_category_factory(name="cat1") + cat2 = tag_category_factory(name="cat2") + tag1 = tag_factory(names=["t1"], category=cat1) + tag2 = tag_factory(names=["t2"], category=cat1) + tag3 = tag_factory(names=["t3"], category=cat2) db.session.add_all([tag1, tag2, tag3]) db.session.flush() verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('creation-time:2014', ['t1', 't2']), - ('creation-date:2014', ['t1', 't2']), - ('-creation-time:2014', ['t3']), - ('-creation-date:2014', ['t3']), - ('creation-time:2014..2014-06', ['t1', 't2']), - ('creation-time:2014-06..2015-01-01', ['t2', 't3']), - ('creation-time:2014-06..', ['t2', 't3']), - ('creation-time:..2014-06', ['t1', 't2']), - ('-creation-time:2014..2014-06', ['t3']), - ('-creation-time:2014-06..2015-01-01', ['t1']), - ('creation-date:2014..2014-06', ['t1', 't2']), - ('creation-date:2014-06..2015-01-01', ['t2', 't3']), - ('creation-date:2014-06..', ['t2', 't3']), - ('creation-date:..2014-06', ['t1', 't2']), - ('-creation-date:2014..2014-06', ['t3']), - ('-creation-date:2014-06..2015-01-01', ['t1']), - ('creation-time:2014-01,2015', ['t1', 't3']), - ('creation-date:2014-01,2015', ['t1', 't3']), - ('-creation-time:2014-01,2015', ['t2']), - ('-creation-date:2014-01,2015', ['t2']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("creation-time:2014", ["t1", "t2"]), + ("creation-date:2014", ["t1", "t2"]), + ("-creation-time:2014", ["t3"]), + ("-creation-date:2014", ["t3"]), + ("creation-time:2014..2014-06", ["t1", "t2"]), + ("creation-time:2014-06..2015-01-01", ["t2", "t3"]), + ("creation-time:2014-06..", ["t2", "t3"]), + ("creation-time:..2014-06", ["t1", "t2"]), + ("-creation-time:2014..2014-06", ["t3"]), + ("-creation-time:2014-06..2015-01-01", ["t1"]), + ("creation-date:2014..2014-06", ["t1", "t2"]), + ("creation-date:2014-06..2015-01-01", ["t2", "t3"]), + ("creation-date:2014-06..", ["t2", "t3"]), + ("creation-date:..2014-06", ["t1", "t2"]), + ("-creation-date:2014..2014-06", ["t3"]), + ("-creation-date:2014-06..2015-01-01", ["t1"]), + ("creation-time:2014-01,2015", ["t1", "t3"]), + ("creation-date:2014-01,2015", ["t1", "t3"]), + ("-creation-time:2014-01,2015", ["t2"]), + ("-creation-date:2014-01,2015", ["t2"]), + ], +) def test_filter_by_creation_time( - verify_unpaged, tag_factory, input, expected_tag_names): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) - tag3 = tag_factory(names=['t3']) + verify_unpaged, tag_factory, input, expected_tag_names +): + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) + tag3 = tag_factory(names=["t3"]) tag1.creation_time = datetime(2014, 1, 1) tag2.creation_time = datetime(2014, 6, 1) tag3.creation_time = datetime(2015, 1, 1) @@ -195,17 +255,21 @@ def test_filter_by_creation_time( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('last-edit-date:2014', ['t1', 't3']), - ('last-edit-time:2014', ['t1', 't3']), - ('edit-date:2014', ['t1', 't3']), - ('edit-time:2014', ['t1', 't3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("last-edit-date:2014", ["t1", "t3"]), + ("last-edit-time:2014", ["t1", "t3"]), + ("edit-date:2014", ["t1", "t3"]), + ("edit-time:2014", ["t1", "t3"]), + ], +) def test_filter_by_edit_time( - verify_unpaged, tag_factory, input, expected_tag_names): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) - tag3 = tag_factory(names=['t3']) + verify_unpaged, tag_factory, input, expected_tag_names +): + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) + tag3 = tag_factory(names=["t3"]) tag1.last_edit_time = datetime(2014, 1, 1) tag2.last_edit_time = datetime(2015, 1, 1) tag3.last_edit_time = datetime(2014, 1, 1) @@ -214,24 +278,28 @@ def test_filter_by_edit_time( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('post-count:2', ['t1']), - ('post-count:1', ['t2']), - ('post-count:1..', ['t1', 't2']), - ('post-count-min:1', ['t1', 't2']), - ('post-count:..1', ['t2']), - ('post-count-max:1', ['t2']), - ('usage-count:2', ['t1']), - ('usage-count:1', ['t2']), - ('usages:2', ['t1']), - ('usages:1', ['t2']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("post-count:2", ["t1"]), + ("post-count:1", ["t2"]), + ("post-count:1..", ["t1", "t2"]), + ("post-count-min:1", ["t1", "t2"]), + ("post-count:..1", ["t2"]), + ("post-count-max:1", ["t2"]), + ("usage-count:2", ["t1"]), + ("usage-count:1", ["t2"]), + ("usages:2", ["t1"]), + ("usages:1", ["t2"]), + ], +) def test_filter_by_post_count( - verify_unpaged, tag_factory, post_factory, input, expected_tag_names): + verify_unpaged, tag_factory, post_factory, input, expected_tag_names +): post1 = post_factory() post2 = post_factory() - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([post1, post2, tag1, tag2]) post1.tags.append(tag1) post1.tags.append(tag2) @@ -240,31 +308,38 @@ def test_filter_by_post_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input', [ - 'post-count:..', - 'post-count:asd', - 'post-count:asd,1', - 'post-count:1,asd', - 'post-count:asd..1', - 'post-count:1..asd', -]) +@pytest.mark.parametrize( + "input", + [ + "post-count:..", + "post-count:asd", + "post-count:asd,1", + "post-count:1,asd", + "post-count:asd..1", + "post-count:1..asd", + ], +) def test_filter_by_invalid_input(executor, input): with pytest.raises(errors.SearchError): executor.execute(input, offset=0, limit=100) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('suggestion-count:2', ['t1']), - ('suggestion-count:1', ['t2']), - ('suggestion-count:0', ['sug1', 'sug2', 'sug3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("suggestion-count:2", ["t1"]), + ("suggestion-count:1", ["t2"]), + ("suggestion-count:0", ["sug1", "sug2", "sug3"]), + ], +) def test_filter_by_suggestion_count( - verify_unpaged, tag_factory, input, expected_tag_names): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - sug3 = tag_factory(names=['sug3']) - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + verify_unpaged, tag_factory, input, expected_tag_names +): + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + sug3 = tag_factory(names=["sug3"]) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([sug1, sug3, tag2, sug2, tag1]) tag1.suggestions.append(sug1) tag1.suggestions.append(sug2) @@ -273,18 +348,22 @@ def test_filter_by_suggestion_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('implication-count:2', ['t1']), - ('implication-count:1', ['t2']), - ('implication-count:0', ['sug1', 'sug2', 'sug3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("implication-count:2", ["t1"]), + ("implication-count:1", ["t2"]), + ("implication-count:0", ["sug1", "sug2", "sug3"]), + ], +) def test_filter_by_implication_count( - verify_unpaged, tag_factory, input, expected_tag_names): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - sug3 = tag_factory(names=['sug3']) - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + verify_unpaged, tag_factory, input, expected_tag_names +): + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + sug3 = tag_factory(names=["sug3"]) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([sug1, sug3, tag2, sug2, tag1]) tag1.implications.append(sug1) tag1.implications.append(sug2) @@ -293,32 +372,39 @@ def test_filter_by_implication_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('', ['t1', 't2']), - ('sort:name', ['t1', 't2']), - ('-sort:name', ['t2', 't1']), - ('sort:name,asc', ['t1', 't2']), - ('sort:name,desc', ['t2', 't1']), - ('-sort:name,asc', ['t2', 't1']), - ('-sort:name,desc', ['t1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("", ["t1", "t2"]), + ("sort:name", ["t1", "t2"]), + ("-sort:name", ["t2", "t1"]), + ("sort:name,asc", ["t1", "t2"]), + ("sort:name,desc", ["t2", "t1"]), + ("-sort:name,asc", ["t2", "t1"]), + ("-sort:name,desc", ["t1", "t2"]), + ], +) def test_sort_by_name(verify_unpaged, tag_factory, input, expected_tag_names): - db.session.add(tag_factory(names=['t2'])) - db.session.add(tag_factory(names=['t1'])) + db.session.add(tag_factory(names=["t2"])) + db.session.add(tag_factory(names=["t1"])) db.session.flush() verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('', ['t1', 't2', 't3']), - ('sort:creation-date', ['t3', 't2', 't1']), - ('sort:creation-time', ['t3', 't2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("", ["t1", "t2", "t3"]), + ("sort:creation-date", ["t3", "t2", "t1"]), + ("sort:creation-time", ["t3", "t2", "t1"]), + ], +) def test_sort_by_creation_time( - verify_unpaged, tag_factory, input, expected_tag_names): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) - tag3 = tag_factory(names=['t3']) + verify_unpaged, tag_factory, input, expected_tag_names +): + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) + tag3 = tag_factory(names=["t3"]) tag1.creation_time = datetime(1991, 1, 1) tag2.creation_time = datetime(1991, 1, 2) tag3.creation_time = datetime(1991, 1, 3) @@ -327,18 +413,22 @@ def test_sort_by_creation_time( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('', ['t1', 't2', 't3']), - ('sort:last-edit-date', ['t3', 't2', 't1']), - ('sort:last-edit-time', ['t3', 't2', 't1']), - ('sort:edit-date', ['t3', 't2', 't1']), - ('sort:edit-time', ['t3', 't2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("", ["t1", "t2", "t3"]), + ("sort:last-edit-date", ["t3", "t2", "t1"]), + ("sort:last-edit-time", ["t3", "t2", "t1"]), + ("sort:edit-date", ["t3", "t2", "t1"]), + ("sort:edit-time", ["t3", "t2", "t1"]), + ], +) def test_sort_by_last_edit_time( - verify_unpaged, tag_factory, input, expected_tag_names): - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) - tag3 = tag_factory(names=['t3']) + verify_unpaged, tag_factory, input, expected_tag_names +): + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) + tag3 = tag_factory(names=["t3"]) tag1.last_edit_time = datetime(1991, 1, 1) tag2.last_edit_time = datetime(1991, 1, 2) tag3.last_edit_time = datetime(1991, 1, 3) @@ -347,17 +437,21 @@ def test_sort_by_last_edit_time( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('sort:post-count', ['t2', 't1']), - ('sort:usage-count', ['t2', 't1']), - ('sort:usages', ['t2', 't1']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("sort:post-count", ["t2", "t1"]), + ("sort:usage-count", ["t2", "t1"]), + ("sort:usages", ["t2", "t1"]), + ], +) def test_sort_by_post_count( - verify_unpaged, tag_factory, post_factory, input, expected_tag_names): + verify_unpaged, tag_factory, post_factory, input, expected_tag_names +): post1 = post_factory() post2 = post_factory() - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([post1, post2, tag1, tag2]) post1.tags.append(tag1) post1.tags.append(tag2) @@ -366,16 +460,20 @@ def test_sort_by_post_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('sort:suggestion-count', ['t1', 't2', 'sug1', 'sug2', 'sug3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("sort:suggestion-count", ["t1", "t2", "sug1", "sug2", "sug3"]), + ], +) def test_sort_by_suggestion_count( - verify_unpaged, tag_factory, input, expected_tag_names): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - sug3 = tag_factory(names=['sug3']) - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + verify_unpaged, tag_factory, input, expected_tag_names +): + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + sug3 = tag_factory(names=["sug3"]) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([sug1, sug3, tag2, sug2, tag1]) tag1.suggestions.append(sug1) tag1.suggestions.append(sug2) @@ -384,16 +482,20 @@ def test_sort_by_suggestion_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('sort:implication-count', ['t1', 't2', 'sug1', 'sug2', 'sug3']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("sort:implication-count", ["t1", "t2", "sug1", "sug2", "sug3"]), + ], +) def test_sort_by_implication_count( - verify_unpaged, tag_factory, input, expected_tag_names): - sug1 = tag_factory(names=['sug1']) - sug2 = tag_factory(names=['sug2']) - sug3 = tag_factory(names=['sug3']) - tag1 = tag_factory(names=['t1']) - tag2 = tag_factory(names=['t2']) + verify_unpaged, tag_factory, input, expected_tag_names +): + sug1 = tag_factory(names=["sug1"]) + sug2 = tag_factory(names=["sug2"]) + sug3 = tag_factory(names=["sug3"]) + tag1 = tag_factory(names=["t1"]) + tag2 = tag_factory(names=["t2"]) db.session.add_all([sug1, sug3, tag2, sug2, tag1]) tag1.implications.append(sug1) tag1.implications.append(sug2) @@ -402,20 +504,24 @@ def test_sort_by_implication_count( verify_unpaged(input, expected_tag_names) -@pytest.mark.parametrize('input,expected_tag_names', [ - ('sort:category', ['t3', 't1', 't2']), -]) +@pytest.mark.parametrize( + "input,expected_tag_names", + [ + ("sort:category", ["t3", "t1", "t2"]), + ], +) def test_sort_by_category( - verify_unpaged, - tag_factory, - tag_category_factory, - input, - expected_tag_names): - cat1 = tag_category_factory(name='cat1') - cat2 = tag_category_factory(name='cat2') - tag1 = tag_factory(names=['t1'], category=cat2) - tag2 = tag_factory(names=['t2'], category=cat2) - tag3 = tag_factory(names=['t3'], category=cat1) + verify_unpaged, + tag_factory, + tag_category_factory, + input, + expected_tag_names, +): + cat1 = tag_category_factory(name="cat1") + cat2 = tag_category_factory(name="cat2") + tag1 = tag_factory(names=["t1"], category=cat2) + tag2 = tag_factory(names=["t2"], category=cat2) + tag3 = tag_factory(names=["t3"], category=cat1) db.session.add_all([tag1, tag2, tag3]) db.session.flush() verify_unpaged(input, expected_tag_names) diff --git a/server/szurubooru/tests/search/configs/test_user_search_config.py b/server/szurubooru/tests/search/configs/test_user_search_config.py index c4d9402..485ab1c 100644 --- a/server/szurubooru/tests/search/configs/test_user_search_config.py +++ b/server/szurubooru/tests/search/configs/test_user_search_config.py @@ -1,6 +1,7 @@ -# pylint: disable=redefined-outer-name from datetime import datetime + import pytest + from szurubooru import db, errors, search @@ -13,42 +14,48 @@ def executor(): def verify_unpaged(executor): def verify(input, expected_user_names): actual_count, actual_users = executor.execute( - input, offset=0, limit=100) + input, offset=0, limit=100 + ) actual_user_names = [u.name for u in actual_users] assert actual_count == len(expected_user_names) assert actual_user_names == expected_user_names + return verify -@pytest.mark.parametrize('input,expected_user_names', [ - ('creation-time:2014', ['u1', 'u2']), - ('creation-date:2014', ['u1', 'u2']), - ('-creation-time:2014', ['u3']), - ('-creation-date:2014', ['u3']), - ('creation-time:2014..2014-06', ['u1', 'u2']), - ('creation-time:2014-06..2015-01-01', ['u2', 'u3']), - ('creation-time:2014-06..', ['u2', 'u3']), - ('creation-time:..2014-06', ['u1', 'u2']), - ('creation-time-min:2014-06', ['u2', 'u3']), - ('creation-time-max:2014-06', ['u1', 'u2']), - ('-creation-time:2014..2014-06', ['u3']), - ('-creation-time:2014-06..2015-01-01', ['u1']), - ('creation-date:2014..2014-06', ['u1', 'u2']), - ('creation-date:2014-06..2015-01-01', ['u2', 'u3']), - ('creation-date:2014-06..', ['u2', 'u3']), - ('creation-date:..2014-06', ['u1', 'u2']), - ('-creation-date:2014..2014-06', ['u3']), - ('-creation-date:2014-06..2015-01-01', ['u1']), - ('creation-time:2014-01,2015', ['u1', 'u3']), - ('creation-date:2014-01,2015', ['u1', 'u3']), - ('-creation-time:2014-01,2015', ['u2']), - ('-creation-date:2014-01,2015', ['u2']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("creation-time:2014", ["u1", "u2"]), + ("creation-date:2014", ["u1", "u2"]), + ("-creation-time:2014", ["u3"]), + ("-creation-date:2014", ["u3"]), + ("creation-time:2014..2014-06", ["u1", "u2"]), + ("creation-time:2014-06..2015-01-01", ["u2", "u3"]), + ("creation-time:2014-06..", ["u2", "u3"]), + ("creation-time:..2014-06", ["u1", "u2"]), + ("creation-time-min:2014-06", ["u2", "u3"]), + ("creation-time-max:2014-06", ["u1", "u2"]), + ("-creation-time:2014..2014-06", ["u3"]), + ("-creation-time:2014-06..2015-01-01", ["u1"]), + ("creation-date:2014..2014-06", ["u1", "u2"]), + ("creation-date:2014-06..2015-01-01", ["u2", "u3"]), + ("creation-date:2014-06..", ["u2", "u3"]), + ("creation-date:..2014-06", ["u1", "u2"]), + ("-creation-date:2014..2014-06", ["u3"]), + ("-creation-date:2014-06..2015-01-01", ["u1"]), + ("creation-time:2014-01,2015", ["u1", "u3"]), + ("creation-date:2014-01,2015", ["u1", "u3"]), + ("-creation-time:2014-01,2015", ["u2"]), + ("-creation-date:2014-01,2015", ["u2"]), + ], +) def test_filter_by_creation_time( - verify_unpaged, input, expected_user_names, user_factory): - user1 = user_factory(name='u1') - user2 = user_factory(name='u2') - user3 = user_factory(name='u3') + verify_unpaged, input, expected_user_names, user_factory +): + user1 = user_factory(name="u1") + user2 = user_factory(name="u2") + user3 = user_factory(name="u3") user1.creation_time = datetime(2014, 1, 1) user2.creation_time = datetime(2014, 6, 1) user3.creation_time = datetime(2015, 1, 1) @@ -57,59 +64,67 @@ def test_filter_by_creation_time( verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('name:user1', ['user1']), - ('name:user2', ['user2']), - ('name:none', []), - ('name:', []), - ('name:*1', ['user1']), - ('name:*2', ['user2']), - ('name:*', ['user1', 'user2', 'user3']), - ('name:u*', ['user1', 'user2', 'user3']), - ('name:*ser*', ['user1', 'user2', 'user3']), - ('name:*zer*', []), - ('name:zer*', []), - ('name:*zer', []), - ('-name:user1', ['user2', 'user3']), - ('-name:user2', ['user1', 'user3']), - ('name:user1,user2', ['user1', 'user2']), - ('-name:user1,user3', ['user2']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("name:user1", ["user1"]), + ("name:user2", ["user2"]), + ("name:none", []), + ("name:", []), + ("name:*1", ["user1"]), + ("name:*2", ["user2"]), + ("name:*", ["user1", "user2", "user3"]), + ("name:u*", ["user1", "user2", "user3"]), + ("name:*ser*", ["user1", "user2", "user3"]), + ("name:*zer*", []), + ("name:zer*", []), + ("name:*zer", []), + ("-name:user1", ["user2", "user3"]), + ("-name:user2", ["user1", "user3"]), + ("name:user1,user2", ["user1", "user2"]), + ("-name:user1,user3", ["user2"]), + ], +) def test_filter_by_name( - verify_unpaged, input, expected_user_names, user_factory): - db.session.add(user_factory(name='user1')) - db.session.add(user_factory(name='user2')) - db.session.add(user_factory(name='user3')) + verify_unpaged, input, expected_user_names, user_factory +): + db.session.add(user_factory(name="user1")) + db.session.add(user_factory(name="user2")) + db.session.add(user_factory(name="user3")) db.session.flush() verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('name:u1', ['u1']), - ('name:u2*', ['u2..']), - ('name:u1,u3..x', ['u1', 'u3..x']), - ('name:u2..', None), - ('name:*..*', None), - ('name:u3..x', None), - ('name:*..x', None), - ('name:u2\\..', ['u2..']), - ('name:*\\..*', ['u2..', 'u3..x']), - ('name:u3\\..x', ['u3..x']), - ('name:*\\..x', ['u3..x']), - ('name:u2.\\.', ['u2..']), - ('name:*.\\.*', ['u2..', 'u3..x']), - ('name:u3.\\.x', ['u3..x']), - ('name:*.\\.x', ['u3..x']), - ('name:u2\\.\\.', ['u2..']), - ('name:*\\.\\.*', ['u2..', 'u3..x']), - ('name:u3\\.\\.x', ['u3..x']), - ('name:*\\.\\.x', ['u3..x']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("name:u1", ["u1"]), + ("name:u2*", ["u2.."]), + ("name:u1,u3..x", ["u1", "u3..x"]), + ("name:u2..", None), + ("name:*..*", None), + ("name:u3..x", None), + ("name:*..x", None), + ("name:u2\\..", ["u2.."]), + ("name:*\\..*", ["u2..", "u3..x"]), + ("name:u3\\..x", ["u3..x"]), + ("name:*\\..x", ["u3..x"]), + ("name:u2.\\.", ["u2.."]), + ("name:*.\\.*", ["u2..", "u3..x"]), + ("name:u3.\\.x", ["u3..x"]), + ("name:*.\\.x", ["u3..x"]), + ("name:u2\\.\\.", ["u2.."]), + ("name:*\\.\\.*", ["u2..", "u3..x"]), + ("name:u3\\.\\.x", ["u3..x"]), + ("name:*\\.\\.x", ["u3..x"]), + ], +) def test_filter_by_name_that_looks_like_range( - verify_unpaged, input, expected_user_names, user_factory): - db.session.add(user_factory(name='u1')) - db.session.add(user_factory(name='u2..')) - db.session.add(user_factory(name='u3..x')) + verify_unpaged, input, expected_user_names, user_factory +): + db.session.add(user_factory(name="u1")) + db.session.add(user_factory(name="u2..")) + db.session.add(user_factory(name="u3..x")) db.session.flush() if not expected_user_names: with pytest.raises(errors.SearchError): @@ -118,30 +133,36 @@ def test_filter_by_name_that_looks_like_range( verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('', ['u1', 'u2']), - ('u1', ['u1']), - ('u2', ['u2']), - ('u1,u2', ['u1', 'u2']), -]) -def test_anonymous( - verify_unpaged, input, expected_user_names, user_factory): - db.session.add(user_factory(name='u1')) - db.session.add(user_factory(name='u2')) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("", ["u1", "u2"]), + ("u1", ["u1"]), + ("u2", ["u2"]), + ("u1,u2", ["u1", "u2"]), + ], +) +def test_anonymous(verify_unpaged, input, expected_user_names, user_factory): + db.session.add(user_factory(name="u1")) + db.session.add(user_factory(name="u2")) db.session.flush() verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('creation-time:2014 u1', ['u1']), - ('creation-time:2014 u2', ['u2']), - ('creation-time:2016 u2', []), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("creation-time:2014 u1", ["u1"]), + ("creation-time:2014 u2", ["u2"]), + ("creation-time:2016 u2", []), + ], +) def test_combining_tokens( - verify_unpaged, input, expected_user_names, user_factory): - user1 = user_factory(name='u1') - user2 = user_factory(name='u2') - user3 = user_factory(name='u3') + verify_unpaged, input, expected_user_names, user_factory +): + user1 = user_factory(name="u1") + user2 = user_factory(name="u2") + user3 = user_factory(name="u3") user1.creation_time = datetime(2014, 1, 1) user2.creation_time = datetime(2014, 6, 1) user3.creation_time = datetime(2015, 1, 1) @@ -151,61 +172,77 @@ def test_combining_tokens( @pytest.mark.parametrize( - 'offset,limit,expected_total_count,expected_user_names', [ - (0, 1, 2, ['u1']), - (1, 1, 2, ['u2']), + "offset,limit,expected_total_count,expected_user_names", + [ + (0, 1, 2, ["u1"]), + (1, 1, 2, ["u2"]), (2, 1, 2, []), (-1, 1, 2, []), - (-1, 2, 2, ['u1']), - (0, 2, 2, ['u1', 'u2']), + (-1, 2, 2, ["u1"]), + (0, 2, 2, ["u1", "u2"]), (3, 1, 2, []), (0, 0, 2, []), - ]) + ], +) def test_paging( - executor, user_factory, offset, limit, - expected_total_count, expected_user_names): - db.session.add(user_factory(name='u1')) - db.session.add(user_factory(name='u2')) + executor, + user_factory, + offset, + limit, + expected_total_count, + expected_user_names, +): + db.session.add(user_factory(name="u1")) + db.session.add(user_factory(name="u2")) db.session.flush() actual_count, actual_users = executor.execute( - '', offset=offset, limit=limit) + "", offset=offset, limit=limit + ) actual_user_names = [u.name for u in actual_users] assert actual_count == expected_total_count assert actual_user_names == expected_user_names -@pytest.mark.parametrize('input,expected_user_names', [ - ('', ['u1', 'u2']), - ('sort:name', ['u1', 'u2']), - ('-sort:name', ['u2', 'u1']), - ('sort:name,asc', ['u1', 'u2']), - ('sort:name,desc', ['u2', 'u1']), - ('-sort:name,asc', ['u2', 'u1']), - ('-sort:name,desc', ['u1', 'u2']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("", ["u1", "u2"]), + ("sort:name", ["u1", "u2"]), + ("-sort:name", ["u2", "u1"]), + ("sort:name,asc", ["u1", "u2"]), + ("sort:name,desc", ["u2", "u1"]), + ("-sort:name,asc", ["u2", "u1"]), + ("-sort:name,desc", ["u1", "u2"]), + ], +) def test_sort_by_name( - verify_unpaged, input, expected_user_names, user_factory): - db.session.add(user_factory(name='u2')) - db.session.add(user_factory(name='u1')) + verify_unpaged, input, expected_user_names, user_factory +): + db.session.add(user_factory(name="u2")) + db.session.add(user_factory(name="u1")) db.session.flush() verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('', ['u1', 'u2', 'u3']), - ('sort:creation-date', ['u3', 'u2', 'u1']), - ('sort:creation-time', ['u3', 'u2', 'u1']), - ('-sort:creation-date', ['u1', 'u2', 'u3']), - ('sort:creation-date,asc', ['u1', 'u2', 'u3']), - ('sort:creation-date,desc', ['u3', 'u2', 'u1']), - ('-sort:creation-date,asc', ['u3', 'u2', 'u1']), - ('-sort:creation-date,desc', ['u1', 'u2', 'u3']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("", ["u1", "u2", "u3"]), + ("sort:creation-date", ["u3", "u2", "u1"]), + ("sort:creation-time", ["u3", "u2", "u1"]), + ("-sort:creation-date", ["u1", "u2", "u3"]), + ("sort:creation-date,asc", ["u1", "u2", "u3"]), + ("sort:creation-date,desc", ["u3", "u2", "u1"]), + ("-sort:creation-date,asc", ["u3", "u2", "u1"]), + ("-sort:creation-date,desc", ["u1", "u2", "u3"]), + ], +) def test_sort_by_creation_time( - verify_unpaged, input, expected_user_names, user_factory): - user1 = user_factory(name='u1') - user2 = user_factory(name='u2') - user3 = user_factory(name='u3') + verify_unpaged, input, expected_user_names, user_factory +): + user1 = user_factory(name="u1") + user2 = user_factory(name="u2") + user3 = user_factory(name="u3") user1.creation_time = datetime(1991, 1, 1) user2.creation_time = datetime(1991, 1, 2) user3.creation_time = datetime(1991, 1, 3) @@ -214,18 +251,22 @@ def test_sort_by_creation_time( verify_unpaged(input, expected_user_names) -@pytest.mark.parametrize('input,expected_user_names', [ - ('', ['u1', 'u2', 'u3']), - ('sort:last-login-date', ['u3', 'u2', 'u1']), - ('sort:last-login-time', ['u3', 'u2', 'u1']), - ('sort:login-date', ['u3', 'u2', 'u1']), - ('sort:login-time', ['u3', 'u2', 'u1']), -]) +@pytest.mark.parametrize( + "input,expected_user_names", + [ + ("", ["u1", "u2", "u3"]), + ("sort:last-login-date", ["u3", "u2", "u1"]), + ("sort:last-login-time", ["u3", "u2", "u1"]), + ("sort:login-date", ["u3", "u2", "u1"]), + ("sort:login-time", ["u3", "u2", "u1"]), + ], +) def test_sort_by_last_login_time( - verify_unpaged, input, expected_user_names, user_factory): - user1 = user_factory(name='u1') - user2 = user_factory(name='u2') - user3 = user_factory(name='u3') + verify_unpaged, input, expected_user_names, user_factory +): + user1 = user_factory(name="u1") + user2 = user_factory(name="u2") + user3 = user_factory(name="u3") user1.last_login_time = datetime(1991, 1, 1) user2.last_login_time = datetime(1991, 1, 2) user3.last_login_time = datetime(1991, 1, 3) @@ -235,39 +276,43 @@ def test_sort_by_last_login_time( def test_random_sort(executor, user_factory): - user1 = user_factory(name='u1') - user2 = user_factory(name='u2') - user3 = user_factory(name='u3') + user1 = user_factory(name="u1") + user2 = user_factory(name="u2") + user3 = user_factory(name="u3") db.session.add_all([user3, user1, user2]) db.session.flush() actual_count, actual_users = executor.execute( - 'sort:random', offset=0, limit=100) + "sort:random", offset=0, limit=100 + ) actual_user_names = [u.name for u in actual_users] assert actual_count == 3 assert len(actual_user_names) == 3 - assert 'u1' in actual_user_names - assert 'u2' in actual_user_names - assert 'u3' in actual_user_names + assert "u1" in actual_user_names + assert "u2" in actual_user_names + assert "u3" in actual_user_names -@pytest.mark.parametrize('input,expected_error', [ - ('creation-date:..', errors.SearchError), - ('creation-date-min:..', errors.ValidationError), - ('creation-date-min:..2014-01-01', errors.ValidationError), - ('creation-date-min:2014-01-01..', errors.ValidationError), - ('creation-date-max:..2014-01-01', errors.ValidationError), - ('creation-date-max:2014-01-01..', errors.ValidationError), - ('creation-date-max:yesterday,today', errors.ValidationError), - ('creation-date:bad..', errors.ValidationError), - ('creation-date:..bad', errors.ValidationError), - ('creation-date:bad..bad', errors.ValidationError), - ('sort:', errors.SearchError), - ('sort:nam', errors.SearchError), - ('sort:name,as', errors.SearchError), - ('sort:name,asc,desc', errors.SearchError), - ('bad:x', errors.SearchError), - ('special:unsupported', errors.SearchError), -]) +@pytest.mark.parametrize( + "input,expected_error", + [ + ("creation-date:..", errors.SearchError), + ("creation-date-min:..", errors.ValidationError), + ("creation-date-min:..2014-01-01", errors.ValidationError), + ("creation-date-min:2014-01-01..", errors.ValidationError), + ("creation-date-max:..2014-01-01", errors.ValidationError), + ("creation-date-max:2014-01-01..", errors.ValidationError), + ("creation-date-max:yesterday,today", errors.ValidationError), + ("creation-date:bad..", errors.ValidationError), + ("creation-date:..bad", errors.ValidationError), + ("creation-date:bad..bad", errors.ValidationError), + ("sort:", errors.SearchError), + ("sort:nam", errors.SearchError), + ("sort:name,as", errors.SearchError), + ("sort:name,asc,desc", errors.SearchError), + ("bad:x", errors.SearchError), + ("special:unsupported", errors.SearchError), + ], +) def test_bad_tokens(executor, input, expected_error): with pytest.raises(expected_error): executor.execute(input, offset=0, limit=100) diff --git a/server/szurubooru/tests/search/test_executor.py b/server/szurubooru/tests/search/test_executor.py index e1b2dac..4530bee 100644 --- a/server/szurubooru/tests/search/test_executor.py +++ b/server/szurubooru/tests/search/test_executor.py @@ -1,23 +1,27 @@ import unittest.mock + import pytest + from szurubooru import search from szurubooru.func import cache def test_retrieving_from_cache(): config = unittest.mock.MagicMock() - with unittest.mock.patch('szurubooru.func.cache.has'), \ - unittest.mock.patch('szurubooru.func.cache.get'): + with unittest.mock.patch("szurubooru.func.cache.has"), unittest.mock.patch( + "szurubooru.func.cache.get" + ): cache.has.side_effect = lambda *args: True executor = search.Executor(config) - executor.execute('test:whatever', 1, 10) + executor.execute("test:whatever", 1, 10) assert cache.get.called def test_putting_equivalent_queries_into_cache(): config = search.configs.PostSearchConfig() - with unittest.mock.patch('szurubooru.func.cache.has'), \ - unittest.mock.patch('szurubooru.func.cache.put'): + with unittest.mock.patch("szurubooru.func.cache.has"), unittest.mock.patch( + "szurubooru.func.cache.put" + ): hashes = [] def appender(key, _value): @@ -26,20 +30,21 @@ def test_putting_equivalent_queries_into_cache(): cache.has.side_effect = lambda *args: False cache.put.side_effect = appender executor = search.Executor(config) - executor.execute('safety:safe test', 1, 10) - executor.execute('safety:safe test', 1, 10) - executor.execute('safety:safe test ', 1, 10) - executor.execute(' safety:safe test', 1, 10) - executor.execute(' SAFETY:safe test', 1, 10) - executor.execute('test safety:safe', 1, 10) + executor.execute("safety:safe test", 1, 10) + executor.execute("safety:safe test", 1, 10) + executor.execute("safety:safe test ", 1, 10) + executor.execute(" safety:safe test", 1, 10) + executor.execute(" SAFETY:safe test", 1, 10) + executor.execute("test safety:safe", 1, 10) assert len(hashes) == 6 assert len(set(hashes)) == 1 def test_putting_non_equivalent_queries_into_cache(): config = search.configs.PostSearchConfig() - with unittest.mock.patch('szurubooru.func.cache.has'), \ - unittest.mock.patch('szurubooru.func.cache.put'): + with unittest.mock.patch("szurubooru.func.cache.has"), unittest.mock.patch( + "szurubooru.func.cache.put" + ): hashes = [] def appender(key, _value): @@ -49,42 +54,42 @@ def test_putting_non_equivalent_queries_into_cache(): cache.put.side_effect = appender executor = search.Executor(config) args = [ - ('', 1, 10), - ('creation-time:2016', 1, 10), - ('creation-time:2015', 1, 10), - ('creation-time:2016-01', 1, 10), - ('creation-time:2016-02', 1, 10), - ('creation-time:2016-01-01', 1, 10), - ('creation-time:2016-01-02', 1, 10), - ('tag-count:1,3', 1, 10), - ('tag-count:1,2', 1, 10), - ('tag-count:1', 1, 10), - ('tag-count:1..3', 1, 10), - ('tag-count:1..4', 1, 10), - ('tag-count:2..3', 1, 10), - ('tag-count:1..', 1, 10), - ('tag-count:2..', 1, 10), - ('tag-count:..3', 1, 10), - ('tag-count:..4', 1, 10), - ('-tag-count:1..3', 1, 10), - ('-tag-count:1..4', 1, 10), - ('-tag-count:2..3', 1, 10), - ('-tag-count:1..', 1, 10), - ('-tag-count:2..', 1, 10), - ('-tag-count:..3', 1, 10), - ('-tag-count:..4', 1, 10), - ('safety:safe', 1, 10), - ('safety:safe', 1, 20), - ('safety:safe', 2, 10), - ('safety:sketchy', 1, 10), - ('safety:safe test', 1, 10), - ('-safety:safe', 1, 10), - ('-safety:safe', 1, 20), - ('-safety:safe', 2, 10), - ('-safety:sketchy', 1, 10), - ('-safety:safe test', 1, 10), - ('safety:safe -test', 1, 10), - ('-test', 1, 10), + ("", 1, 10), + ("creation-time:2016", 1, 10), + ("creation-time:2015", 1, 10), + ("creation-time:2016-01", 1, 10), + ("creation-time:2016-02", 1, 10), + ("creation-time:2016-01-01", 1, 10), + ("creation-time:2016-01-02", 1, 10), + ("tag-count:1,3", 1, 10), + ("tag-count:1,2", 1, 10), + ("tag-count:1", 1, 10), + ("tag-count:1..3", 1, 10), + ("tag-count:1..4", 1, 10), + ("tag-count:2..3", 1, 10), + ("tag-count:1..", 1, 10), + ("tag-count:2..", 1, 10), + ("tag-count:..3", 1, 10), + ("tag-count:..4", 1, 10), + ("-tag-count:1..3", 1, 10), + ("-tag-count:1..4", 1, 10), + ("-tag-count:2..3", 1, 10), + ("-tag-count:1..", 1, 10), + ("-tag-count:2..", 1, 10), + ("-tag-count:..3", 1, 10), + ("-tag-count:..4", 1, 10), + ("safety:safe", 1, 10), + ("safety:safe", 1, 20), + ("safety:safe", 2, 10), + ("safety:sketchy", 1, 10), + ("safety:safe test", 1, 10), + ("-safety:safe", 1, 10), + ("-safety:safe", 1, 20), + ("-safety:safe", 2, 10), + ("-safety:sketchy", 1, 10), + ("-safety:safe test", 1, 10), + ("safety:safe -test", 1, 10), + ("-test", 1, 10), ] for arg in args: executor.execute(*arg) @@ -92,18 +97,22 @@ def test_putting_non_equivalent_queries_into_cache(): assert len(set(hashes)) == len(args) -@pytest.mark.parametrize('input', [ - 'special:fav', - 'special:liked', - 'special:disliked', - '-special:fav', - '-special:liked', - '-special:disliked', -]) +@pytest.mark.parametrize( + "input", + [ + "special:fav", + "special:liked", + "special:disliked", + "-special:fav", + "-special:liked", + "-special:disliked", + ], +) def test_putting_auth_dependent_queries_into_cache(user_factory, input): config = search.configs.PostSearchConfig() - with unittest.mock.patch('szurubooru.func.cache.has'), \ - unittest.mock.patch('szurubooru.func.cache.put'): + with unittest.mock.patch("szurubooru.func.cache.has"), unittest.mock.patch( + "szurubooru.func.cache.put" + ): hashes = [] def appender(key, _value): diff --git a/server/wait-for-es b/server/wait-for-es deleted file mode 100755 index 6855c75..0000000 --- a/server/wait-for-es +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -''' -Docker helper script. Blocks until the ElasticSearch service is ready. -''' -import logging -import time -import elasticsearch -from szurubooru import config, errors - - -def main(): - print('Looking for ElasticSearch connection...') - logging.basicConfig(level=logging.ERROR) - es = elasticsearch.Elasticsearch([{ - 'host': config.config['elasticsearch']['host'], - 'port': config.config['elasticsearch']['port'], - }]) - - TIMEOUT = 30 - DELAY = 0.1 - for _ in range(int(TIMEOUT / DELAY)): - try: - es.cluster.health(wait_for_status='yellow') - print('Connected to ElasticSearch!') - return - except Exception: - time.sleep(DELAY) - pass - raise errors.ThirdPartyError('Error connecting to ElasticSearch') - - -if __name__ == '__main__': - main() |