family: weekly agenda cell (#56027)
gitea-wip/combo/pipeline/head There was a failure building this commit Details
gitea/combo/pipeline/head Build started... Details

This commit is contained in:
Lauréline Guérin 2021-08-09 15:47:48 +02:00
parent 85f3ac7733
commit 5d94130a44
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
9 changed files with 387 additions and 5 deletions

1
.gitignore vendored
View File

@ -13,6 +13,7 @@ combo/apps/maps/static/css/combo.map.css
combo/apps/maps/static/css/combo.map.css.map
combo/apps/pwa/static/css/combo.manager.pwa.css
combo/apps/pwa/static/css/combo.manager.pwa.css.map
combo/apps/family/static/css/combo.weekly_agenda.css
data/themes/gadjo/static/css/agent-portal.css
data/themes/gadjo/static/css/agent-portal.css.map
.cache

View File

@ -0,0 +1,50 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0011_update_proxy_permissions'),
('data', '0047_auto_20210723_1318'),
('family', '0005_familyinfoscell_template_name'),
]
operations = [
migrations.CreateModel(
name='WeeklyAgendaCell',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(blank=True, verbose_name='Slug')),
(
'extra_css_class',
models.CharField(
blank=True, max_length=100, verbose_name='Extra classes for CSS styling'
),
),
(
'template_name',
models.CharField(blank=True, max_length=50, null=True, verbose_name='Cell Template'),
),
('public', models.BooleanField(default=True, verbose_name='Public')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('last_update_timestamp', models.DateTimeField(auto_now=True)),
('title', models.CharField(blank=True, max_length=150, verbose_name='Title')),
('agenda_reference', models.CharField(max_length=128, verbose_name='Agenda')),
('user_external_id_key', models.CharField(max_length=50)),
('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')),
],
options={
'verbose_name': 'Weekly agenda cell',
},
),
]

View File

@ -14,10 +14,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.db import models
from django.utils.translation import ugettext_lazy as _
from combo.data.library import register_cell_class
from combo.data.models import CellBase
from combo.data.models import CellBase, JsonCellBase
from .utils import get_family, is_family_enabled
@ -50,3 +52,47 @@ class FamilyInfosCell(CellBase):
if response.status_code == 200:
return {'family': response.json()}
return {'error': _('An error occured while retrieving family details.')}
@register_cell_class
class WeeklyAgendaCell(JsonCellBase):
title = models.CharField(_('Title'), max_length=150, blank=True)
agenda_reference = models.CharField(_('Agenda'), max_length=128)
user_external_id_key = models.CharField(max_length=50)
default_template_name = 'family/weekly_agenda.html'
force_async = True
class Meta:
verbose_name = _('Weekly agenda cell')
class Media:
js = ('js/combo.weekly_agenda.js',)
css = {'all': ('css/combo.weekly_agenda.css',)}
@classmethod
def is_enabled(cls):
return settings.PUBLIK_FAMILY_CELL_ENABLED
@property
def url(self):
chrono = list(settings.KNOWN_SERVICES.get('chrono').values())[0]
chrono_url = chrono.get('url') or ''
if not chrono_url.endswith('/'):
chrono_url += '/'
# XXX events=all param is not supported for now
return (
'%sapi/agendas/datetimes/?agendas=%s&user_external_id=%s:{{ user_external_id|default:user_nameid }}'
% (chrono_url, self.agenda_reference, self.user_external_id_key)
)
def is_visible(self, **kwargs):
user = kwargs.get('user')
if not user or user.is_anonymous:
return False
return super().is_visible(**kwargs)
def get_cell_extra_context(self, context):
if context.get('placeholder_search_mode'):
return {}
return super().get_cell_extra_context(context)

View File

@ -0,0 +1,95 @@
.weeklyagenda-cell {
margin-top: 1em;
display: flex;
ul {
padding: 0;
margin: 0;
ul {
list-style: none;
}
}
& > ul {
width: 100%;
& > li {
display: none;
&.shown {
display: block;
}
}
}
button.previous-week,
button.next-week {
height: 3em;
z-index: 10;
}
button.next-week {
margin-left: 1em;
margin-right: 0;
}
.week-title {
display: block;
height: 3em;
line-height: 3em;
}
.no-activity {
color: #888;
}
li {
&.day-title {
margin: 1em 0 0.5em 0;
}
&.disabled {
color: #888;
}
& > span {
position: relative;
}
}
span > span {
padding-left: 1.8em;
&::before {
display: block;
content: '';
position: absolute;
margin: auto;
height: calc(0.66rem + 2px);
width: calc(0.66rem + 2px);
background: transparent;
top: 0.33rem;
left: 0;
border: 1px solid #aaa;
border-radius: 2px;
}
&::after {
display: block;
content: '';
position: absolute;
margin: auto;
height: calc(0.66rem);
width: calc(0.66rem);
background: transparent;
transition: background 0.1s linear;
top: calc(0.33rem + 1px);
left: 1px;
}
}
& [data-status=green] span > span::after {
background: #3c3;
}
}
br.weekbreak {
display: none;
}
@media screen and (max-width: 500px) {
br.weekbreak { display: block; }
div.weeklyagenda-cell > ul {
margin: 0 -4em;
& .week-title {
margin: 0 4em;
line-height: 150%;
text-align: center;
}
}
}

View File

@ -0,0 +1,46 @@
$(function () {
init_agenda = function(cell_id) {
$('.weeklyagenda-cell-' + cell_id + ' .previous-week').on('click', function() {
var $cell = $(this).parents('.weeklyagenda-cell-' + cell_id);
var $prev = $('li.shown', $cell).prev();
if ($prev.length) { $('li.shown', $cell).removeClass('shown'); $prev.addClass('shown'); }
return false;
});
$('.weeklyagenda-cell-' + cell_id + ' .next-week').on('click', function() {
var $cell = $(this).parents('.weeklyagenda-cell-' + cell_id);
var $next = $('li.shown', $cell).next();
if ($next.length) { $('li.shown', $cell).removeClass('shown'); $next.addClass('shown'); }
return false;
});
$('.weeklyagenda-cell-' + cell_id + ' li.day-title').each(function(idx, elem) {
/* hide empty days */
var $next = $(elem).next();
if ($next.length == 0 || $next.is('.day-title')) {
$(elem).hide();
}
});
$('.weeklyagenda-cell-' + cell_id + ' li.week').each(function(idx, elem) {
/* hide no-activity message if not empty */
if ($('.activity', $(elem)).length > 0) {
$('.no-activity', $(elem)).hide();
}
/* hide booking button if all items are disabled */
if ($('.activity', $(elem)).not('.disabled').length == 0) {
$('.booking-btn', $(elem)).hide();
}
});
$('.weeklyagenda-cell-' + cell_id).each(function(idx, elem) {
/* init first week shown */
var $cell = $(this);
$('li', $cell).removeClass('shown');
if ($('li.day-title.current', $cell).length) {
$('li.day-title.current', $cell).parent().parent().addClass('shown');
} else {
$('li', $cell).first().addClass('shown');
}
});
}
$(document).on('combo:cell-loaded', function(ev, cell) {
init_agenda($('.weeklyagenda-cell', $(cell)).data('cell-id'));
});
});

View File

@ -0,0 +1,51 @@
{% load i18n %}
{% block cell-content %}
{% if json.data %}
{% if cell.title %}
<h2>
{{ cell.title }}
</h2>
{% endif %}
<div class="weeklyagenda-cell weeklyagenda-cell-{{ cell.pk }}" data-cell-id="{{ cell.pk }}">
{% with first_monday=json.data.0.date|date|adjust_to_week_monday last_item=json.data|last %}
{% with last_day=last_item.date|date|adjust_to_week_monday|add_days:6 %}
{% now 'Y-m-d' as now %}
{% spaceless %}
<button class="previous-week"></button>
<ul>
{% for day in first_monday|iterate_days_until:last_day %}
{% if day.weekday == 0 %}
{% with sunday=day|add_days:6 %}
<li class="week"><span class="week-title">{% blocktrans with day_date=day|date:"d/m" sunday_date=sunday|date:"d/m" %}Semaine<br class="weekbreak"> du {{ day_date }} au {{ sunday_date }}{% endblocktrans %}</span><ul>
{% endwith %}
{% endif %}
<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>
{% with day_str=day|date:"Y-m-d" %}
{% for item in json.data %}
{% if item.date == day_str %}
<li class="activity {% if item.disabled %}disabled{% endif %}"
{% if item.booked_for_external_user %}data-status="green"{% endif %}
><span><span>{{ item.label }}</span></span>
</li>
{% endif %}
{% endfor %}
{% endwith %}
{% if day.weekday == 6 %}
</ul>
<p class="no-activity">{% trans "No activity this week" %}</p>
</li>
{% endif %}
{% endfor %}
</ul>
<button class="next-week"></button>
{% endspaceless %}
{% endwith %}
{% endwith %}
</div>
{% endif %}
{% endblock %}

View File

@ -366,6 +366,7 @@ PWA_NOTIFICATION_ICON_URL = None
# hide work-in-progress/experimental/broken/legacy/whatever cells for now
BOOKING_CALENDAR_CELL_ENABLED = False
LEGACY_CHART_CELL_ENABLED = False
PUBLIK_FAMILY_CELL_ENABLED = False
def debug_show_toolbar(request):

92
tests/test_family.py Normal file
View File

@ -0,0 +1,92 @@
import json
from unittest import mock
import pytest
from django.core.cache import cache
from django.test.client import RequestFactory
from combo.apps.family.models import WeeklyAgendaCell
from combo.data.models import Page
from combo.utils import NothingInCacheException
pytestmark = pytest.mark.django_db
@pytest.fixture
def context():
ctx = {'request': RequestFactory().get('/')}
ctx['request'].user = None
ctx['request'].session = {}
return ctx
class MockUser:
email = 'foo@example.net'
is_authenticated = True
is_anonymous = False
def get_name_id(self):
return None
class MockUserWithNameId:
email = 'foo@example.net'
is_authenticated = True
is_anonymous = False
def get_name_id(self):
return 'xyz'
class MockedRequestResponse(mock.Mock):
status_code = 200
def json(self):
return json.loads(self.content)
def test_weeklyagenda_cell(settings, context):
settings.PUBLIK_FAMILY_CELL_ENABLED = True
page = Page.objects.create(title='Family', slug='index', template_name='standard')
cell = WeeklyAgendaCell.objects.create(page=page, placeholder='content', order=0)
context['request'].user = MockUser()
# query should fail as nothing is cached
cache.clear()
with pytest.raises(NothingInCacheException):
cell.render(context)
context['synchronous'] = True # to get fresh content
data = {'data': []}
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
cell.render(context)
# wrong url
assert (
requests_get.call_args_list[0][0][0]
== 'http://chrono.example.org/api/agendas/datetimes/?agendas=&user_external_id=:'
)
cell.agenda_reference = 'some-agenda'
cell.save()
context['request'].user = MockUserWithNameId()
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
cell.render(context)
assert (
requests_get.call_args_list[0][0][0]
== 'http://chrono.example.org/api/agendas/datetimes/?agendas=some-agenda&user_external_id=:xyz'
)
cell.user_external_id_key = 'some-key'
cell.save()
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
cell.render(context)
assert (
requests_get.call_args_list[0][0][0]
== 'http://chrono.example.org/api/agendas/datetimes/?agendas=some-agenda&user_external_id=some-key:xyz'
)

View File

@ -880,7 +880,7 @@ def test_site_export_import_json(app, admin_user):
resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
with CaptureQueriesContext(connection) as ctx:
resp = resp.form.submit()
assert len(ctx.captured_queries) in [298, 299]
assert len(ctx.captured_queries) in [303, 304]
assert Page.objects.count() == 4
assert PageSnapshot.objects.all().count() == 4
@ -891,7 +891,7 @@ def test_site_export_import_json(app, admin_user):
resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
with CaptureQueriesContext(connection) as ctx:
resp = resp.form.submit()
assert len(ctx.captured_queries) == 268
assert len(ctx.captured_queries) == 272
assert set(Page.objects.get(slug='one').related_cells['cell_types']) == {'data_textcell', 'data_linkcell'}
assert Page.objects.count() == 4
assert LinkCell.objects.count() == 2
@ -2222,7 +2222,7 @@ def test_page_versionning(app, admin_user):
with CaptureQueriesContext(connection) as ctx:
resp2 = resp.click('view', index=1)
assert len(ctx.captured_queries) == 70
assert len(ctx.captured_queries) == 71
assert Page.snapshots.latest('pk').related_cells == {'cell_types': ['data_textcell']}
assert resp2.text.index('Hello world') < resp2.text.index('Foobar3')
@ -2283,7 +2283,7 @@ def test_page_versionning(app, admin_user):
resp = resp.click('restore', index=6)
with CaptureQueriesContext(connection) as ctx:
resp = resp.form.submit().follow()
assert len(ctx.captured_queries) == 142
assert len(ctx.captured_queries) == 144
resp2 = resp.click('See online')
assert resp2.text.index('Foobar1') < resp2.text.index('Foobar2') < resp2.text.index('Foobar3')