diff options
| -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 %} | 
