From fa27a54e34b6912e671254a3103495aa9b765b7e Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Sun, 20 Oct 2019 16:09:56 +0100 Subject: Add is_staff to role mappings, and the logic to go with it --- pydis_site/apps/staff/models/role_mapping.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'pydis_site/apps/staff') diff --git a/pydis_site/apps/staff/models/role_mapping.py b/pydis_site/apps/staff/models/role_mapping.py index 10c09cf1..dff8081a 100644 --- a/pydis_site/apps/staff/models/role_mapping.py +++ b/pydis_site/apps/staff/models/role_mapping.py @@ -21,6 +21,11 @@ class RoleMapping(models.Model): unique=True, # Unique in order to simplify group assignment logic ) + is_staff = models.BooleanField( + help_text="Whether this role mapping related to a Django staff group", + default=False + ) + def __str__(self): """Returns the mapping, for display purposes.""" return f"@{self.role.name} -> {self.group.name}" -- cgit v1.2.3 From 2b6513fb6ff455b5d3fccc5786764122759bfae1 Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Wed, 23 Oct 2019 01:06:03 +0100 Subject: Add role mapping migration I forgot to commit --- .../migrations/0002_add_is_staff_to_role_mappings.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 pydis_site/apps/staff/migrations/0002_add_is_staff_to_role_mappings.py (limited to 'pydis_site/apps/staff') diff --git a/pydis_site/apps/staff/migrations/0002_add_is_staff_to_role_mappings.py b/pydis_site/apps/staff/migrations/0002_add_is_staff_to_role_mappings.py new file mode 100644 index 00000000..0404d270 --- /dev/null +++ b/pydis_site/apps/staff/migrations/0002_add_is_staff_to_role_mappings.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2019-10-20 14:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('staff', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='rolemapping', + name='is_staff', + field=models.BooleanField(default=False, help_text='Whether this role mapping relates to a Django staff group'), + ), + ] -- cgit v1.2.3 From 49376a76289ab22f1aff55d8971b0ea198ec9316 Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Wed, 23 Oct 2019 01:07:24 +0100 Subject: Add user settings modal, with connections management and account deletion --- pydis_site/apps/home/urls.py | 3 + pydis_site/apps/home/views/account/settings.py | 48 +++++++- pydis_site/apps/staff/models/role_mapping.py | 2 +- pydis_site/static/css/base/base.css | 10 ++ pydis_site/static/js/base/modal.js | 100 +++++++++++++++++ pydis_site/templates/base/base.html | 1 + pydis_site/templates/base/navbar.html | 18 ++- pydis_site/templates/home/account/settings.html | 141 ++++++++++++++++++++++-- 8 files changed, 310 insertions(+), 13 deletions(-) create mode 100644 pydis_site/static/js/base/modal.js (limited to 'pydis_site/apps/staff') diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py index 70a41177..61e87a39 100644 --- a/pydis_site/apps/home/urls.py +++ b/pydis_site/apps/home/urls.py @@ -10,7 +10,10 @@ from .views import AccountDeleteView, AccountSettingsView, HomeView app_name = 'home' urlpatterns = [ + # We do this twice because Allauth expects specific view names to exist path('', HomeView.as_view(), name='home'), + path('', HomeView.as_view(), name='socialaccount_connections'), + path('pages/', include('wiki.urls')), path('accounts/', include('allauth.socialaccount.providers.discord.urls')), diff --git a/pydis_site/apps/home/views/account/settings.py b/pydis_site/apps/home/views/account/settings.py index ee9011da..84a2dea0 100644 --- a/pydis_site/apps/home/views/account/settings.py +++ b/pydis_site/apps/home/views/account/settings.py @@ -1,12 +1,20 @@ +from allauth.socialaccount.models import SocialAccount +from allauth.socialaccount.providers import registry from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.messages import ERROR, INFO, add_message from django.http import HttpRequest, HttpResponse -from django.shortcuts import render +from django.shortcuts import redirect, render from django.urls import reverse from django.views import View class SettingsView(LoginRequiredMixin, View): - """Account settings view, for managing and deleting user accounts and connections.""" + """ + Account settings view, for managing and deleting user accounts and connections. + + This view actually renders a template with a bare modal, and is intended to be + inserted into another template using JavaScript. + """ def __init__(self, *args, **kwargs): self.login_url = reverse("home") @@ -14,4 +22,38 @@ class SettingsView(LoginRequiredMixin, View): def get(self, request: HttpRequest) -> HttpResponse: """HTTP GET: Return the view template.""" - return render(request, "home/account/settings.html") + context = { + "groups": request.user.groups.all(), + + "discord": None, + "github": None, + + "discord_provider": registry.provider_map.get("discord"), + "github_provider": registry.provider_map.get("github"), + } + + for account in SocialAccount.objects.filter(user=request.user).all(): + if account.provider == "discord": + context["discord"] = account + + elif account.provider == "github": + context["github"] = account + + return render(request, "home/account/settings.html", context=context) + + def post(self, request: HttpRequest) -> HttpResponse: + """HTTP POST: Process account disconnections.""" + provider = request.POST["provider"] + + if provider == "github": + try: + account = SocialAccount.objects.get(user=request.user, provider=provider) + except SocialAccount.DoesNotExist: + add_message(request, ERROR, "You do not have a GitHub account linked.") + else: + account.delete() + add_message(request, INFO, "The social account has been disconnected.") + else: + add_message(request, ERROR, f"Unknown provider: {provider}") + + return redirect(reverse("home")) diff --git a/pydis_site/apps/staff/models/role_mapping.py b/pydis_site/apps/staff/models/role_mapping.py index dff8081a..8a1fac2e 100644 --- a/pydis_site/apps/staff/models/role_mapping.py +++ b/pydis_site/apps/staff/models/role_mapping.py @@ -22,7 +22,7 @@ class RoleMapping(models.Model): ) is_staff = models.BooleanField( - help_text="Whether this role mapping related to a Django staff group", + help_text="Whether this role mapping relates to a Django staff group", default=False ) diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index 7db9499d..75702d47 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -96,3 +96,13 @@ button.is-size-navbar-menu, a.is-size-navbar-menu { padding-right: 1rem; } } + +/* Fix for modals being behind the navbar */ + +.modal * { + z-index: 1020; +} + +.modal-background { + z-index: 1010; +} diff --git a/pydis_site/static/js/base/modal.js b/pydis_site/static/js/base/modal.js new file mode 100644 index 00000000..eccc8845 --- /dev/null +++ b/pydis_site/static/js/base/modal.js @@ -0,0 +1,100 @@ +/* + modal.js: A simple way to wire up Bulma modals. + + This library is intended to be used with Bulma's modals, as described in the + official Bulma documentation. It's based on the JavaScript that Bulma + themselves use for this purpose on the modals documentation page. + + Note that, just like that piece of JavaScript, this library assumes that + you will only ever want to have one modal open at once. + */ + +"use strict"; + +// Event handler for the "esc" key, for closing modals. + +document.addEventListener("keydown", (event) => { + const e = event || window.event; + + if (e.code === "Escape" || e.keyCode === 27) { + closeModals(); + } +}); + +// An array of all the modal buttons we've already set up + +const modal_buttons = []; + +// Public API functions + +function setupModal(target) { + // Set up a modal's events, given a DOM element. This can be + // used later in order to set up a modal that was added after + // this library has been run. + + // We need to collect a bunch of elements to work with + const modal_background = Array.from(target.getElementsByClassName("modal-background")); + const modal_close = Array.from(target.getElementsByClassName("modal-close")); + + const modal_head = Array.from(target.getElementsByClassName("modal-card-head")); + const modal_foot = Array.from(target.getElementsByClassName("modal-card-foot")); + + const modal_delete = []; + const modal_button = []; + + modal_head.forEach((element) => modal_delete.concat(Array.from(element.getElementsByClassName("delete")))); + modal_foot.forEach((element) => modal_button.concat(Array.from(element.getElementsByClassName("button")))); + + // Collect all the elements that can be used to close modals + const modal_closers = modal_background.concat(modal_close).concat(modal_delete).concat(modal_button); + + // Assign click events for closing modals + modal_closers.forEach((element) => { + element.addEventListener("click", () => { + closeModals(); + }); + }); + + setupOpeningButtons(); +} + +function setupOpeningButtons() { + // Wire up all the opening buttons, avoiding buttons we've already wired up. + const modal_opening_buttons = Array.from(document.getElementsByClassName("modal-button")); + + modal_opening_buttons.forEach((element) => { + if (!modal_buttons.includes(element)) { + element.addEventListener("click", () => { + openModal(element.dataset.target); + }); + + modal_buttons.push(element); + } + }); +} + +function openModal(target) { + // Open a modal, given a string ID + const element = document.getElementById(target); + + document.documentElement.classList.add("is-clipped"); + element.classList.add("is-active"); +} + +function closeModals() { + // Close all open modals + const modals = Array.from(document.getElementsByClassName("modal")); + document.documentElement.classList.remove("is-clipped"); + + modals.forEach((element) => { + element.classList.remove("is-active"); + }); +} + +(function () { + // Set up all the modals currently on the page + const modals = Array.from(document.getElementsByClassName("modal")); + + modals.forEach((modal) => setupModal(modal)); + setupOpeningButtons(); +}()); diff --git a/pydis_site/templates/base/base.html b/pydis_site/templates/base/base.html index a9b31c0f..4c70d778 100644 --- a/pydis_site/templates/base/base.html +++ b/pydis_site/templates/base/base.html @@ -28,6 +28,7 @@ {# Font-awesome here is defined explicitly so that we can have Pro #} + diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index bd0bab40..6943c2b6 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -105,7 +105,7 @@ + +{% if user.is_authenticated %} + +{% endif %} diff --git a/pydis_site/templates/home/account/settings.html b/pydis_site/templates/home/account/settings.html index ba1d38a2..9f48d736 100644 --- a/pydis_site/templates/home/account/settings.html +++ b/pydis_site/templates/home/account/settings.html @@ -1,12 +1,137 @@ -{% extends 'base/base.html' %} -{% load static %} +{% load socialaccount %} -{% block title %}My Account{% endblock %} +{# This template is just for a modal, which is actually inserted into the navbar #} +{# template. Take a look at `navbar.html` to see how it's inserted. #} -{% block content %} - {% include "base/navbar.html" %} + - -{% endblock %} -- cgit v1.2.3 From 14cde62b4d8358032f1b1f706250efaaf9c646ea Mon Sep 17 00:00:00 2001 From: Akarys42 Date: Mon, 28 Oct 2019 18:50:23 +0100 Subject: Write tests for message.attachments --- pydis_site/apps/api/tests/test_deleted_messages.py | 9 ++++++--- pydis_site/apps/staff/tests/test_logs_view.py | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) (limited to 'pydis_site/apps/staff') diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index d1e9f2f5..b3a8197b 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -25,14 +25,16 @@ class DeletedMessagesWithoutActorTests(APISubdomainTestCase): 'id': 55, 'channel_id': 5555, 'content': "Terror Billy is a meanie", - 'embeds': [] + 'embeds': [], + 'attachments': [] }, { 'author': cls.author.id, 'id': 56, 'channel_id': 5555, 'content': "If you purge this, you're evil", - 'embeds': [] + 'embeds': [], + 'attachments': [] } ] } @@ -64,7 +66,8 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase): 'id': 12903, 'channel_id': 1824, 'content': "I hate trailing commas", - 'embeds': [] + 'embeds': [], + 'attachments': [] }, ] } diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py index 32cb6bbf..b01e3f3e 100644 --- a/pydis_site/apps/staff/tests/test_logs_view.py +++ b/pydis_site/apps/staff/tests/test_logs_view.py @@ -37,6 +37,7 @@ class TestLogsView(TestCase): channel_id=1984, content='I think my tape has run out...', embeds=[], + attachments=[], deletion_context=cls.deletion_context, ) @@ -101,6 +102,7 @@ class TestLogsView(TestCase): channel_id=1984, content='Does that mean this thing will halt?', embeds=[cls.embed_one, cls.embed_two], + attachments=['https://http.cat/100', 'https://http.cat/402'], deletion_context=cls.deletion_context, ) -- cgit v1.2.3 From 8e4ed686ebe06e8e15604d204969042efb434b30 Mon Sep 17 00:00:00 2001 From: Akarys42 Date: Wed, 20 Nov 2019 18:21:16 +0100 Subject: Test for the attachment image to be in the staff log --- pydis_site/apps/staff/tests/test_logs_view.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'pydis_site/apps/staff') diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py index b01e3f3e..1415c558 100644 --- a/pydis_site/apps/staff/tests/test_logs_view.py +++ b/pydis_site/apps/staff/tests/test_logs_view.py @@ -151,6 +151,21 @@ class TestLogsView(TestCase): self.assertInHTML(embed_colour_needle.format(colour=embed_one_colour), html_response) self.assertInHTML(embed_colour_needle.format(colour=embed_two_colour), html_response) + def test_if_both_attachments_are_included_html_response(self): + url = reverse('logs', host="staff", args=(self.deletion_context.id,)) + response = self.client.get(url) + + html_response = response.content.decode() + attachment_needle = 'Attachment' + self.assertInHTML( + attachment_needle.format(url=self.deleted_message_two.attachments[0]), + html_response + ) + self.assertInHTML( + attachment_needle.format(url=self.deleted_message_two.attachments[1]), + html_response + ) + def test_if_html_in_content_is_properly_escaped(self): url = reverse('logs', host="staff", args=(self.deletion_context.id,)) response = self.client.get(url) -- cgit v1.2.3