diff options
author | 2019-10-23 01:07:24 +0100 | |
---|---|---|
committer | 2019-10-23 01:07:24 +0100 | |
commit | 49376a76289ab22f1aff55d8971b0ea198ec9316 (patch) | |
tree | 5ad08c22d37dcf2c922e3d15c9df5df435bbdf2f | |
parent | Add role mapping migration I forgot to commit (diff) |
Add user settings modal, with connections management and account deletion
-rw-r--r-- | pydis_site/apps/home/urls.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/home/views/account/settings.py | 48 | ||||
-rw-r--r-- | pydis_site/apps/staff/models/role_mapping.py | 2 | ||||
-rw-r--r-- | pydis_site/static/css/base/base.css | 10 | ||||
-rw-r--r-- | pydis_site/static/js/base/modal.js | 100 | ||||
-rw-r--r-- | pydis_site/templates/base/base.html | 1 | ||||
-rw-r--r-- | pydis_site/templates/base/navbar.html | 18 | ||||
-rw-r--r-- | pydis_site/templates/home/account/settings.html | 141 |
8 files changed, 310 insertions, 13 deletions
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 #} <script src="https://kit.fontawesome.com/ae6a3152d8.js"></script> + <script src="{% static "js/base/modal.js" %}"></script> <link rel="stylesheet" href="{% static "css/base/base.css" %}"> <link rel="stylesheet" href="{% static "css/base/notification.css" %}"> 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 @@ <div class="field navbar-item is-paddingless is-fullwidth is-grouped"> <button type="submit" class="button is-white is-inline is-fullwidth has-text-left is-size-navbar-menu has-text-grey-dark">Logout</button> - <a title="Settings" class="button is-white is-inline has-text-right is-size-navbar-menu has-text-grey-dark" href="{% url "account_settings" %}"> + <a title="Settings" class="button is-white is-inline has-text-right is-size-navbar-menu has-text-grey-dark modal-button" data-target="account-modal"> <span class="is-icon"> <i class="fas fa-cog"></i> </span> @@ -124,3 +124,19 @@ </a> </div> </nav> + +{% if user.is_authenticated %} + <script defer type="text/javascript"> + "use strict"; + + let element = document.createElement("div"); + document.body.prepend(element); + + fetch("{% url "account_settings" %}") + .then((response) => response.text()) + .then((text) => { + element.outerHTML = text; + setupModal(document.getElementById("account-modal")); + }); + </script> +{% 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" %} +<div class="modal" id="account-modal"> + <div class="modal-background"></div> + <div class="modal-card"> + <div class="modal-card-head"> + <div class="modal-card-title">Settings for {{ user.username }}</div> - <section class="section"> + {% if groups %} + <div> + {% for group in groups %} + <span class="tag is-primary">{{ group.name }}</span> + {% endfor %} + </div> + {% else %} + <span class="tag is-dark">No groups</span> + {% endif %} + </div> + <div class="modal-card-body"> + <h3 class="title">Connections</h3> + <div class="columns"> + {% if discord_provider is not None %} + <div class="column"> + <div class="box"> + {% if not discord %} + <div class="media"> + <div class="media-left"> + <div class="image"> + <i class="fab fa-discord fa-3x has-text-primary"></i> + </div> + </div> + <div class="media-content"> + <div class="title is-5">Discord</div> + <div class="subtitle is-6">Not connected</div> + </div> + </div> + <div> + <br /> + <a class="button is-primary" href="{% provider_login_url "discord" process="connect" %}"> + <span class="icon"> + <i class="fad fa-link"></i> + </span> + <span>Connect</span> + </a> + </div> + {% else %} + <div class="media"> + <div class="media-left"> + <div class="image"> + <i class="fab fa-discord fa-3x has-text-primary"></i> + </div> + </div> + <div class="media-content"> + <div class="title is-5">Discord</div> + <div class="subtitle is-6">{{ user.username }}</div> + </div> + </div> + <div> + <br /> + <button class="button" disabled> + <span class="icon"> + <i class="fas fa-check"></i> + </span> + <span>Connected</span> + </button> + </div> + {% endif %} + </div> + </div> + {% endif %} + + {% if github_provider is not None %} + <div class="column"> + <div class="box"> + {% if not github %} + <div class="media"> + <div class="media-left"> + <div class="image"> + <i class="fab fa-github fa-3x"></i> + </div> + </div> + <div class="media-content"> + <div class="title is-5">GitHub</div> + <div class="subtitle is-6">Not connected</div> + </div> + </div> + <div> + <br /> + <a class="button is-primary" href="{% provider_login_url "github" process="connect" %}"> + <span class="icon"> + <i class="fad fa-link"></i> + </span> + <span>Connect</span> + </a> + </div> + {% else %} + <div class="media"> + <div class="media-left"> + <div class="image"> + <i class="fab fa-github fa-3x"></i> + </div> + </div> + <div class="media-content"> + <div class="title is-5">GitHub</div> + <div class="subtitle is-6">{{ github.extra_data.name }}</div> + </div> + </div> + <div> + <form method="post" action="{% url "account_settings" %}" type="submit"> + {% csrf_token %} + + <input type="hidden" name="provider" value="github" /> + + <br /> + <button type="submit" class="button is-danger"> + <span class="icon"> + <i class="fas fa-times"></i> + </span> + <span>Disconnect</span> + </button> + </form> + </div> + {% endif %} + </div> + </div> + {% endif %} + </div> + </div> + <div class="modal-card-foot"> + <a class="button is-danger" href="{% url "account_delete" %}">Delete Account</a> + </div> + </div> +</div> - </section> -{% endblock %} |