Projet

Général

Profil

0001-manager-redo-day-view-to-always-have-one-cell-one-ho.patch

Frédéric Péters, 17 janvier 2018 11:30

Télécharger (9,89 ko)

Voir les différences:

Subject: [PATCH] manager: redo day view to always have one cell/one hour
 (#21213)

 chrono/manager/static/css/style.scss               | 32 ++++++++--------
 .../templates/chrono/manager_agenda_day_view.html  | 19 ++++++----
 chrono/manager/views.py                            | 43 +++++++++++++---------
 tests/test_manager.py                              | 23 +++++++-----
 4 files changed, 67 insertions(+), 50 deletions(-)
chrono/manager/static/css/style.scss
88 88
}
89 89

  
90 90
$dayview-column-width: 14vw;
91
$dayview-row-height: 4.5ex;
92 91

  
93 92
.dayview thead th {
94 93
	width: $dayview-column-width;
......
98 97
.dayview tbody th {
99 98
	box-sizing: border-box;
100 99
	text-align: left;
101
	padding: 0 2ex;
102
	line-height: $dayview-row-height;
103
	height: $dayview-row-height;
100
	padding: 1ex 2ex;
101
	vertical-align: top;
104 102
}
105 103

  
106 104
.dayview tbody tr:nth-child(2n+1) th,
......
111 109
	}
112 110
}
113 111

  
114
.dayview td {
115
	padding: 0.5ex 1ex;
112
.dayview tbody td {
113
	padding: 0 1ex;
114
	vertical-align: top;
115
	position: relative;
116 116
}
117 117

  
118
/* attr(data-rowspan) is not supported by browsers; emulate it by getting
119
 * the attribute value into a CSS variable. */
120
@for $i from 2 through 100 {
121
	[data-rowspan="#{$i}"] { --rowspan: #{$i}; }
118
@for $i from 1 through 60 {
119
	table.hourspan-#{$i} tbody td {
120
		height: calc(#{$i} * 2.5em);
121
	}
122 122
}
123 123

  
124
.dayview div[data-rowspan] {
125
	margin-top: -1ex;
124
.dayview tbody td div {
125
	z-index: 1;
126 126
	box-sizing: border-box;
127 127
	padding: 1ex;
128 128
	background: #eef;
129 129
	position: absolute;
130 130
	width: calc(#{$dayview-column-width} - 2ex);
131
	height: calc(#{$dayview-row-height} * var(--rowspan) - 2ex);
132
	min-height: calc(#{$dayview-row-height} * var(--rowspan) - 2ex);
133
	border: 1px solid #666;
131
	border: 1px solid #aaa;
134 132
	overflow: hidden;
135 133
	&:hover {
136 134
		height: auto;
137 135
	}
138 136
}
137

  
138
span.start-time {
139
	font-size: 80%;
140
}
chrono/manager/templates/chrono/manager_agenda_day_view.html
25 25
{% for period, desk_bookings in view.get_timeperiods %}
26 26

  
27 27
{% if forloop.first %}
28
<table>
28
<table class="hourspan-{{ hour_span }}">
29 29
  <thead>
30 30
    <tr>
31 31
      <td></td>
......
39 39

  
40 40
    <tr>
41 41
      <th>{{ period|date:"TIME_FORMAT" }}</th>
42
      {% for booking in desk_bookings %}
42
      {% for bookings in desk_bookings %}
43 43
      <td>
44
         {% if booking %}
45
         <div class="booked" {% if booking.rowspan %}data-rowspan="{{ booking.rowspan }}"{% endif %}>
46
           <a {% if booking.backoffice_url %}href="{{booking.backoffice_url}}"{% endif %}
44
        {% for booking in bookings %}
45
          <div class="booked"
46
              style="height: {{ booking.css_height }}%; top: {{ booking.css_top }}%;"
47
            ><span class="start-time">{{booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
48
            <a {% if booking.backoffice_url %}href="{{booking.backoffice_url}}"{% endif %}
47 49
             >{% if booking.label or booking.user_name %}
48 50
                {{booking.label}}{% if booking.label and booking.user_name %} - {% endif %} {{booking.user_name}}
49 51
              {% else %}{% trans "booked" %}{% endif %}</a>
50
         </div>
51
         {% endif %}
52
      </td>{% endfor %}
52
          </div>
53
        {% endfor %}
54
      </td>
55
      {% endfor %}
53 56
    </tr>
54 57

  
55 58
{% if forloop.last %}
chrono/manager/views.py
175 175
    def get_context_data(self, **kwargs):
176 176
        context = super(AgendaDayView, self).get_context_data(**kwargs)
177 177
        context['agenda'] = self.agenda
178
        try:
179
            context['hour_span'] = max(60 / self.agenda.get_base_meeting_duration(), 1)
180
        except ValueError:  # no meeting types defined
181
            context['hour_span'] = 1
178 182
        context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
179 183
        return context
180 184

  
......
205 209
        min_timeperiod = min([x.start_time for x in timeperiods])
206 210
        max_timeperiod = max([x.end_time for x in timeperiods])
207 211

  
208
        interval = datetime.timedelta(minutes=self.agenda.get_base_meeting_duration())
209
        current_date = self.date.replace(hour=min_timeperiod.hour, minute=min_timeperiod.minute)
210
        max_date = self.date.replace(hour=max_timeperiod.hour, minute=max_timeperiod.minute)
212
        interval = datetime.timedelta(minutes=60)
213
        current_date = self.date.replace(hour=min_timeperiod.hour, minute=0)
214
        if max_timeperiod.minute == 0:
215
            max_date = self.date.replace(hour=max_timeperiod.hour, minute=0)
216
        else:
217
            # until the end of the last hour.
218
            max_date = self.date.replace(hour=max_timeperiod.hour + 1, minute=0)
211 219

  
212 220
        desks = self.agenda.desk_set.all()
213 221

  
214 222
        while current_date < max_date:
215 223
            # for each timeslot return the timeslot date and a list of per-desk
216 224
            # bookings
217
            bookings = []
225
            desks_bookings = []  # list of bookings for each desk
218 226
            for desk in desks:
219
                booking = None
220
                event = [x for x in self.object_list if x.desk_id == desk.id and x.start_datetime == current_date]
221
                if event:
222
                    # if an event exist, check it has a non cancelled booking
223
                    event = event[0]
224
                    event_bookings = [x for x in event.booking_set.all() if not x.cancellation_datetime]
225
                    if event_bookings:
226
                        booking = event_bookings[0]
227
                        if event.meeting_type.duration > (interval.total_seconds() / 60):
228
                            booking.rowspan = int(event.meeting_type.duration / (interval.total_seconds() / 60))
229

  
230
                bookings.append(booking)
231

  
232
            yield current_date, bookings
227
                bookings = []  # bookings for this desk
228
                finish_datetime = current_date + interval
229
                for event in [x for x in self.object_list if x.desk_id == desk.id and
230
                              x.start_datetime >= current_date and x.start_datetime < finish_datetime]:
231
                    # don't consider cancelled bookings
232
                    for booking in [x for x in event.booking_set.all() if not x.cancellation_datetime]:
233
                        booking.css_top = int(100 * event.start_datetime.minute / 60)
234
                        booking.css_height = int(100 * event.meeting_type.duration / 60)
235
                        bookings.append(booking)
236

  
237
                desks_bookings.append(bookings)
238

  
239
            yield current_date, desks_bookings
233 240
            current_date += interval
234 241

  
235 242
agenda_day_view = AgendaDayView.as_view()
tests/test_manager.py
1192 1192
    resp = resp.follow()
1193 1193
    assert 'No opening hours this day.' in resp.body # no time pediod
1194 1194

  
1195
    TimePeriod(desk=desk, weekday=today.weekday(),
1195
    timeperiod = TimePeriod(desk=desk, weekday=today.weekday(),
1196 1196
            start_time=datetime.time(10, 0),
1197
            end_time=datetime.time(18, 0)).save()
1197
            end_time=datetime.time(18, 0))
1198
    timeperiod.save()
1198 1199
    resp = app.get('/manage/agendas/%s/' % agenda.id, status=302).follow()
1199 1200
    assert not 'No opening hours this day.' in resp.body
1200 1201
    assert not 'div class="booked' in resp.body
1201
    assert resp.body.count('<tr') == 17
1202
    assert resp.body.count('<tr') == 9  # 10->18 (not included)
1203

  
1204
    timeperiod.end_time = datetime.time(18, 30)  # end during an hour
1205
    timeperiod.save()
1206
    resp = app.get('/manage/agendas/%s/' % agenda.id, status=302).follow()
1207
    assert resp.body.count('<tr') == 10  # 10->18 (included)
1202 1208

  
1203 1209
    # book some slots
1204 1210
    app.reset()
......
1216 1222
    resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (
1217 1223
        agenda.id, date.year, date.month, date.day))
1218 1224
    assert resp.body.count('div class="booked') == 2
1219
    assert resp.body.count('data-rowspan') == 0
1225
    assert 'hourspan-2' in resp.body  # table CSS class
1226
    assert 'height: 50%; top: 0%;' in resp.body  # booking cells
1220 1227

  
1221
    # create a shorted meeting type, this will double the number of rows and
1222
    # the bookings will have to span multiple rows.
1228
    # create a shorter meeting type, this will change the table CSS class
1229
    # (and visually this will give more room for events)
1223 1230
    meetingtype = MeetingType(agenda=agenda, label='Baz', duration=15)
1224 1231
    meetingtype.save()
1225 1232
    resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (
1226 1233
        agenda.id, date.year, date.month, date.day))
1227
    assert resp.body.count('<tr') == 33
1228 1234
    assert resp.body.count('div class="booked') == 2
1229
    assert resp.body.count('data-rowspan') == 2
1235
    assert 'hourspan-4' in resp.body  # table CSS class
1230 1236

  
1231 1237
    # cancel a booking
1232 1238
    app.reset()
......
1240 1246
    resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (
1241 1247
        agenda.id, date.year, date.month, date.day))
1242 1248
    assert resp.body.count('div class="booked') == 1
1243
    assert resp.body.count('data-rowspan') == 1
1244 1249

  
1245 1250
    # wrong type
1246 1251
    agenda2 = Agenda(label=u'Foo bar')
1247
-