Remove jquery, selectize and bootstrap js from conference frontend
authorMagnus Hagander <magnus@hagander.net>
Mon, 29 Jan 2024 08:50:55 +0000 (09:50 +0100)
committerMagnus Hagander <magnus@hagander.net>
Mon, 29 Jan 2024 08:50:55 +0000 (09:50 +0100)
This was used for the call for papers form handling of multiple
speakers at this point, and caused issues with skins that didn't realize
this and thus didn't include them.

Replace it with a native js version, which instead of doing autocomplete
just adds a button for "Add speaker" to add secondary speakers. This
shows a browser popup asking for the email address of an existing
profile.

In practice I think this actually makes it more clear to the user what's
going on than the previous autocomplete version, just not as pretty.

It was also used for tags, which being a much shorter list, could just
be replaced with a set of checkboxes. Tags was also something most
conferences didn't use, and it only showed up if explicitly enabled on
the conference (non-default).

postgresqleu/confreg/forms.py
postgresqleu/confreg/templatetags/dictutil.py
postgresqleu/confreg/views.py
postgresqleu/urls.py
postgresqleu/util/templates/forms/widgets/speaker_select.html [new file with mode: 0644]
postgresqleu/util/widgets.py
template.jinja/base.html
template.jinja/confreg/callforpapersform.html

index 6952b859720bedb8755d692f492bb4bc2b6fde20..cec0d7af237f86d26c4da581100dc28f957e7037 100644 (file)
@@ -6,6 +6,7 @@ from django.contrib.auth.models import User
 from django.utils.safestring import mark_safe
 from django.utils.html import escape
 from django.utils import timezone
+import django.db.models
 
 from postgresqleu.confsponsor.models import ScannedAttendee
 from .models import Conference
@@ -19,6 +20,7 @@ from .regtypes import validate_special_reg_type
 from .twitter import get_all_conference_social_media
 from postgresqleu.util.fields import UserModelChoiceField
 from postgresqleu.util.widgets import EmailTextWidget, MonospaceTextarea
+from postgresqleu.util.widgets import CallForPapersSpeakersWidget
 from postgresqleu.util.db import exec_to_list
 from postgresqleu.util.magic import magicdb
 from postgresqleu.util.backendlookups import GeneralAccountLookup
@@ -534,15 +536,17 @@ class CallForPapersForm(forms.ModelForm):
         if 'data' in kwargs and 'speaker' in kwargs['data']:
             vals.extend([int(x) for x in kwargs['data'].getlist('speaker')])
 
-        self.fields['speaker'].queryset = Speaker.objects.defer('photo', 'photo512').filter(pk__in=vals)
-        self.fields['speaker'].label_from_instance = lambda x: "{0} <{1}>".format(x.fullname, x.email)
+        self.fields['speaker'].widget = CallForPapersSpeakersWidget()
+        self.fields['speaker'].queryset = Speaker.objects.defer('photo', 'photo512').filter(pk__in=vals).annotate(
+            iscurrent=django.db.models.Case(django.db.models.When(pk=currentspeaker.pk, then=True), output_field=django.db.models.BooleanField())
+        ).order_by('iscurrent', 'fullname')
         self.fields['speaker'].required = True
-        self.fields['speaker'].help_text = "Type the beginning of a speakers email address to add more speakers"
 
         if not self.instance.conference.skill_levels:
             del self.fields['skill_level']
 
         if self.instance.conference.callforpaperstags:
+            self.fields['tags'].widget = forms.CheckboxSelectMultiple()
             self.fields['tags'].queryset = ConferenceSessionTag.objects.filter(conference=self.instance.conference)
             self.fields['tags'].label_from_instance = lambda x: x.tag
             self.fields['tags'].required = False
index 4360c096912a09a675deb1448399db207900bba9..9f3e5c7dd11f0b6f4badb20ce30e6b385b9973fb 100644 (file)
@@ -17,4 +17,4 @@ def arrayelement(value, key):
 def join_dictkeys(list_to_join, attrname, separator=', '):
     if not list_to_join:
         return ''
-    return separator.join(item[attrname] for item in list_to_join)
+    return separator.join(str(item[attrname]) for item in list_to_join)
index 452859b87eeae6ac4afd59b763fb4e04b1449149..10cc69bbc383ba86be7eeb277bab0be26d400866 100644 (file)
@@ -1742,7 +1742,7 @@ def callforpapers(request, confname):
 
     try:
         speaker = Speaker.objects.get(user=request.user)
-        sessions = ConferenceSession.objects.filter(conference=conference, speaker=speaker).order_by('title')
+        sessions = ConferenceSession.objects.filter(conference=conference, speaker=speaker).order_by('status', 'title')
         other_submissions = ConferenceSession.objects.filter(speaker=speaker).exclude(conference=conference).exists()
     except Speaker.DoesNotExist:
         other_submissions = False
@@ -1916,39 +1916,13 @@ def public_speaker_lookup(request, confname):
         raise Http404("No query")
 
     conference = get_conference_or_404(confname)
-    speaker = get_object_or_404(Speaker, user=request.user)
-
-    # This is a lookup for speakers that's public. To avoid harvesting, we allow
-    # only *prefix* matching of email addresses, and you have to type at least 6 characters
-    # before you get anything.
-    prefix = request.GET['query'].lower()
-    if len(prefix) > 5:
-        vals = [{
-            'id': s.id,
-            'value': "{0} <{1}>".format(s.fullname, s.email),
-        } for s in Speaker.objects.filter(user__email__startswith=prefix).exclude(fullname='')]
-    else:
-        vals = []
-    return HttpResponse(json.dumps({
-        'values': vals,
-    }), content_type='application/json')
-
-
-@login_required
-def public_tags_lookup(request, confname):
     if 'query' not in request.GET:
-        raise Http404("No query")
-
-    conference = get_conference_or_404(confname)
-    speaker = get_object_or_404(Speaker, user=request.user)
+        raise Http404("Query missing")
+    speaker = get_object_or_404(Speaker, user__email=request.GET.get('query', '').lower())
 
-    prefix = request.GET['query']
-    vals = [{
-        'id': t.id,
-        'value': t.tag,
-    } for t in ConferenceSessionTag.objects.filter(conference=conference)]
     return HttpResponse(json.dumps({
-        'values': vals,
+        'id': speaker.id,
+        'name': speaker.fullname,
     }), content_type='application/json')
 
 
index c8ea0acca908ba741f84b00e21acb2b3029f20d4..0d30f2a171b2fd80a13e1175838ff20afb010a6a 100644 (file)
@@ -142,7 +142,6 @@ urlpatterns.extend([
     url(r'^events/([^/]+)/callforpapers/(\d+)/delslides/(\d+)/$', postgresqleu.confreg.views.callforpapers_delslides),
     url(r'^events/([^/]+)/callforpapers/(\d+)/speakerconfirm/$', postgresqleu.confreg.views.callforpapers_confirm),
     url(r'^events/([^/]+)/callforpapers/lookups/speakers/$', postgresqleu.confreg.views.public_speaker_lookup),
-    url(r'^events/([^/]+)/callforpapers/lookups/tags/$', postgresqleu.confreg.views.public_tags_lookup),
     url(r'^events/callforpapers/$', postgresqleu.confreg.views.callforpaperslist),
     url(r'^events/([^/]+)/register/confirm/$', postgresqleu.confreg.views.confirmreg),
     url(r'^events/([^/]+)/register/policy/$', postgresqleu.confreg.views.regconfirmpolicy),
diff --git a/postgresqleu/util/templates/forms/widgets/speaker_select.html b/postgresqleu/util/templates/forms/widgets/speaker_select.html
new file mode 100644 (file)
index 0000000..94a5104
--- /dev/null
@@ -0,0 +1,9 @@
+<select name="{{ widget.name }}" style="display: none" multiple>{% for option in widget.options %}
+  <option value="{{option.value}}" selected>
+{%endfor%}
+</select>
+<ul class="pgeu-speaker-list">
+{% for option in widget.options %}<li data-id="{{option.value}}">{{option.label}}{% if not forloop.first %} (<a class="pgeu-speaker-remove" href="#">remove</a>){%endif%}</li>
+{%endfor%}
+</ul>
+<button id="{{ widget.name}}-add" class="pgeu-speaker-add">Add speaker</button>
index 146cd458ed924e1418b815e4dca1fede41a56cf5..8a3302047664baf129c0cb1d80594026cacae4c7 100644 (file)
@@ -168,3 +168,12 @@ class SelectSetValueWidget(forms.Select):
         context = super().get_context(name, value, attrs)
         context['setmap'] = self.setvalues
         return context
+
+
+class CallForPapersSpeakersWidget(forms.SelectMultiple):
+    template_name = 'forms/widgets/speaker_select.html'
+
+    def get_context(self, name, value, attrs):
+        context = super().get_context(name, value, attrs)
+        context['widget']['options'] = list(self.options(name, context['widget']['value'], attrs))
+        return context
index 394580ee418ba8d8bacf14ad7532080793b35ab6..06d6b9183f0718f237bd9cc1fc7aff2c7ad1067f 100644 (file)
@@ -43,9 +43,6 @@
       </div>
     </footer>
 
-
-    {{ asset("js", "jquery3") }}
-    {{ asset("js", "bootstrap4") }}
 {%block pagescript%}{%endblock%}
   </body>
 </html>
index c47cef1ab9bb01b4d2430771ee886a7a39646a29..41fe0d6b447935cb61657e17dfc2696fe0141463 100644 (file)
@@ -1,44 +1,67 @@
 {%extends "base.html" %}
 {%block title%}Call for Papers - {{conference}}{%endblock%}
 {%block pagescript%}
-{{asset("css", "selectize")}}
-{{asset("js", "selectize")}}
-
-<script language="javascript">
-$(function() {
-  /* Re-enable the speaker field, and turn it into selectize */
-  $('tr#tr_speaker').css({'display': 'table-row'});
-  $('#id_speaker').selectize({
-    plugins: ['remove_button'],
-    valueField: 'id',
-    labelField: 'value',
-    searchField: 'value',
-    load: async function(query, callback) {
-       if (!query.length) return callback();
-       let url = new URL('/events/{{conference.urlname}}/callforpapers/lookups/speakers/', document.location.href);
-       url.searchParams.append('query', query);
-       let response = await fetch(url);
-       let data = await response.json();
-       callback(data.values);
-    },
-  });
+<script language="javascript" defer>
+  async function addSpeakerClick(event) {
+      event.preventDefault();
+
+      let email = prompt('Enter the email address of the speaker to add.');
+      if (email) {
+         let res = await fetch('/events/{{conference.urlname}}/callforpapers/lookups/speakers/?' + new URLSearchParams({'query': email}));
+         if (res.status != 200) {
+             alert('Speaker not found.\n\nNote that thee speaker must have an existing profile on this site with the given email address before they can be adde to a session.\n');
+             return;
+         }
+         let speaker = await res.json();
+
+         let ul = event.target.previousElementSibling;
+         let select = ul.previousElementSibling;
+
+         if (select.querySelector('option[value="' + speaker.id + '"]')) {
+             alert('This speaker has already been added.');
+             return;
+         }
+
+         let newli = document.createElement('li');
+         newli.dataset.id = speaker.id;
+         newli.innerHTML = speaker.name + ' (<a class="pgeu-speaker-remove" href=#"#>remove</a>)';
+         ul.appendChild(newli);
+
+         let newoption = document.createElement('option');
+         newoption.value = speaker.id;
+         newoption.selected = true;
+         select.appendChild(newoption);
+      }
+
+      return false;
+  }
+
+  function removeSpeakerClick(event) {
+      if (event.target.tagName == 'A' && event.target.classList.contains('pgeu-speaker-remove')) {
+         event.preventDefault();
+
+         let idtoremove = event.target.parentNode.dataset.id;
 
-  /* Selectize the tags field, if it exists */
-  $('#id_tags').selectize({
-    plugins: ['remove_button'],
-    valueField: 'id',
-    labelField: 'value',
-    searchField: 'value',
-    load: async function(query, callback) {
-       if (!query.length) return callback();
-       let url = new URL('/events/{{conference.urlname}}/callforpapers/lookups/tags/', document.location.href);
-       url.searchParams.append('query', query);
-       let response = await fetch(url);
-       let data = await response.json();
-       callback(data.values);
-    },
+         if (!confirm('Are you sure you want to remove this speaker?')) {
+             return;
+         }
+
+         /* <a>.<li>.<ul>.<select> */
+         event.target.parentNode.parentNode.previousElementSibling.querySelector('option[value="' + idtoremove + '"]').remove();
+
+         event.target.parentNode.remove();
+
+         alert('Speaker removed. You have to also save the form to make it permanent.');
+      }
+  }
+
+
+  document.querySelectorAll("button.pgeu-speaker-add").forEach((button) => {
+      button.addEventListener('click', addSpeakerClick);
+  });
+  document.querySelectorAll("ul.pgeu-speaker-list").forEach((ul) => {
+      ul.addEventListener('click', removeSpeakerClick);
   });
-});
 </script>
 {%endblock%}
 
@@ -47,11 +70,6 @@ $(function() {
 tr.err {
    background-color: #ffb6b6;
 }
-
-/* Hide the speaker field for non-javascript sessions */
-tr#tr_speaker {
-   display:none;
-}
 </style>
 {%endblock%}