Projet

Général

Profil

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

Valentin Deniaud, 10 novembre 2020 18:34

Télécharger (15,2 ko)

Voir les différences:

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

 .../migrations/0070_auto_20201110_1456.py     | 49 ++++++++++++++
 chrono/agendas/models.py                      | 20 ++++++
 chrono/api/views.py                           |  9 ++-
 chrono/manager/static/css/style.scss          |  9 +++
 .../chrono/booking_color_legend.html          |  8 +++
 .../chrono/manager_agenda_day_view.html       |  6 +-
 .../manager_meetings_agenda_month_view.html   |  6 +-
 tests/test_api.py                             | 30 +++++++++
 tests/test_manager.py                         | 67 +++++++++++++++++++
 9 files changed, 201 insertions(+), 3 deletions(-)
 create mode 100644 chrono/agendas/migrations/0070_auto_20201110_1456.py
 create mode 100644 chrono/manager/templates/chrono/booking_color_legend.html
chrono/agendas/migrations/0070_auto_20201110_1456.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-11-10 13:56
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
                (
25
                    'code',
26
                    models.CharField(blank=True, default='', max_length=6, verbose_name='Color code (hex)'),
27
                ),
28
                (
29
                    'agenda',
30
                    models.ForeignKey(
31
                        on_delete=django.db.models.deletion.CASCADE,
32
                        related_name='booking_colors',
33
                        to='agendas.Agenda',
34
                    ),
35
                ),
36
            ],
37
        ),
38
        migrations.AddField(
39
            model_name='booking',
40
            name='color',
41
            field=models.ForeignKey(
42
                null=True,
43
                on_delete=django.db.models.deletion.SET_NULL,
44
                related_name='bookings',
45
                to='agendas.BookingColor',
46
            ),
47
        ),
48
        migrations.AlterUniqueTogether(name='bookingcolor', unique_together=set([('agenda', 'label')]),),
49
    ]
chrono/agendas/models.py
1016 1016
                self.save()
1017 1017

  
1018 1018

  
1019
class BookingColor(models.Model):
1020
    agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE, related_name='booking_colors')
1021
    label = models.CharField(_('Label'), max_length=250)
1022
    code = models.CharField(_('Color code (hex)'), max_length=6, blank=True, default='')
1023

  
1024
    palette = ('f1b37d', 'a3dbe1', 'cecf2e', 'c0c0c0', 'dfb7b7', 'b5cfb5', 'b8aac5', 'f2dfc3')
1025

  
1026
    class Meta:
1027
        unique_together = ('agenda', 'label')
1028

  
1029
    def save(self, *args, **kwargs):
1030
        if not self.code:
1031
            already_used = BookingColor.objects.filter(agenda=self.agenda).values_list('code', flat=True)
1032
            available = set(self.palette) - set(already_used)
1033
            if available:
1034
                self.code = next(iter(available))
1035
        super().save(*args, **kwargs)
1036

  
1037

  
1019 1038
class Booking(models.Model):
1020 1039
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
1021 1040
    extra_data = JSONField(null=True)
......
1040 1059
    form_url = models.URLField(blank=True)
1041 1060
    backoffice_url = models.URLField(blank=True)
1042 1061
    cancel_callback_url = models.URLField(blank=True)
1062
    color = models.ForeignKey(BookingColor, null=True, on_delete=models.SET_NULL, related_name='bookings')
1043 1063

  
1044 1064
    def save(self, *args, **kwargs):
1045 1065
        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

  
......
694 694
    count = serializers.IntegerField(min_value=1)
695 695
    cancel_booking_id = serializers.CharField(max_length=250, allow_blank=True, allow_null=True)
696 696
    force_waiting_list = serializers.BooleanField(default=False)
697
    use_color_for = serializers.CharField(max_length=250, allow_blank=True)
697 698

  
698 699

  
699 700
class StringOrListField(serializers.ListField):
......
811 812
                extra_data[k] = v
812 813

  
813 814
        available_desk = None
815
        color = None
814 816

  
815 817
        if agenda.accept_meetings():
816 818
            # slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
......
862 864
            for slot in all_free_slots:
863 865
                datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
864 866

  
867
            color_label = payload.get('use_color_for')
868
            if color_label:
869
                color, _ = BookingColor.objects.get_or_create(agenda=agenda, label=color_label)
870

  
865 871
            available_desk = None
866 872

  
867 873
            if agenda.kind == 'virtual':
......
1006 1012
                        cancel_callback_url=payload.get('cancel_callback_url', ''),
1007 1013
                        user_display_label=payload.get('user_display_label', ''),
1008 1014
                        extra_data=extra_data,
1015
                        color=color,
1009 1016
                    )
1010 1017
                    if primary_booking is not None:
1011 1018
                        new_booking.primary_booking = primary_booking
chrono/manager/static/css/style.scss
211 211
			z-index: 3;
212 212
			height: auto !important;
213 213
		}
214
		a {
215
			color: #003199;
216
			border-bottom-color: #003199;
217
		}
214 218
	}
215 219
}
216 220

  
......
311 315
p.email-subject {
312 316
	text-align: center;
313 317
}
318

  
319
span.color-legend-label {
320
	padding: 0.2em;
321
	border-radius: 0.5em;
322
}
chrono/manager/templates/chrono/booking_color_legend.html
1
{% load i18n %}
2

  
3
<div>
4
<strong>{% trans "Booking colors:" %}</strong>
5
{% for color in agenda.booking_colors.all %}
6
<span class="color-legend-label" style="background: #{{ color.code }};">{{ color.label }}</span>
7
{% endfor %}
8
</div>
chrono/manager/templates/chrono/manager_agenda_day_view.html
69 69

  
70 70
        {% for booking in desk_info.bookings %}
71 71
          <div class="booking"
72
              style="height: {{ booking.css_height }}%; min-height: {{ booking.css_height }}%; top: {{ booking.css_top }}%;"
72
              style="height: {{ booking.css_height }}%; min-height: {{ booking.css_height }}%; top: {{ booking.css_top }}%;{% if booking.color.code %}background:#{{ booking.color.code }};{% endif %}"
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>
......
90 90
</div>
91 91
{% endfor %}
92 92

  
93
{% if agenda.booking_colors.exists %}
94
{% include "chrono/booking_color_legend.html" %}
95
{% endif %}
96

  
93 97
{% 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" 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" }}%;{% if slot.booking.color.code %}background:#{{ slot.booking.color.code }};{% endif %}">
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>
......
58 58
</div>
59 59
{% endfor %}
60 60

  
61
{% if agenda.booking_colors.exists %}
62
{% include "chrono/booking_color_legend.html" %}
63
{% endif %}
64

  
61 65
{% endblock %}
tests/test_api.py
1263 1263
    assert Booking.objects.count() == 2
1264 1264

  
1265 1265

  
1266
def test_booking_api_meeting_colors(app, meetings_agenda, user):
1267
    agenda_id = meetings_agenda.slug
1268
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
1269
    datetimes_resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
1270
    event_id = datetimes_resp.json['data'][2]['id']
1271
    app.authorization = ('Basic', ('john.doe', 'password'))
1272

  
1273
    resp = app.post(
1274
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), params={'use_color_for': 'Cooking',},
1275
    )
1276
    booking = Booking.objects.get(id=resp.json['booking_id'])
1277
    assert booking.color.label == 'Cooking'
1278
    assert booking.color.code != ''
1279

  
1280
    event_id = datetimes_resp.json['data'][3]['id']
1281
    resp = app.post_json(
1282
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), params={'use_color_for': 'Cooking',},
1283
    )
1284
    new_booking = Booking.objects.get(id=resp.json['booking_id'])
1285
    assert new_booking.color == booking.color
1286

  
1287
    event_id = datetimes_resp.json['data'][4]['id']
1288
    resp = app.post_json(
1289
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), params={'use_color_for': 'Swimming',},
1290
    )
1291
    new_booking = Booking.objects.get(id=resp.json['booking_id'])
1292
    assert new_booking.color.label == 'Swimming'
1293
    assert new_booking.color != booking.color
1294

  
1295

  
1266 1296
def test_booking_api_meeting_with_resources(app, user):
1267 1297
    tomorrow = datetime.date.today() + datetime.timedelta(days=1)
1268 1298
    tomorrow_str = tomorrow.isoformat()
tests/test_manager.py
5027 5027
    unavailability_calendar.edit_role = group
5028 5028
    unavailability_calendar.save()
5029 5029
    app.get(url)
5030

  
5031

  
5032
@pytest.mark.parametrize(
5033
    'view',
5034
    (
5035
        '/manage/agendas/%(agenda)s/%(year)d/%(month)d/%(day)d/',
5036
        '/manage/agendas/%(agenda)s/%(year)d/%(month)d/',
5037
    ),
5038
)
5039
def test_agenda_booking_colors(app, admin_user, api_user, view):
5040
    agenda = Agenda.objects.create(label='New Example', kind='meetings')
5041
    desk = Desk.objects.create(agenda=agenda, label='New Desk')
5042
    meetingtype = MeetingType.objects.create(agenda=agenda, label='Bar', duration=30)
5043
    today = datetime.date.today()
5044
    timeperiod = TimePeriod.objects.create(
5045
        desk=desk, weekday=today.weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
5046
    )
5047

  
5048
    app.authorization = ('Basic', ('john.doe', 'password'))
5049
    datetimes_resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meetingtype.slug))
5050
    booking_url = datetimes_resp.json['data'][0]['api']['fillslot_url']
5051

  
5052
    # book first slot without colors
5053
    resp = app.post(booking_url)
5054
    date = Booking.objects.all()[0].event.start_datetime
5055

  
5056
    app.reset()
5057
    login(app)
5058

  
5059
    url = view % {'agenda': agenda.id, 'year': date.year, 'month': date.month, 'day': date.day}
5060
    resp = app.get(url)
5061
    assert len(resp.pyquery.find('div.booking')) == 1
5062
    assert 'background' not in resp.pyquery.find('div.booking')[0].attrib
5063
    assert 'Booking colors:' not in resp.text
5064

  
5065
    app.reset()
5066
    app.authorization = ('Basic', ('john.doe', 'password'))
5067
    booking_url2 = datetimes_resp.json['data'][1]['api']['fillslot_url']
5068
    booking_url3 = datetimes_resp.json['data'][2]['api']['fillslot_url']
5069
    resp = app.post_json(booking_url2, params={'use_color_for': 'Cooking'})
5070
    resp = app.post_json(booking_url3, params={'use_color_for': 'Cooking'})
5071
    booking = Booking.objects.get(pk=resp.json['booking_id'])
5072

  
5073
    app.reset()
5074
    login(app)
5075

  
5076
    resp = app.get(url)
5077
    assert len(resp.pyquery.find('div.booking')) == 3
5078
    assert len(resp.pyquery.find('span.color-legend-label')) == 1
5079
    assert 'background:#%s' % booking.color.code in resp.pyquery.find('div.booking')[1].attrib['style']
5080
    assert 'background:#%s' % booking.color.code in resp.pyquery.find('div.booking')[2].attrib['style']
5081
    assert 'Booking colors:' in resp.text
5082

  
5083
    app.reset()
5084
    app.authorization = ('Basic', ('john.doe', 'password'))
5085
    booking_url4 = datetimes_resp.json['data'][3]['api']['fillslot_url']
5086
    resp = app.post_json(booking_url4, params={'use_color_for': 'Swimming'})
5087
    new_booking = Booking.objects.get(pk=resp.json['booking_id'])
5088

  
5089
    app.reset()
5090
    login(app)
5091

  
5092
    resp = app.get(url)
5093
    assert len(resp.pyquery.find('div.booking')) == 4
5094
    assert len(resp.pyquery.find('span.color-legend-label')) == 2
5095
    assert 'background:#%s' % new_booking.color.code in resp.pyquery.find('div.booking')[3].attrib['style']
5096
    assert new_booking.color.code != booking.color.code
5030
-