import io import pytest from django.contrib.auth.models import Group from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse from PIL import Image from apps.content.models import EditorialPage, MediaAsset, PageRevision, SiteIdentity from apps.users.models import User pytestmark = [pytest.mark.django_db, pytest.mark.integration] def staff(name, group): user = User.objects.create_user(username=name, mobile=f"0912{User.objects.count():07d}", password="TestPass123!") user.is_staff = True user.save(update_fields=["is_staff"]) user.groups.add(Group.objects.get(name=group)) return user def home_post(): data = {"title": "Home", "intro": "Welcome", "seo_title": "Home", "seo_description": "Association"} for position, key in enumerate(("hero", "featured_events", "featured_posts", "participation")): data[f"section_{key}-heading"] = key.replace("_", " ").title() if key in {"featured_events", "featured_posts"}: data[f"section_{key}-all_link_label"] = "See all" data[f"section_{key}-body"] = "Introduction" if key in {"hero", "participation"} else "" return data def test_editor_can_save_and_preview_draft_without_publishing(client): page = EditorialPage.objects.create(slug="home", page_kind="sections") editor = staff("editor", "content_editor") client.force_login(editor) draft_url = reverse("admin:content_editorialpage_draft", args=[page.pk]) response = client.get(draft_url) assert response.status_code == 200 assert b"section_hero-heading" in response.content assert b"primary_action" not in response.content response = client.post(draft_url, home_post(), follow=True) assert response.status_code == 200 revision = PageRevision.objects.get(page=page) assert revision.status == "draft" assert page.published_revision_id is None preview_url = reverse("admin:content_editorialpage_preview", args=[page.pk, revision.pk]) response = client.get(preview_url) assert response.status_code == 200 assert b"Home" in response.content assert "private" in response["Cache-Control"] and "no-store" in response["Cache-Control"] publish_url = reverse("admin:content_editorialpage_publish", args=[page.pk, revision.pk]) assert client.post(publish_url, {"expected_published_id": ""}).status_code == 403 def test_publisher_can_publish_after_preview(client): page = EditorialPage.objects.create(slug="home", page_kind="sections") editor = staff("editor", "content_editor") publisher = staff("publisher", "content_publisher") client.force_login(editor) client.post(reverse("admin:content_editorialpage_draft", args=[page.pk]), home_post()) revision = PageRevision.objects.get(page=page) client.force_login(publisher) response = client.post(reverse("admin:content_editorialpage_publish", args=[page.pk, revision.pk]), {"expected_published_id": ""}, follow=True) assert response.status_code == 200 page.refresh_from_db() assert page.published_revision_id == revision.pk assert b"Published revision" in response.content def test_site_admin_uses_named_contact_fields_and_rejects_bad_url(client): site = SiteIdentity.objects.create() editor = staff("editor", "content_editor") client.force_login(editor) url = reverse("admin:content_siteidentity_draft", args=[site.pk]) response = client.get(url) assert response.status_code == 200 assert b"contact_email_url" in response.content assert b'name="navigation"' not in response.content data = {"official_name": "Association", "short_name": "ACE", "contact_email_url": "javascript:alert(1)", "contact_email_label": "Email"} for position, route in enumerate(("home", "about", "events", "blog")): data[f"nav_{route}_label"] = route data[f"nav_{route}_position"] = position data[f"nav_{route}_visible"] = "on" response = client.post(url, data) assert response.status_code == 200 assert b"contact_email_url" in response.content assert site.revisions.count() == 0 def test_about_optional_section_can_be_hidden_and_reordered(client): page = EditorialPage.objects.create(slug="about", page_kind="sections") client.force_login(staff("editor", "content_editor")) data = {"title": "About", "intro": "Introduction", "seo_title": "About", "seo_description": "Association"} for key, heading in (("identity", "Identity"), ("mission", "Mission"), ("contact", "Contact")): data[f"section_{key}-heading"] = heading data["section_identity-body"] = "Official story" data["section_mission-position"] = "2" data["section_mission-body"] = "Our mission" for key in ("mission", "activities", "values", "committees", "affiliations", "faq"): data[f"entries_{key}-TOTAL_FORMS"] = "1" data[f"entries_{key}-INITIAL_FORMS"] = "0" data[f"entries_{key}-MIN_NUM_FORMS"] = "0" data[f"entries_{key}-MAX_NUM_FORMS"] = "20" response = client.post(reverse("admin:content_editorialpage_draft", args=[page.pk]), data, follow=True) assert response.status_code == 200 revision = PageRevision.objects.get(page=page) assert list(revision.sections.values_list("key", "position", "visible")) == [ ("identity", 0, True), ("mission", 2, False), ("contact", 3, True), ] def test_site_editor_can_save_named_links_and_preview(client): site = SiteIdentity.objects.create() client.force_login(staff("editor", "content_editor")) data = {"official_name": "Association", "short_name": "ACE", "contact_email_label": "Email", "contact_email_url": "mailto:test@example.com"} for position, route in enumerate(("home", "about", "events", "blog")): data[f"nav_{route}_label"] = route data[f"nav_{route}_position"] = str(position) data[f"nav_{route}_visible"] = "on" response = client.post(reverse("admin:content_siteidentity_draft", args=[site.pk]), data, follow=True) assert response.status_code == 200 revision = site.revisions.get() assert revision.contacts == [{"kind": "email", "label": "Email", "url": "mailto:test@example.com", "position": 0, "visible": False}] assert site.published_revision_id is None preview = client.get(reverse("admin:content_siteidentity_preview", args=[site.pk, revision.pk])) assert preview.status_code == 200 and b"test@example.com" in preview.content def test_anonymous_cannot_preview_draft(client): page = EditorialPage.objects.create(slug="home", page_kind="sections") client.force_login(staff("editor", "content_editor")) client.post(reverse("admin:content_editorialpage_draft", args=[page.pk]), home_post()) revision = page.revisions.get() client.logout() response = client.get(reverse("admin:content_editorialpage_preview", args=[page.pk, revision.pk])) assert response.status_code == 302 and "/admin/login/" in response["Location"] def test_staff_without_content_permission_cannot_preview_or_publish(client): page = EditorialPage.objects.create(slug="home", page_kind="sections") client.force_login(staff("editor", "content_editor")) client.post(reverse("admin:content_editorialpage_draft", args=[page.pk]), home_post()) revision = page.revisions.get() plain_staff = User.objects.create_user(username="plain", mobile="09121111111", password="TestPass123!", is_staff=True) client.force_login(plain_staff) assert client.get(reverse("admin:content_editorialpage_preview", args=[page.pk, revision.pk])).status_code == 403 assert client.post(reverse("admin:content_editorialpage_publish", args=[page.pk, revision.pk]), {"expected_published_id": ""}).status_code == 403 def test_stale_admin_draft_post_reports_conflict(client): page = EditorialPage.objects.create(slug="home", page_kind="sections") client.force_login(staff("editor", "content_editor")) url = reverse("admin:content_editorialpage_draft", args=[page.pk]) assert client.get(url).status_code == 200 client.post(url, home_post()) response = client.post(url, home_post()) assert response.status_code == 200 assert b"newer revision" in response.content assert page.revisions.count() == 1 def test_media_approval_is_publisher_only_and_locks_asset(client, settings, tmp_path): settings.MEDIA_ROOT = tmp_path buffer = io.BytesIO() Image.new("RGB", (20, 20)).save(buffer, format="PNG") client.force_login(staff("editor", "content_editor")) upload = SimpleUploadedFile("logo.png", buffer.getvalue(), content_type="image/png") response = client.post(reverse("admin:content_mediaasset_add"), { "file": upload, "alt": "Association logo", "source": "Owner", "license": "Owned", "approved": "on", "_save": "Save", }) assert response.status_code == 302 asset = MediaAsset.objects.get() assert not asset.approved client.force_login(staff("publisher", "content_publisher")) change_url = reverse("admin:content_mediaasset_change", args=[asset.pk]) response = client.post(change_url, { "alt": asset.alt, "source": asset.source, "license": asset.license, "approved": "on", "_save": "Save", }) assert response.status_code == 302 asset.refresh_from_db() assert asset.approved assert client.get(change_url).status_code == 200 assert client.post(change_url, {"approved": ""}).status_code == 403 def test_listing_intro_page_saves_without_sections(client): page = EditorialPage.objects.create(slug="events", page_kind="listing_intro") client.force_login(staff("editor", "content_editor")) response = client.post(reverse("admin:content_editorialpage_draft", args=[page.pk]), { "title": "Events", "intro": "Find upcoming events", "seo_title": "Events", "seo_description": "Association events", }, follow=True) assert response.status_code == 200 revision = page.revisions.get() assert revision.intro == "Find upcoming events" and revision.sections.count() == 0 def test_admin_restore_creates_new_published_revision(client): page = EditorialPage.objects.create(slug="home", page_kind="sections") client.force_login(staff("publisher", "content_publisher")) draft_url = reverse("admin:content_editorialpage_draft", args=[page.pk]) client.post(draft_url, home_post() | {"title": "First"}) first = page.revisions.get() client.post(reverse("admin:content_editorialpage_publish", args=[page.pk, first.pk]), {"expected_published_id": ""}) client.post(draft_url, home_post() | {"title": "Second", "base_revision_id": str(first.pk)}) second = page.revisions.order_by("-number").first() client.post(reverse("admin:content_editorialpage_publish", args=[page.pk, second.pk]), {"expected_published_id": str(first.pk)}) response = client.post(reverse("admin:content_editorialpage_restore", args=[page.pk, first.pk]), {"expected_published_id": str(second.pk)}, follow=True) assert response.status_code == 200 page.refresh_from_db() assert page.published_revision.number == 3 and page.published_revision.title == "First"