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/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
|
||||
|
|
|
@ -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
|
||||
# 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)
|
||||
|
|
|
@ -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
|
||||
BOOKING_CALENDAR_CELL_ENABLED = False
|
||||
LEGACY_CHART_CELL_ENABLED = False
|
||||
PUBLIK_FAMILY_CELL_ENABLED = False
|
||||
|
||||
|
||||
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')
|
||||
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')
|
||||
|
|
Loading…
Reference in New Issue