0001-agendas-mark-MeetingType-for-deletion-44132.patch
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 |
- |