diff --git a/.dockerignore b/.dockerignore index 533c7c0..992be0f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,14 @@ -env/ -htmlcov/ -docs/ -dist/ -build/ +*.egg-info/ +*.sqlite3 +*.gitbundle +.idea/ .git/ -*.egg-info .github/ .pytest_cache/ +__pycache__/ +env/ +env-minimum/ +env-no-wagtail/ +dist/ +build/ +wiki/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 889fd0e..712c480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 4.0.2 (2023-05-27) +- Resolves [#50](https://github.com/beatonma/django-wm/issues/50): broken search field on QuotableAdmin. +- Added tests for admin pages to avoid that sort of thing happening again. +- Minor touch-ups for the admin pages. + - Source and target URL fields are now read-only. + - Added appropriate search fields and list filters for each model. + - `quote` field now uses a textarea widget for comfier editing. + + ## 4.0.1 (2022-12-22) - Added management command `mentions_reverify [filters ...] [--all]` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d63d3df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,79 @@ +FROM python:3.11-alpine AS common +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONBUFFERED=1 + +RUN apk add curl + +WORKDIR /var/www/static + +WORKDIR /tmp/src/ +COPY ./mentions ./mentions +COPY ./tests ./tests +COPY ./pyproject.toml . +COPY ./requirements.txt . +COPY ./setup.cfg . +COPY ./runtests.py . +RUN --mount=type=cache,target=/root/.cache/pip pip install -r /tmp/src/requirements.txt +RUN python /tmp/src/runtests.py + +WORKDIR /project +COPY ./sample-project/requirements.txt /project +RUN --mount=type=cache,target=/root/.cache/pip pip install -r /project/requirements.txt + +# Pass a random CACHEBUST value to ensure data is updated and not taken from cache. +ARG CACHEBUST=0 +RUN echo "CACHEBUST: $CACHEBUST" + +WORKDIR /project + +COPY ./sample-project/docker/entrypoint.sh / + +ENTRYPOINT ["/entrypoint.sh"] + + +################################################################################ +FROM common AS with_celery + +# Install extra dependencies but remove our package - will be mounted in compose +# to allow Django runserver to reload on code changes. +RUN --mount=type=cache,target=/root/.cache/pip pip install -e /tmp/src[celery,test] +RUN pip uninstall -y django-wm +RUN rm -r /tmp/src + +CMD ["python", "manage.py", "sample_app_init"] + + +################################################################################ +FROM common AS with_wagtail + +# Install extra dependencies but remove our package - will be mounted in compose +# to allow Django runserver to reload on code changes. +RUN --mount=type=cache,target=/root/.cache/pip pip install -e /tmp/src[wagtail,test] +RUN pip uninstall -y django-wm +RUN rm -r /tmp/src + +CMD ["python", "manage.py", "wagtail_app_init"] + + +################################################################################ +FROM with_celery AS with_celery_celery + +ENTRYPOINT celery -A sample_project worker -l info + + +################################################################################ +FROM with_celery AS with_celery_cron + +COPY ./sample-project/docker/with-celery/cron-schedule / +RUN crontab /cron-schedule + +ENTRYPOINT crond -l 2 -f + + +################################################################################ +FROM with_wagtail AS with_wagtail_cron + +COPY ./sample-project/docker/with-wagtail/cron-schedule / +RUN crontab /cron-schedule + +ENTRYPOINT crond -l 2 -f diff --git a/docker-compose.upgrade-check.yml b/docker-compose.upgrade-check.yml deleted file mode 100644 index c0965f6..0000000 --- a/docker-compose.upgrade-check.yml +++ /dev/null @@ -1,98 +0,0 @@ -version: "3" - -# Run an instance of `sample-project` using the latest available public release -# of `django-wm`. This will run the `selfcheck` management command which should -# help to catch any packaging errors (e.g. missing templates). - -# Run two servers. -# `with-celery` installs and runs the local django-wm library. -# `upgrade-check` installs the previous public version, creates some data, -# then upgrades to the latest public pre-release to catch incompatibilities. - - -services: - upgrade-check-db: - image: postgres - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/upgrade-check/.env - ports: - - "5432" - - upgrade-check-rabbitmq: - image: rabbitmq:3-alpine - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/upgrade-check/.env - ports: - - "5672" - - upgrade-check-web.org: - build: - dockerfile: ./sample-project/docker/upgrade-check/Dockerfile - context: . - depends_on: - - upgrade-check-db - - upgrade-check-rabbitmq - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/upgrade-check/.env - ports: - - "8003:80" - command: ["bash", "/usr/src/app/docker/wait-for-it.sh", "upgrade-check-db:5432", "--", "bash", "/usr/src/app/docker/upgrade-check/entrypoint.sh"] - - upgrade-check-cron: - build: - dockerfile: ./sample-project/docker/upgrade-check/Dockerfile-cron - context: . - depends_on: - - upgrade-check-db - - upgrade-check-web.org - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/upgrade-check/.env - command: crond -l 2 -f - - - with-celery-db: - image: postgres - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/upgrade-check/.env-other - ports: - - "5432" - - with-celery-rabbitmq: - image: rabbitmq:3-alpine - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/upgrade-check/.env-other - ports: - - "5672" - - with-celery-web.org: - build: - dockerfile: ./sample-project/docker/with-celery/Dockerfile - context: . - depends_on: - - with-celery-db - - with-celery-rabbitmq - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/upgrade-check/.env-other - ports: - - "8001:80" - command: ["bash", "/usr/src/app/docker/wait-for-it.sh", "with-celery-db:5432", "--", "bash", "/usr/src/app/docker/with-celery/entrypoint.sh"] - - - with-celery-cron: - build: - dockerfile: ./sample-project/docker/with-celery/Dockerfile - context: . - depends_on: - - with-celery-db - - with-celery-web.org - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/upgrade-check/.env-other - command: crond -l 2 -f diff --git a/docker-compose.yml b/docker-compose.yml index bbde391..4f6e658 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3" +version: "3.9" # Run two instances of `sample-project` # @@ -11,81 +11,102 @@ version: "3" # - Uses `cron` to handle webmentions, scheduled to run every minute. # - Also uses Wagtail. # -# Each instance can send mentions to the other one. +# - Each instance can send mentions to the other one +# - Each instance has a cron job which has a chance of sending a mention to the +# other each minute. + +x-healthy: &healthy + interval: 10s + timeout: 2s + retries: 3 + start_period: 20s + +x-database: &database + image: postgres + healthcheck: + test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}" ] + <<: *healthy + +x-app-volumes: &app-volumes + volumes: + - ./sample-project:/project + - ./mentions:/project/mentions + +x-with-celery: &with-celery + env_file: + - ./sample-project/docker/.env + - ./sample-project/docker/with-celery/.env + +x-with-wagtail: &with-wagtail + env_file: + - ./sample-project/docker/.env + - ./sample-project/docker/with-wagtail/.env services: # This version uses Celery with-celery-db: - image: postgres - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/with-celery/.env - ports: - - "5432" + <<: [*with-celery, *database] with-celery-rabbitmq: image: rabbitmq:3-alpine - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/with-celery/.env + <<: *with-celery + healthcheck: + test: rabbitmq-diagnostics -q ping + <<: *healthy ports: - "5672" with-celery-web.org: + <<: [*with-celery, *app-volumes] build: - dockerfile: ./sample-project/docker/with-celery/Dockerfile - context: . + target: with_celery depends_on: - - with-celery-db - - with-celery-rabbitmq - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/with-celery/.env + with-celery-db: + condition: service_healthy + healthcheck: + test: "curl --fail http://localhost" ports: - "8001:80" - command: ["bash", "/usr/src/app/docker/wait-for-it.sh", "with-celery-db:5432", "--", "bash", "/usr/src/app/docker/with-celery/entrypoint.sh"] - with-celery-cron: + with-celery-celery: + <<: [*with-celery, *app-volumes] build: - dockerfile: ./sample-project/docker/with-celery/Dockerfile - context: . + target: with_celery_celery depends_on: - - with-celery-db - - with-celery-web.org - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/with-celery/.env - command: crond -l 2 -f - + with-celery-rabbitmq: + condition: service_healthy + with-celery-web.org: + condition: service_healthy + with-celery-cron: + <<: [*with-celery, *app-volumes] + build: + target: with_celery_cron + depends_on: + with-celery-web.org: + condition: service_healthy -# This version uses Wagtail and does not use Celery + # This version uses Wagtail and does not use Celery with-wagtail-db: - image: postgres - env_file: ./sample-project/docker/with-wagtail/.env + <<: [*with-wagtail, *database] with-wagtail-web.org: + <<: [*with-wagtail, *app-volumes] build: - dockerfile: ./sample-project/docker/with-wagtail/Dockerfile - context: . + target: with_wagtail depends_on: - - with-wagtail-db - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/with-wagtail/.env + with-wagtail-db: + condition: service_healthy + healthcheck: + test: "curl --fail http://localhost" ports: - "8002:80" - command: ["bash", "/usr/src/app/docker/wait-for-it.sh", "with-wagtail-db:5432", "--", "bash", "/usr/src/app/docker/with-wagtail/entrypoint.sh"] with-wagtail-cron: + <<: [*with-wagtail, *app-volumes] build: - dockerfile: ./sample-project/docker/with-wagtail/Dockerfile - context: . + target: with_wagtail_cron depends_on: - - with-wagtail-db - - with-wagtail-web.org - env_file: - - ./sample-project/docker/.env - - ./sample-project/docker/with-wagtail/.env - command: crond -l 2 -f + with-wagtail-web.org: + condition: service_healthy diff --git a/mentions/__init__.py b/mentions/__init__.py index d32be17..603a7df 100644 --- a/mentions/__init__.py +++ b/mentions/__init__.py @@ -1,2 +1,2 @@ -__version__ = "4.0.1" +__version__ = "4.0.2" __url__ = "https://github.com/beatonma/django-wm/" diff --git a/mentions/admin.py b/mentions/admin.py index e76288c..511322b 100644 --- a/mentions/admin.py +++ b/mentions/admin.py @@ -1,5 +1,7 @@ from django import forms from django.contrib import admin +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ from mentions.models import ( HCard, @@ -31,41 +33,61 @@ class BaseAdmin(admin.ModelAdmin): save_on_top = True +class ClickableUrlMixin: + def clickable_source_url(self, obj): + return clickable_link(obj.source_url) + + clickable_source_url.short_description = _("source URL") + + def clickable_target_url(self, obj): + return clickable_link(obj.target_url) + + clickable_target_url.short_description = _("target URL") + + +class TextAreaForm(forms.ModelForm): + class Meta: + widgets = { + "quote": forms.Textarea(attrs={"rows": 3}), + "notes": forms.Textarea(attrs={"rows": 3}), + } + + @admin.register(SimpleMention) class QuotableAdmin(BaseAdmin): + form = TextAreaForm + date_hierarchy = "published" list_display = [ "source_url", "target_url", - "hcard", + "get_hcard_name", + "published", ] - search_fields = [ - "source_url", - "target_url", - "hcard", + list_filter = [ + "post_type", ] readonly_fields = [ "target_object", "published", ] - date_hierarchy = "published" + search_fields = [ + "quote", + "source_url", + "target_url", + "hcard__name", + "hcard__homepage", + ] + def get_hcard_name(self, obj): + if obj.hcard: + return obj.hcard.name -class WebmentionModelForm(forms.ModelForm): - class Meta: - model = Webmention - widgets = { - "notes": forms.Textarea(attrs={"rows": 3}), - } - fields = "__all__" + get_hcard_name.short_description = _("h-card name") @admin.register(Webmention) -class WebmentionAdmin(QuotableAdmin): - form = WebmentionModelForm - readonly_fields = QuotableAdmin.readonly_fields + [ - "content_type", - "object_id", - ] +class WebmentionAdmin(ClickableUrlMixin, QuotableAdmin): + form = TextAreaForm actions = [ approve_webmention, disapprove_webmention, @@ -73,17 +95,19 @@ class WebmentionAdmin(QuotableAdmin): list_display = [ "source_url", "target_url", - "published", + "get_hcard_name", "validated", "approved", + "published", "target_object", ] + list_filter = ["validated", "approved"] + QuotableAdmin.list_filter fieldsets = ( ( "Remote source", { "fields": ( - "source_url", + "clickable_source_url", "sent_by", "hcard", "quote", @@ -95,7 +119,7 @@ class WebmentionAdmin(QuotableAdmin): "Local target", { "fields": ( - "target_url", + "clickable_target_url", "content_type", "object_id", "target_object", @@ -114,27 +138,46 @@ class WebmentionAdmin(QuotableAdmin): }, ), ) + readonly_fields = QuotableAdmin.readonly_fields + [ + "content_type", + "object_id", + "clickable_source_url", + "clickable_target_url", + "sent_by", + ] @admin.register(OutgoingWebmentionStatus) -class OutgoingWebmentionStatusAdmin(BaseAdmin): - readonly_fields = [ +class OutgoingWebmentionStatusAdmin(ClickableUrlMixin, BaseAdmin): + date_hierarchy = "created_at" + list_display = [ + "source_url", + "target_url", + "successful", "created_at", + ] + list_filter = [ + "successful", + "is_awaiting_retry", + ] + search_fields = [ + "source_url", + "target_url", + ] + exclude = [ "source_url", "target_url", + ] + readonly_fields = [ + "created_at", + "clickable_source_url", + "clickable_target_url", "target_webmention_endpoint", "status_message", "response_code", "successful", *RETRYABLEMIXIN_FIELDS, ] - list_display = [ - "source_url", - "target_url", - "successful", - "created_at", - ] - date_hierarchy = "created_at" @admin.register(HCard) @@ -145,6 +188,9 @@ class HCardAdmin(BaseAdmin): @admin.register(PendingIncomingWebmention) class PendingIncomingAdmin(BaseAdmin): + list_filter = [ + "is_awaiting_retry", + ] readonly_fields = [ "created_at", "source_url", @@ -152,6 +198,10 @@ class PendingIncomingAdmin(BaseAdmin): "sent_by", *RETRYABLEMIXIN_FIELDS, ] + search_fields = [ + "source_url", + "target_url", + ] @admin.register(PendingOutgoingContent) @@ -161,3 +211,11 @@ class PendingOutgoingAdmin(BaseAdmin): "absolute_url", "text", ] + search_fields = [ + "absolute_url", + "text", + ] + + +def clickable_link(url: str) -> str: + return format_html(f"{url}") diff --git a/mentions/templates/mentions/webmention-dashboard.html b/mentions/templates/mentions/webmention-dashboard.html index 393ed22..a96600f 100644 --- a/mentions/templates/mentions/webmention-dashboard.html +++ b/mentions/templates/mentions/webmention-dashboard.html @@ -42,6 +42,12 @@ background-color: #e4e4e4; } + .icon { + font-size: large; + margin: 0 .5ch; + cursor: default; + } + .item-summary { display: flex; flex-direction: row; @@ -79,12 +85,6 @@ .end { font-size: smaller; } - - .icon { - font-size: large; - margin: 0 .5ch; - cursor: default; - } @@ -230,15 +230,17 @@

Pending outgoing

document.querySelectorAll(".item").forEach(item => { // Click on received webmention summary to show more detail. if (item.querySelector(".item-detail")) { - item.dataset.expanded = defaultExpanded; + item.dataset.expanded = `${defaultExpanded}`; const summary = item.querySelector(".item-summary"); summary.addEventListener("click", () => { const isExpanded = item.dataset.expanded === "true"; - item.dataset.expanded = !isExpanded; + item.dataset.expanded = `${!isExpanded}`; }); } }); + + setTimeout(() => location.reload(), 10_000) diff --git a/runtests.py b/runtests.py index d1bd276..5b2f4c5 100644 --- a/runtests.py +++ b/runtests.py @@ -34,7 +34,7 @@ def parse_clargs(): parser.add_argument( "--makemigrations", nargs=1, - help="Run makemigrations for the given app.", + help="Run makemigrations for the given test-specific app.", ) parsed, remaining_ = parser.parse_known_args() diff --git a/sample-project/docker/.env b/sample-project/docker/.env index 4e4c4fc..aff8480 100644 --- a/sample-project/docker/.env +++ b/sample-project/docker/.env @@ -1,3 +1,8 @@ -DJANGO_SUPERUSER_PASSWORD=badpass! +PASSWORD=badpass! + +DJANGO_SUPERUSER_PASSWORD=$PASSWORD DJANGO_SUPERUSER_USERNAME=super DJANGO_SUPERUSER_EMAIL=fake@beatonma.org +PGUSER=user +POSTGRES_USER=$PGUSER +POSTGRES_PASSWORD=$PASSWORD diff --git a/sample-project/docker/no-celery/entrypoint.sh b/sample-project/docker/entrypoint.sh similarity index 75% rename from sample-project/docker/no-celery/entrypoint.sh rename to sample-project/docker/entrypoint.sh index 8660e85..ad449b4 100644 --- a/sample-project/docker/no-celery/entrypoint.sh +++ b/sample-project/docker/entrypoint.sh @@ -1,3 +1,4 @@ +#!/bin/ash echo "Setting up server..." python manage.py migrate @@ -7,5 +8,8 @@ echo "- Superuser ready" python manage.py collectstatic --noinput echo "- Static files collected" +echo "Running init CMD '$@'" +"$@" + echo "Starting server..." -python manage.py runserver 0.0.0.0:80 --noreload +python manage.py runserver 0.0.0.0:80 diff --git a/sample-project/docker/no-celery/.env b/sample-project/docker/no-celery/.env index eb95cf7..54fb112 100644 --- a/sample-project/docker/no-celery/.env +++ b/sample-project/docker/no-celery/.env @@ -1,7 +1,5 @@ DOMAIN_NAME=no-celery-web.org DB_HOST=no-celery-db POSTGRES_DB=no-celery-db -POSTGRES_USER=user -POSTGRES_PASSWORD=badpassword2! DEFAULT_MENTION_TARGET_DOMAIN=with-celery-web.org AUTOMENTION_URLS=/unreliable/,/unreliable/,/unreliable/,/article/1/,/article/1/,/timeout/ diff --git a/sample-project/docker/no-celery/Dockerfile b/sample-project/docker/no-celery/Dockerfile deleted file mode 100644 index f1df96e..0000000 --- a/sample-project/docker/no-celery/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM python:3-alpine -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONBUFFERED=1 - -RUN apk add --no-cache \ - bash - -WORKDIR /var/www/static/ -WORKDIR /src/ -COPY . /src/ -RUN --mount=type=cache,target=/root/.cache/pip pip install -e .[test] -RUN python runtests.py - -WORKDIR /usr/src/app -COPY ./sample-project . -RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt - -RUN crontab /usr/src/app/docker/no-celery/cron-schedule - -CMD bash /usr/src/app/docker/no-celery/entrypoint.sh diff --git a/sample-project/docker/no-celery/cron-schedule b/sample-project/docker/no-celery/cron-schedule index eaa8dca..98fac29 100644 --- a/sample-project/docker/no-celery/cron-schedule +++ b/sample-project/docker/no-celery/cron-schedule @@ -1,3 +1,8 @@ # Check for pending incoming/outgoing mentions every minute. * * * * * python /usr/src/app/manage.py pending_mentions + +# Maybe send a webmention every minute. * * * * * python /usr/src/app/manage.py automention + +# Show that cron is working. +* * * * * echo "cron heartbeat" diff --git a/sample-project/docker/upgrade-check/.env b/sample-project/docker/upgrade-check/.env deleted file mode 100644 index 3bfb327..0000000 --- a/sample-project/docker/upgrade-check/.env +++ /dev/null @@ -1,11 +0,0 @@ -DOMAIN_NAME=upgrade-check-web.org -DB_HOST=upgrade-check-db -POSTGRES_DB=upgrade-check-db -POSTGRES_USER=user -POSTGRES_PASSWORD=badpassword1! -RABBITMQ_DEFAULT_USER=sample_project -RABBITMQ_DEFAULT_PASS=badpassword -CELERY_BROKER_URL=amqp://sample_project:badpassword@upgrade-check-rabbitmq:5672 -DEFAULT_MENTION_TARGET=/unreliable/ -DEFAULT_MENTION_TARGET_DOMAIN=with-celery-web.org -AUTOMENTION_URLS=/article/1/ diff --git a/sample-project/docker/upgrade-check/.env-other b/sample-project/docker/upgrade-check/.env-other deleted file mode 100644 index 66ee4c5..0000000 --- a/sample-project/docker/upgrade-check/.env-other +++ /dev/null @@ -1,11 +0,0 @@ -DOMAIN_NAME=with-celery-web.org -DB_HOST=with-celery-db -POSTGRES_DB=with-celery-db -POSTGRES_USER=user -POSTGRES_PASSWORD=badpassword1! -RABBITMQ_DEFAULT_USER=sample_project -RABBITMQ_DEFAULT_PASS=badpassword -CELERY_BROKER_URL=amqp://sample_project:badpassword@with-celery-rabbitmq:5672 -DEFAULT_MENTION_TARGET=/unreliable/ -DEFAULT_MENTION_TARGET_DOMAIN=upgrade-check-web.org -AUTOMENTION_URLS=/article/1/ diff --git a/sample-project/docker/upgrade-check/Dockerfile b/sample-project/docker/upgrade-check/Dockerfile deleted file mode 100644 index a94af3b..0000000 --- a/sample-project/docker/upgrade-check/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM python:3-alpine -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONBUFFERED=1 - -RUN apk add --no-cache \ - bash - -WORKDIR /var/www/static/ - -WORKDIR /usr/src/app -COPY ./sample-project . -RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt - -CMD bash /usr/src/app/docker/upgrade-check/entrypoint.sh diff --git a/sample-project/docker/upgrade-check/Dockerfile-cron b/sample-project/docker/upgrade-check/Dockerfile-cron deleted file mode 100644 index 6035a06..0000000 --- a/sample-project/docker/upgrade-check/Dockerfile-cron +++ /dev/null @@ -1,13 +0,0 @@ -FROM python:3-alpine -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONBUFFERED=1 - -RUN apk add --no-cache \ - bash - -WORKDIR /usr/src/app -COPY ./sample-project . -RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt -RUN --mount=type=cache,target=/root/.cache/pip pip install --pre --upgrade django-wm[celery] - -RUN crontab /usr/src/app/docker/upgrade-check/cron-schedule diff --git a/sample-project/docker/upgrade-check/cron-schedule b/sample-project/docker/upgrade-check/cron-schedule deleted file mode 100644 index 0cccb19..0000000 --- a/sample-project/docker/upgrade-check/cron-schedule +++ /dev/null @@ -1,2 +0,0 @@ -# Maybe send a webmention every minute -* * * * * python /usr/src/app/manage.py automention diff --git a/sample-project/docker/upgrade-check/entrypoint.sh b/sample-project/docker/upgrade-check/entrypoint.sh deleted file mode 100644 index 75e5c97..0000000 --- a/sample-project/docker/upgrade-check/entrypoint.sh +++ /dev/null @@ -1,28 +0,0 @@ -echo "Installing previous django-wm..." -pip install django-wm[celery] -echo "[OK] previous django-wm installed." - -echo "Setting up old server..." -python manage.py makemigrations -python manage.py migrate -python manage.py createsuperuser --noinput || true -python manage.py collectstatic --noinput -echo "[OK] Old server setup." - -echo "Creating sample webmention data..." -python manage.py create_sample_webmentions -echo "[OK] Created sample webmention data." - -echo "Upgrading django-wm to latest pre-release..." -pip install --pre --upgrade django-wm[celery] -echo "[OK] Upgraded django-wm to latest pre-release." - -python manage.py makemigrations -python manage.py migrate -python manage.py collectstatic --noinput - -celery -A sample_project worker -E & - -python manage.py selfcheck & -echo "New server starting..." -python manage.py runserver 0.0.0.0:80 --noreload diff --git a/sample-project/docker/wait-for-it.sh b/sample-project/docker/wait-for-it.sh deleted file mode 100644 index 480ecea..0000000 --- a/sample-project/docker/wait-for-it.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/bin/bash -# Use this script to test if a given TCP host/port are available - -WAITFORIT_cmdname=${0##*/} - -echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } - -usage() -{ - cat << USAGE >&2 -Usage: - $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] - -h HOST | --host=HOST Host or IP under test - -p PORT | --port=PORT TCP port under test - Alternatively, you specify the host and port as host:port - -s | --strict Only execute subcommand if the test succeeds - -q | --quiet Don't output any status messages - -t TIMEOUT | --timeout=TIMEOUT - Timeout in seconds, zero for no timeout - -- COMMAND ARGS Execute command with args after the test finishes -USAGE - exit 1 -} - -wait_for() -{ - if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then - echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" - else - echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" - fi - WAITFORIT_start_ts=$(date +%s) - while : - do - if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then - nc -z $WAITFORIT_HOST $WAITFORIT_PORT - WAITFORIT_result=$? - else - (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 - WAITFORIT_result=$? - fi - if [[ $WAITFORIT_result -eq 0 ]]; then - WAITFORIT_end_ts=$(date +%s) - echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" - break - fi - sleep 1 - done - return $WAITFORIT_result -} - -wait_for_wrapper() -{ - # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 - if [[ $WAITFORIT_QUIET -eq 1 ]]; then - timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & - else - timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & - fi - WAITFORIT_PID=$! - trap "kill -INT -$WAITFORIT_PID" INT - wait $WAITFORIT_PID - WAITFORIT_RESULT=$? - if [[ $WAITFORIT_RESULT -ne 0 ]]; then - echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" - fi - return $WAITFORIT_RESULT -} - -# process arguments -while [[ $# -gt 0 ]] -do - case "$1" in - *:* ) - WAITFORIT_hostport=(${1//:/ }) - WAITFORIT_HOST=${WAITFORIT_hostport[0]} - WAITFORIT_PORT=${WAITFORIT_hostport[1]} - shift 1 - ;; - --child) - WAITFORIT_CHILD=1 - shift 1 - ;; - -q | --quiet) - WAITFORIT_QUIET=1 - shift 1 - ;; - -s | --strict) - WAITFORIT_STRICT=1 - shift 1 - ;; - -h) - WAITFORIT_HOST="$2" - if [[ $WAITFORIT_HOST == "" ]]; then break; fi - shift 2 - ;; - --host=*) - WAITFORIT_HOST="${1#*=}" - shift 1 - ;; - -p) - WAITFORIT_PORT="$2" - if [[ $WAITFORIT_PORT == "" ]]; then break; fi - shift 2 - ;; - --port=*) - WAITFORIT_PORT="${1#*=}" - shift 1 - ;; - -t) - WAITFORIT_TIMEOUT="$2" - if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi - shift 2 - ;; - --timeout=*) - WAITFORIT_TIMEOUT="${1#*=}" - shift 1 - ;; - --) - shift - WAITFORIT_CLI=("$@") - break - ;; - --help) - usage - ;; - *) - echoerr "Unknown argument: $1" - usage - ;; - esac -done - -if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then - echoerr "Error: you need to provide a host and port to test." - usage -fi - -WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} -WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} -WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} -WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} - -# Check to see if timeout is from busybox? -WAITFORIT_TIMEOUT_PATH=$(type -p timeout) -WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) - -WAITFORIT_BUSYTIMEFLAG="" -if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then - WAITFORIT_ISBUSY=1 - # Check if busybox timeout uses -t flag - # (recent Alpine versions don't support -t anymore) - if timeout &>/dev/stdout | grep -q -e '-t '; then - WAITFORIT_BUSYTIMEFLAG="-t" - fi -else - WAITFORIT_ISBUSY=0 -fi - -if [[ $WAITFORIT_CHILD -gt 0 ]]; then - wait_for - WAITFORIT_RESULT=$? - exit $WAITFORIT_RESULT -else - if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then - wait_for_wrapper - WAITFORIT_RESULT=$? - else - wait_for - WAITFORIT_RESULT=$? - fi -fi - -if [[ $WAITFORIT_CLI != "" ]]; then - if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then - echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" - exit $WAITFORIT_RESULT - fi - exec "${WAITFORIT_CLI[@]}" -else - exit $WAITFORIT_RESULT -fi diff --git a/sample-project/docker/with-celery/.env b/sample-project/docker/with-celery/.env index ebb9e1b..1406ed7 100644 --- a/sample-project/docker/with-celery/.env +++ b/sample-project/docker/with-celery/.env @@ -1,8 +1,6 @@ DOMAIN_NAME=with-celery-web.org DB_HOST=with-celery-db POSTGRES_DB=with-celery-db -POSTGRES_USER=user -POSTGRES_PASSWORD=badpassword1! RABBITMQ_DEFAULT_USER=sample_project RABBITMQ_DEFAULT_PASS=badpassword CELERY_BROKER_URL=amqp://sample_project:badpassword@with-celery-rabbitmq:5672 diff --git a/sample-project/docker/with-celery/Dockerfile b/sample-project/docker/with-celery/Dockerfile deleted file mode 100644 index ca3c5fd..0000000 --- a/sample-project/docker/with-celery/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM python:3-alpine -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONBUFFERED=1 - -RUN apk add --no-cache \ - bash - -WORKDIR /var/www/static/ -WORKDIR /src/ -COPY . /src/ -RUN --mount=type=cache,target=/root/.cache/pip pip install -e .[celery,test] -RUN python runtests.py - -WORKDIR /usr/src/app -COPY ./sample-project . -RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt - -RUN crontab /usr/src/app/docker/with-celery/cron-schedule - -CMD bash /usr/src/app/docker/with-celery/entrypoint.sh diff --git a/sample-project/docker/with-celery/cron-schedule b/sample-project/docker/with-celery/cron-schedule index 0cccb19..716a3e4 100644 --- a/sample-project/docker/with-celery/cron-schedule +++ b/sample-project/docker/with-celery/cron-schedule @@ -1,2 +1,5 @@ -# Maybe send a webmention every minute -* * * * * python /usr/src/app/manage.py automention +# Maybe send a webmention every minute. +* * * * * python /project/manage.py automention + +# Show that cron is working. +* * * * * echo "cron heartbeat" diff --git a/sample-project/docker/with-celery/entrypoint.sh b/sample-project/docker/with-celery/entrypoint.sh deleted file mode 100644 index 65d4eda..0000000 --- a/sample-project/docker/with-celery/entrypoint.sh +++ /dev/null @@ -1,15 +0,0 @@ -echo "Setting up server..." - -python manage.py migrate -echo "- Migrations complete" -python manage.py createsuperuser --noinput || true -echo "- Superuser ready" -python manage.py collectstatic --noinput -echo "- Static files collected" - -echo "Starting celery worker..." -celery -A sample_project worker -E & -echo "- Celery started" - -echo "Starting server..." -python manage.py runserver 0.0.0.0:80 --noreload diff --git a/sample-project/docker/with-wagtail/.env b/sample-project/docker/with-wagtail/.env index e4a68bf..de33df4 100644 --- a/sample-project/docker/with-wagtail/.env +++ b/sample-project/docker/with-wagtail/.env @@ -1,7 +1,5 @@ DOMAIN_NAME=with-wagtail-web.org DB_HOST=with-wagtail-db POSTGRES_DB=with-wagtail-db -POSTGRES_USER=user -POSTGRES_PASSWORD=badpassword2! DEFAULT_MENTION_TARGET_DOMAIN=with-celery-web.org AUTOMENTION_URLS=/unreliable/,/unreliable/,/article/1/,/timeout/ diff --git a/sample-project/docker/with-wagtail/Dockerfile b/sample-project/docker/with-wagtail/Dockerfile deleted file mode 100644 index 1565be2..0000000 --- a/sample-project/docker/with-wagtail/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM python:3-alpine -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONBUFFERED=1 - -RUN apk add --no-cache \ - bash - -WORKDIR /var/www/static/ -WORKDIR /src/ -COPY . /src/ -RUN --mount=type=cache,target=/root/.cache/pip pip install -e .[test] -RUN --mount=type=cache,target=/root/.cache/pip pip install wagtail -RUN python runtests.py - -WORKDIR /usr/src/app -COPY ./sample-project . -RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt - -RUN crontab /usr/src/app/docker/with-wagtail/cron-schedule - -CMD bash /usr/src/app/docker/with-wagtail/entrypoint.sh diff --git a/sample-project/docker/with-wagtail/cron-schedule b/sample-project/docker/with-wagtail/cron-schedule index 8d7a8b7..1c49760 100644 --- a/sample-project/docker/with-wagtail/cron-schedule +++ b/sample-project/docker/with-wagtail/cron-schedule @@ -1,3 +1,8 @@ # Check for pending incoming/outgoing mentions every minute. -* * * * * python /usr/src/app/manage.py pending_mentions -* * * * * python /usr/src/app/manage.py wagtail_automention +* * * * * python /project/manage.py pending_mentions + +# Maybe send a webmention every minute. +* * * * * python /project/manage.py wagtail_automention + +# Show that cron is working. +* * * * * echo "cron heartbeat" diff --git a/sample-project/docker/with-wagtail/entrypoint.sh b/sample-project/docker/with-wagtail/entrypoint.sh deleted file mode 100644 index 8660e85..0000000 --- a/sample-project/docker/with-wagtail/entrypoint.sh +++ /dev/null @@ -1,11 +0,0 @@ -echo "Setting up server..." - -python manage.py migrate -echo "- Migrations complete" -python manage.py createsuperuser --noinput || true -echo "- Superuser ready" -python manage.py collectstatic --noinput -echo "- Static files collected" - -echo "Starting server..." -python manage.py runserver 0.0.0.0:80 --noreload diff --git a/sample-project/sample_app/apps.py b/sample-project/sample_app/apps.py index 9c0d4c7..fbabc9f 100644 --- a/sample-project/sample_app/apps.py +++ b/sample-project/sample_app/apps.py @@ -1,5 +1,4 @@ import logging -import sys from django.apps import AppConfig @@ -8,27 +7,3 @@ class SampleAppConfig(AppConfig): name = "sample_app" - - def ready(self): - if "runserver" not in sys.argv: - # Don't create default article when running tests or migrations. - return - - from sample_app.models import Article - from sample_app.tasks import create_initial_articles - - from mentions import __version__ as mentions_version - from mentions.models import OutgoingWebmentionStatus - - log.info(f"django-wm=={mentions_version}") - - if Article.objects.all().exists(): - # Run once when server is created, never again. - return - - create_initial_articles() - - OutgoingWebmentionStatus.objects.get_or_create( - target_url="#s3", - source_url="/article/2/", - ) # Invalid target url: should be deleted when handle_pending_webmentions is called. diff --git a/sample-project/sample_app/management/commands/sample_app_init.py b/sample-project/sample_app/management/commands/sample_app_init.py new file mode 100644 index 0000000..925e4ad --- /dev/null +++ b/sample-project/sample_app/management/commands/sample_app_init.py @@ -0,0 +1,20 @@ +import logging + +from django.core.management import BaseCommand +from sample_app.models import Article +from sample_app.tasks import create_initial_articles + +from mentions import __version__ as mentions_version + +log = logging.getLogger(__name__) + + +class Command(BaseCommand): + def handle(self, *args, **options): + log.info(f"django-wm=={mentions_version}") + + if Article.objects.all().exists(): + # Run once when server is created, never again. + return + + create_initial_articles() diff --git a/sample-project/sample_app_wagtail/apps.py b/sample-project/sample_app_wagtail/apps.py index 4bc2583..bf05780 100644 --- a/sample-project/sample_app_wagtail/apps.py +++ b/sample-project/sample_app_wagtail/apps.py @@ -1,5 +1,4 @@ import logging -import sys from django.apps import AppConfig @@ -8,20 +7,3 @@ class SampleAppWagtailConfig(AppConfig): name = "sample_app_wagtail" - - def ready(self): - if "runserver" not in sys.argv: - # Don't create default article when running tests or migrations. - return - - from sample_app_wagtail.models import BlogIndexPage - from sample_app_wagtail.tasks import create_initial_pages - - from mentions import __version__ as mentions_version - - log.info(f"django-wm=={mentions_version}") - - if BlogIndexPage.objects.all().exists(): - return - - create_initial_pages() diff --git a/sample-project/sample_app_wagtail/management/commands/wagtail_app_init.py b/sample-project/sample_app_wagtail/management/commands/wagtail_app_init.py new file mode 100644 index 0000000..0c2afff --- /dev/null +++ b/sample-project/sample_app_wagtail/management/commands/wagtail_app_init.py @@ -0,0 +1,22 @@ +import logging + +from django.core.management import BaseCommand +from sample_app_wagtail.models import BlogIndexPage +from sample_app_wagtail.tasks import create_initial_pages + +from mentions import __version__ as mentions_version + +log = logging.getLogger(__name__) + + +class Command(BaseCommand): + def handle(self, *args, **options): + log.info(f"django-wm=={mentions_version}") + + if BlogIndexPage.objects.all().exists(): + return + + try: + create_initial_pages() + except Exception as e: + log.error(f"create_initial_pages failed | {e}") diff --git a/sample-project/sample_project/settings/__init__.py b/sample-project/sample_project/settings/__init__.py index 8be9306..6c3d328 100644 --- a/sample-project/sample_project/settings/__init__.py +++ b/sample-project/sample_project/settings/__init__.py @@ -1,4 +1,3 @@ -import os import random from .app_settings import * diff --git a/setup.cfg b/setup.cfg index 5a321d0..225dac0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,3 +40,4 @@ celery = celery >= 5.2.2 test = pytest pytest-django +wagtail = wagtail >= 3.0.3 diff --git a/tests/tests/test_admin/__init__.py b/tests/tests/test_admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests/test_admin/test_admin.py b/tests/tests/test_admin/test_admin.py new file mode 100644 index 0000000..7da2987 --- /dev/null +++ b/tests/tests/test_admin/test_admin.py @@ -0,0 +1,170 @@ +from typing import Type + +from django.contrib import admin +from django.contrib.auth.models import User +from django.db import models +from django.test.utils import override_settings +from django.urls import path, reverse + +from mentions.apps import MentionsConfig +from mentions.models import HCard, SimpleMention, Webmention +from tests.config.urls import core_urlpatterns +from tests.tests.util import testfunc +from tests.tests.util.testcase import WebmentionTestCase + +urlpatterns = [ + *core_urlpatterns, + path(f"{testfunc.random_str()}/", admin.site.urls), +] + + +def admin_url(model_class: Type[models.Model]) -> str: + return reverse( + f"admin:{MentionsConfig.name}_{model_class._meta.model_name}_changelist" + ) + + +def admin_search_url(model_class: Type[models.Model], query: str) -> str: + return f"{admin_url(model_class)}?q={query}" + + +def admin_instance_url(instance: models.Model) -> str: + return reverse( + f"admin:{MentionsConfig.name}_{instance._meta.model_name}_change", + args=[instance.pk], + ) + + +@override_settings( + ROOT_URLCONF=__name__, + STATIC_URL="/static/", +) +class AdminTests(WebmentionTestCase): + def assert_results(self, url: str, contains: str = None, not_contains: str = None): + response = self.client.get(url) + if contains is None and not_contains is None: + raise Exception( + "assert_results should provide at least one of (contains|not_contains) args" + ) + + if contains is not None: + self.assertContains(response, contains) + if not_contains is not None: + self.assertNotContains(response, not_contains) + + def setUp(self) -> None: + user = User.objects.create_superuser( + username="super", + ) + self.client.force_login(user) + + self.hcard_one = testfunc.create_hcard() + self.hcard_two = testfunc.create_hcard() + + self.mention_one = testfunc.create_webmention(hcard=self.hcard_one) + self.mention_two = testfunc.create_webmention() + + self.simple = testfunc.create_simple_mention() + + +class AdminChangelistTests(AdminTests): + def test_changelist(self): + """Model list renders correctly.""" + for model in testfunc.mentions_model_classes(): + url = admin_url(model) + with self.subTest(model=model, url=url): + response = self.client.get(url) + self.assertEqual(200, response.status_code) + + def test_search_generic(self): + """Test that model list with a search query renders correctly.""" + for model in testfunc.mentions_model_classes(): + url = admin_search_url(model, query="whatever") + with self.subTest(model=model, url=url): + response = self.client.get(url) + self.assertEqual(200, response.status_code) + + def test_search_hcard(self): + self.assert_results( + admin_search_url(HCard, query=self.hcard_one.name), + contains=self.hcard_one.homepage, + not_contains=self.hcard_two.homepage, + ) + + self.assert_results( + admin_search_url(HCard, query=self.hcard_two.homepage), + contains=self.hcard_two.name, + not_contains=self.hcard_one.name, + ) + + def test_search_simple(self): + self.assert_results( + admin_search_url(SimpleMention, query=self.simple.quote), + contains=self.simple.source_url, + ) + self.assert_results( + admin_search_url(SimpleMention, query="unrelated"), + not_contains=self.simple.source_url, + ) + + def test_search_webmention(self): + with self.subTest(msg="Search by hcard name"): + self.assert_results( + admin_search_url(Webmention, query=self.hcard_one.name), + contains=self.mention_one.source_url, + not_contains=self.mention_two.source_url, + ) + + with self.subTest(msg="Search by hcard homepage"): + self.assert_results( + admin_search_url(Webmention, query=self.hcard_one.homepage), + contains=self.mention_one.source_url, + not_contains=self.mention_two.source_url, + ) + + with self.subTest(msg="Search by source_url"): + self.assert_results( + admin_search_url(Webmention, query=self.mention_two.source_url), + contains=self.mention_two.source_url, + not_contains=self.mention_one.source_url, + ) + + with self.subTest(msg="Search by target_url"): + self.assert_results( + admin_search_url(Webmention, query=self.mention_one.target_url), + contains=self.mention_one.target_url, + ) + + with self.subTest(msg="Search by quote"): + self.assert_results( + admin_search_url(Webmention, query=self.mention_two.quote), + contains=self.mention_two.source_url, + not_contains=self.mention_one.source_url, + ) + + +class AdminInstanceTests(AdminTests): + def assert_instance_page_accessible(self, instance: models.Model): + url = admin_instance_url(instance) + response = self.client.get(url) + self.assertEqual(200, response.status_code, msg=f"url={url}") + + def test_webmention_instance(self): + self.assert_instance_page_accessible(self.mention_one) + self.assert_instance_page_accessible(self.mention_two) + + def test_simplemention_instance(self): + self.assert_instance_page_accessible(self.simple) + + def test_hcard_instance(self): + self.assert_instance_page_accessible(self.hcard_one) + self.assert_instance_page_accessible(self.hcard_two) + + def test_outgoing_status_instance(self): + self.assert_instance_page_accessible(testfunc.create_outgoing_status()) + + def test_pending_incoming_instance(self): + self.assert_instance_page_accessible(testfunc.create_pending_incoming()) + + def test_pending_outgoing_instance(self): + self.assert_instance_page_accessible(testfunc.create_pending_outgoing()) diff --git a/tests/tests/test_endpoints/test_endpoint_get.py b/tests/tests/test_endpoints/test_endpoint_get.py index 383396b..8c05a9a 100644 --- a/tests/tests/test_endpoints/test_endpoint_get.py +++ b/tests/tests/test_endpoints/test_endpoint_get.py @@ -5,7 +5,7 @@ import logging from typing import Dict, List -from mentions.models import SimpleMention, Webmention +from mentions.models import Webmention from tests.tests.util import testfunc from tests.tests.util.testcase import WebmentionTestCase @@ -44,14 +44,12 @@ def setUp(self): self.webmention_source_url = testfunc.random_url() self.simplemention_source_url = testfunc.random_url() - Webmention.objects.create( + testfunc.create_webmention( source_url=self.webmention_source_url, target_object=self.target_object, - validated=True, - approved=True, ) - SimpleMention.objects.create( + testfunc.create_simple_mention( source_url=self.simplemention_source_url, target_object=self.target_object, ) @@ -83,14 +81,11 @@ def setUp(self): self.webmention_source_url = testfunc.random_url() self.simplemention_source_url = testfunc.random_url() - Webmention.objects.create( + testfunc.create_webmention( source_url=self.webmention_source_url, target_url=self.target_url, - validated=True, - approved=True, ) - - SimpleMention.objects.create( + testfunc.create_simple_mention( source_url=self.simplemention_source_url, target_url=self.target_url, ) diff --git a/tests/tests/test_wagtail/test_with_wagtail.py b/tests/tests/test_wagtail/test_with_wagtail.py index e2c67ff..b22bbbe 100644 --- a/tests/tests/test_wagtail/test_with_wagtail.py +++ b/tests/tests/test_wagtail/test_with_wagtail.py @@ -18,8 +18,8 @@ import wagtail wagtail_version = wagtail.__version__ - major, minor, patch = wagtail_version.split(".") - wagtail_version_four = int(major) >= 4 + major, *rest = wagtail_version.split(".") + wagtail_has_path_decorator = int(major) >= 4 from wagtail import urls as wagtail_urls from wagtail.models import Page, Site @@ -31,7 +31,7 @@ IndexPage = None MentionablePage = None wagtail_urls = {"urlpatterns": []} - wagtail_version_four = False + wagtail_has_path_decorator = False urlpatterns = base_urlpatterns + [ @@ -81,7 +81,7 @@ def assert_resolves_target(self, url: str): self.assertEqual(self.target, result) -@skipIf(not wagtail_version_four, "@path decorator not available until v4") +@skipIf(not wagtail_has_path_decorator, "@path decorator not available until v4") class PathWagtailTests(WagtailTestCase): def test_page_lookup(self): url = "such-content/" @@ -149,7 +149,7 @@ def test_page_without_mentionablemixin(self): with self.assertRaises(NoModelForUrlPath): resolution.get_model_for_url(self.build_url("simple-page/")) - @skipIf(wagtail_version_four, "@path decorator not available until v4") + @skipIf(wagtail_has_path_decorator, "Only applies to wagtail version 3") def test_page_lookup_by_altpath_wagtail_v3(self): url = "2022/11/16/" with self.assertRaises(OptionalDependency): diff --git a/tests/tests/util/testcase.py b/tests/tests/util/testcase.py index 2dfd02d..46bfe8d 100644 --- a/tests/tests/util/testcase.py +++ b/tests/tests/util/testcase.py @@ -8,6 +8,7 @@ from mentions import options from mentions.views import view_names +from tests.tests.util import testfunc M = TypeVar("M", bound=models.Model) @@ -39,14 +40,6 @@ def get_endpoint_mentions_by_type(self, url: str): class WebmentionTestCase(ClientTestCase, SimpleTestCase): def tearDown(self) -> None: super().tearDown() - from mentions.models import ( - HCard, - OutgoingWebmentionStatus, - PendingIncomingWebmention, - PendingOutgoingContent, - SimpleMention, - Webmention, - ) from tests.test_app.models import ( BadTestModelMissingAllText, BadTestModelMissingGetAbsoluteUrl, @@ -55,14 +48,8 @@ def tearDown(self) -> None: SampleBlog, ) - app_models = [ - Webmention, - OutgoingWebmentionStatus, - PendingIncomingWebmention, - PendingOutgoingContent, - HCard, - SimpleMention, - ] + app_models = testfunc.mentions_model_classes() + test_models = [ MentionableTestModel, BadTestModelMissingAllText, diff --git a/tests/tests/util/testfunc.py b/tests/tests/util/testfunc.py index e64b988..e04510d 100644 --- a/tests/tests/util/testfunc.py +++ b/tests/tests/util/testfunc.py @@ -7,7 +7,14 @@ from django.urls import reverse from mentions import config -from mentions.models import Webmention +from mentions.models import ( + HCard, + OutgoingWebmentionStatus, + PendingIncomingWebmention, + PendingOutgoingContent, + SimpleMention, + Webmention, +) from mentions.models.mixins import IncomingMentionType, MentionableMixin from mentions.views import view_names from tests.test_app.models import MentionableTestModel @@ -28,7 +35,8 @@ def create_webmention( approved: bool = True, validated: bool = True, quote: Optional[str] = None, - notes: Optional[str] = "", + notes: Optional[str] = None, + hcard: Optional[HCard] = None, ) -> Webmention: return Webmention.objects.create( source_url=source_url or random_url(), @@ -38,8 +46,75 @@ def create_webmention( sent_by=sent_by or random_url(), approved=approved, validated=validated, - quote=quote, - notes=notes, + quote=quote or random_str(), + notes=notes or random_url(), + hcard=hcard, + ) + + +def create_simple_mention( + source_url: Optional[str] = None, + target_url: Optional[str] = None, + target_object: Optional[MentionableMixin] = None, + quote: Optional[str] = None, +) -> SimpleMention: + return SimpleMention.objects.create( + target_url=target_url or random_url(), + source_url=source_url or random_url(), + target_object=target_object, + quote=quote or random_str(), + ) + + +def create_hcard( + name: Optional[str] = None, + homepage: Optional[str] = None, + avatar: Optional[str] = None, +) -> HCard: + return HCard.objects.create( + name=name or random_str(), + homepage=homepage or random_url(), + avatar=avatar or random_url(), + ) + + +def create_outgoing_status( + source_url: Optional[str] = None, + target_url: Optional[str] = None, + target_webmention_endpoint: Optional[str] = None, + status_message: Optional[str] = None, + response_code: Optional[int] = 200, + successful: Optional[bool] = True, +) -> OutgoingWebmentionStatus: + return OutgoingWebmentionStatus.objects.create( + source_url=source_url or random_url(), + target_url=target_url or random_url(), + target_webmention_endpoint=target_webmention_endpoint or random_url(), + status_message=status_message or random_str(), + response_code=response_code, + successful=successful, + ) + + +def create_pending_incoming( + source_url: Optional[str] = None, + target_url: Optional[str] = None, + sent_by: Optional[str] = None, +) -> PendingIncomingWebmention: + return PendingIncomingWebmention.objects.create( + source_url=source_url or random_url(), + target_url=target_url or get_simple_url(), + sent_by=sent_by or random_url(), + ) + + +def create_pending_outgoing( + absolute_url: Optional[str] = None, + text: Optional[str] = None, +) -> PendingOutgoingContent: + return PendingOutgoingContent.objects.create( + absolute_url=absolute_url or random_url(), + text=text or random_str(), ) @@ -95,3 +170,14 @@ def random_url() -> str: def random_str(length: int = 5) -> str: """Generate a short string of random characters.""" return uuid.uuid4().hex[:length] + + +def mentions_model_classes(): + return [ + Webmention, + OutgoingWebmentionStatus, + PendingIncomingWebmention, + PendingOutgoingContent, + HCard, + SimpleMention, + ]