summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorHunternif <hunternif@gmail.com>2021-08-04 01:56:00 +0100
committerHunternif <hunternif@gmail.com>2021-08-04 01:56:00 +0100
commitca861cdc44ca476cec2949f237bbc7503862ad58 (patch)
tree520f918a9a77432bf283b46b4296812eb4332ffe /server
parentdd03540398fcab11bfc2c3cebed2fb7a78592c16 (diff)
parent59452711668b9f7a7aeea3c57301b22882987c2b (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')
-rw-r--r--server/.dockerignore17
-rw-r--r--server/.flake85
-rw-r--r--server/.pylintrc37
-rw-r--r--server/Dockerfile112
-rw-r--r--server/config.yaml.dist54
-rw-r--r--server/dev-requirements.txt1
-rwxr-xr-xserver/docker-start.sh8
-rwxr-xr-xserver/generate-thumb27
-rwxr-xr-xserver/hooks/build7
-rwxr-xr-xserver/hooks/post_push19
-rwxr-xr-xserver/hooks/test8
-rw-r--r--server/mypy.ini14
-rw-r--r--server/pyproject.toml10
-rw-r--r--server/requirements.txt6
-rw-r--r--server/setup.cfg3
-rwxr-xr-xserver/szuru-admin131
-rw-r--r--server/szurubooru/api/__init__.py14
-rw-r--r--server/szurubooru/api/comment_api.py82
-rw-r--r--server/szurubooru/api/info_api.py66
-rw-r--r--server/szurubooru/api/password_reset_api.py59
-rw-r--r--server/szurubooru/api/pool_api.py113
-rw-r--r--server/szurubooru/api/pool_category_api.py109
-rw-r--r--server/szurubooru/api/post_api.py300
-rw-r--r--server/szurubooru/api/snapshot_api.py14
-rw-r--r--server/szurubooru/api/tag_api.py125
-rw-r--r--server/szurubooru/api/tag_category_api.py94
-rw-r--r--server/szurubooru/api/upload_api.py18
-rw-r--r--server/szurubooru/api/user_api.py101
-rw-r--r--server/szurubooru/api/user_token_api.py96
-rw-r--r--server/szurubooru/config.py80
-rw-r--r--server/szurubooru/db.py15
-rw-r--r--server/szurubooru/errors.py7
-rw-r--r--server/szurubooru/facade.py131
-rw-r--r--server/szurubooru/func/auth.py99
-rw-r--r--server/szurubooru/func/cache.py11
-rw-r--r--server/szurubooru/func/comments.py43
-rw-r--r--server/szurubooru/func/diff.py26
-rw-r--r--server/szurubooru/func/favorites.py11
-rw-r--r--server/szurubooru/func/file_uploads.py12
-rw-r--r--server/szurubooru/func/files.py9
-rw-r--r--server/szurubooru/func/image_hash.py414
-rw-r--r--server/szurubooru/func/images.py337
-rw-r--r--server/szurubooru/func/mailer.py14
-rw-r--r--server/szurubooru/func/mime.py65
-rw-r--r--server/szurubooru/func/net.py94
-rw-r--r--server/szurubooru/func/pool_categories.py207
-rw-r--r--server/szurubooru/func/pools.py337
-rw-r--r--server/szurubooru/func/posts.py668
-rw-r--r--server/szurubooru/func/scores.py22
-rw-r--r--server/szurubooru/func/serialization.py12
-rw-r--r--server/szurubooru/func/snapshots.py141
-rw-r--r--server/szurubooru/func/tag_categories.py113
-rw-r--r--server/szurubooru/func/tags.py162
-rw-r--r--server/szurubooru/func/user_tokens.py70
-rw-r--r--server/szurubooru/func/users.py185
-rw-r--r--server/szurubooru/func/util.py69
-rw-r--r--server/szurubooru/func/versions.py12
-rw-r--r--server/szurubooru/middleware/__init__.py2
-rw-r--r--server/szurubooru/middleware/authenticator.py57
-rw-r--r--server/szurubooru/middleware/cache_purger.py2
-rw-r--r--server/szurubooru/middleware/request_logger.py7
-rw-r--r--server/szurubooru/migrations/env.py52
-rw-r--r--server/szurubooru/migrations/script.py.mako1
-rw-r--r--server/szurubooru/migrations/versions/00cb3a2734db_create_tag_tables.py85
-rw-r--r--server/szurubooru/migrations/versions/02ef5f73f4ab_add_hashes_to_post_file_names.py34
-rw-r--r--server/szurubooru/migrations/versions/055d0e048fb3_add_default_column_to_tag_categories.py20
-rw-r--r--server/szurubooru/migrations/versions/1cd4c7b22846_change_flags_column_to_string.py57
-rw-r--r--server/szurubooru/migrations/versions/1e280b5d5df1_longer_tag_names.py50
-rw-r--r--server/szurubooru/migrations/versions/23abaf4a0a4b_add_mime_type_to_posts.py13
-rw-r--r--server/szurubooru/migrations/versions/336a76ec1338_create_post_tables.py89
-rw-r--r--server/szurubooru/migrations/versions/3c1f0316fa7f_resize_post_columns.py34
-rw-r--r--server/szurubooru/migrations/versions/46cd5229839b_add_snapshot_resource_repr.py15
-rw-r--r--server/szurubooru/migrations/versions/46df355634dc_add_comment_tables.py50
-rw-r--r--server/szurubooru/migrations/versions/49ab4e1139ef_create_indexes.py105
-rw-r--r--server/szurubooru/migrations/versions/4a020f1d271a_rename_snapshot_columns.py59
-rw-r--r--server/szurubooru/migrations/versions/4c526f869323_add_description_to_tags.py13
-rw-r--r--server/szurubooru/migrations/versions/52d6ea6584b8_generate_post_signature_table.py30
-rw-r--r--server/szurubooru/migrations/versions/54de8acc6cef_add_default_pool_category.py60
-rw-r--r--server/szurubooru/migrations/versions/565e01e3cf6d_create_snapshot_table.py31
-rw-r--r--server/szurubooru/migrations/versions/5f00af3004a4_add_default_tag_category.py43
-rw-r--r--server/szurubooru/migrations/versions/6a2f424ec9d2_create_pool_tables.py67
-rw-r--r--server/szurubooru/migrations/versions/7f6baf38c27c_add_versions.py21
-rw-r--r--server/szurubooru/migrations/versions/840b460c5613_fix_foreignkeys.py29
-rw-r--r--server/szurubooru/migrations/versions/84bd402f15f0_change_flags_column_type.py19
-rw-r--r--server/szurubooru/migrations/versions/9587de88a84b_create_aux_post_tables.py80
-rw-r--r--server/szurubooru/migrations/versions/9837fc981ec7_add_order_to_tag_names.py30
-rw-r--r--server/szurubooru/migrations/versions/9ef1a1643c2a_update_user_table_for_hardened_passwords.py75
-rw-r--r--server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py43
-rw-r--r--server/szurubooru/migrations/versions/c867abb456b1_support_large_file_uploads.py26
-rw-r--r--server/szurubooru/migrations/versions/c97dc1bf184a_add_order_column_to_tag_categories.py28
-rw-r--r--server/szurubooru/migrations/versions/e5c1216a8503_create_user_table.py33
-rw-r--r--server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py57
-rw-r--r--server/szurubooru/model/__init__.py22
-rw-r--r--server/szurubooru/model/base.py3
-rw-r--r--server/szurubooru/model/comment.py69
-rw-r--r--server/szurubooru/model/pool.py113
-rw-r--r--server/szurubooru/model/pool_category.py34
-rw-r--r--server/szurubooru/model/post.py351
-rw-r--r--server/szurubooru/model/snapshot.py37
-rw-r--r--server/szurubooru/model/tag.py122
-rw-r--r--server/szurubooru/model/tag_category.py25
-rw-r--r--server/szurubooru/model/user.py123
-rw-r--r--server/szurubooru/model/util.py29
-rw-r--r--server/szurubooru/rest/__init__.py2
-rw-r--r--server/szurubooru/rest/app.py80
-rw-r--r--server/szurubooru/rest/context.py143
-rw-r--r--server/szurubooru/rest/errors.py36
-rw-r--r--server/szurubooru/rest/middleware.py5
-rw-r--r--server/szurubooru/rest/routes.py17
-rw-r--r--server/szurubooru/search/__init__.py2
-rw-r--r--server/szurubooru/search/configs/__init__.py9
-rw-r--r--server/szurubooru/search/configs/base_search_config.py5
-rw-r--r--server/szurubooru/search/configs/comment_search_config.py70
-rw-r--r--server/szurubooru/search/configs/pool_search_config.py111
-rw-r--r--server/szurubooru/search/configs/post_search_config.py702
-rw-r--r--server/szurubooru/search/configs/snapshot_search_config.py33
-rw-r--r--server/szurubooru/search/configs/tag_search_config.py189
-rw-r--r--server/szurubooru/search/configs/user_search_config.py59
-rw-r--r--server/szurubooru/search/configs/util.py129
-rw-r--r--server/szurubooru/search/criteria.py16
-rw-r--r--server/szurubooru/search/executor.py123
-rw-r--r--server/szurubooru/search/parser.py79
-rw-r--r--server/szurubooru/search/query.py16
-rw-r--r--server/szurubooru/search/tokens.py13
-rw-r--r--server/szurubooru/search/typing.py1
-rw-r--r--server/szurubooru/tests/api/test_comment_creating.py69
-rw-r--r--server/szurubooru/tests/api/test_comment_deleting.py46
-rw-r--r--server/szurubooru/tests/api/test_comment_rating.py124
-rw-r--r--server/szurubooru/tests/api/test_comment_retrieving.py77
-rw-r--r--server/szurubooru/tests/api/test_comment_updating.py102
-rw-r--r--server/szurubooru/tests/api/test_info.py138
-rw-r--r--server/szurubooru/tests/api/test_password_reset.py96
-rw-r--r--server/szurubooru/tests/api/test_pool_category_creating.py73
-rw-r--r--server/szurubooru/tests/api/test_pool_category_deleting.py91
-rw-r--r--server/szurubooru/tests/api/test_pool_category_retrieving.py68
-rw-r--r--server/szurubooru/tests/api/test_pool_category_updating.py136
-rw-r--r--server/szurubooru/tests/api/test_pool_creating.py95
-rw-r--r--server/szurubooru/tests/api/test_pool_deleting.py72
-rw-r--r--server/szurubooru/tests/api/test_pool_merging.py118
-rw-r--r--server/szurubooru/tests/api/test_pool_retrieving.py82
-rw-r--r--server/szurubooru/tests/api/test_pool_updating.py160
-rw-r--r--server/szurubooru/tests/api/test_post_creating.py457
-rw-r--r--server/szurubooru/tests/api/test_post_deleting.py34
-rw-r--r--server/szurubooru/tests/api/test_post_favoriting.py107
-rw-r--r--server/szurubooru/tests/api/test_post_featuring.py99
-rw-r--r--server/szurubooru/tests/api/test_post_merging.py82
-rw-r--r--server/szurubooru/tests/api/test_post_rating.py121
-rw-r--r--server/szurubooru/tests/api/test_post_retrieving.py149
-rw-r--r--server/szurubooru/tests/api/test_post_updating.py271
-rw-r--r--server/szurubooru/tests/api/test_snapshot_retrieving.py45
-rw-r--r--server/szurubooru/tests/api/test_tag_category_creating.py58
-rw-r--r--server/szurubooru/tests/api/test_tag_category_deleting.py71
-rw-r--r--server/szurubooru/tests/api/test_tag_category_retrieving.py63
-rw-r--r--server/szurubooru/tests/api/test_tag_category_updating.py131
-rw-r--r--server/szurubooru/tests/api/test_tag_creating.py91
-rw-r--r--server/szurubooru/tests/api/test_tag_deleting.py57
-rw-r--r--server/szurubooru/tests/api/test_tag_merging.py104
-rw-r--r--server/szurubooru/tests/api/test_tag_retrieving.py82
-rw-r--r--server/szurubooru/tests/api/test_tag_siblings_retrieving.py42
-rw-r--r--server/szurubooru/tests/api/test_tag_updating.py234
-rw-r--r--server/szurubooru/tests/api/test_user_creating.py103
-rw-r--r--server/szurubooru/tests/api/test_user_deleting.py51
-rw-r--r--server/szurubooru/tests/api/test_user_retrieving.py75
-rw-r--r--server/szurubooru/tests/api/test_user_token_creating.py28
-rw-r--r--server/szurubooru/tests/api/test_user_token_deleting.py23
-rw-r--r--server/szurubooru/tests/api/test_user_token_retrieving.py32
-rw-r--r--server/szurubooru/tests/api/test_user_token_updating.py46
-rw-r--r--server/szurubooru/tests/api/test_user_updating.py172
-rw-r--r--server/szurubooru/tests/assets/webp.webpbin0 -> 4594 bytes
-rw-r--r--server/szurubooru/tests/conftest.py220
-rw-r--r--server/szurubooru/tests/func/test_auth.py23
-rw-r--r--server/szurubooru/tests/func/test_comments.py47
-rw-r--r--server/szurubooru/tests/func/test_diff.py409
-rw-r--r--server/szurubooru/tests/func/test_image_hash.py47
-rw-r--r--server/szurubooru/tests/func/test_mime.py115
-rw-r--r--server/szurubooru/tests/func/test_net.py177
-rw-r--r--server/szurubooru/tests/func/test_posts.py938
-rw-r--r--server/szurubooru/tests/func/test_snapshots.py201
-rw-r--r--server/szurubooru/tests/func/test_tag_categories.py137
-rw-r--r--server/szurubooru/tests/func/test_tags.py482
-rw-r--r--server/szurubooru/tests/func/test_user_tokens.py103
-rw-r--r--server/szurubooru/tests/func/test_users.py402
-rw-r--r--server/szurubooru/tests/func/test_util.py53
-rw-r--r--server/szurubooru/tests/middleware/test_authenticator.py62
-rw-r--r--server/szurubooru/tests/model/test_comment.py5
-rw-r--r--server/szurubooru/tests/model/test_pool.py97
-rw-r--r--server/szurubooru/tests/model/test_post.py45
-rw-r--r--server/szurubooru/tests/model/test_tag.py58
-rw-r--r--server/szurubooru/tests/model/test_user.py80
-rw-r--r--server/szurubooru/tests/model/test_user_token.py3
-rw-r--r--server/szurubooru/tests/rest/test_context.py123
-rw-r--r--server/szurubooru/tests/search/configs/test_comment_search_config.py205
-rw-r--r--server/szurubooru/tests/search/configs/test_pool_search_config.py433
-rw-r--r--server/szurubooru/tests/search/configs/test_post_search_config.py923
-rw-r--r--server/szurubooru/tests/search/configs/test_tag_search_config.py626
-rw-r--r--server/szurubooru/tests/search/configs/test_user_search_config.py383
-rw-r--r--server/szurubooru/tests/search/test_executor.py127
-rwxr-xr-xserver/wait-for-es33
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
new file mode 100644
index 0000000..d116e1e
--- /dev/null
+++ b/server/szurubooru/tests/assets/webp.webp
Binary files differ
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()