Skip to content

Commit

Permalink
Convert site from Bootstrap4 to Bootstrap5 (#159)
Browse files Browse the repository at this point in the history
* convert to bootstrap5

* add support for directing back to engineering when on engineering post

* fix support for blog on mobile

* add test form

* Remove JQuery from form validation code (#160)

* Replace JQuery in input file and error message code

* Replace JQuery in form validation script

* Fix event listeners and clean up code

* Remove JQuery script

* Use query selectors for easier iteration

* Make error list clearing more efficient

* Use template string literals instead of concatenation

Co-authored-by: David Alexander <TheLonelyGhost@users.noreply.github.com>

* Fix form labels being deleted when displaying form errors

Co-authored-by: David Alexander <TheLonelyGhost@users.noreply.github.com>

* remove test form

Co-authored-by: Joe Kaufeld <joe@kenzie.academy>
Co-authored-by: TimJentzsch <tim-jentzsch@gmx.de>
Co-authored-by: David Alexander <TheLonelyGhost@users.noreply.github.com>
  • Loading branch information
4 people committed Jun 25, 2021
1 parent de4ad5a commit e956e40
Show file tree
Hide file tree
Showing 14 changed files with 328 additions and 101 deletions.
8 changes: 5 additions & 3 deletions authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
from django.contrib.auth import login, logout
from django.http.response import HttpResponse
from django.shortcuts import HttpResponseRedirect, render
from django.shortcuts import HttpResponseRedirect, render, reverse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
from rest_framework.request import Request

from authentication.backends import EmailBackend
from website.forms import LoginForm
from website.helpers import get_additional_context


@method_decorator(csrf_exempt, name="dispatch")
class LoginView(TemplateView):
def get(self, request: Request, *args: object, **kwargs: object) -> HttpResponse:
"""Retrieve the rendered login form."""
form = LoginForm()
return render(request, "website/generic_form.html", {"form": form})
context = get_additional_context({"form": form, "slim_form": True})
return render(request, "website/generic_form.html", context)

def post(
self, request: Request, *args: object, **kwargs: object
Expand All @@ -41,4 +43,4 @@ def post(
def logout_view(request: Request) -> HttpResponseRedirect:
"""Log out the user who has sent the request."""
logout(request)
return HttpResponseRedirect("/")
return HttpResponseRedirect(request.META.get("HTTP_REFERER", reverse("homepage")))
12 changes: 6 additions & 6 deletions blossom/management/commands/bootstrap_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,19 @@
<div class="row">
<div class="col-lg-3">
<div class="input-group input-group-lg needs-validation mb-3">
<div class="input-group-prepend">
<span class="input-group-text">$</span>
</div>
<span class="input-group-text" id="donationAmountLabel">$</span>
<input type="text" id="donationAmount" class="form-control" value="10"
aria-label="Amount (to the nearest dollar)">
aria-label="Amount (to the nearest dollar)" aria-describedby="donationAmountLabel">
</div>
<div class="invalid-feedback">
Please provide a valid number.
</div>
</div>
<div class="col-lg-9">
<button class="btn btn-block btn-outline-secondary btn-lg shadow" id="checkout-button">Donate!
</button>
<div class="d-grid">
<button class="btn btn-block btn-outline-secondary btn-lg shadow" id="checkout-button">Donate!
</button>
</div>
</div>
</div>
<div class="card-footer">
Expand Down
2 changes: 2 additions & 0 deletions blossom/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.forms",
"django.contrib.staticfiles",
# additional functionality
"widget_tweaks",
Expand Down Expand Up @@ -108,6 +109,7 @@
},
},
]
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"

WSGI_APPLICATION = "blossom.wsgi.application"

Expand Down
8 changes: 8 additions & 0 deletions blossom/static_dev/js/potentially.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 23 additions & 24 deletions blossom/templates/website/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
{% load static %}

{% block content %}
<link rel="stylesheet" href="//cdn.datatables.net/1.10.20/css/jquery.dataTables.min.css">

<link href="https://cdn.jsdelivr.net/npm/simple-datatables@latest/dist/style.css" rel="stylesheet" type="text/css">
<h1>
Admin Panel - Posts
</h1>
Expand All @@ -13,32 +12,32 @@ <h1>

<a href="{% url 'post_create' %}" class="btn btn-primary">Create new post</a>
<p></p>
<table class="table table-hover" id="postTable">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Title</th>
<th scope="col">Published?</th>
<th scope="col">Date</th>
</tr>
</thead>
<tbody>
{% for p in posts %}
<div class="table-responsive">
<table class="table table-hover" id="postTable">
<thead>
<tr>
<th scope="row"><a href="{{ p.get_absolute_url }}edit">{{ p.id }}</a></th>
<td><a href="{{ p.get_absolute_url }}edit">{{ p.title }}</a></td>
<td>{% if p.published %}✓{% else %}✗{% endif %}</td>
<td>{{ p.date }}</td>
<th scope="col">#</th>
<th scope="col">Title</th>
<th scope="col">Published?</th>
<th scope="col">Date</th>
</tr>
{% endfor %}
</tbody>
</table>
</thead>
<tbody>
{% for p in posts %}
<tr>
<th scope="row"><a href="{{ p.get_absolute_url }}edit">{{ p.id }}</a></th>
<td><a href="{{ p.get_absolute_url }}edit">{{ p.title }}</a></td>
<td>{% if p.published %}✓{% else %}✗{% endif %}</td>
<td>{{ p.date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest" type="text/javascript"></script>

<script src="//cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function () {
$('#postTable').DataTable();
});
const dataTable = new simpleDatatables.DataTable("#postTable")
</script>

{% endblock %}
18 changes: 18 additions & 0 deletions blossom/templates/website/error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% extends 'website/partials/base.partial' %}

{% block content %}
<div class="my-5">
<div class="text-center">
<h1 class="mb-5">{{ error_message }}</h1>
<div class="row">
<div class="col-md-2"></div>
<div class="col-md-8">
<div class="d-grid">
<a href="{% url 'homepage' %}" class="btn btn-primary">Back to safety!</a>
</div>
</div>
<div class="col-md-2"></div>
</div>
</div>
</div>
{% endblock %}
174 changes: 164 additions & 10 deletions blossom/templates/website/generic_form.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,59 @@
{% extends 'website/partials/base.partial' %}
{% load widget_tweaks %}
{% load static %}
{% load string_helpers %}

{% block content %}
<h1>{{ header }}</h1>
<h4>{{ subheader }}</h4>
<div class="container mt-4">
{% if slim_form %}
<div class="row">
<div class="col-sm-0 col-md-2 col-lg-3"></div>
<div class="col-sm-0 col-md-8 col-lg-6">
{% endif %}

<div class="container">
{# Lifted from https://simpleisbetterthancomplex.com/article/2017/08/19/how-to-render-django-form-manually.html#}
<form method="post">
<h1>{{ header }}</h1>
<h4>{{ subheader }}</h4>

{% if messages %}
{% for message in messages %}
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% else %}
<div class="alert alert-dark" role="alert">
{{ message }}
</div>
{% endif %}
{% endfor %}
{% endif %}

{% if header_js_buttons or header_link_buttons %}
<div class="row">
{% for item in header_js_buttons %}
<div class="col mt-2">
<button class="btn btn-info btn-block"
onclick="{{ item.onclick }}">{{ item.text }}</button>
</div>
{% endfor %}
{% for item in header_link_buttons %}
<div class="col mt-2">
<a href="{% url item.reverse_url %}"
class="btn btn-info btn-block">{{ item.text }}</a>
</div>
{% endfor %}
</div>
{% endif %}

<div class="container mt-3">
{# Lifted from https://simpleisbetterthancomplex.com/article/2017/08/19/how-to-render-django-form-manually.html#}
{% if form.is_multipart %}
{# properly handle the file uploads #}
<form novalidate enctype="multipart/form-data" method="post" action="">
{% else %}
<form novalidate method="post" action="">
{% endif %}
<div class="errorMessages"></div>
{% csrf_token %}
{% for hidden_field in form.hidden_fields %}
{{ hidden_field }}
Expand All @@ -24,8 +69,6 @@ <h4>{{ subheader }}</h4>

{% for field in form.visible_fields %}
<div class="form-group">
{{ field.label_tag }}

{% if form.is_bound %}
{% if field.errors %}
{% render_field field class="form-control is-invalid" %}
Expand All @@ -35,21 +78,132 @@ <h4>{{ subheader }}</h4>
</div>
{% endfor %}
{% else %}
{{ field.label_tag }}
{% render_field field class="form-control is-valid" %}
{% endif %}
{% else %}
{% render_field field class="form-control" %}
{% if field.field.widget.input_type == 'file' %}
{{ field.label_tag }}
<div class="btn-group-toggle">
<label for="{{ field.auto_id }}"
class="form-control-file btn btn-secondary">browse</label>
{% render_field field class="inputfile" style="visibility:hidden;margin-top:-30px" %}
</div>
{% elif field.field.widget.input_type == 'checkbox' %}
<div class="form-check form-switch">
{{ field.label_tag|trim_label:1 }}
{% render_field field class="form-check-input" %}
</div>
{% else %}
{{ field.label_tag }}
{% render_field field class="form-control" %}
{% endif %}
{% endif %}

{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary">Submit</button>
</form>

<div class="row mt-3">
<div class="col-md-1 col-lg-2"></div>
<div class="col-md-10 col-lg-8">
<div class="d-grid gap-1">
<button type="submit" class="btn btn-primary mt-3">Submit</button>
</div>
</div>
<div class="col-md-1 col-lg-2"></div>
</div>
</form>
</div>
{% if slim_form %}
</div>
<div class="col-sm-0 col-md-2 col-lg-3"></div>
</div>
{% endif %}
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll('.inputfile').forEach(function (input) {
let label = input.previousElementSibling

input.addEventListener('change', function (e) {
let fileName = '';

fileName = e.target.value.split('\\').pop();

label.innerText = fileName ? fileName : "browse";
});
});

// Adapted from https://www.tjvantoll.com/2012/08/05/html5-form-validation-showing-all-error-messages/
document.querySelectorAll("form").forEach(function (form) {
const errorList = form.querySelector(".errorMessages");

if (errorList) {
const showAllErrorMessages = function () {
errorList.textContent = "";

// Find all invalid fields within the form.
form.querySelectorAll(":invalid").forEach(function (node) {
// Find the field's corresponding label
const label = form.querySelector(`label[for="${node.id}"]`);
// Opera incorrectly does not fill the validationMessage property.
const message = node.validationMessage || 'Invalid value.';

if (!errorList.classList.contains("mt-3")) {
errorList.classList.add("mt-3");
}

// Add the error message to the list
const errorMessage = document.createElement("span");
errorMessage.innerText = " " + message;

const errorElement = document.createElement("div");
errorElement.classList.add("alert");
errorElement.classList.add("alert-danger");
errorElement.role = "alert";
if (label) {
errorElement.appendChild(label.cloneNode(true));
}
errorElement.appendChild(errorMessage);

errorList.appendChild(errorElement);
});
};

// Support Safari
form.addEventListener("submit", function (event) {
if (this.checkValidity && !this.checkValidity()) {
const firstInvalid = form.querySelector(":invalid");
if (firstInvalid) {
firstInvalid.focus();
}
event.preventDefault();
}
});

const submitButton = form.querySelector("input[type=submit], button:not([type=button])");
if (submitButton) {
submitButton.addEventListener("click", showAllErrorMessages);
}

form.querySelectorAll("input").forEach(function (inputElem) {
inputElem.addEventListener("keypress", function (event) {
const checkedTypes = ["date", "email", "month", "number", "search", "tel", "text", "time", "url", "week"]
if (checkedTypes.includes(inputElem.type)
&& event.code === "Enter") {
showAllErrorMessages();
}
});
});
}
});
})
</script>

{% if enable_trumbowyg %}
{# base #}
<link rel="stylesheet" href="{% static "js/trumbowyg/ui/trumbowyg.min.css" %}">
Expand Down
Loading

0 comments on commit e956e40

Please sign in to comment.