Projet

Général

Profil

0001-manager-differentiate-bookings-with-colors-39794.patch

Valentin Deniaud, 09 décembre 2020 10:52

Télécharger (23,4 ko)

Voir les différences:

Subject: [PATCH] manager: differentiate bookings with colors (#39794)

 .../migrations/0070_auto_20201202_1834.py     |  47 +++++++
 chrono/agendas/models.py                      |  22 +++
 chrono/api/views.py                           |   9 +-
 chrono/manager/static/css/style.scss          | 131 ++++++++++++++----
 .../chrono/booking_color_legend.html          |   8 ++
 .../chrono/manager_agenda_day_view.html       |   7 +-
 .../manager_meetings_agenda_month_view.html   |   7 +-
 chrono/manager/views.py                       |   4 +
 tests/test_api.py                             |  45 ++++++
 tests/test_manager.py                         |  77 +++++++++-
 10 files changed, 323 insertions(+), 34 deletions(-)
 create mode 100644 chrono/agendas/migrations/0070_auto_20201202_1834.py
 create mode 100644 chrono/manager/templates/chrono/booking_color_legend.html
chrono/agendas/migrations/0070_auto_20201202_1834.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.29 on 2020-12-02 17:34
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('agendas', '0069_translate_holidays'),
13
    ]
14

  
15
    operations = [
16
        migrations.CreateModel(
17
            name='BookingColor',
18
            fields=[
19
                (
20
                    'id',
21
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
22
                ),
23
                ('label', models.CharField(max_length=250, verbose_name='Label')),
24
                ('index', models.PositiveSmallIntegerField()),
25
                (
26
                    'agenda',
27
                    models.ForeignKey(
28
                        on_delete=django.db.models.deletion.CASCADE,
29
                        related_name='booking_colors',
30
                        to='agendas.Agenda',
31
                    ),
32
                ),
33
            ],
34
            options={'ordering': ('pk',),},
35
        ),
36
        migrations.AddField(
37
            model_name='booking',
38
            name='color',
39
            field=models.ForeignKey(
40
                null=True,
41
                on_delete=django.db.models.deletion.SET_NULL,
42
                related_name='bookings',
43
                to='agendas.BookingColor',
44
            ),
45
        ),
46
        migrations.AlterUniqueTogether(name='bookingcolor', unique_together=set([('agenda', 'label')]),),
47
    ]
chrono/agendas/models.py
1021 1021
                self.save()
1022 1022

  
1023 1023

  
1024
class BookingColor(models.Model):
1025
    COLOR_COUNT = 8
1026

  
1027
    agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE, related_name='booking_colors')
1028
    label = models.CharField(_('Label'), max_length=250)
1029
    index = models.PositiveSmallIntegerField()
1030

  
1031
    class Meta:
1032
        unique_together = ('agenda', 'label')
1033
        ordering = ('pk',)
1034

  
1035
    def save(self, *args, **kwargs):
1036
        if self.index is None:
1037
            last_color = BookingColor.objects.filter(agenda=self.agenda).last() or BookingColor(index=-1)
1038
            self.index = (last_color.index + 1) % self.COLOR_COUNT
1039
        super().save(*args, **kwargs)
1040

  
1041
    def __str__(self):
1042
        return '%s' % self.label
1043

  
1044

  
1024 1045
class Booking(models.Model):
1025 1046
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
1026 1047
    extra_data = JSONField(null=True)
......
1045 1066
    form_url = models.URLField(blank=True)
1046 1067
    backoffice_url = models.URLField(blank=True)
1047 1068
    cancel_callback_url = models.URLField(blank=True)
1069
    color = models.ForeignKey(BookingColor, null=True, on_delete=models.SET_NULL, related_name='bookings')
1048 1070

  
1049 1071
    def save(self, *args, **kwargs):
1050 1072
        with transaction.atomic():
chrono/api/views.py
37 37
from rest_framework.views import APIView
38 38

  
39 39
from chrono.api.utils import Response
40
from ..agendas.models import Agenda, Event, Booking, MeetingType, TimePeriodException, Desk
40
from ..agendas.models import Agenda, Event, Booking, MeetingType, TimePeriodException, Desk, BookingColor
41 41
from ..interval import IntervalSet
42 42

  
43 43

  
......
739 739
    count = serializers.IntegerField(min_value=1)
740 740
    cancel_booking_id = serializers.CharField(max_length=250, allow_blank=True, allow_null=True)
741 741
    force_waiting_list = serializers.BooleanField(default=False)
742
    use_color_for = serializers.CharField(max_length=250, allow_blank=True)
742 743

  
743 744

  
744 745
class StringOrListField(serializers.ListField):
......
856 857
                extra_data[k] = v
857 858

  
858 859
        available_desk = None
860
        color = None
859 861

  
860 862
        if agenda.accept_meetings():
861 863
            # slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
......
914 916
            for slot in all_free_slots:
915 917
                datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
916 918

  
919
            color_label = payload.get('use_color_for')
920
            if color_label:
921
                color = BookingColor.objects.get_or_create(agenda=agenda, label=color_label)[0]
922

  
917 923
            available_desk = None
918 924

  
919 925
            if agenda.kind == 'virtual':
......
1058 1064
                        cancel_callback_url=payload.get('cancel_callback_url', ''),
1059 1065
                        user_display_label=payload.get('user_display_label', ''),
1060 1066
                        extra_data=extra_data,
1067
                        color=color,
1061 1068
                    )
1062 1069
                    if primary_booking is not None:
1063 1070
                        new_booking.primary_booking = primary_booking
chrono/manager/static/css/style.scss
107 107
	margin-top: 0;
108 108
}
109 109

  
110

  
111
table.agenda-table {
112
	border-spacing: 1vw 0;
113
	table-layout: fixed;
114
	background-color: hsla(0, 0%, 0%, 0.06);
115
	padding: 0.5em 0;
116
	padding-bottom: 2em;
117
}
118

  
110 119
.agenda-table thead th {
111
	width: 14vw;
112 120
	padding-bottom: 1ex;
113 121
	font-weight: normal;
114 122
}
......
126 134
	box-sizing: border-box;
127 135
	padding: 1.2ex 2ex;
128 136
	vertical-align: top;
129
	width: 8ex;
130 137
	font-weight: normal;
131 138
	&.hour {
132
		width: 5%;
133 139
		text-align: left;
134 140
	}
135 141
	a {
......
137 143
		border: 0;
138 144
	}
139 145
	&.weekday {
140
		width: 12.5%;
141 146
		padding-top: 3rem;
142 147
		&.today {
143 148
			font-weight: bold;
......
149 154
	// don't add extra padding on top row
150 155
	padding-top: 1ex;
151 156
}
157
// hour cells width
158
.agenda-table thead tr:first-child td:first-child,
159
.agenda-table tbody tr:first-child th:not(.weekday) {
160
	width: 5em;
161
}
152 162

  
153 163
.agenda-table tbody tr.odd th.hour,
154 164
.agenda-table tbody tr.odd td {
155
	background: #f0f0f0;
165
	background: hsla(0, 0%, 0%, 0.06);
156 166
	@media print {
157 167
		border-top: 1px solid #aaa;
158 168
	}
159 169
}
160 170

  
161 171
.agenda-table tbody tr.odd td.other-month {
162
	background: #f8f8f8;
172
	background: transparent;
163 173
}
164 174

  
165 175

  
......
170 180
	border: 0;
171 181
}
172 182

  
173
.agenda-table.month-view {
174
	border-spacing: 0;
175
}
176

  
177
.agenda-table.month-view tbody td {
178
	border: 5px solid white;
179
	border-width: 0 5px;
180
}
181

  
182 183
@for $i from 1 through 60 {
183 184
	table.hourspan-#{$i} tbody td {
184 185
		height: calc(#{$i} * 2.5em);
......
187 188

  
188 189
.agenda-table tbody td div {
189 190
	box-sizing: border-box;
190
	padding: 1ex;
191 191
	position: absolute;
192 192
	overflow: hidden;
193 193
	&.opening-hours, &.exception-hours {
194 194
		z-index: 1;
195
		background: #b1ea4d linear-gradient(135deg, #b1ea4d 0%, #459522 100%);
196
		opacity: 0.6;
195
		background:
196
			linear-gradient(
197
				135deg,
198
				hsl(100, 20%, 97%) 20%,
199
				hsla(100, 30%, 99%, 0.7) 40%,
200
				hsla(100, 40%, 94%, 0.7) 60%,
201
				hsl(100, 55%, 93%) 100%)
202
			fixed;
197 203
		left: 0.5ex;
198 204
		width: calc(100% - 1ex);
199 205
	}
206
	&.opening-hours {
207
		border-left: 0.5em solid white;
208
	}
200 209
	&.exception-hours {
201
		background: #fee linear-gradient(135deg, #fee 0%, #fdd 100%);
210
		background:
211
			repeating-linear-gradient(
212
				135deg,
213
				hsla(10, 10%, 75%, 0.7) 0,
214
				hsla(10, 10%, 80%, 0.55) 10px,
215
				transparent 11px,
216
				transparent 20px);
202 217
		text-align: center;
203 218
	}
204 219
	&.booking {
205
		background: #eef linear-gradient(135deg, #eef 0%, #ddf 100%);
206
		box-shadow: 0 0 1px 0 #2d2dad;
207
		width: calc(100% - 2ex);
208
		border: 1px solid #aaa;
220
		left: 0.5ex;
221
		color: #5382CF;
222
		padding: 1ex;
223
		background:
224
			linear-gradient(
225
				110deg,
226
				hsla(0, 0%, 100%, 0.85) 0%,
227
				hsla(0, 0%, 100%, 0.65) 100%)
228
			currentColor
229
			fixed;
230
		width: calc(100% - 1ex);
231
		border-color: currentColor;
232
		border-left: .5em solid currentcolor;
233

  
209 234
		z-index: 2;
210 235
		&:hover {
211 236
			z-index: 3;
212 237
			height: auto !important;
213 238
		}
239
		> * {
240
			color: hsla(0, 0%, 0%, 0.6);
241
		}
242
		a {
243
			// color: currentColor;
244
			color: hsla(0, 0%, 0%, 0.7);
245
			border-bottom-color: inherit;
246

  
247
			&:hover {
248
				color: black;
249
			}
250
		}
251

  
214 252
	}
215 253
}
216 254

  
217 255
.monthview tbody td div.booking {
218
	padding: 0;
219
	transition: width 100ms ease-in, left 100ms ease-in, color 200ms ease-in;
256
	box-shadow: 0 0 0 0 #888;
257
	transition:
258
		width 100ms ease-in,
259
		left 100ms ease-in,
260
		color 200ms ease-in,
261
		box-shadow 200ms ease-in,
262
		padding 100ms ease-in;
220 263
	text-indent: -9999px;
264
	&:not(:hover) {
265
		padding-top: 0;
266
		padding-bottom: 0;
267
	}
221 268
	&:hover {
222 269
		text-indent: 0;
223
		color: inherit;
224 270
		left: 0% !important;
225
		width: 100% !important
271
		width: 100% !important;
272
		box-shadow: 0 0 1em 0 #888;
226 273
	}
227 274
	span.desk {
228 275
		display: block;
......
311 358
p.email-subject {
312 359
	text-align: center;
313 360
}
361

  
362
// booking colors
363
$booking-colors: (
364
	0: hsl(30, 100%, 46%),
365
	1: hsl(120, 57%, 35%),
366
	2: hsl(270, 40%, 50%),
367
	3: hsl(355, 80%, 45%),
368
	4: hsl(10, 70%, 30%),
369
	5: hsl(60, 98%, 30%),
370
	6: hsl(150, 57%, 25%),
371
	7: hsl(320, 70%, 60%)
372
);
373
.booking-colors {
374
	margin-top: 1.5rem;
375
}
376
.booking-color-label {
377
	padding: .33em .66em;
378
	border-radius: 0.33em;
379
	color: hsla(0, 0%, 100%, 0.9) !important;
380
	font-weight: bold;
381
	font-size: .65em;
382
}
383
@each $index, $color in $booking-colors {
384
	.agenda-table tbody td div.booking-color-#{$index} {
385
	  color: $color;
386
	}
387
	.booking-bg-color-#{$index} {
388
		background-color: $color;
389
	}
390
}
chrono/manager/templates/chrono/booking_color_legend.html
1
{% load i18n %}
2

  
3
<div class="booking-colors">
4
<strong>{% trans "Booking colors:" %}</strong>
5
{% for color in colors %}
6
<span class="booking-color-label booking-bg-color-{{ color.index }}">{{ color }}</span>
7
{% endfor %}
8
</div>
chrono/manager/templates/chrono/manager_agenda_day_view.html
68 68
        {% endif %}
69 69

  
70 70
        {% for booking in desk_info.bookings %}
71
          <div class="booking"
71
          <div class="booking{% if booking.color %} booking-color-{{ booking.color.index }}{% endif %}"
72 72
              style="height: {{ booking.css_height }}%; min-height: {{ booking.css_height }}%; top: {{ booking.css_top }}%;"
73 73
            ><span class="start-time">{{booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
74 74
            <a {% if booking.backoffice_url %}href="{{booking.backoffice_url}}"{% endif %}>{{ booking.meetings_display }}</a>
75 75
            <a rel="popup" class="cancel" href="{% url 'chrono-manager-booking-cancel' pk=booking.event.agenda_id booking_pk=booking.pk %}?next={{ request.path }}">{% trans "Cancel" %}</a>
76
            {% if booking.color %}<span class="booking-color-label booking-bg-color-{{ booking.color.index }}">{{ booking.color }}</span>{% endif %}
76 77
          </div>
77 78
        {% endfor %}
78 79
      </td>
......
90 91
</div>
91 92
{% endfor %}
92 93

  
94
{% if booking_colors %}
95
{% include "chrono/booking_color_legend.html" with colors=booking_colors %}
96
{% endif %}
97

  
93 98
{% endblock %}
chrono/manager/templates/chrono/manager_meetings_agenda_month_view.html
34 34
      {% endfor %}
35 35

  
36 36
      {% for slot in day.infos.booked_slots %}
37
      <div class="booking" style="left:{{ slot.css_left|stringformat:".1f" }}%;height:{{ slot.css_height|stringformat:".1f" }}%;min-height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%">
37
      <div class="booking{% if slot.booking.color %} booking-color-{{ slot.booking.color.index }}{% endif %}" style="left:{{ slot.css_left|stringformat:".1f" }}%;height:{{ slot.css_height|stringformat:".1f" }}%;min-height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%;">
38 38
        <span class="start-time">{{slot.booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
39 39
        <a {% if slot.booking.backoffice_url %}href="{{slot.booking.backoffice_url}}"{% endif %}>{{ slot.booking.meetings_display }}</a>
40 40
          <a rel="popup" class="cancel" href="{% url 'chrono-manager-booking-cancel' pk=slot.booking.event.agenda_id booking_pk=slot.booking.id %}?next={{ request.path }}">{% trans "Cancel" %}</a>
41 41
        {% if not single_desk %}<span class="desk">{{ slot.desk }}</span>{% endif %}
42
        {% if slot.booking.color %}<span class="booking-color-label booking-bg-color-{{ slot.booking.color.index }}">{{ slot.booking.color }}</span>{% endif %}
42 43
        </div>
43 44
      {% endfor %}
44 45
      {% endif %}
......
58 59
</div>
59 60
{% endfor %}
60 61

  
62
{% if booking_colors %}
63
{% include "chrono/booking_color_legend.html" with colors=booking_colors %}
64
{% endif %}
65

  
61 66
{% endblock %}
chrono/manager/views.py
67 67
    AgendaNotificationsSettings,
68 68
    AgendaReminderSettings,
69 69
    UnavailabilityCalendar,
70
    BookingColor,
70 71
)
71 72

  
72 73
from .forms import (
......
871 872
                context['hour_span'] = max(60 // self.agenda.get_base_meeting_duration(), 1)
872 873
            except ValueError:  # no meeting types defined
873 874
                context['hour_span'] = 1
875
            context['booking_colors'] = BookingColor.objects.filter(
876
                agenda=self.agenda, bookings__event__in=self.object_list
877
            ).distinct()
874 878
        context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
875 879
        return context
876 880

  
tests/test_api.py
23 23
    TimePeriodException,
24 24
    UnavailabilityCalendar,
25 25
    VirtualMember,
26
    BookingColor,
26 27
)
27 28
import chrono.api.views
28 29

  
......
1261 1262
    assert Booking.objects.count() == 2
1262 1263

  
1263 1264

  
1265
def test_booking_api_meeting_colors(app, user):
1266
    agenda = Agenda.objects.create(
1267
        label='foo', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=56
1268
    )
1269
    meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=30)
1270
    default_desk = Desk.objects.create(agenda=agenda, label='Desk 1')
1271
    time_period = TimePeriod.objects.create(
1272
        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=default_desk
1273
    )
1274
    datetimes_resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
1275
    event_id = datetimes_resp.json['data'][2]['id']
1276
    app.authorization = ('Basic', ('john.doe', 'password'))
1277

  
1278
    resp = app.post(
1279
        '/api/agenda/%s/fillslot/%s/' % (agenda.pk, event_id), params={'use_color_for': 'Cooking',},
1280
    )
1281
    booking = Booking.objects.get(id=resp.json['booking_id'])
1282
    assert booking.color.label == 'Cooking'
1283
    assert booking.color.index == 0
1284

  
1285
    event_id = datetimes_resp.json['data'][3]['id']
1286
    resp = app.post_json(
1287
        '/api/agenda/%s/fillslot/%s/' % (agenda.pk, event_id), params={'use_color_for': 'Cooking',},
1288
    )
1289
    new_booking = Booking.objects.get(id=resp.json['booking_id'])
1290
    assert new_booking.color.index == 0
1291

  
1292
    event_id = datetimes_resp.json['data'][4]['id']
1293
    resp = app.post_json(
1294
        '/api/agenda/%s/fillslot/%s/' % (agenda.pk, event_id), params={'use_color_for': 'Swimming',},
1295
    )
1296
    new_booking = Booking.objects.get(id=resp.json['booking_id'])
1297
    assert new_booking.color.label == 'Swimming'
1298
    assert new_booking.color.index == 1
1299

  
1300
    for i in range((BookingColor.COLOR_COUNT * 2) - 2):
1301
        event_id = datetimes_resp.json['data'][i]['id']
1302
        resp = app.post_json(
1303
            '/api/agenda/%s/fillslot/%s/' % (agenda.pk, event_id), params={'use_color_for': str(i),},
1304
        )
1305
    assert BookingColor.objects.count() == BookingColor.COLOR_COUNT * 2
1306
    assert BookingColor.objects.distinct('index').order_by().count() == BookingColor.COLOR_COUNT
1307

  
1308

  
1264 1309
def test_booking_api_meeting_with_resources(app, user):
1265 1310
    tomorrow = datetime.date.today() + datetime.timedelta(days=1)
1266 1311
    tomorrow_str = tomorrow.isoformat()
tests/test_manager.py
2994 2994
        resp = app.get(
2995 2995
            '/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day), status=200
2996 2996
        )
2997
        assert len(ctx.captured_queries) == 14
2997
        assert len(ctx.captured_queries) == 15
2998 2998
    # day is displaying rows from 10am to 6pm,
2999 2999
    # opening hours, 10am to 1pm gives top: 300%
3000 3000
    # rest of the day, 1pm to 6(+1)pm gives 600%
......
3303 3303
    )
3304 3304
    with CaptureQueriesContext(connection) as ctx:
3305 3305
        resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
3306
        assert len(ctx.captured_queries) == 9
3306
        assert len(ctx.captured_queries) == 10
3307 3307
    assert resp.pyquery.find('.exception-hours')[0].attrib == {
3308 3308
        'class': 'exception-hours',
3309 3309
        'style': 'height:800.0%;top:0.0%;width:48.0%;left:50.0%;',
......
3934 3934
        resp = app.get(
3935 3935
            '/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day), status=200
3936 3936
        )
3937
        assert len(ctx.captured_queries) == 15
3937
        assert len(ctx.captured_queries) == 16
3938 3938
    # day is displaying rows from 10am to 6pm,
3939 3939
    # opening hours, 10am to 1pm gives top: 300%
3940 3940
    # rest of the day, 1pm to 6(+1)pm gives 600%
......
4032 4032
    )
4033 4033
    with CaptureQueriesContext(connection) as ctx:
4034 4034
        resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
4035
        assert len(ctx.captured_queries) == 10
4035
        assert len(ctx.captured_queries) == 11
4036 4036
    assert resp.pyquery.find('.exception-hours')[0].attrib == {
4037 4037
        'class': 'exception-hours',
4038 4038
        'style': 'height:800.0%;top:0.0%;width:48.0%;left:1.0%;',
......
5252 5252
    assert '42 days' in resp.text
5253 5253
    agenda.refresh_from_db()
5254 5254
    assert agenda.maximal_booking_delay == 42
5255

  
5256

  
5257
@pytest.mark.parametrize(
5258
    'view',
5259
    (
5260
        '/manage/agendas/%(agenda)s/%(year)d/%(month)d/%(day)d/',
5261
        '/manage/agendas/%(agenda)s/%(year)d/%(month)d/',
5262
    ),
5263
)
5264
def test_agenda_booking_colors(app, admin_user, api_user, view):
5265
    agenda = Agenda.objects.create(label='New Example', kind='meetings')
5266
    desk = Desk.objects.create(agenda=agenda, label='New Desk')
5267
    meetingtype = MeetingType.objects.create(agenda=agenda, label='Bar', duration=30)
5268
    today = datetime.date.today()
5269
    timeperiod = TimePeriod.objects.create(
5270
        desk=desk, weekday=today.weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
5271
    )
5272

  
5273
    app.authorization = ('Basic', ('john.doe', 'password'))
5274
    datetimes_resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meetingtype.slug))
5275
    booking_url = datetimes_resp.json['data'][0]['api']['fillslot_url']
5276

  
5277
    # book first slot without colors
5278
    resp = app.post(booking_url)
5279
    date = Booking.objects.all()[0].event.start_datetime
5280

  
5281
    app.reset()
5282
    login(app)
5283

  
5284
    url = view % {'agenda': agenda.id, 'year': date.year, 'month': date.month, 'day': date.day}
5285
    resp = app.get(url)
5286
    assert len(resp.pyquery.find('div.booking')) == 1
5287
    assert 'booking-color-' not in resp.text
5288
    assert 'Booking colors:' not in resp.text
5289

  
5290
    app.reset()
5291
    app.authorization = ('Basic', ('john.doe', 'password'))
5292
    booking_url2 = datetimes_resp.json['data'][1]['api']['fillslot_url']
5293
    booking_url3 = datetimes_resp.json['data'][2]['api']['fillslot_url']
5294
    resp = app.post_json(booking_url2, params={'use_color_for': 'Cooking'})
5295
    resp = app.post_json(booking_url3, params={'use_color_for': 'Cooking'})
5296
    booking = Booking.objects.get(pk=resp.json['booking_id'])
5297

  
5298
    app.reset()
5299
    login(app)
5300

  
5301
    resp = app.get(url)
5302
    assert len(resp.pyquery.find('div.booking')) == 3
5303
    assert len(resp.pyquery.find('div.booking.booking-color-%s' % booking.color.index)) == 2
5304
    assert 'Booking colors:' in resp.text
5305
    assert len(resp.pyquery.find('div.booking-colors span.booking-color-label')) == 1
5306
    assert resp.text.count('Cooking') == 3  # 2 bookings + legend
5307

  
5308
    app.reset()
5309
    app.authorization = ('Basic', ('john.doe', 'password'))
5310
    booking_url4 = datetimes_resp.json['data'][3]['api']['fillslot_url']
5311
    resp = app.post_json(booking_url4, params={'use_color_for': 'Swimming'})
5312
    new_booking = Booking.objects.get(pk=resp.json['booking_id'])
5313

  
5314
    app.reset()
5315
    login(app)
5316

  
5317
    resp = app.get(url)
5318
    assert len(resp.pyquery.find('div.booking')) == 4
5319
    assert len(resp.pyquery.find('div.booking.booking-color-%s' % booking.color.index)) == 2
5320
    assert len(resp.pyquery.find('div.booking.booking-color-%s' % new_booking.color.index)) == 1
5321
    assert resp.text.count('Swimming') == 2  # 1 booking + legend
5322
    assert 'Booking colors:' in resp.text
5323
    assert len(resp.pyquery.find('div.booking-colors span.booking-color-label')) == 2
5255
-