Skip to content

Commit 1016b3a

Browse files
committed
Allow slides drag-drop between sessions for groups meeting with multiple sessions. Fixes ietf-tools#2700. Commit ready to merge.
- Legacy-Id: 17024
1 parent 34f9393 commit 1016b3a

7 files changed

Lines changed: 493 additions & 57 deletions

File tree

ietf/externals/static/Sortable/Sortable.min.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ietf/meeting/tests_views.py

Lines changed: 326 additions & 13 deletions
Large diffs are not rendered by default.

ietf/meeting/urls.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright The IETF Trust 2007, All Rights Reserved
1+
# Copyright The IETF Trust 2007-2019, All Rights Reserved
22

33
from django.conf.urls import include
44
from django.views.generic import RedirectView
@@ -15,7 +15,9 @@
1515
url(r'^session/(?P<session_id>\d+)/agenda$', views.upload_session_agenda),
1616
url(r'^session/(?P<session_id>\d+)/propose_slides$', views.propose_session_slides),
1717
url(r'^session/(?P<session_id>\d+)/slides(?:/%(name)s)?$' % settings.URL_REGEXPS, views.upload_session_slides),
18-
url(r'^session/(?P<session_id>\d+)/slides/%(name)s/order$' % settings.URL_REGEXPS, views.set_slide_order),
18+
url(r'^session/(?P<session_id>\d+)/add_to_session$', views.ajax_add_slides_to_session),
19+
url(r'^session/(?P<session_id>\d+)/remove_from_session$', views.ajax_remove_slides_from_session),
20+
url(r'^session/(?P<session_id>\d+)/reorder_in_session$', views.ajax_reorder_slides_in_session),
1921
url(r'^session/(?P<session_id>\d+)/doc/%(name)s/remove$' % settings.URL_REGEXPS, views.remove_sessionpresentation),
2022
url(r'^session/(?P<session_id>\d+)\.ics$', views.ical_agenda),
2123
url(r'^sessions/(?P<acronym>[-a-z0-9]+)\.ics$', views.ical_agenda),

ietf/meeting/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ietf.group.utils import can_manage_materials
2020
from ietf.person.models import Email
2121
from ietf.secr.proceedings.proc_utils import import_audio_files
22+
from ietf.utils.log import unreachable
2223

2324
def group_sessions(sessions):
2425

@@ -178,3 +179,15 @@ def sort_accept_tuple(accept):
178179
tup.append((keys[0], q))
179180
return sorted(tup, key = lambda x: float(x[1]), reverse = True)
180181
return tup
182+
183+
184+
185+
def condition_slide_order(session):
186+
qs = session.sessionpresentation_set.filter(document__type_id='slides').order_by('order')
187+
order_list = qs.values_list('order',flat=True)
188+
#assertion('list(order_list) == range(1,qs.count()+1)')
189+
if list(order_list) != range(1,qs.count()+1):
190+
for num, sp in enumerate(qs, start=1):
191+
sp.order=num
192+
sp.save()
193+
unreachable('2019-11-15')

ietf/meeting/views.py

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from django.core.exceptions import ValidationError
3535
from django.core.validators import URLValidator
3636
from django.urls import reverse,reverse_lazy
37-
from django.db.models import Min, Max, Q
37+
from django.db.models import Min, Max, Q, F
3838
from django.forms.models import modelform_factory, inlineformset_factory
3939
from django.template import TemplateDoesNotExist
4040
from django.template.loader import render_to_string
@@ -67,8 +67,7 @@
6767
from ietf.meeting.helpers import send_interim_cancellation_notice
6868
from ietf.meeting.helpers import send_interim_approval_request
6969
from ietf.meeting.helpers import send_interim_announcement_request
70-
from ietf.meeting.utils import finalize
71-
from ietf.meeting.utils import sort_accept_tuple
70+
from ietf.meeting.utils import finalize, sort_accept_tuple, condition_slide_order
7271
from ietf.message.utils import infer_message
7372
from ietf.secr.proceedings.utils import handle_upload_file
7473
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
@@ -1663,31 +1662,119 @@ def remove_sessionpresentation(request, session_id, num, name):
16631662

16641663
return render(request,'meeting/remove_sessionpresentation.html', {'sp': sp })
16651664

1666-
def set_slide_order(request, session_id, num, name):
1667-
# num is redundant, but we're dragging it along an artifact of where we are in the current URL structure
1665+
def ajax_add_slides_to_session(request, session_id, num):
16681666
session = get_object_or_404(Session,pk=session_id)
1669-
if not Document.objects.filter(type_id='slides',name=name).exists():
1670-
raise Http404
1667+
16711668
if not session.can_manage_materials(request.user):
16721669
return HttpResponseForbidden("You don't have permission to upload slides for this session.")
16731670
if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"):
16741671
return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.")
16751672

16761673
if request.method != 'POST' or not request.POST:
16771674
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'No data submitted or not POST' }),content_type='application/json')
1678-
order_str = request.POST.get('order', None)
1675+
1676+
order_str = request.POST.get('order', None)
16791677
try:
16801678
order = int(order_str)
1681-
except ValueError:
1679+
except (ValueError, TypeError):
16821680
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Supplied order is not valid' }),content_type='application/json')
1683-
if order <=0 or order > 32767 :
1681+
if order < 1 or order > session.sessionpresentation_set.filter(document__type_id='slides').count() + 1 :
16841682
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Supplied order is not valid' }),content_type='application/json')
1685-
1686-
sp = session.sessionpresentation_set.get(document__name = name)
1687-
sp.order = order
1683+
1684+
name = request.POST.get('name', None)
1685+
doc = Document.objects.filter(name=name).first()
1686+
if not doc:
1687+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Supplied name is not valid' }),content_type='application/json')
1688+
1689+
if not session.sessionpresentation_set.filter(document=doc).exists():
1690+
condition_slide_order(session)
1691+
session.sessionpresentation_set.filter(document__type_id='slides', order__gte=order).update(order=F('order')+1)
1692+
session.sessionpresentation_set.create(document=doc,rev=doc.rev,order=order)
1693+
DocEvent.objects.create(type="added_comment", doc=doc, rev=doc.rev, by=request.user.person, desc="Added to session: %s" % session)
1694+
1695+
return HttpResponse(json.dumps({'success':True}), content_type='application/json')
1696+
1697+
1698+
def ajax_remove_slides_from_session(request, session_id, num):
1699+
session = get_object_or_404(Session,pk=session_id)
1700+
1701+
if not session.can_manage_materials(request.user):
1702+
return HttpResponseForbidden("You don't have permission to upload slides for this session.")
1703+
if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"):
1704+
return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.")
1705+
1706+
if request.method != 'POST' or not request.POST:
1707+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'No data submitted or not POST' }),content_type='application/json')
1708+
1709+
oldIndex_str = request.POST.get('oldIndex', None)
1710+
try:
1711+
oldIndex = int(oldIndex_str)
1712+
except (ValueError, TypeError):
1713+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Supplied index is not valid' }),content_type='application/json')
1714+
if oldIndex < 1 or oldIndex > session.sessionpresentation_set.filter(document__type_id='slides').count() :
1715+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Supplied index is not valid' }),content_type='application/json')
1716+
1717+
name = request.POST.get('name', None)
1718+
doc = Document.objects.filter(name=name).first()
1719+
if not doc:
1720+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Supplied name is not valid' }),content_type='application/json')
1721+
1722+
condition_slide_order(session)
1723+
affected_presentations = session.sessionpresentation_set.filter(document=doc).first()
1724+
if affected_presentations:
1725+
if affected_presentations.order == oldIndex:
1726+
affected_presentations.delete()
1727+
session.sessionpresentation_set.filter(document__type_id='slides', order__gt=oldIndex).update(order=F('order')-1)
1728+
DocEvent.objects.create(type="added_comment", doc=doc, rev=doc.rev, by=request.user.person, desc="Removed from session: %s" % session)
1729+
return HttpResponse(json.dumps({'success':True}), content_type='application/json')
1730+
else:
1731+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Name does not match index' }),content_type='application/json')
1732+
else:
1733+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'SessionPresentation not found' }),content_type='application/json')
1734+
1735+
1736+
def ajax_reorder_slides_in_session(request, session_id, num):
1737+
session = get_object_or_404(Session,pk=session_id)
1738+
1739+
if not session.can_manage_materials(request.user):
1740+
return HttpResponseForbidden("You don't have permission to upload slides for this session.")
1741+
if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"):
1742+
return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.")
1743+
1744+
if request.method != 'POST' or not request.POST:
1745+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'No data submitted or not POST' }),content_type='application/json')
1746+
1747+
num_slides_in_session = session.sessionpresentation_set.filter(document__type_id='slides').count()
1748+
oldIndex_str = request.POST.get('oldIndex', None)
1749+
try:
1750+
oldIndex = int(oldIndex_str)
1751+
except (ValueError, TypeError):
1752+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Supplied index is not valid' }),content_type='application/json')
1753+
if oldIndex < 1 or oldIndex > num_slides_in_session :
1754+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Supplied index is not valid' }),content_type='application/json')
1755+
1756+
newIndex_str = request.POST.get('newIndex', None)
1757+
try:
1758+
newIndex = int(newIndex_str)
1759+
except (ValueError, TypeError):
1760+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Supplied index is not valid' }),content_type='application/json')
1761+
if newIndex < 1 or newIndex > num_slides_in_session :
1762+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Supplied index is not valid' }),content_type='application/json')
1763+
1764+
if newIndex == oldIndex:
1765+
return HttpResponse(json.dumps({ 'success' : False, 'error' : 'Supplied index is not valid' }),content_type='application/json')
1766+
1767+
condition_slide_order(session)
1768+
sp = session.sessionpresentation_set.get(order=oldIndex)
1769+
if oldIndex < newIndex:
1770+
session.sessionpresentation_set.filter(order__gt=oldIndex, order__lte=newIndex).update(order=F('order')-1)
1771+
else:
1772+
session.sessionpresentation_set.filter(order__gte=newIndex, order__lt=oldIndex).update(order=F('order')+1)
1773+
sp.order = newIndex
16881774
sp.save()
16891775

1690-
return HttpResponse(json.dumps({'success':True}),content_type='application/json')
1776+
return HttpResponse(json.dumps({'success':True}), content_type='application/json')
1777+
16911778

16921779
@role_required('Secretariat')
16931780
def make_schedule_official(request, num, owner, name):

ietf/templates/meeting/session_details.html

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,15 @@ <h1>{{ meeting }} : {{ acronym }}
6161

6262
{% endblock %}
6363

64-
{# TODO don't rely on secr/js version of jquery-ui #}
65-
{# Sorting based loosely on the original secr upload sorting and on http://www.avtex.com/blog/2015/01/27/drag-and-drop-sorting-of-table-rows-in-priority-order/ #}
6664
{% block js %}
6765
{% if can_manage_materials %}
6866
<script type="text/javascript" src="{% static 'jquery/jquery.min.js' %}"></script>
69-
<script type="text/javascript" src="{% static 'secr/js/jquery-ui-1.11.4.custom.min.js' %}"></script>
7067
<script type="text/javascript" src="{% static 'jquery.cookie/jquery.cookie.js' %}"></script>
68+
<script type="text/javascript" src={% static 'Sortable/Sortable.min.js' %}></script>
7169

7270
<script type="text/javascript">
7371

72+
7473
$.ajaxSetup({
7574
crossDomain: false,
7675
beforeSend: function(xhr, settings) {
@@ -80,33 +79,53 @@ <h1>{{ meeting }} : {{ acronym }}
8079
}
8180
});
8281

83-
$(document).ready(function() {
84-
var rowWidthHelper = function (e, tr) {
85-
var $originals = tr.children();
86-
var $helper = tr.clone();
87-
$helper.children().each(function(index)
88-
{
89-
$(this).width($originals.eq(index).width())
90-
});
91-
return $helper;
92-
};
9382

94-
$(".slides tbody").sortable({
95-
helper: rowWidthHelper,
96-
stop: function(event,ui) {adjustDatabase(ui.item.parent())}
97-
}).disableSelection();
98-
});
83+
var sortables=[];
84+
var options = {
85+
group: "slides",
86+
animation: 150,
87+
onAdd: function(event) {onAdd(event)},
88+
onRemove: function(event) {onRemove(event)},
89+
onEnd: function(event) {onEnd(event)}
90+
};
91+
92+
function onAdd(event) {
93+
var old_session = event.from.getAttribute("session");
94+
var new_session = event.to.getAttribute("session");
95+
$.post(event.to.getAttribute("addToSession"), {
96+
'order': event.newIndex + 1,
97+
'name': event.item.getAttribute("name")
98+
});
99+
$(event.item).find("td:eq(1)").find("a").each(function(){
100+
$(this).attr("href", $(this).attr("href").replace(old_session,new_session) );
101+
});
102+
}
99103

100-
function adjustDatabase(tbody) {
101-
tbody.find('tr').each(function() {
102-
count = $(this).parent().children().index($(this)) + 1;
103-
old_order = $(this).attr("data-order");
104-
if ( count != old_order ) {
105-
$(this).attr("data-order", count);
106-
$.post($(this).attr("data-url"),{'order':count});
107-
}
104+
function onRemove(event) {
105+
var old_session = event.from.getAttribute("session");
106+
$.post(event.from.getAttribute("removeFromSession"),{
107+
'oldIndex': event.oldIndex + 1,
108+
'name': event.item.getAttribute("name")
108109
});
109110
}
111+
112+
function onEnd(event) {
113+
if (event.to == event.from) {
114+
$.post(event.from.getAttribute("reorderInSession"),{
115+
'oldIndex': event.oldIndex + 1,
116+
'newIndex': event.newIndex + 1
117+
});
118+
}
119+
}
120+
121+
$(document).ready(function() {
122+
123+
$(".slides tbody").each(function() {
124+
sortables.push(Sortable.create(this, options));
125+
});
126+
127+
});
128+
110129
</script>
111130

112131
{% endif %}

ietf/templates/meeting/session_details_panel.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ <h2 class="anchor-target" id="session_{{session.pk}}">{% if sessions|length > 1
6666
<div class="panel-heading" data-toggle="tooltip" title="Drag and drop to reorder slides">Slides</div>
6767
<div class="panel-body">
6868
<table class="table table-condensed table-striped slides" id="slides_{{session.pk}}">
69-
<tbody>
69+
<tbody session="{{session.pk}}" addToSession="{% url 'ietf.meeting.views.ajax_add_slides_to_session' session_id=session.pk num=session.meeting.number %}" removeFromSession="{% url 'ietf.meeting.views.ajax_remove_slides_from_session' session_id=session.pk num=session.meeting.number %}" reorderInSession="{% url 'ietf.meeting.views.ajax_reorder_slides_in_session' session_id=session.pk num=session.meeting.number %}">
7070
{% for pres in session.filtered_slides %}
71-
<tr data-order="{{pres.order}}" data-url="{% url 'ietf.meeting.views.set_slide_order' session_id=session.pk num=session.meeting.number name=pres.document.name %}">
71+
<tr name="{{pres.document.name}}">
7272
{% url 'ietf.doc.views_doc.document_main' name=pres.document.name as url %}
7373
<td>
7474
<a href="{{pres.document.href}}">{{pres.document.title}} </a>

0 commit comments

Comments
 (0)