family: weekly agenda cell (#56027)
This commit is contained in:
parent
85f3ac7733
commit
5d94130a44
|
@ -13,6 +13,7 @@ combo/apps/maps/static/css/combo.map.css
|
||||||
combo/apps/maps/static/css/combo.map.css.map
|
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
|
||||||
combo/apps/pwa/static/css/combo.manager.pwa.css.map
|
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
|
||||||
data/themes/gadjo/static/css/agent-portal.css.map
|
data/themes/gadjo/static/css/agent-portal.css.map
|
||||||
.cache
|
.cache
|
||||||
|
|
|
@ -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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -14,10 +14,12 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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 django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from combo.data.library import register_cell_class
|
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
|
from .utils import get_family, is_family_enabled
|
||||||
|
|
||||||
|
@ -50,3 +52,47 @@ class FamilyInfosCell(CellBase):
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return {'family': response.json()}
|
return {'family': response.json()}
|
||||||
return {'error': _('An error occured while retrieving family details.')}
|
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)
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'));
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 %}
|
|
@ -366,6 +366,7 @@ PWA_NOTIFICATION_ICON_URL = None
|
||||||
# hide work-in-progress/experimental/broken/legacy/whatever cells for now
|
# hide work-in-progress/experimental/broken/legacy/whatever cells for now
|
||||||
BOOKING_CALENDAR_CELL_ENABLED = False
|
BOOKING_CALENDAR_CELL_ENABLED = False
|
||||||
LEGACY_CHART_CELL_ENABLED = False
|
LEGACY_CHART_CELL_ENABLED = False
|
||||||
|
PUBLIK_FAMILY_CELL_ENABLED = False
|
||||||
|
|
||||||
|
|
||||||
def debug_show_toolbar(request):
|
def debug_show_toolbar(request):
|
||||||
|
|
|
@ -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'
|
||||||
|
)
|
|
@ -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')
|
resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
|
||||||
with CaptureQueriesContext(connection) as ctx:
|
with CaptureQueriesContext(connection) as ctx:
|
||||||
resp = resp.form.submit()
|
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 Page.objects.count() == 4
|
||||||
assert PageSnapshot.objects.all().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')
|
resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
|
||||||
with CaptureQueriesContext(connection) as ctx:
|
with CaptureQueriesContext(connection) as ctx:
|
||||||
resp = resp.form.submit()
|
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 set(Page.objects.get(slug='one').related_cells['cell_types']) == {'data_textcell', 'data_linkcell'}
|
||||||
assert Page.objects.count() == 4
|
assert Page.objects.count() == 4
|
||||||
assert LinkCell.objects.count() == 2
|
assert LinkCell.objects.count() == 2
|
||||||
|
@ -2222,7 +2222,7 @@ def test_page_versionning(app, admin_user):
|
||||||
|
|
||||||
with CaptureQueriesContext(connection) as ctx:
|
with CaptureQueriesContext(connection) as ctx:
|
||||||
resp2 = resp.click('view', index=1)
|
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 Page.snapshots.latest('pk').related_cells == {'cell_types': ['data_textcell']}
|
||||||
assert resp2.text.index('Hello world') < resp2.text.index('Foobar3')
|
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)
|
resp = resp.click('restore', index=6)
|
||||||
with CaptureQueriesContext(connection) as ctx:
|
with CaptureQueriesContext(connection) as ctx:
|
||||||
resp = resp.form.submit().follow()
|
resp = resp.form.submit().follow()
|
||||||
assert len(ctx.captured_queries) == 142
|
assert len(ctx.captured_queries) == 144
|
||||||
|
|
||||||
resp2 = resp.click('See online')
|
resp2 = resp.click('See online')
|
||||||
assert resp2.text.index('Foobar1') < resp2.text.index('Foobar2') < resp2.text.index('Foobar3')
|
assert resp2.text.index('Foobar1') < resp2.text.index('Foobar2') < resp2.text.index('Foobar3')
|
||||||
|
|
Loading…
Reference in New Issue