diff --git a/activities/views/posts.py b/activities/views/posts.py index bfac8eb..99539fa 100644 --- a/activities/views/posts.py +++ b/activities/views/posts.py @@ -58,6 +58,7 @@ class Individual(TemplateView): "link_original": True, "ancestors": ancestors, "descendants": descendants, + "public_styling": True, } ) diff --git a/requirements.txt b/requirements.txt index 2ad2603..7ad2dc1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ django-hatchway~=0.5.1 django-htmx~=1.13.0 django-oauth-toolkit~=2.2.0 django-storages[google,boto3]~=1.13.1 -django~=4.1 +django~=4.1.0 email-validator~=1.3.0 gunicorn~=20.1.0 httpx~=0.23 diff --git a/static/css/style.css b/static/css/style.css index 407b8ff..fe761c8 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -737,7 +737,7 @@ form.inline { div.follow { float: right; - margin: 20px 0 0 0; + margin: 30px 0 0 0; font-size: 16px; text-align: center; } @@ -1124,7 +1124,7 @@ section.identity .banner { object-fit: cover; display: block; width: calc(100% + 30px); - margin: -5px -15px 20px -15px; + margin: -5px -15px 0px -15px; border-radius: 5px 0 0 0; } @@ -1132,7 +1132,7 @@ section.identity .icon { width: 80px; height: 80px; float: left; - margin: 0 20px 15px 0; + margin: 15px 20px 15px 0; cursor: pointer; } @@ -1141,7 +1141,7 @@ section.identity .emoji { } section.identity h1 { - margin: 30px 0 0 0; + margin: 25px 0 0 0; } section.identity small { diff --git a/templates/admin/domain_edit.html b/templates/admin/domain_edit.html index fbaa46b..0c4f955 100644 --- a/templates/admin/domain_edit.html +++ b/templates/admin/domain_edit.html @@ -3,7 +3,7 @@ {% block subtitle %}{{ domain.domain }}{% endblock %} {% block settings_content %} -
+ {% csrf_token %}
Domain Details diff --git a/templates/base.html b/templates/base.html index 2d88ffa..d4afc1d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -18,8 +18,14 @@ --color-text-link: {{ config.highlight_color }}; } - {% if config_identity.custom_css %} - + {% if identity and public_styling %} + {% if identity.domain.config_domain.custom_css %} + + {% endif %} + {% else %} + {% if request.domain.config_domain.custom_css %} + + {% endif %} {% endif %} {% block opengraph %} {% include "_opengraph.html" with opengraph_local=opengraph_defaults %} @@ -35,8 +41,13 @@
{% if user.is_authenticated %} @@ -58,6 +69,8 @@ {% include "_announcements.html" %} {% endblock %} + {% include "activities/_image_viewer.html" %} + {% block content %} {% endblock %} {% endblock %} diff --git a/templates/identity/follows.html b/templates/identity/follows.html index 26744c6..0749015 100644 --- a/templates/identity/follows.html +++ b/templates/identity/follows.html @@ -1,6 +1,6 @@ {% extends "identity/view.html" %} -{% block title %}{% if self.inbound %}Followers{% else %}Following{% endif %} - {{ identity }}{% endblock %} +{% block title %}{% if inbound %}Followers{% else %}Following{% endif %} - {{ identity }}{% endblock %} {% block subcontent %} @@ -8,7 +8,7 @@ {% include "activities/_identity.html" %} {% empty %} - This person has no {% if self.inbound %}followers{% else %}follows{% endif %} yet. + This person has no {% if inbound %}followers{% else %}follows{% endif %} yet. {% endfor %} diff --git a/tests/activities/views/test_compose.py b/tests/activities/views/test_compose.py index 2fdec3a..ded9c69 100644 --- a/tests/activities/views/test_compose.py +++ b/tests/activities/views/test_compose.py @@ -8,51 +8,20 @@ from users.models import Identity @pytest.mark.django_db -def test_content_warning_text( - client_with_identity: Client, - config_system: Config.SystemOptions, -): - """ - Tests that changing the content warning name works - """ - config_system.content_warning_text = "Content Summary" - response = client_with_identity.get("/compose/") - assertContains(response, 'placeholder="Content Summary"', status_code=200) - assertContains( - response, "", html=True - ) - - -@pytest.mark.django_db -def test_post_edit_security(client_with_identity: Client, other_identity: Identity): - """ - Tests that you can't edit other users' posts with URL fiddling - """ - other_post = Post.objects.create( - content="

OTHER POST!

", - author=other_identity, - local=True, - visibility=Post.Visibilities.public, - ) - response = client_with_identity.get(other_post.urls.action_edit) - assert response.status_code == 403 - - -@pytest.mark.django_db -def test_rate_limit(identity: Identity, client_with_identity: Client): +def test_rate_limit(identity: Identity, client_with_user: Client): """ Tests that the posting rate limit comes into force """ # First post should go through assert identity.posts.count() == 0 - response = client_with_identity.post( - "/compose/", data={"text": "post 1", "visibility": "0"} + response = client_with_user.post( + f"/@{identity.handle}/compose/", data={"text": "post 1", "visibility": "0"} ) assert response.status_code == 302 assert identity.posts.count() == 1 # Second should not - response = client_with_identity.post( - "/compose/", data={"text": "post 2", "visibility": "0"} + response = client_with_user.post( + f"/@{identity.handle}/compose/", data={"text": "post 2", "visibility": "0"} ) assertContains(response, "You must wait at least", status_code=200) assert identity.posts.count() == 1 diff --git a/tests/activities/views/test_posts.py b/tests/activities/views/test_posts.py deleted file mode 100644 index a3dbc64..0000000 --- a/tests/activities/views/test_posts.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest -from django.test.client import Client - -from activities.models import Post -from users.models import Identity - - -@pytest.mark.django_db -def test_post_delete_security(client_with_identity: Client, other_identity: Identity): - """ - Tests that you can't delete other users' posts with URL fiddling - """ - other_post = Post.objects.create( - content="

OTHER POST!

", - author=other_identity, - local=True, - visibility=Post.Visibilities.public, - ) - response = client_with_identity.get(other_post.urls.action_delete) - assert response.status_code == 403 diff --git a/tests/activities/views/test_timelines.py b/tests/activities/views/test_timelines.py deleted file mode 100644 index a2cbb32..0000000 --- a/tests/activities/views/test_timelines.py +++ /dev/null @@ -1,12 +0,0 @@ -import pytest - - -@pytest.mark.django_db -def test_content_warning_text(client_with_identity, config_system): - - config_system.content_warning_text = "Content Summary" - - response = client_with_identity.get("/") - - assert response.status_code == 200 - assert 'placeholder="Content Summary"' in str(response.rendered_content) diff --git a/tests/conftest.py b/tests/conftest.py index cca5c7c..9703d6d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,6 +62,8 @@ def _test_settings(settings): settings.STATICFILES_STORAGE = ( "django.contrib.staticfiles.storage.StaticFilesStorage" ) + settings.SETUP.MAIN_DOMAIN = "example.com" + settings.MAIN_DOMAIN = "example.com" @pytest.fixture @@ -77,15 +79,11 @@ def config_system(keypair): @pytest.fixture -def client_with_identity(client, identity, user): +def client_with_user(client, user): """ - Provides a logged-in test client with an identity selected + Provides a logged-in test client """ client.force_login(user) - session = client.session - session["identity_id"] = identity.id - session.save() - client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key return client diff --git a/tests/users/views/settings/test_privacy.py b/tests/users/views/settings/test_privacy.py index a40c881..d2f089e 100644 --- a/tests/users/views/settings/test_privacy.py +++ b/tests/users/views/settings/test_privacy.py @@ -13,7 +13,7 @@ def test_stats(client, identity, other_identity): Follow.objects.create(source=other_identity, target=identity) Config.set_identity(identity, "visible_follows", True) response = client.get(identity.urls.view) - assertContains(response, "1 follower", status_code=200) + assertContains(response, "1 Follower", status_code=200) @pytest.mark.django_db @@ -23,7 +23,7 @@ def test_visible_follows_disabled(client, identity): """ Config.set_identity(identity, "visible_follows", True) response = client.get(identity.urls.view) - assertContains(response, "follower", status_code=200) + assertContains(response, "Follower", status_code=200) Config.set_identity(identity, "visible_follows", False) response = client.get(identity.urls.view) - assertNotContains(response, "follower", status_code=200) + assertNotContains(response, "Follower", status_code=200) diff --git a/tests/users/views/test_import_export.py b/tests/users/views/test_import_export.py index f97431d..b50aa80 100644 --- a/tests/users/views/test_import_export.py +++ b/tests/users/views/test_import_export.py @@ -10,7 +10,7 @@ from users.services import IdentityService @pytest.mark.django_db def test_import_following( - client_with_identity: Client, + client_with_user: Client, identity: Identity, remote_identity: Identity, stator: StatorRunner, @@ -24,8 +24,8 @@ def test_import_following( "follows.csv", b"Account address,Show boosts,Notify on new posts,Languages\ntest@remote.test,true,false,", ) - response = client_with_identity.post( - "/settings/import_export/", + response = client_with_user.post( + f"/@{identity.handle}/settings/import_export/", { "csv": csv_file, "import_type": "following", @@ -45,7 +45,7 @@ def test_import_following( @pytest.mark.django_db def test_export_following( - client_with_identity: Client, + client_with_user: Client, identity: Identity, remote_identity: Identity, stator: StatorRunner, @@ -58,7 +58,9 @@ def test_export_following( IdentityService(identity).follow(remote_identity) # Download the CSV - response = client_with_identity.get("/settings/import_export/following.csv") + response = client_with_user.get( + f"/@{identity.handle}/settings/import_export/following.csv" + ) assert response.status_code == 200 assert ( response.content.strip() @@ -68,7 +70,7 @@ def test_export_following( @pytest.mark.django_db def test_export_followers( - client_with_identity: Client, + client_with_user: Client, identity: Identity, identity2: Identity, stator: StatorRunner, @@ -81,6 +83,8 @@ def test_export_followers( IdentityService(identity2).follow(identity) # Download the CSV - response = client_with_identity.get("/settings/import_export/followers.csv") + response = client_with_user.get( + f"/@{identity.handle}/settings/import_export/followers.csv" + ) assert response.status_code == 200 assert response.content.strip() == b"Account address\r\ntest@example2.com" diff --git a/users/middleware.py b/users/middleware.py index 54ada7c..5a7f30b 100644 --- a/users/middleware.py +++ b/users/middleware.py @@ -10,6 +10,8 @@ class DomainMiddleware: self.get_response = get_response def __call__(self, request): - request.domain = Domain.get_domain(request.META["HTTP_HOST"]) + request.domain = None + if "HTTP_HOST" in request.META: + request.domain = Domain.get_domain(request.META["HTTP_HOST"]) response = self.get_response(request) return response diff --git a/users/views/admin/domains.py b/users/views/admin/domains.py index bd06b8c..6980b66 100644 --- a/users/views/admin/domains.py +++ b/users/views/admin/domains.py @@ -4,6 +4,7 @@ from django.db import models from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator from django.views.generic import FormView, TemplateView +from django.core.files import File from core.models import Config from users.decorators import admin_required @@ -214,7 +215,8 @@ class DomainEdit(FormView): Domain.objects.exclude(pk=self.domain.pk).update(default=False) Config.set_domain(self.domain, "hide_login", form.cleaned_data["hide_login"]) Config.set_domain(self.domain, "site_name", form.cleaned_data["site_name"]) - Config.set_domain(self.domain, "site_icon", form.cleaned_data["site_icon"]) + if isinstance(form.cleaned_data["site_icon"], File): + Config.set_domain(self.domain, "site_icon", form.cleaned_data["site_icon"]) Config.set_domain(self.domain, "custom_css", form.cleaned_data["custom_css"]) return redirect(Domain.urls.root) diff --git a/users/views/identity.py b/users/views/identity.py index fbc8d43..3b2fc14 100644 --- a/users/views/identity.py +++ b/users/views/identity.py @@ -71,6 +71,7 @@ class ViewIdentity(ListView): def get_context_data(self): context = super().get_context_data() context["identity"] = self.identity + context["public_styling"] = True context["post_count"] = self.identity.posts.count() if self.identity.config_identity.visible_follows: context["followers_count"] = self.identity.inbound_follows.filter( @@ -214,11 +215,18 @@ class IdentityFollows(ListView): raise Http404("Hidden follows") return super().get(request, identity=self.identity) + def get_queryset(self): + if self.inbound: + return IdentityService(self.identity).followers() + else: + return IdentityService(self.identity).following() + def get_context_data(self): context = super().get_context_data() context["identity"] = self.identity context["inbound"] = self.inbound context["section"] = "follows" + context["public_styling"] = True context["followers_count"] = self.identity.inbound_follows.filter( state__in=FollowStates.group_active() ).count() @@ -255,6 +263,7 @@ class IdentitySearch(FormView): context = super().get_context_data(**kwargs) context["identity"] = self.identity context["section"] = "search" + context["public_styling"] = True context["followers_count"] = self.identity.inbound_follows.filter( state__in=FollowStates.group_active() ).count() diff --git a/users/views/settings/import_export.py b/users/views/settings/import_export.py index c641f6d..f675363 100644 --- a/users/views/settings/import_export.py +++ b/users/views/settings/import_export.py @@ -70,7 +70,7 @@ class ImportExportPage(IdentityViewMixin, FormView): return context -class CsvView(View): +class CsvView(IdentityViewMixin, View): """ Generic view that exports a queryset as a CSV """ @@ -85,7 +85,7 @@ class CsvView(View): def get_queryset(self): raise NotImplementedError() - def get(self, request): + def get(self, request, *args, **kwargs): response = HttpResponse( content_type="text/csv", headers={"Content-Disposition": f'attachment; filename="{self.filename}"'}, @@ -113,7 +113,7 @@ class CsvView(View): return response -class CsvFollowing(IdentityViewMixin, CsvView): +class CsvFollowing(CsvView): columns = { "Account address": "get_handle", "Show boosts": "boosts", @@ -136,7 +136,7 @@ class CsvFollowing(IdentityViewMixin, CsvView): return "" -class CsvFollowers(IdentityViewMixin, CsvView): +class CsvFollowers(CsvView): columns = { "Account address": "get_handle", }