From c735911925ee7314cc9ce92c4fff49db88d5da71 Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Wed, 17 Jun 2020 13:11:18 +0200
Subject: [PATCH] agendas: add a 'disabled' flag on MeetingType (#44132)

---
 .../0047_meeting_type_disabled_flag.py        | 20 +++++++++++
 chrono/agendas/models.py                      |  9 ++---
 chrono/api/views.py                           |  2 +-
 chrono/manager/forms.py                       | 14 ++++++--
 tests/test_api.py                             | 35 ++++++++++++++++++-
 tests/test_manager.py                         | 33 +++++++++++++++--
 6 files changed, 101 insertions(+), 12 deletions(-)
 create mode 100644 chrono/agendas/migrations/0047_meeting_type_disabled_flag.py

diff --git a/chrono/agendas/migrations/0047_meeting_type_disabled_flag.py b/chrono/agendas/migrations/0047_meeting_type_disabled_flag.py
new file mode 100644
index 0000000..fae4105
--- /dev/null
+++ b/chrono/agendas/migrations/0047_meeting_type_disabled_flag.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2020-06-16 11:52
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0046_event_duration'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='meetingtype',
+            name='disabled',
+            field=models.BooleanField(default=False, verbose_name='Disabled'),
+        ),
+    ]
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index d4ec3e3..81cb585 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -172,7 +172,7 @@ class Agenda(models.Model):
         the real ones shared by every real agendas.
         """
         if self.kind == 'virtual':
-            base_qs = MeetingType.objects.filter(agenda__virtual_agendas__in=[self])
+            base_qs = MeetingType.objects.filter(agenda__virtual_agendas__in=[self], disabled=False)
             real_agendas = self.real_agendas
             if excluded_agenda:
                 base_qs = base_qs.exclude(agenda=excluded_agenda)
@@ -187,7 +187,7 @@ class Agenda(models.Model):
                 for mt in queryset.order_by('slug')
             ]
 
-        return self.meetingtype_set.all().order_by('slug')
+        return self.meetingtype_set.filter(disabled=False).all().order_by('slug')
 
     def get_meetingtype(self, id_=None, slug=None):
         match = id_ or slug
@@ -205,8 +205,8 @@ class Agenda(models.Model):
             return meeting_type
 
         if id_:
-            return MeetingType.objects.get(id=id_, agenda=self)
-        return MeetingType.objects.get(slug=slug, agenda=self)
+            return MeetingType.objects.get(id=id_, agenda=self, disabled=False)
+        return MeetingType.objects.get(slug=slug, agenda=self, disabled=False)
 
     def get_virtual_members(self):
         return VirtualMember.objects.filter(virtual_agenda=self)
@@ -637,6 +637,7 @@ class MeetingType(models.Model):
     label = models.CharField(_('Label'), max_length=150)
     slug = models.SlugField(_('Identifier'), max_length=160)
     duration = models.IntegerField(_('Duration (in minutes)'), default=30)
+    disabled = models.BooleanField(_('Disabled'), default=False)
 
     class Meta:
         ordering = ['duration', 'label']
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 13b5662..633f37c 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -451,7 +451,7 @@ class MeetingDatetimes(APIView):
         try:
             if agenda_identifier is None:
                 # legacy access by meeting id
-                meeting_type = MeetingType.objects.get(id=meeting_identifier)
+                meeting_type = MeetingType.objects.get(id=meeting_identifier, disabled=False)
                 agenda = meeting_type.agenda
             else:
                 agenda = Agenda.objects.get(slug=agenda_identifier)
diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 7176326..9b2227c 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -148,9 +148,17 @@ class MeetingTypeForm(forms.ModelForm):
                     and mt.slug == self.instance.slug
                     and mt.duration == self.instance.duration
                 ):
-                    raise ValidationError(
-                        _('This meetingtype is used by a virtual agenda: %s' % virtual_agenda)
-                    )
+                    # edited meeting type is used by a virtual agenda
+                    # can't change it
+                    if (
+                        self.cleaned_data['disabled'] is True
+                        or mt.label != self.cleaned_data['label']
+                        or mt.slug != self.cleaned_data['slug']
+                        or mt.duration != self.cleaned_data['duration']
+                    ):
+                        raise ValidationError(
+                            _('This meetingtype is used by a virtual agenda: %s' % virtual_agenda)
+                        )
 
 
 class TimePeriodAddForm(forms.Form):
diff --git a/tests/test_api.py b/tests/test_api.py
index f22a767..198cb1b 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -231,7 +231,7 @@ def test_agendas_api(app):
 
 def test_agendas_meetingtypes_api(app, some_data, meetings_agenda):
     resp = app.get('/api/agenda/%s/meetings/' % meetings_agenda.slug)
-    assert resp.json == {
+    expected_resp = {
         'data': [
             {
                 'text': 'Blah',
@@ -243,6 +243,14 @@ def test_agendas_meetingtypes_api(app, some_data, meetings_agenda):
             }
         ]
     }
+    assert resp.json == expected_resp
+
+    # disabled meeting type does not show up
+    MeetingType.objects.create(
+        agenda=meetings_agenda, slug='disabled-meeting-type', duration=43, disabled=True
+    )
+    resp = app.get('/api/agenda/%s/meetings/' % meetings_agenda.slug)
+    assert resp.json == expected_resp
 
     # wrong kind
     agenda1 = Agenda.objects.filter(label=u'Foo bar')[0]
@@ -2950,6 +2958,31 @@ def test_agenda_meeting_gcd_durations_and_exceptions(app, meetings_agenda, user)
     assert len(resp.json['data']) == 0
 
 
+def test_agenda_meeting_disabled_meetingtype(app, meetings_agenda, user):
+    MeetingType.objects.all().delete()
+    meeting_type = MeetingType.objects.create(
+        agenda=meetings_agenda, label='Blah 20', duration=20, disabled=True
+    )
+    resp = app.get(
+        '/api/agenda/%s/meetings/%s/datetimes/' % (meetings_agenda.slug, meeting_type.slug), status=404
+    )
+
+    meeting_type.disabled = False
+    meeting_type.save()
+    resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (meetings_agenda.slug, meeting_type.slug))
+    data = resp.json['data']
+    assert len(data) == 216
+
+    # try to book if disabled
+    meeting_type.disabled = True
+    meeting_type.save()
+
+    fillslot_url = data[0]['api']['fillslot_url']
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp_booking = app.post(fillslot_url, status=400)
+    assert 'invalid meeting type id' in resp_booking.json['err_desc']
+
+
 def test_datetimes_api_meetings_agenda_start_hour_change(app, meetings_agenda):
     meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
     api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug)
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 78f420f..7f44a0d 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -1385,16 +1385,21 @@ def test_meetings_agenda_add_meeting_type(app, admin_user):
     resp.form['label'] = 'Blah'
     resp.form['duration'] = '60'
     resp = resp.form.submit()
-    assert MeetingType.objects.get(agenda=agenda).label == 'Blah'
-    assert MeetingType.objects.get(agenda=agenda).duration == 60
+    meeeting_type = MeetingType.objects.get(agenda=agenda)
+    assert meeeting_type.label == 'Blah'
+    assert meeeting_type.duration == 60
+    assert meeeting_type.disabled is False
     resp = resp.follow()
     assert 'Blah' in resp.text
 
     # and edit
     resp = resp.click('Blah')
     resp.form['duration'] = '30'
+    resp.form['disabled'] = True
     resp = resp.form.submit()
-    assert MeetingType.objects.get(agenda=agenda).duration == 30
+    meeeting_type = MeetingType.objects.get(agenda=agenda)
+    assert meeeting_type.duration == 30
+    assert meeeting_type.disabled is True
 
 
 def test_meetings_agenda_delete_meeting_type(app, admin_user):
@@ -3230,6 +3235,28 @@ def test_cant_modify_meetingtype_used_by_virtual_agenda(app, admin_user):
     assert mt.label == 'MTT'
 
 
+def test_cant_disable_meetingtype_used_by_virtual_agenda(app, admin_user):
+    agenda = Agenda.objects.create(label='My Virtual agenda', kind='virtual')
+    meeting_agenda_1 = Agenda.objects.create(label='Meeting agenda 1', kind='meetings')
+    meeting_agenda_2 = Agenda.objects.create(label='Meeting agenda 1', kind='meetings')
+    MeetingType.objects.create(agenda=meeting_agenda_1, label='MT', slug='mt', duration=10)
+    mt2 = MeetingType.objects.create(agenda=meeting_agenda_2, label='MT', slug='mt', duration=10)
+    VirtualMember.objects.create(virtual_agenda=agenda, real_agenda=meeting_agenda_1)
+    VirtualMember.objects.create(virtual_agenda=agenda, real_agenda=meeting_agenda_2)
+
+    app = login(app)
+    # can't disable a meeting type used by virtual agenda
+    resp = app.get('/manage/meetingtypes/%s/edit' % mt2.pk)
+    resp.form['disabled'].value = True
+    resp = resp.form.submit()
+    assert 'This meetingtype is used by a virtual agenda' in resp.text
+
+    # enable it back
+    resp.form['disabled'].value = False
+    resp = resp.form.submit()
+    assert 'This meetingtype is used by a virtual agenda' not in resp.text
+
+
 def test_cant_add_meetingtype_if_virtual_agenda(app, admin_user):
     agenda = Agenda.objects.create(label='My Virtual agenda', kind='virtual')
     meeting_agenda_1 = Agenda.objects.create(label='Meeting agenda 1', kind='meetings')
-- 
2.20.1

