Projet

Général

Profil

0001-agendas-mark-MeetingType-for-deletion-44132.patch

Emmanuel Cazenave, 17 juin 2020 17:57

Télécharger (15,4 ko)

Voir les différences:

Subject: [PATCH] agendas: mark MeetingType for deletion (#44132)

 .../0048_meeting_type_deleted_flag.py         | 20 +++++++
 chrono/agendas/models.py                      |  9 +--
 chrono/api/views.py                           |  2 +-
 chrono/manager/forms.py                       |  4 +-
 .../manager_meetings_agenda_settings.html     |  6 +-
 chrono/manager/views.py                       | 53 +++++++++--------
 tests/test_api.py                             | 33 ++++++++++-
 tests/test_manager.py                         | 57 +++++++------------
 8 files changed, 108 insertions(+), 76 deletions(-)
 create mode 100644 chrono/agendas/migrations/0048_meeting_type_deleted_flag.py
chrono/agendas/migrations/0048_meeting_type_deleted_flag.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-06-17 13:23
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('agendas', '0047_auto_20200617_1521'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='meetingtype',
17
            name='deleted',
18
            field=models.BooleanField(default=False, verbose_name='Deleted'),
19
        ),
20
    ]
chrono/agendas/models.py
186 186
        the real ones shared by every real agendas.
187 187
        """
188 188
        if self.kind == 'virtual':
189
            base_qs = MeetingType.objects.filter(agenda__virtual_agendas__in=[self])
189
            base_qs = MeetingType.objects.filter(agenda__virtual_agendas__in=[self], deleted=False)
190 190
            real_agendas = self.real_agendas
191 191
            if excluded_agenda:
192 192
                base_qs = base_qs.exclude(agenda=excluded_agenda)
......
201 201
                for mt in queryset.order_by('slug')
202 202
            ]
203 203

  
204
        return self.meetingtype_set.all().order_by('slug')
204
        return self.meetingtype_set.filter(deleted=False).all().order_by('slug')
205 205

  
206 206
    def get_meetingtype(self, id_=None, slug=None):
207 207
        match = id_ or slug
......
219 219
            return meeting_type
220 220

  
221 221
        if id_:
222
            return MeetingType.objects.get(id=id_, agenda=self)
223
        return MeetingType.objects.get(slug=slug, agenda=self)
222
            return MeetingType.objects.get(id=id_, agenda=self, deleted=False)
223
        return MeetingType.objects.get(slug=slug, agenda=self, deleted=False)
224 224

  
225 225
    def get_virtual_members(self):
226 226
        return VirtualMember.objects.filter(virtual_agenda=self)
......
679 679
    label = models.CharField(_('Label'), max_length=150)
680 680
    slug = models.SlugField(_('Identifier'), max_length=160)
681 681
    duration = models.IntegerField(_('Duration (in minutes)'), default=30)
682
    deleted = models.BooleanField(_('Deleted'), default=False)
682 683

  
683 684
    class Meta:
684 685
        ordering = ['duration', 'label']
chrono/api/views.py
451 451
        try:
452 452
            if agenda_identifier is None:
453 453
                # legacy access by meeting id
454
                meeting_type = MeetingType.objects.get(id=meeting_identifier)
454
                meeting_type = MeetingType.objects.get(id=meeting_identifier, deleted=False)
455 455
                agenda = meeting_type.agenda
456 456
            else:
457 457
                agenda = Agenda.objects.get(slug=agenda_identifier)
chrono/manager/forms.py
117 117
        widgets = {
118 118
            'agenda': forms.HiddenInput(),
119 119
        }
120
        exclude = ['slug']
120
        exclude = ['slug', 'deleted']
121 121

  
122 122
    def clean(self):
123 123
        super().clean()
......
136 136
        widgets = {
137 137
            'agenda': forms.HiddenInput(),
138 138
        }
139
        exclude = []
139
        exclude = ['deleted']
140 140

  
141 141
    def clean(self):
142 142
        super().clean()
chrono/manager/templates/chrono/manager_meetings_agenda_settings.html
23 23
<div class="section">
24 24
<h3>{% trans 'Meeting Types' %}</h3>
25 25
<div>
26
{% with object.meetingtype_set.all as meetingtypes %}
27
{% if meetingtypes %}
26
{% if meeting_types %}
28 27
  <ul class="objects-list single-links">
29
    {% for meeting_type in meetingtypes %}
28
    {% for meeting_type in meeting_types %}
30 29
    <li><a rel="popup" href="{% url 'chrono-manager-meeting-type-edit' pk=meeting_type.id %}">
31 30
        {{meeting_type.label}}
32 31
        <span class="duration">({{meeting_type.duration}} {% trans "minutes" %})</span>
......
44 43
  {% endblocktrans %}
45 44
</div>
46 45
{% endif %}
47
{% endwith %}
48 46
</div>
49 47
</div>
50 48

  
chrono/manager/views.py
1110 1110

  
1111 1111
    def get_context_data(self, **kwargs):
1112 1112
        context = super(AgendaSettings, self).get_context_data(**kwargs)
1113
        if self.agenda.accept_meetings():
1114
            context['meeting_types'] = self.object.iter_meetingtypes()
1113 1115
        if self.agenda.kind == 'virtual':
1114 1116
            context['virtual_members'] = [
1115 1117
                (virtual_member, virtual_member.real_agenda.can_be_managed(self.request.user))
1116 1118
                for virtual_member in self.object.get_virtual_members()
1117 1119
            ]
1118
            context['meeting_types'] = self.object.iter_meetingtypes()
1119 1120
        if self.agenda.kind == 'meetings':
1120 1121
            context['has_resources'] = Resource.objects.exists()
1121 1122
        return context
......
1340 1341
    def get_context_data(self, **kwargs):
1341 1342
        context = super(MeetingTypeDeleteView, self).get_context_data(**kwargs)
1342 1343
        meeting_type = self.get_object()
1344
        context['cannot_delete'] = False
1345

  
1346
        for virtual_agenda in self.get_object().agenda.virtual_agendas.all():
1347
            if virtual_agenda.real_agendas.count() == 1:
1348
                continue
1349
            for mt in virtual_agenda.iter_meetingtypes():
1350
                if (
1351
                    meeting_type.slug == mt.slug
1352
                    and meeting_type.label == mt.label
1353
                    and meeting_type.duration == mt.duration
1354
                ):
1355
                    context['cannot_delete'] = True
1356
                    context['cannot_delete_msg'] = _(
1357
                        'This cannot be removed as it used by a virtual agenda: %(agenda)s'
1358
                        % {'agenda': virtual_agenda}
1359
                    )
1360
                    break
1343 1361

  
1344
        cannot_delete = Booking.objects.filter(
1345
            event__meeting_type=meeting_type,
1346
            event__start_datetime__gt=now(),
1347
            cancellation_datetime__isnull=True,
1348
        ).exists()
1349
        if cannot_delete:
1350
            context['cannot_delete_msg'] = FUTURE_BOOKING_ERROR_MSG
1351
        else:
1352
            for virtual_agenda in self.get_object().agenda.virtual_agendas.all():
1353
                if virtual_agenda.real_agendas.count() == 1:
1354
                    continue
1355
                for mt in virtual_agenda.iter_meetingtypes():
1356
                    if (
1357
                        meeting_type.slug == mt.slug
1358
                        and meeting_type.label == mt.label
1359
                        and meeting_type.duration == mt.duration
1360
                    ):
1361
                        cannot_delete = True
1362
                        context['cannot_delete_msg'] = _(
1363
                            'This cannot be removed as it used by a virtual agenda: %(agenda)s'
1364
                            % {'agenda': virtual_agenda}
1365
                        )
1366
                        break
1367

  
1368
        context['cannot_delete'] = cannot_delete
1369 1362
        return context
1370 1363

  
1371 1364
    def delete(self, request, *args, **kwargs):
......
1373 1366
        context = self.get_context_data()
1374 1367
        if context['cannot_delete']:
1375 1368
            raise PermissionDenied()
1376
        return super(MeetingTypeDeleteView, self).delete(request, *args, **kwargs)
1369

  
1370
        # rewrite django/views/generic/edit.py::DeletionMixin.delete
1371
        # to mark for deletion instead of actually delete
1372
        success_url = self.get_success_url()
1373
        self.object.deleted = True
1374
        self.object.save()
1375
        return HttpResponseRedirect(success_url)
1377 1376

  
1378 1377

  
1379 1378
meeting_type_delete = MeetingTypeDeleteView.as_view()
tests/test_api.py
231 231

  
232 232
def test_agendas_meetingtypes_api(app, some_data, meetings_agenda):
233 233
    resp = app.get('/api/agenda/%s/meetings/' % meetings_agenda.slug)
234
    assert resp.json == {
234
    expected_resp = {
235 235
        'data': [
236 236
            {
237 237
                'text': 'Blah',
......
243 243
            }
244 244
        ]
245 245
    }
246
    assert resp.json == expected_resp
247

  
248
    # deleted meeting type does not show up
249
    MeetingType.objects.create(agenda=meetings_agenda, slug='deleted-meeting-type', duration=43, deleted=True)
250
    resp = app.get('/api/agenda/%s/meetings/' % meetings_agenda.slug)
251
    assert resp.json == expected_resp
246 252

  
247 253
    # wrong kind
248 254
    agenda1 = Agenda.objects.filter(label=u'Foo bar')[0]
......
2950 2956
    assert len(resp.json['data']) == 0
2951 2957

  
2952 2958

  
2959
def test_agenda_meeting_deleted_meetingtype(app, meetings_agenda, user):
2960
    MeetingType.objects.all().delete()
2961
    meeting_type = MeetingType.objects.create(
2962
        agenda=meetings_agenda, label='Blah 20', duration=20, deleted=True
2963
    )
2964
    resp = app.get(
2965
        '/api/agenda/%s/meetings/%s/datetimes/' % (meetings_agenda.slug, meeting_type.slug), status=404
2966
    )
2967

  
2968
    meeting_type.deleted = False
2969
    meeting_type.save()
2970
    resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (meetings_agenda.slug, meeting_type.slug))
2971
    data = resp.json['data']
2972
    assert len(data) == 216
2973

  
2974
    # try to book if disabled
2975
    meeting_type.deleted = True
2976
    meeting_type.save()
2977

  
2978
    fillslot_url = data[0]['api']['fillslot_url']
2979
    app.authorization = ('Basic', ('john.doe', 'password'))
2980
    resp_booking = app.post(fillslot_url, status=400)
2981
    assert 'invalid meeting type id' in resp_booking.json['err_desc']
2982

  
2983

  
2953 2984
def test_datetimes_api_meetings_agenda_start_hour_change(app, meetings_agenda):
2954 2985
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
2955 2986
    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug)
tests/test_manager.py
850 850
    resp = resp.form.submit(status=403)
851 851

  
852 852

  
853
def test_delete_busy_meeting_type(app, admin_user):
854
    agenda = Agenda.objects.create(label='Foo', kind='meetings')
855
    meeting_type = MeetingType.objects.create(agenda=agenda, label='Meeting Type Foo', duration=30)
856
    desk = Desk.objects.create(agenda=agenda, label='Desk', slug='desk')
857

  
858
    app = login(app)
859
    resp = app.get('/manage/', status=200)
860
    resp = resp.click('Foo').follow()
861
    resp = resp.click('Settings')
862
    mt_page = resp.click('Meeting Type Foo')
863
    mt_delete_page = mt_page.click('Delete')
864
    assert 'Are you sure you want to delete this?' in mt_delete_page.text
865
    # make sure the deleting is not disabled
866
    assert 'disabled' not in mt_delete_page.text
867

  
868
    event = Event(
869
        start_datetime=now() + datetime.timedelta(days=10),
870
        meeting_type=meeting_type,
871
        desk=desk,
872
        agenda=agenda,
873
        full=False,
874
        places=1,
875
    )
876
    event.save()
877
    booking = Booking(event=event)
878
    booking.save()
879
    assert Booking.objects.count() == 1
880

  
881
    resp = mt_page.click('Delete')
882
    assert 'This cannot be removed' in resp.text
883

  
884

  
885 853
def test_delete_agenda_as_manager(app, manager_user):
886 854
    agenda = Agenda(label=u'Foo bar')
887 855
    agenda.edit_role = manager_user.groups.all()[0]
......
1416 1384
    resp = resp.click('New Meeting Type')
1417 1385
    resp.form['label'] = 'Blah'
1418 1386
    resp.form['duration'] = '60'
1387
    assert 'deleted' not in resp.form.fields
1419 1388
    resp = resp.form.submit()
1420
    assert MeetingType.objects.get(agenda=agenda).label == 'Blah'
1421
    assert MeetingType.objects.get(agenda=agenda).duration == 60
1389
    meeeting_type = MeetingType.objects.get(agenda=agenda)
1390
    assert meeeting_type.label == 'Blah'
1391
    assert meeeting_type.duration == 60
1392
    assert meeeting_type.deleted is False
1422 1393
    resp = resp.follow()
1423 1394
    assert 'Blah' in resp.text
1424 1395

  
1425 1396
    # and edit
1426 1397
    resp = resp.click('Blah')
1427 1398
    resp.form['duration'] = '30'
1399
    assert 'deleted' not in resp.form.fields
1428 1400
    resp = resp.form.submit()
1429
    assert MeetingType.objects.get(agenda=agenda).duration == 30
1401
    meeeting_type = MeetingType.objects.get(agenda=agenda)
1402
    assert meeeting_type.duration == 30
1430 1403

  
1431 1404

  
1432 1405
def test_meetings_agenda_delete_meeting_type(app, admin_user):
......
1441 1414
    resp = resp.click('Settings')
1442 1415
    resp = resp.click('Blah')
1443 1416
    resp = resp.click('Delete')
1417
    assert 'Are you sure you want to delete this?' in resp.text
1418
    assert 'disabled' not in resp.text
1444 1419
    resp = resp.form.submit()
1445 1420
    assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.id)
1446
    assert MeetingType.objects.count() == 0
1421
    meeting_type.refresh_from_db()
1422
    assert meeting_type.deleted is True
1423

  
1424
    # meeting type not showing up anymore
1425
    resp = app.get('/manage/', status=200)
1426
    resp = resp.click('Foo').follow()
1427
    resp = resp.click('Settings')
1428
    assert 'Meeting Type Foo' not in resp.text
1447 1429

  
1448 1430

  
1449 1431
def test_meetings_agenda_add_time_period(app, admin_user):
......
3224 3206
    resp = resp.click('Delete')
3225 3207
    resp = resp.form.submit()
3226 3208
    assert not meeting_agenda_1.iter_meetingtypes()
3227
    MeetingType.objects.create(agenda=meeting_agenda_1, label='MT', slug='mt', duration=10)
3209
    mt1.deleted = False
3210
    mt1.save()
3228 3211

  
3229 3212
    meeting_agenda_2 = Agenda.objects.create(label='Meeting agenda 2', kind='meetings')
3230 3213
    mt2 = MeetingType.objects.create(agenda=meeting_agenda_2, label='MT', slug='mt', duration=10)
3231
-