Skip to content

Commit

Permalink
Replace bootstrap multiselect with django-select2
Browse files Browse the repository at this point in the history
  • Loading branch information
rebkwok committed Sep 2, 2024
1 parent 22a507b commit c4cc479
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 111 deletions.
10 changes: 10 additions & 0 deletions pipsevents/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
'crispy_forms',
'crispy_bootstrap4',
'debug_toolbar',
'django_select2',
'accounts',
'booking',
'common',
Expand Down Expand Up @@ -122,15 +123,24 @@
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'test-pips',
},
"select2": {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'test-select2',
}
}
else: # pragma: no cover
CACHES = {
"default": {
"BACKEND": 'django.core.cache.backends.filebased.FileBasedCache',
"LOCATION": root("cache"),
},
"select2": {
"BACKEND": 'django.core.cache.backends.filebased.FileBasedCache',
"LOCATION": os.path.join(root("cache"), "select2"),
}
}
SELECT2_CACHE_BACKEND = "select2"


AUTHENTICATION_BACKENDS = (
Expand Down
1 change: 1 addition & 0 deletions pipsevents/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

urlpatterns = [
path('admin/', admin.site.urls),
path("select2/", include("django_select2.urls")),
path('studioadmin/', include('studioadmin.urls')),
path('', include('booking.urls')),
path(
Expand Down
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ django-migration-testcase
django-one-time-notices
django-paypal
djangorestframework
django-select2
dj-static
psycopg2
python-dateutil
Expand Down
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ django==5.1
# crispy-bootstrap4
# dj-database-url
# django-allauth
# django-appconf
# django-braces
# django-ckeditor
# django-classy-tags
Expand All @@ -47,10 +48,13 @@ django==5.1
# django-migration-testcase
# django-one-time-notices
# django-paypal
# django-select2
# djangorestframework
# model-bakery
django-allauth[socialaccount]==64.0.0
# via -r requirements.in
django-appconf==1.0.6
# via django-select2
django-braces==1.15.0
# via -r requirements.in
django-ckeditor==6.7.1
Expand Down Expand Up @@ -84,6 +88,8 @@ django-one-time-notices==1.0.0
# via -r requirements.in
django-paypal==2.1
# via -r requirements.in
django-select2==8.2.1
# via -r requirements.in
djangorestframework==3.15.2
# via -r requirements.in
docopt==0.6.2
Expand Down
71 changes: 40 additions & 31 deletions studioadmin/forms/email_users_forms.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,76 @@
# -*- coding: utf-8 -*-

from collections.abc import Mapping
from datetime import datetime
from typing import Any
from django.forms.utils import ErrorList
import pytz
from django import forms
from django.conf import settings
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.models import User
from django.forms.models import modelformset_factory, BaseModelFormSet
from django.utils import timezone

from django_select2 import forms as s2forms

from booking.models import Event

from ckeditor.widgets import CKEditorWidget


def get_event_names(event_type):
class StudentWidget(s2forms.ModelSelect2MultipleWidget):
search_fields = [
"username__icontains",
"first_name__icontains",
"last_name__icontains",
]

def callable():
def _format_datetime(dt):
uk = pytz.timezone('Europe/London')
uk_date = dt.astimezone(uk)
return uk_date.strftime('%d-%b-%y, %H:%M')
def get_queryset(self):
return User.objects.all().order_by("first_name", "last_name")

EVENT_CHOICES = [(event.id, f"{event.name} - {_format_datetime(event.date)}") for event in Event.objects.filter(
event_type__event_type=event_type, date__gte=timezone.now()
).order_by('date')]
return tuple(EVENT_CHOICES)
def label_from_instance(self, obj):
return f"{obj.first_name} {obj.last_name} ({obj.username})"

return callable

class EventWidget(s2forms.ModelSelect2MultipleWidget):
search_fields = [
"name__icontains",
]

def get_students():
def get_queryset(self):
return Event.objects.filter(
event_type__event_type="EV", date__gte=timezone.now()
).order_by('date')


class LessonWidget(s2forms.ModelSelect2MultipleWidget):
search_fields = [
"name__icontains",
]
def get_queryset(self):
return Event.objects.filter(
event_type__event_type="CL", date__gte=timezone.now()
).order_by('date')

def callable():
return tuple(
[
(user.id, '{} {} ({})'.format(
user.first_name, user.last_name, user.username
)) for user in User.objects.all().order_by("first_name")
]
)
return callable


class UserFilterForm(forms.Form):

events = forms.MultipleChoiceField(
choices=get_event_names('EV'),
widget=forms.SelectMultiple(attrs={"class": "form-control"}),
widget=EventWidget(attrs={"class": "form-control"}),
required=False,
label=""
label="Choose events/workshops (search by name)",
)

lessons = forms.MultipleChoiceField(
choices=get_event_names('CL'),
widget=forms.SelectMultiple(attrs={"class": "form-control"}),
widget=LessonWidget(attrs={"class": "form-control"}),
required=False,
label=""
label="Choose classes (search by name)"
)
students = forms.MultipleChoiceField(
choices=get_students(),
widget=forms.SelectMultiple(attrs={"class": "form-control"}),
widget=StudentWidget(attrs={"class": "form-control"}),
required=False,
label=""
label="Choose students (search by first name, last name or username)"
)


Expand Down
16 changes: 6 additions & 10 deletions studioadmin/tests/test_forms/test_users_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,9 @@ def setUp(self):
def test_events_dropdown(self):
form = UserFilterForm()
event_field = form.fields['events']
event_choices = [
choice for choice in event_field.widget.choices
]
self.assertEqual(len(event_choices), 3)
event_ids = [id for (id, name) in event_choices]
qs = event_field.widget.get_queryset()
self.assertEqual(qs.count(), 3)
event_ids = [event.id for event in qs]
event_type = set([
event.event_type.event_type
for event in Event.objects.filter(id__in=event_ids)
Expand All @@ -110,11 +108,9 @@ def test_events_dropdown(self):
def test_lessons_dropdown(self):
form = UserFilterForm()
lesson_field = form.fields['lessons']
lesson_choices = [
choice for choice in lesson_field.widget.choices
]
self.assertEqual(len(lesson_choices), 4)
lesson_ids = [id for (id, name) in lesson_choices]
qs = lesson_field.widget.get_queryset()
self.assertEqual(qs.count(), 4)
lesson_ids = [lesson.id for lesson in qs]
event_type = set([
event.event_type.event_type
for event in Event.objects.filter(id__in=lesson_ids)
Expand Down
2 changes: 2 additions & 0 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@

<script src="https://unpkg.com/[email protected]" integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h" crossorigin="anonymous" defer></script>
<script src="https://unpkg.com/htmx.org/dist/ext/class-tools.js" defer></script>
{% block jquery %}
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
{% endblock %}
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU=" crossorigin="anonymous"></script>
{#<script src="https://formbuilder.online/assets/js/form-builder.min.js"></script>#}

Expand Down
77 changes: 7 additions & 70 deletions templates/studioadmin/choose_users_form.html
Original file line number Diff line number Diff line change
@@ -1,31 +1,17 @@
{% extends "studioadmin/base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% load bookingtags %}

{% block extra_head %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-multiselect/0.9.15/css/bootstrap-multiselect.css" integrity="sha512-EvvoSMXERW4Pe9LjDN9XDzHd66p8Z49gcrB7LCUplh0GcEHiV816gXGwIhir6PJiwl0ew8GFM2QaIg2TW02B9A==" crossorigin="anonymous" />
{% endblock %}

{% block studioadmincontent %}
<h2>Choose Students to Email </h2>
<p>Select by class, workshop/event and/or select specific students.</p>

<div id="spin-container">
<i id="spin1" class="fa-2x fas fa-spinner fa-spin email-spinner"></i>
<i id="spin2" class="fa-2x fas fa-spinner fa-spin email-spinner"></i>
<i id="spin3" class="fa-2x fas fa-spinner fa-spin email-spinner"></i>
</div>
<a id="toggler" href="#" data-toggle="collapse" data-target="#userfilters">
<h4><span class="fa fa-plus-square"></span> View/hide selection options</h4>
</a>
<div class="card card-wm">
<span id="userfilters" class="collapse">

{{ userfilterform.media.css }}
<div class="card-body">
<form action="" method="post">
{% csrf_token %}
{{ userfilterform|crispy }}
{{ userfilterform.as_p }}
<div class="col-12 mt-2">
<input class="btn table-btn btn-secondary" name="filter" type="submit" value="Show Students" /><br/>
</div>
Expand Down Expand Up @@ -79,59 +65,10 @@ <h4><span class="fa fa-plus-square"></span> View/hide selection options</h4>
{% endif %}
</div>

{% comment %} js needs to be loaded in the body for django-select2 {% endcomment %}
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
{{ userfilterform.media.js }}
{% endblock studioadmincontent %}


{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-multiselect/0.9.15/js/bootstrap-multiselect.min.js" integrity="sha512-aFvi2oPv3NjnjQv1Y/hmKD7RNMendo4CZ2DwQqMWzoURKxcqAoktj0nNG4LU8m23+Ws9X5uVDD4OXLqpUVXD5Q==" crossorigin="anonymous"></script>
<script type="text/javascript">
$jq(function() {
alerted1 = function () {
document.getElementById('spin1').remove();
}
alerted2 = function () {
document.getElementById('spin2').remove();
}
alerted3 = function () {
document.getElementById('spin3').remove();
}

$jq('#id_filter-lessons').multiselect({
buttonClass: 'btn btn-sm btn-wm mt-2',
buttonWidth: '100%',
maxHeight: 400,
disableIfEmpty: true,
numberDisplayed: 1,
nonSelectedText: 'Click to choose classes',
enableFiltering: true,
enableCaseInsensitiveFiltering: true,
onInitialized: alerted1
});
$jq('#id_filter-events').multiselect({
buttonClass: 'btn btn-sm btn-wm mt-2',
buttonWidth: '100%',
maxHeight: 400,
disableIfEmpty: true,
numberDisplayed: 1,
nonSelectedText: 'Click to choose workshops/events',
enableFiltering: true,
enableCaseInsensitiveFiltering: true,
onInitialized: alerted2
});
$jq('#id_filter-students').multiselect({
buttonClass: 'btn btn-sm btn-wm mt-2',
buttonWidth: '100%',
maxHeight: 400,
disableIfEmpty: true,
numberDisplayed: 1,
nonSelectedText: 'Click to choose students',
enableFiltering: true,
enableCaseInsensitiveFiltering: true,
onInitialized: alerted3
});


});
</script>

{% endblock %}
{% comment %} don't load the base js again {% endcomment %}
{% block jquery %}{% endblock %}

0 comments on commit c4cc479

Please sign in to comment.