0001-manager-redo-day-view-to-always-have-one-cell-one-ho.patch
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 |
- |