0001-family-weekly-agenda-many-weeks-display-69454.patch
combo/apps/family/static/css/combo.weekly_agenda.scss | ||
---|---|---|
1 |
.weeklyagenda-cell { |
|
1 |
.weekly-agenda-cell {
|
|
2 | 2 |
margin-top: 1em; |
3 |
display: grid; |
|
4 |
grid-template-columns: repeat(3, auto); |
|
5 |
grid-template-rows: repeat(2, auto); |
|
6 |
justify-items: left; |
|
7 |
--min-column-width: 250; |
|
8 |
} |
|
9 | ||
10 |
.weekly-agenda-cell--previous-week, |
|
11 |
.weekly-agenda-cell--next-week { |
|
12 |
height: 3em; |
|
13 |
width: 3em; |
|
14 |
} |
|
15 | ||
16 |
.weekly-agenda-cell--next-week { |
|
17 |
margin-left: 1rem; |
|
18 |
} |
|
19 | ||
20 |
.weekly-agenda-cell--edit-btn { |
|
21 |
grid-column: 2; |
|
22 |
grid-row: 2; |
|
23 |
} |
|
24 | ||
25 |
.weekly-agenda-cell--week-list { |
|
26 |
overflow: hidden; |
|
27 |
margin-bottom: 2rem; |
|
28 |
width: 100%; |
|
29 |
} |
|
30 | ||
31 |
.weekly-agenda-cell--slider { |
|
32 |
column-gap: 0px; |
|
33 |
display: grid; |
|
34 |
grid-auto-columns: 0px; |
|
35 |
grid-auto-rows: 0px; |
|
36 |
grid-auto-flow: column; |
|
37 |
position: relative; |
|
38 |
} |
|
39 | ||
40 |
.weekly-agenda-cell--week-item:nth-child(even) { |
|
41 |
background: #fafafa; |
|
42 |
} |
|
43 | ||
44 |
.weekly-agenda-cell--week-title { |
|
45 |
border-bottom: 1px solid #888; |
|
46 |
font-weight: bold; |
|
47 |
margin-bottom: 1rem; |
|
3 | 48 |
display: flex; |
4 |
ul { |
|
5 |
padding: 0; |
|
6 |
margin: 0; |
|
7 |
ul { |
|
8 |
list-style: none; |
|
9 |
} |
|
10 |
} |
|
11 |
& > ul { |
|
12 |
width: 100%; |
|
13 |
& > li { |
|
14 |
display: none; |
|
15 |
&.shown { |
|
16 |
display: block; |
|
17 |
} |
|
18 |
} |
|
19 |
} |
|
20 |
button.previous-week, |
|
21 |
button.next-week { |
|
22 |
height: 3em; |
|
23 |
z-index: 10; |
|
24 |
} |
|
25 |
button.previous-week { |
|
26 |
margin-right: 1em; |
|
27 |
} |
|
28 |
button.next-week { |
|
29 |
margin-left: 1em; |
|
30 |
margin-right: 0; |
|
31 |
} |
|
32 |
.week-title { |
|
33 |
display: block; |
|
34 |
height: 3em; |
|
35 |
line-height: 3em; |
|
49 |
justify-content: center; |
|
50 |
align-items: center; |
|
51 |
height: 3.2rem; |
|
52 |
} |
|
53 | ||
54 |
.weekly-agenda-cell--day-item { |
|
55 |
margin: 0 1rem; |
|
56 |
} |
|
57 | ||
58 |
.weekly-agenda-cell--day-title { |
|
59 |
border-bottom: 1px solid #888; |
|
60 |
margin: 1rem 0; |
|
61 |
font-weight: bold; |
|
62 |
} |
|
63 | ||
64 |
.weekly-agenda-cell--day-no-activity { |
|
65 |
color: #888; |
|
66 |
} |
|
67 | ||
68 |
.weekly-agenda-cell--activity-item { |
|
69 |
display: flex; |
|
70 |
align-items: baseline; |
|
71 |
margin: 0.3rem 0; |
|
72 |
&+ .weekly-agenda-cell--day-no-activity { |
|
73 |
display: none; |
|
36 | 74 |
} |
37 |
.no-activity {
|
|
75 |
&.disabled {
|
|
38 | 76 |
color: #888; |
39 | 77 |
} |
40 |
li { |
|
41 |
&.day-title { |
|
42 |
margin: 1em 0 0.5em 0; |
|
43 |
} |
|
44 |
&.disabled { |
|
45 |
color: #888; |
|
46 |
} |
|
47 |
& > span { |
|
48 |
position: relative; |
|
49 |
} |
|
50 |
} |
|
51 |
span > span { |
|
52 |
padding-left: 1.8em; |
|
53 |
&::before { |
|
54 |
display: block; |
|
55 |
content: ''; |
|
56 |
position: absolute; |
|
57 |
margin: auto; |
|
58 |
height: calc(0.66rem + 2px); |
|
59 |
width: calc(0.66rem + 2px); |
|
60 |
background: transparent; |
|
61 |
top: 0.33rem; |
|
62 |
left: 0; |
|
63 |
border: 1px solid #aaa; |
|
64 |
border-radius: 2px; |
|
65 |
} |
|
66 |
&::after { |
|
67 |
display: block; |
|
68 |
content: ''; |
|
69 |
position: absolute; |
|
70 |
margin: auto; |
|
71 |
height: calc(0.66rem); |
|
72 |
width: calc(0.66rem); |
|
73 |
background: transparent; |
|
74 |
transition: background 0.1s linear; |
|
75 |
top: calc(0.33rem + 1px); |
|
76 |
left: 1px; |
|
77 |
} |
|
78 |
} |
|
79 |
& [data-status=booked] span > span::after { |
|
80 |
background: #3c3; |
|
78 |
} |
|
79 | ||
80 |
.weekly-agenda-cell--activity-status { |
|
81 |
min-height: calc(0.66rem + 2px); |
|
82 |
min-width: calc(0.66rem + 2px); |
|
83 |
background: transparent; |
|
84 |
border: 1px solid #aaa; |
|
85 |
border-radius: 2px; |
|
86 |
margin-right: 1rem; |
|
87 |
&.booked { |
|
88 |
background: green; |
|
81 | 89 |
} |
82 |
& [data-status=cancelled] span > span::after {
|
|
90 |
&.cancelled {
|
|
83 | 91 |
background: yellow; |
84 | 92 |
} |
85 |
& [data-status=absence] span > span::after {
|
|
93 |
&.absence {
|
|
86 | 94 |
background: red; |
87 | 95 |
} |
88 | 96 |
} |
89 | 97 | |
90 |
br.weekbreak { |
|
91 |
display: none; |
|
92 |
} |
|
93 | ||
94 |
@media screen and (max-width: 500px) { |
|
95 |
br.weekbreak { display: block; } |
|
96 |
div.weeklyagenda-cell > ul { |
|
97 |
margin: 0 -4em; |
|
98 |
& .week-title { |
|
99 |
margin: 0 4em; |
|
100 |
line-height: 150%; |
|
101 |
text-align: center; |
|
102 |
} |
|
103 |
} |
|
98 |
.weekly-agenda-cell--activity-label { |
|
99 |
flex-grow: 1; |
|
104 | 100 |
} |
combo/apps/family/static/js/combo.weekly_agenda.js | ||
---|---|---|
1 | 1 |
$(function () { |
2 |
init_agenda = function(cell_id) { |
|
3 |
$('.weeklyagenda-cell-' + cell_id + ' .previous-week').on('click', function() { |
|
4 |
var $cell = $(this).parents('.weeklyagenda-cell-' + cell_id); |
|
5 |
var $prev = $('li.shown', $cell).prev(); |
|
6 |
if ($prev.length) { $('li.shown', $cell).removeClass('shown'); $prev.addClass('shown'); } |
|
7 |
return false; |
|
8 |
}); |
|
9 |
$('.weeklyagenda-cell-' + cell_id + ' .next-week').on('click', function() { |
|
10 |
var $cell = $(this).parents('.weeklyagenda-cell-' + cell_id); |
|
11 |
var $next = $('li.shown', $cell).next(); |
|
12 |
if ($next.length) { $('li.shown', $cell).removeClass('shown'); $next.addClass('shown'); } |
|
13 |
return false; |
|
14 |
}); |
|
15 |
$('.weeklyagenda-cell-' + cell_id + ' li.day-title').each(function(idx, elem) { |
|
16 |
/* hide empty days */ |
|
17 |
var $next = $(elem).next(); |
|
18 |
if ($next.length == 0 || $next.is('.day-title')) { |
|
19 |
$(elem).hide(); |
|
2 |
function initAgendaCell($cell, prefix) { |
|
3 |
const $weekList = $(prefix + '--week-list', $cell); |
|
4 |
const $slider = $(prefix + '--slider', $cell); |
|
5 |
const $currentWeek = $(prefix + '--week-title.current', $cell); |
|
6 |
const $nextWeekButton = $(prefix + '--next-week', $cell); |
|
7 |
const $previousWeekButton = $(prefix + '--previous-week', $cell); |
|
8 |
const $editAgendaBtn = $(prefix + '--edit-btn', $cell); |
|
9 | ||
10 |
let $selectedWeek = $currentWeek; |
|
11 | ||
12 |
function getPreviousWeek() { return $selectedWeek.prevAll(prefix + '--week-title:first'); } |
|
13 |
function getNextWeek() { return $selectedWeek.nextAll(prefix + '--week-title:first'); } |
|
14 | ||
15 |
function setCurrentWeek($week, enableTransition) { |
|
16 |
if(!$week.length) { |
|
17 |
return; |
|
20 | 18 |
} |
19 |
const currentOffset = $week[0].offsetLeft; |
|
20 |
$slider.css({ |
|
21 |
'transform': `translate(-${currentOffset}px)`, |
|
22 |
'transition': (enableTransition ? 'transform 0.5s' : '') |
|
23 |
}); |
|
24 | ||
25 |
$selectedWeek = $week; |
|
26 |
$nextWeekButton.prop('disabled', getNextWeek().length == 0); |
|
27 |
$previousWeekButton.prop('disabled', getPreviousWeek().length == 0); |
|
28 | ||
29 |
const selectedWeekIsInPast = $selectedWeek.nextAll(prefix + '--week-title.current').length; |
|
30 |
$weekToEdit = selectedWeekIsInPast ? $currentWeek : $selectedWeek; |
|
31 |
$editAgendaBtn.prop('href', $weekToEdit.attr('data-edit-url')); |
|
32 |
} |
|
33 | ||
34 |
function setPreviousWeek() { setCurrentWeek(getPreviousWeek(), true); } |
|
35 |
function setNextWeek() { setCurrentWeek(getNextWeek(), true); } |
|
36 | ||
37 |
$previousWeekButton.on('click', setPreviousWeek); |
|
38 |
$nextWeekButton.on('click', setNextWeek); |
|
39 | ||
40 |
let touchStartX = 0; |
|
41 |
$cell.on('touchstart', (e) => { |
|
42 |
touchStartX = e.changedTouches[0].screenX; |
|
21 | 43 |
}); |
22 |
$('.weeklyagenda-cell-' + cell_id + ' li.week').each(function(idx, elem) { |
|
23 |
/* hide no-activity message if not empty */ |
|
24 |
if ($('.activity', $(elem)).length > 0) { |
|
25 |
$('.no-activity', $(elem)).hide(); |
|
26 |
} |
|
27 |
/* hide booking button if all items are disabled */ |
|
28 |
if ($('.activity', $(elem)).not('.disabled').length == 0) { |
|
29 |
$('.booking-btn', $(elem)).hide(); |
|
44 | ||
45 |
$cell.on('touchend', (e) => { |
|
46 |
const touchEndX = e.changedTouches[0].screenX; |
|
47 |
if (touchEndX - touchStartX < -30) { |
|
48 |
setNextWeek(); |
|
49 |
} else if (touchEndX - touchStartX > 30) { |
|
50 |
setPreviousWeek(); |
|
30 | 51 |
} |
31 | 52 |
}); |
32 |
$('.weeklyagenda-cell-' + cell_id).each(function(idx, elem) { |
|
33 |
/* init first week shown */ |
|
34 |
var $cell = $(this); |
|
35 |
$('li', $cell).removeClass('shown'); |
|
36 |
if ($('li.day-title.current', $cell).length) { |
|
37 |
$('li.day-title.current', $cell).parent().parent().addClass('shown'); |
|
38 |
} else { |
|
39 |
$('li', $cell).first().addClass('shown'); |
|
53 | ||
54 |
function updateColumnsWidth() { |
|
55 |
const minWidth = $slider.css('--min-column-width'); |
|
56 |
const availableWidth = $weekList[0].offsetWidth; |
|
57 |
let columnsWidth = availableWidth < minWidth ? availableWidth : availableWidth / Math.floor(availableWidth / minWidth); |
|
58 |
$slider.css('grid-template-columns', `repeat(53, ${columnsWidth}px)`); |
|
59 | ||
60 |
if($selectedWeek) { |
|
61 |
// Update slider offset to bring it at the new position of the selectedWeek |
|
62 |
setCurrentWeek($selectedWeek, false); |
|
40 | 63 |
} |
41 |
}); |
|
64 |
} |
|
65 | ||
66 |
function hideEmptyDays() { |
|
67 |
let nbRows = 8; |
|
68 |
for(let i = 0; i < 7; ++i) { |
|
69 |
const $days = $(prefix + `--day-item[data-weekday=${i}]`, $cell); |
|
70 |
const $activities = $(prefix + '--activity-item', $days); |
|
71 |
if(!$activities.length) { |
|
72 |
$days.css('display', 'none'); |
|
73 |
nbRows -= 1; |
|
74 |
} |
|
75 | ||
76 |
$slider.css('grid-template-rows', `repeat(${nbRows}, auto`); |
|
77 |
} |
|
78 |
} |
|
79 | ||
80 |
hideEmptyDays(); |
|
81 |
new ResizeObserver(updateColumnsWidth).observe($weekList[0]); |
|
82 |
addEventListener('resize', updateColumnsWidth); |
|
42 | 83 |
} |
84 | ||
43 | 85 |
$(document).on('combo:cell-loaded', function(ev, cell) { |
44 |
init_agenda($('.weeklyagenda-cell', $(cell)).data('cell-id')); |
|
86 |
if ($('.weekly-agenda-cell', $(cell)).length) { |
|
87 |
initAgendaCell($(cell), '.weekly-agenda-cell'); |
|
88 |
} |
|
45 | 89 |
}); |
46 | 90 |
}); |
combo/apps/family/templates/combo/family/weekly_agenda.html | ||
---|---|---|
8 | 8 |
</h2> |
9 | 9 |
{% endif %} |
10 | 10 | |
11 |
<div class="weeklyagenda-cell weeklyagenda-cell-{{ cell.pk }}" data-cell-id="{{ cell.pk }}">
|
|
11 |
<div class="weekly-agenda-cell weekly-agenda-cell-{{ cell.pk }}" data-cell-id="{{ cell.pk }}">
|
|
12 | 12 |
{% with first_monday=json.data.0.date|date|adjust_to_week_monday last_item=json.data|last %} |
13 |
{% with last_day=last_item.date|date|adjust_to_week_monday|add_days:6 %} |
|
14 |
{% now 'Y-m-d' as now %} |
|
15 | ||
16 |
{% spaceless %} |
|
17 |
<button class="previous-week">←</button> |
|
18 |
<ul> |
|
19 |
{% for day in first_monday|iterate_days_until:last_day %} |
|
20 |
{% if day.weekday == 0 %} |
|
21 |
{% with sunday=day|add_days:6 %} |
|
22 |
<li class="week"><span class="week-title">{% blocktrans with day_date=day|date:"d/m" sunday_date=sunday|date:"d/m" %}Week<br class="weekbreak"> of {{ day_date }} to {{ sunday_date }}{% endblocktrans %}</span><ul> |
|
23 |
{% endwith %} |
|
24 |
{% endif %} |
|
25 |
<li class="day-title {% if now == day|date:"Y-m-d" %}current{% endif %}" data-weekday="{{ day.weekday }}"><strong>{{ day|date:"l d/m" }}</strong></li> |
|
26 |
{% with day_str=day|date:"Y-m-d" %} |
|
27 |
{% for item in json.data %} |
|
28 |
{% if item.date == day_str %} |
|
29 | ||
30 |
<li class="activity {% if item.disabled %}disabled{% endif %}" data-status="{{ item.status }}"> |
|
31 |
<span><span>{{ item.label }}</span></span> |
|
32 |
</li> |
|
33 | ||
13 |
{% with last_day=last_item.date|date|adjust_to_week_monday|add_days:6 %} |
|
14 |
{% now 'Y-m-W' as current_week %} |
|
15 |
{% spaceless %} |
|
16 |
<button class="weekly-agenda-cell--previous-week">←</button> |
|
17 |
<div class="weekly-agenda-cell--week-list"> |
|
18 |
<div class="weekly-agenda-cell--slider"> |
|
19 |
{% for day in first_monday|iterate_days_until:last_day %} |
|
20 |
{% if day.weekday == 0 %} |
|
21 |
{% with sunday=day|add_days:6 %} |
|
22 |
<div class="weekly-agenda-cell--week-title {% if current_week == day|date:'Y-m-W' %}current{% endif %}" |
|
23 |
{% if booking_form_url %}data-edit-url="{{ booking_form_url }}{% if '?' in booking_form_url %}&{% else %}?{% endif %}current={{ day|adjust_to_week_monday|date:'Y-m-d' }}"{% endif %}> |
|
24 |
{% blocktrans with day_date=day|date:'d/m' sunday_date=sunday|date:'d/m' %}Week<br class="weekbreak"> of {{ day_date }} to {{ sunday_date }}{% endblocktrans %} |
|
25 |
</div> |
|
26 |
{% endwith %} |
|
27 |
{% endif %} |
|
28 |
<div class="weekly-agenda-cell--day-item" data-weekday="{{ day.weekday }}"> |
|
29 |
<div class="weekly-agenda-cell--day-title" > |
|
30 |
{{ day|date:"l d/m" }} |
|
31 |
</div> |
|
32 |
{% with day_str=day|date:"Y-m-d" %} |
|
33 |
{% for item in json.data %} |
|
34 |
{% if item.date == day_str %} |
|
35 |
<div class="weekly-agenda-cell--activity-item {% if item.disabled %}disabled{% endif %}"> |
|
36 |
<div class="weekly-agenda-cell--activity-status {{ item.status }}"/></div> |
|
37 |
<div class="weekly-agenda-cell--activity-label"/>{{ item.label }}</div> |
|
38 |
</div> |
|
39 |
{% endif %} |
|
40 |
{% endfor %} |
|
41 |
{% endwith %} |
|
42 |
<div class="weekly-agenda-cell--day-no-activity">{% trans "No activity this week" %}</div> |
|
43 |
</div> |
|
44 |
{% endfor %} |
|
45 |
</div> |
|
46 |
</div> |
|
47 |
<button class="weekly-agenda-cell--next-week">→</button> |
|
48 |
{% if booking_form_url %} |
|
49 |
<a class="pk-button weekly-agenda-cell--edit-btn"> |
|
50 |
{% trans "Update bookings" %} |
|
51 |
</a> |
|
34 | 52 |
{% endif %} |
35 |
{% endfor %}
|
|
53 |
{% endspaceless %}
|
|
36 | 54 |
{% endwith %} |
37 |
{% if day.weekday == 6 %} |
|
38 |
</ul> |
|
39 |
{% if booking_form_url %}<p class="booking-btn"><a class="pk-button" href="{{ booking_form_url }}{% if '?' in booking_form_url %}&{% else %}?{% endif %}current={{ day|adjust_to_week_monday|date:"Y-m-d" }}">{% trans "Update bookings" %}</a></p>{% endif %} |
|
40 |
<p class="no-activity">{% trans "No activity this week" %}</p> |
|
41 |
</li> |
|
42 |
{% endif %} |
|
43 |
{% endfor %} |
|
44 |
</ul> |
|
45 |
<button class="next-week">→</button> |
|
46 |
{% endspaceless %} |
|
47 |
{% endwith %} |
|
48 | 55 |
{% endwith %} |
49 | 56 |
</div> |
50 | 57 |
{% endif %} |
tests/test_family.py | ||
---|---|---|
295 | 295 |
with mock.patch('requests.Session.get') as requests_get: |
296 | 296 |
requests_get.return_value = MockedRequestResponse(content=json.dumps(data)) |
297 | 297 |
result = cell.render(context) |
298 |
assert 'booking-btn' not in result |
|
298 |
assert 'pk-button weekly-agenda-cell--edit-btn' not in result |
|
299 |
assert 'data-edit-url' not in result |
|
299 | 300 | |
300 | 301 |
cell.booking_form_url = 'http://example.com/foobar/' |
301 | 302 |
cell.save() |
302 | 303 |
with mock.patch('requests.Session.get') as requests_get: |
303 | 304 |
requests_get.return_value = MockedRequestResponse(content=json.dumps(data)) |
304 | 305 |
result = cell.render(context) |
305 |
assert 'booking-btn' in result
|
|
306 |
assert 'http://example.com/foobar/?current=2022-02-28' in result
|
|
306 |
assert 'pk-button weekly-agenda-cell--edit-btn' in result
|
|
307 |
assert 'data-edit-url="http://example.com/foobar/?current=2022-02-28"' in result
|
|
307 | 308 | |
308 | 309 |
cell.booking_form_url = 'http://example.com/foobar/?user={{ user_nameid }}' |
309 | 310 |
cell.save() |
310 | 311 |
with mock.patch('requests.Session.get') as requests_get: |
311 | 312 |
requests_get.return_value = MockedRequestResponse(content=json.dumps(data)) |
312 | 313 |
result = cell.render(context) |
313 |
assert 'booking-btn' in result |
|
314 |
assert 'http://example.com/foobar/?user=xyz¤t=2022-02-28' in result |
|
314 |
assert 'pk-button weekly-agenda-cell--edit-btn' in result |
|
315 |
assert 'data-edit-url="http://example.com/foobar/?user=xyz¤t=2022-02-28"' in result |
|
315 |
- |