aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Gareth Coles <[email protected]>2019-10-23 01:07:24 +0100
committerGravatar Gareth Coles <[email protected]>2019-10-23 01:07:24 +0100
commit49376a76289ab22f1aff55d8971b0ea198ec9316 (patch)
tree5ad08c22d37dcf2c922e3d15c9df5df435bbdf2f
parentAdd 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.py3
-rw-r--r--pydis_site/apps/home/views/account/settings.py48
-rw-r--r--pydis_site/apps/staff/models/role_mapping.py2
-rw-r--r--pydis_site/static/css/base/base.css10
-rw-r--r--pydis_site/static/js/base/modal.js100
-rw-r--r--pydis_site/templates/base/base.html1
-rw-r--r--pydis_site/templates/base/navbar.html18
-rw-r--r--pydis_site/templates/home/account/settings.html141
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 %}