diff --git a/chrono/manager/static/css/style.css b/chrono/manager/static/css/style.css new file mode 100644 index 00000000..f2c97aa9 --- /dev/null +++ b/chrono/manager/static/css/style.css @@ -0,0 +1,430 @@ +.occupation-bar { + display: block; + position: absolute; + bottom: 0; + height: 3px; + background: #88e; + transition: max-width 300ms ease-in; + max-width: 0%; + width: 100%; } + +.overbooking .occupation-bar { + background: #e33; } + +li.not-bookable { + opacity: 0.7; } + +li.full { + background: #f8f8fe; } + +li span.duration { + font-size: 80%; } + +li span.identifier { + font-size: 80%; + opacity: 0.6; } + +h2 span.identifier { + font-size: 1rem; + opacity: 0.6; } + +.time-only-picker thead { + display: none; } + +.time-only-picker tbody td { + width: 200px; } + +.timeperiods { + display: flex; + flex-wrap: wrap; + margin-right: -10px; } + +.timeperiods .timeperiod { + flex: 1 0 auto; + width: 18%; + margin-right: 10px; + position: relative; } + +.timeperiods .timeperiod h4 { + margin-top: 0; } + +.timeperiods .timeperiod ul.objects-list { + margin-top: 0; } + +.timeperiods .timeperiod:first-child:last-child ul.objects-list { + margin: -1rem; } + +.timeperiods .timeperiod a.add::before { + content: "\f055"; + /* plus-circle */ + font-family: FontAwesome; + padding-right: 1ex; } + +a.timeperiod-exception-all { + font-style: italic; } + +.link-action-icon.upload::before { + content: "\f093"; + /* upload-sign */ } + +.dayview h2 a, +.monthview h2 a { + padding: 0 1ex; } + +.dayview h2 > span { + display: inline-block; + min-width: 24ex; } + +.monthview h2 > span { + display: inline-block; + min-width: 16ex; } + +.agenda-table thead th { + width: 14vw; + padding-bottom: 1ex; + font-weight: normal; } + +.agenda-table { + width: 100%; } + .agenda-table .desks-1 thead th { + width: 99%; } + +.agenda-table { + width: 100%; } + .agenda-table .desks-2 thead th { + width: 49%; } + +.agenda-table { + width: 100%; } + .agenda-table .desks-3 thead th { + width: 32.33333%; } + +.agenda-table { + width: 100%; } + .agenda-table .desks-4 thead th { + width: 24%; } + +.agenda-table { + width: 100%; } + .agenda-table .desks-5 thead th { + width: 19%; } + +.agenda-table { + width: 100%; } + .agenda-table .desks-6 thead th { + width: 15.66667%; } + +.agenda-table { + width: 100%; } + .agenda-table .desks-7 thead th { + width: 13.28571%; } + +.agenda-table tbody tr th { + box-sizing: border-box; + padding: 1ex 2ex; + vertical-align: top; + width: 8ex; + font-weight: normal; } + .agenda-table tbody tr th.hour { + width: 5%; + text-align: left; } + .agenda-table tbody tr th a { + color: #000; + border: 0; } + .agenda-table tbody tr th.weekday { + width: 12.5%; + padding-top: 3rem; } + .agenda-table tbody tr th.weekday.today { + font-weight: bold; } + +.agenda-table tbody tr:first-child th.weekday { + padding-top: 1ex; } + +.agenda-table tbody tr.odd th.hour, +.agenda-table tbody tr.odd td { + background: #f0f0f0; } + @media print { + .agenda-table tbody tr.odd th.hour, + .agenda-table tbody tr.odd td { + border-top: 1px solid #aaa; } } + +.agenda-table tbody tr.odd td.other-month { + background: #f8f8f8; } + +.agenda-table tbody td { + padding: 0 1ex; + vertical-align: top; + position: relative; + border: 0; } + +.agenda-table.month-view { + border-spacing: 0; } + +.agenda-table.month-view tbody td { + border: 5px solid white; + border-width: 0 5px; } + +table.hourspan-1 tbody td { + height: calc(1 * 2.5em); } + +table.hourspan-2 tbody td { + height: calc(2 * 2.5em); } + +table.hourspan-3 tbody td { + height: calc(3 * 2.5em); } + +table.hourspan-4 tbody td { + height: calc(4 * 2.5em); } + +table.hourspan-5 tbody td { + height: calc(5 * 2.5em); } + +table.hourspan-6 tbody td { + height: calc(6 * 2.5em); } + +table.hourspan-7 tbody td { + height: calc(7 * 2.5em); } + +table.hourspan-8 tbody td { + height: calc(8 * 2.5em); } + +table.hourspan-9 tbody td { + height: calc(9 * 2.5em); } + +table.hourspan-10 tbody td { + height: calc(10 * 2.5em); } + +table.hourspan-11 tbody td { + height: calc(11 * 2.5em); } + +table.hourspan-12 tbody td { + height: calc(12 * 2.5em); } + +table.hourspan-13 tbody td { + height: calc(13 * 2.5em); } + +table.hourspan-14 tbody td { + height: calc(14 * 2.5em); } + +table.hourspan-15 tbody td { + height: calc(15 * 2.5em); } + +table.hourspan-16 tbody td { + height: calc(16 * 2.5em); } + +table.hourspan-17 tbody td { + height: calc(17 * 2.5em); } + +table.hourspan-18 tbody td { + height: calc(18 * 2.5em); } + +table.hourspan-19 tbody td { + height: calc(19 * 2.5em); } + +table.hourspan-20 tbody td { + height: calc(20 * 2.5em); } + +table.hourspan-21 tbody td { + height: calc(21 * 2.5em); } + +table.hourspan-22 tbody td { + height: calc(22 * 2.5em); } + +table.hourspan-23 tbody td { + height: calc(23 * 2.5em); } + +table.hourspan-24 tbody td { + height: calc(24 * 2.5em); } + +table.hourspan-25 tbody td { + height: calc(25 * 2.5em); } + +table.hourspan-26 tbody td { + height: calc(26 * 2.5em); } + +table.hourspan-27 tbody td { + height: calc(27 * 2.5em); } + +table.hourspan-28 tbody td { + height: calc(28 * 2.5em); } + +table.hourspan-29 tbody td { + height: calc(29 * 2.5em); } + +table.hourspan-30 tbody td { + height: calc(30 * 2.5em); } + +table.hourspan-31 tbody td { + height: calc(31 * 2.5em); } + +table.hourspan-32 tbody td { + height: calc(32 * 2.5em); } + +table.hourspan-33 tbody td { + height: calc(33 * 2.5em); } + +table.hourspan-34 tbody td { + height: calc(34 * 2.5em); } + +table.hourspan-35 tbody td { + height: calc(35 * 2.5em); } + +table.hourspan-36 tbody td { + height: calc(36 * 2.5em); } + +table.hourspan-37 tbody td { + height: calc(37 * 2.5em); } + +table.hourspan-38 tbody td { + height: calc(38 * 2.5em); } + +table.hourspan-39 tbody td { + height: calc(39 * 2.5em); } + +table.hourspan-40 tbody td { + height: calc(40 * 2.5em); } + +table.hourspan-41 tbody td { + height: calc(41 * 2.5em); } + +table.hourspan-42 tbody td { + height: calc(42 * 2.5em); } + +table.hourspan-43 tbody td { + height: calc(43 * 2.5em); } + +table.hourspan-44 tbody td { + height: calc(44 * 2.5em); } + +table.hourspan-45 tbody td { + height: calc(45 * 2.5em); } + +table.hourspan-46 tbody td { + height: calc(46 * 2.5em); } + +table.hourspan-47 tbody td { + height: calc(47 * 2.5em); } + +table.hourspan-48 tbody td { + height: calc(48 * 2.5em); } + +table.hourspan-49 tbody td { + height: calc(49 * 2.5em); } + +table.hourspan-50 tbody td { + height: calc(50 * 2.5em); } + +table.hourspan-51 tbody td { + height: calc(51 * 2.5em); } + +table.hourspan-52 tbody td { + height: calc(52 * 2.5em); } + +table.hourspan-53 tbody td { + height: calc(53 * 2.5em); } + +table.hourspan-54 tbody td { + height: calc(54 * 2.5em); } + +table.hourspan-55 tbody td { + height: calc(55 * 2.5em); } + +table.hourspan-56 tbody td { + height: calc(56 * 2.5em); } + +table.hourspan-57 tbody td { + height: calc(57 * 2.5em); } + +table.hourspan-58 tbody td { + height: calc(58 * 2.5em); } + +table.hourspan-59 tbody td { + height: calc(59 * 2.5em); } + +table.hourspan-60 tbody td { + height: calc(60 * 2.5em); } + +.agenda-table tbody td div { + box-sizing: border-box; + padding: 1ex; + position: absolute; + overflow: hidden; } + .agenda-table tbody td div.opening-hours { + z-index: 1; + background: #b1ea4d linear-gradient(135deg, #b1ea4d 0%, #459522 100%); + opacity: 0.6; + left: 0.5ex; + width: calc(100% - 1ex); } + .agenda-table tbody td div.booking { + background: #eef linear-gradient(135deg, #eef 0%, #ddf 100%); + box-shadow: 0 0 1px 0 #2d2dad; + width: calc(100% - 2ex); + border: 1px solid #aaa; + z-index: 2; } + .agenda-table tbody td div.booking:hover { + z-index: 3; + height: auto !important; } + +.monthview tbody td div.booking { + padding: 0; + transition: width 100ms ease-in, left 100ms ease-in, color 200ms ease-in; + text-indent: -9999px; } + .monthview tbody td div.booking:hover { + text-indent: 0; + color: inherit; + left: 0% !important; + width: 100% !important; } + .monthview tbody td div.booking span.desk { + display: block; } + +span.start-time { + font-size: 80%; } + +.date-title { + cursor: pointer; } + .date-title::after { + content: "\f073"; + /* calendar */ + font-family: FontAwesome; + padding-left: 1ex; + padding-right: 0ex; + opacity: 0.3; + font-size: 80%; + transition: opacity 200ms linear; } + .date-title:hover::after { + opacity: 0.8; } + +.date-picker { + position: absolute; + background: #FAFAFA; + border: 1px solid #d0d0d0; + box-shadow: 0px 1px 1px 2px rgba(0, 0, 0, 0.04); + padding: 1ex 4ex; + left: 0; + top: 3ex; + z-index: 100; } + .date-picker button, .date-picker select { + font-size: 1rem; } + .date-picker::before { + position: absolute; + content: ""; + display: block; + width: 1ex; + height: 1ex; + border: 1px solid #d0d0d0; + border-width: 1px 0 0 1px; + top: -0.6ex; + left: 5rem; + background: #FAFAFA; + transform: rotate(45deg); } + +ul#id_weekdays { + margin: 0; + padding: 0; + list-style: none; + max-width: 42em; } + ul#id_weekdays li { + margin: 0; + padding: 0; + display: inline-block; + width: 10em; } diff --git a/chrono/manager/templates/chrono/manager_confirm_source_delete.html b/chrono/manager/templates/chrono/manager_confirm_source_delete.html new file mode 100644 index 00000000..8eaf1b63 --- /dev/null +++ b/chrono/manager/templates/chrono/manager_confirm_source_delete.html @@ -0,0 +1,19 @@ +{% extends "chrono/manager_home.html" %} +{% load i18n %} + +{% block appbar %} +

{{ object }}

+{% endblock %} + +{% block content %} +
+ {% csrf_token %} +

+ {% blocktrans %}Are you sure you want to delete this exception source?{% endblocktrans %} +

+
+ + {% trans 'Cancel' %} +
+
+{% endblock %} diff --git a/chrono/manager/templates/chrono/manager_import_exceptions.html b/chrono/manager/templates/chrono/manager_import_exceptions.html index bf778f9b..a6358279 100644 --- a/chrono/manager/templates/chrono/manager_import_exceptions.html +++ b/chrono/manager/templates/chrono/manager_import_exceptions.html @@ -35,7 +35,7 @@ {% if object.ics_filename %}{% trans "replace" %}{% else %}{% trans "refresh" %}{% endif %} - {% trans "remove" %} + {% trans "remove" %} {% endfor %} diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py index 39866c0b..5ccd361e 100644 --- a/chrono/manager/urls.py +++ b/chrono/manager/urls.py @@ -100,6 +100,11 @@ urlpatterns = [ views.time_period_exception_list, name='chrono-manager-time-period-exception-list', ), + url( + r'^time-period-exceptions-source/(?P\d+)/delete$', + views.time_period_exception_source_delete, + name='chrono-manager-time-period-exception-source-delete', + ), url( r'^agendas/events.csv$', views.agenda_import_events_sample_csv, diff --git a/chrono/manager/views.py b/chrono/manager/views.py index aa9dc8a7..3e90ae0f 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -51,6 +51,7 @@ from chrono.agendas.models import ( TimePeriodException, ICSError, AgendaImportError, + TimePeriodExceptionSource, ) from .forms import ( @@ -914,6 +915,17 @@ class DeskImportTimePeriodExceptionsView(ManagedAgendaSubobjectMixin, UpdateView desk_import_time_period_exceptions = DeskImportTimePeriodExceptionsView.as_view() +class TimePeriodExceptionSourceDeleteView(ManagedDeskSubobjectMixin, DeleteView): + template_name = 'chrono/manager_confirm_source_delete.html' + model = TimePeriodExceptionSource + + def get_success_url(self): + return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda_id}) + + +time_period_exception_source_delete = TimePeriodExceptionSourceDeleteView.as_view() + + def menu_json(request): label = _('Agendas') json_str = json.dumps( diff --git a/tests/test_manager.py b/tests/test_manager.py index da99de94..eb219893 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1412,6 +1412,64 @@ def test_agenda_import_time_period_exception_from_remote_ics_with_ssl_error(mock assert 'Failed to retrieve remote calendar (https://example.com/foo.ics, SSL error).' in resp.text +def test_meetings_agenda_delete_time_period_exception_source(app, admin_user): + agenda = Agenda.objects.create(label='Foo bar', kind='meetings') + desk = Desk.objects.create(agenda=agenda, label='Desk A') + MeetingType(agenda=agenda, label='Blah').save() + TimePeriod.objects.create( + weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) + ) + login(app) + # import a source + resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() + resp = resp.click('Settings') + resp = resp.click('upload') + ics_with_recurrent_exceptions = b"""BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//foo.bar//EN +BEGIN:VEVENT +DTSTART:20180101 +DTEND:20180101 +SUMMARY:New Year's Eve +RRULE:FREQ=YEARLY +END:VEVENT +END:VCALENDAR""" + resp.form['ics_file'] = Upload('exceptions.ics', ics_with_recurrent_exceptions, 'text/calendar') + resp = resp.form.submit(status=302).follow() + assert TimePeriodException.objects.filter(desk=desk).count() == 2 + source1 = TimePeriodExceptionSource.objects.latest('pk') + assert source1.timeperiodexception_set.count() == 2 + + # import another one + resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() + resp = resp.click('Settings') + resp = resp.click('upload') + resp.form['ics_file'] = Upload('exceptions.ics', ics_with_recurrent_exceptions, 'text/calendar') + resp = resp.form.submit(status=302).follow() + assert TimePeriodException.objects.filter(desk=desk).count() == 4 + source2 = TimePeriodExceptionSource.objects.latest('pk') + assert source2.timeperiodexception_set.count() == 2 + + # delete the second one + resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() + resp = resp.click('Settings') + resp = resp.click('upload') + resp = resp.click(href='/manage/time-period-exceptions-source/%d/delete' % source2.pk) + resp = resp.form.submit().follow() + assert TimePeriodException.objects.count() == 2 + assert source1.timeperiodexception_set.count() == 2 + assert TimePeriodExceptionSource.objects.filter(pk=source2.pk).exists() is False + + # delete the first one + resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() + resp = resp.click('Settings') + resp = resp.click('upload') + resp = resp.click(href='/manage/time-period-exceptions-source/%d/delete' % source1.pk) + resp = resp.form.submit().follow() + assert TimePeriodException.objects.count() == 0 + assert TimePeriodExceptionSource.objects.filter(pk=source1.pk).exists() is False + + def test_agenda_day_view(app, admin_user, manager_user, api_user): agenda = Agenda.objects.create(label='New Example', kind='meetings') desk = Desk.objects.create(agenda=agenda, label='New Desk')