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 @@