manager: view to duplicate an agenda (#44127)

This commit is contained in:
Valentin Deniaud 2020-06-17 10:52:25 +02:00
parent 3ba233792d
commit 7433750574
8 changed files with 241 additions and 4 deletions

View File

@ -321,6 +321,32 @@ class Agenda(models.Model):
return created
def duplicate(self, label=None):
# clone current agenda
new_agenda = copy.deepcopy(self)
new_agenda.pk = None
new_agenda.label = label or _('Copy of %s') % self.label
# reset slug
new_agenda.slug = None
new_agenda.save()
# clone related objects
if self.kind == 'meetings':
for meeting_type in self.meetingtype_set.all():
meeting_type.duplicate(agenda_target=new_agenda)
for desk in self.desk_set.all():
desk.duplicate(agenda_target=new_agenda)
new_agenda.resources.set(self.resources.all())
elif self.kind == 'events':
for event in self.event_set.all():
event.duplicate(agenda_target=new_agenda)
elif self.kind == 'virtual':
for timeperiod in self.excluded_timeperiods.all():
timeperiod.duplicate(agenda_target=new_agenda)
for real_agenda in self.real_agendas.all():
VirtualMember.objects.create(virtual_agenda=new_agenda, real_agenda=real_agenda)
return new_agenda
def get_effective_time_periods(self):
'''Regroup timeperiods by desks.
@ -513,12 +539,14 @@ class TimePeriod(models.Model):
'end_time': self.end_time.strftime('%H:%M'),
}
def duplicate(self, desk_target=None):
def duplicate(self, desk_target=None, agenda_target=None):
# clone current period
new_period = copy.deepcopy(self)
new_period.pk = None
# set desk
new_period.desk = desk_target or self.desk
# set agenda
new_period.agenda = agenda_target or self.agenda
# store new period
new_period.save()
@ -680,6 +708,17 @@ class MeetingType(models.Model):
'duration': self.duration,
}
def duplicate(self, agenda_target=None):
new_meeting_type = copy.deepcopy(self)
new_meeting_type.pk = None
if agenda_target:
new_meeting_type.agenda = agenda_target
else:
new_meeting_type.slug = None
new_meeting_type.save()
return new_meeting_type
class Event(models.Model):
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
@ -829,6 +868,17 @@ class Event(models.Model):
'pricing': self.pricing,
}
def duplicate(self, agenda_target=None):
new_event = copy.deepcopy(self)
new_event.pk = None
if agenda_target:
new_event.agenda = agenda_target
else:
new_event.slug = None
new_event.save()
return new_event
class Booking(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE)
@ -956,14 +1006,16 @@ class Desk(models.Model):
'exceptions': [exception.export_json() for exception in self.timeperiodexception_set.all()],
}
def duplicate(self, label=None):
def duplicate(self, label=None, agenda_target=None):
# clone current desk
new_desk = copy.deepcopy(self)
new_desk.pk = None
# set label
new_desk.label = label or _('Copy of %s') % self.label
# reset slug
new_desk.slug = None
if agenda_target:
new_desk.agenda = agenda_target
else:
new_desk.slug = None
# store new desk
new_desk.save()

View File

@ -414,3 +414,7 @@ class TimePeriodExceptionSourceReplaceForm(forms.ModelForm):
class AgendasImportForm(forms.Form):
agendas_json = forms.FileField(label=_('Agendas Export File'))
class AgendaDuplicateForm(forms.Form):
label = forms.CharField(label=_('New label'), max_length=150, required=False)

View File

@ -0,0 +1,22 @@
{% extends "chrono/manager_agenda_view.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="">{% trans "Duplicate Agenda" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Duplicate Agenda" %}</h2>
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button class="submit-button">{% trans "Duplicate" %}</button>
<a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -16,12 +16,16 @@
</h2>
<span class="actions">
{% if user.is_staff %}
<a class="extra-actions-menu-opener"></a>
<a rel="popup" href="{% url 'chrono-manager-agenda-delete' pk=object.id %}">{% trans 'Delete' %}</a>
{% endif %}
<a download href="{% url 'chrono-manager-agenda-export' pk=object.id %}">{% trans 'Export' %}</a>
<a rel="popup" href="{% url 'chrono-manager-agenda-edit' pk=object.id %}">{% trans 'Options' %}</a>
{% block agenda-extra-management-actions %}
{% endblock %}
<ul class="extra-actions-menu">
<li><a rel="popup" class="action-duplicate" href="{% url 'chrono-manager-agenda-duplicate' pk=object.pk %}">{% trans 'Duplicate' %}</a></li>
</ul>
</span>
{% endblock %}

View File

@ -53,6 +53,7 @@ urlpatterns = [
url(r'^agendas/(?P<pk>\d+)/delete$', views.agenda_delete, name='chrono-manager-agenda-delete'),
url(r'^agendas/(?P<pk>\d+)/export$', views.agenda_export, name='chrono-manager-agenda-export'),
url(r'^agendas/(?P<pk>\d+)/add-event$', views.agenda_add_event, name='chrono-manager-agenda-add-event'),
url(r'^agendas/(?P<pk>\d+)/duplicate$', views.agenda_duplicate, name='chrono-manager-agenda-duplicate'),
url(
r'^agendas/(?P<pk>\d+)/import-events$',
views.agenda_import_events,

View File

@ -81,6 +81,7 @@ from .forms import (
ResourceAddForm,
ResourceEditForm,
AgendaResourceForm,
AgendaDuplicateForm,
)
from .utils import import_site
@ -1142,6 +1143,22 @@ class AgendaExport(ManagedAgendaMixin, DetailView):
agenda_export = AgendaExport.as_view()
class AgendaDuplicate(ManagedAgendaMixin, FormView):
form_class = AgendaDuplicateForm
template_name = 'chrono/manager_agenda_duplicate_form.html'
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.new_agenda.pk})
def form_valid(self, form):
agenda = Agenda.objects.get(pk=self.kwargs['pk'])
self.new_agenda = agenda.duplicate(label=form.cleaned_data['label'])
return super().form_valid(form)
agenda_duplicate = AgendaDuplicate.as_view()
class AgendaAddEventView(ManagedAgendaMixin, CreateView):
template_name = 'chrono/manager_event_form.html'
model = Event

View File

@ -844,3 +844,117 @@ def test_desk_duplicate():
# duplicate again !
new_desk = desk.duplicate()
assert new_desk.slug == 'copy-of-desk-1'
def test_agenda_meetings_duplicate():
group = Group(name=u'Group')
group.save()
agenda = Agenda.objects.create(label='Agenda', kind='meetings', view_role=group)
desk = Desk.objects.create(label='Desk', agenda=agenda)
meeting_type = MeetingType.objects.create(agenda=agenda, label='meeting', duration=30)
time_period = TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics')
source2 = TimePeriodExceptionSource.objects.create(
desk=desk,
ics_filename='sample.ics',
ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics'),
)
time_period_exception = TimePeriodException.objects.create(
label='Exception',
desk=desk,
start_datetime=now() + datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=2),
)
resource = Resource.objects.create(label=u'Foo bar')
agenda.resources.add(resource)
new_agenda = agenda.duplicate()
assert new_agenda.pk != agenda.pk
assert new_agenda.label == 'Copy of Agenda'
assert new_agenda.slug == 'copy-of-agenda'
assert new_agenda.kind == 'meetings'
assert new_agenda.view_role == group
assert new_agenda.resources.first() == resource
new_meeting_type = new_agenda.meetingtype_set.first()
assert new_meeting_type.pk != meeting_type.pk
assert new_meeting_type.label == meeting_type.label
assert new_meeting_type.duration == meeting_type.duration
assert new_meeting_type.slug == meeting_type.slug
new_desk = new_agenda.desk_set.first()
assert new_desk.pk != desk.pk
assert new_desk.slug == desk.slug
assert new_desk.timeperiod_set.count() == 1
new_time_period = TimePeriod.objects.get(desk=new_desk)
assert new_time_period.weekday == time_period.weekday
assert new_time_period.start_time == time_period.start_time
assert new_time_period.end_time == time_period.end_time
assert new_desk.timeperiodexception_set.count() == 1
new_time_period_exception = TimePeriodException.objects.get(desk=new_desk)
assert new_time_period_exception.label == time_period_exception.label
assert new_time_period_exception.start_datetime == time_period_exception.start_datetime
assert new_time_period_exception.end_datetime == time_period_exception.end_datetime
assert new_desk.timeperiodexceptionsource_set.count() == 2
assert TimePeriodExceptionSource.objects.filter(
desk=new_desk, ics_url='http://example.com/sample.ics'
).exists()
new_source2 = TimePeriodExceptionSource.objects.get(desk=new_desk, ics_filename='sample.ics')
assert new_source2.ics_file.path != source2.ics_file.path
# duplicate again !
new_agenda = agenda.duplicate()
assert new_agenda.slug == 'copy-of-agenda-1'
def test_agenda_events_duplicate():
agenda = Agenda.objects.create(label='Agenda', kind='events')
event = Event.objects.create(
agenda=agenda,
start_datetime=now() + datetime.timedelta(days=1),
duration=10,
places=10,
label='event',
slug='event',
)
new_agenda = agenda.duplicate()
assert new_agenda.pk != agenda.pk
assert new_agenda.kind == 'events'
new_event = new_agenda.event_set.first()
assert new_event.pk != event.pk
assert new_event.label == event.label
assert new_event.duration == event.duration
assert new_event.duration == event.duration
assert new_event.places == event.places
assert new_event.start_datetime == event.start_datetime
def test_agenda_virtual_duplicate():
agenda1 = Agenda.objects.create(label=u'Agenda 1', kind='meetings')
agenda2 = Agenda.objects.create(label=u'Agenda 2', kind='meetings')
virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual')
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2)
virt_agenda.save()
excluded_timeperiod = TimePeriod.objects.create(
weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), agenda=virt_agenda
)
new_agenda = virt_agenda.duplicate()
assert new_agenda.pk != virt_agenda.pk
assert new_agenda.kind == 'virtual'
new_time_period = new_agenda.excluded_timeperiods.first()
assert new_time_period.pk != excluded_timeperiod.pk
assert new_time_period.agenda == new_agenda
assert new_time_period.weekday == excluded_timeperiod.weekday
assert new_time_period.start_time == excluded_timeperiod.start_time
assert new_time_period.end_time == excluded_timeperiod.end_time
assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda1).exists()
assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda2).exists()

View File

@ -3301,3 +3301,26 @@ def test_event_digit_slug(app, admin_user):
resp.form['slug'] = 42
resp = resp.form.submit()
assert 'value cannot be a number' in resp.text
def test_duplicate_agenda(app, admin_user):
agenda = Agenda.objects.create(label=u'Foo Bar', slug='foo-bar', kind='meetings')
assert Agenda.objects.count() == 1
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('Duplicate')
resp = resp.form.submit()
assert Agenda.objects.count() == 2
new_agenda = Agenda.objects.exclude(pk=agenda.pk).first()
assert resp.location == '/manage/agendas/%s/settings' % new_agenda.pk
assert new_agenda.pk != agenda.pk
resp = resp.follow()
assert 'copy-of-foo-bar' in resp.text
resp = resp.click('Duplicate')
resp.form['label'] = 'hop'
resp = resp.form.submit().follow()
assert 'hop' in resp.text