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') 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