Projet

Général

Profil

0001-family-weekly-agenda-cell-56027.patch

Lauréline Guérin, 03 septembre 2021 08:54

Télécharger (16,2 ko)

Voir les différences:

Subject: [PATCH] family: weekly agenda cell (#56027)

 .../migrations/0006_weekly_agenda_cell.py     |  50 +++++
 combo/apps/family/models.py                   |  43 +++-
 .../templates/family/weekly_agenda.html       | 190 ++++++++++++++++++
 combo/settings.py                             |   1 +
 tests/test_family.py                          |  92 +++++++++
 tests/test_manager.py                         |   8 +-
 6 files changed, 379 insertions(+), 5 deletions(-)
 create mode 100644 combo/apps/family/migrations/0006_weekly_agenda_cell.py
 create mode 100644 combo/apps/family/templates/family/weekly_agenda.html
 create mode 100644 tests/test_family.py
combo/apps/family/migrations/0006_weekly_agenda_cell.py
1
import django.db.models.deletion
2
from django.db import migrations, models
3

  
4

  
5
class Migration(migrations.Migration):
6

  
7
    dependencies = [
8
        ('auth', '0011_update_proxy_permissions'),
9
        ('data', '0047_auto_20210723_1318'),
10
        ('family', '0005_familyinfoscell_template_name'),
11
    ]
12

  
13
    operations = [
14
        migrations.CreateModel(
15
            name='WeeklyAgendaCell',
16
            fields=[
17
                (
18
                    'id',
19
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
20
                ),
21
                ('placeholder', models.CharField(max_length=20)),
22
                ('order', models.PositiveIntegerField()),
23
                ('slug', models.SlugField(blank=True, verbose_name='Slug')),
24
                (
25
                    'extra_css_class',
26
                    models.CharField(
27
                        blank=True, max_length=100, verbose_name='Extra classes for CSS styling'
28
                    ),
29
                ),
30
                (
31
                    'template_name',
32
                    models.CharField(blank=True, max_length=50, null=True, verbose_name='Cell Template'),
33
                ),
34
                ('public', models.BooleanField(default=True, verbose_name='Public')),
35
                (
36
                    'restricted_to_unlogged',
37
                    models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
38
                ),
39
                ('last_update_timestamp', models.DateTimeField(auto_now=True)),
40
                ('title', models.CharField(blank=True, max_length=150, verbose_name='Title')),
41
                ('agenda_reference', models.CharField(max_length=128, verbose_name='Agenda')),
42
                ('user_external_id_key', models.CharField(max_length=50)),
43
                ('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')),
44
                ('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')),
45
            ],
46
            options={
47
                'verbose_name': 'Weekly agenda cell',
48
            },
49
        ),
50
    ]
combo/apps/family/models.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
from django.conf import settings
18
from django.db import models
17 19
from django.utils.translation import ugettext_lazy as _
18 20

  
19 21
from combo.data.library import register_cell_class
20
from combo.data.models import CellBase
22
from combo.data.models import CellBase, JsonCellBase
21 23

  
22 24
from .utils import get_family, is_family_enabled
23 25

  
......
50 52
        if response.status_code == 200:
51 53
            return {'family': response.json()}
52 54
        return {'error': _('An error occured while retrieving family details.')}
55

  
56

  
57
@register_cell_class
58
class WeeklyAgendaCell(JsonCellBase):
59
    title = models.CharField(_('Title'), max_length=150, blank=True)
60
    agenda_reference = models.CharField(_('Agenda'), max_length=128)
61
    user_external_id_key = models.CharField(max_length=50)
62

  
63
    default_template_name = 'family/weekly_agenda.html'
64

  
65
    class Meta:
66
        verbose_name = _('Weekly agenda cell')
67

  
68
    @classmethod
69
    def is_enabled(cls):
70
        return settings.PUBLIK_FAMILY_CELL_ENABLED
71

  
72
    @property
73
    def url(self):
74
        chrono = list(settings.KNOWN_SERVICES.get('chrono').values())[0]
75
        chrono_url = chrono.get('url') or ''
76
        if not chrono_url.endswith('/'):
77
            chrono_url += '/'
78
        # XXX events=all param is not supported for now
79
        return (
80
            '%sapi/agendas/datetimes/?agendas=%s&user_external_id=%s:{{ user_external_id|default:user_nameid }}'
81
            % (chrono_url, self.agenda_reference, self.user_external_id_key)
82
        )
83

  
84
    def is_visible(self, **kwargs):
85
        user = kwargs.get('user')
86
        if not user or user.is_anonymous:
87
            return False
88
        return super().is_visible(**kwargs)
89

  
90
    def get_cell_extra_context(self, context):
91
        if context.get('placeholder_search_mode'):
92
            return {}
93
        return super().get_cell_extra_context(context)
combo/apps/family/templates/family/weekly_agenda.html
1
{% load i18n %}
2
{% block cell-content %}
3
{% if json.data %}
4

  
5
{% if cell.title %}
6
<h2>
7
    {{ cell.title }}
8
</h2>
9
{% endif %}
10

  
11
<div class="weeklyagenda-cell weeklyagenda-cell-{{ cell.pk }}">
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" %}Semaine<br class="weekbreak"> du {{ day_date }} au {{ 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 %}"
31
      {% if item.booked_for_external_user %}data-status="green"{% endif %}
32
   ><span><span>{{ item.label }}</span></span>
33
   </li>
34

  
35
      {% endif %}
36
    {% endfor %}
37
  {% endwith %}
38
{% if day.weekday == 6 %}
39
</ul>
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
{% endwith %}
49

  
50
<style>
51
.weeklyagenda-cell {
52
        margin-top: 1em;
53
        display: flex;
54
}
55
.weeklyagenda-cell ul {
56
        padding: 0;
57
        margin: 0;
58
}
59
.weeklyagenda-cell > ul {
60
        width: 100%;
61
}
62
.weeklyagenda-cell > ul > li {
63
        display: none;
64
}
65
.weeklyagenda-cell > ul > li.shown {
66
        display: block;
67
}
68
.weeklyagenda-cell button.previous-week,
69
.weeklyagenda-cell button.next-week {
70
        height: 3em;
71
        z-index: 10;
72
}
73
.weeklyagenda-cell button.next-week {
74
        margin-left: 1em;
75
        margin-right: 0;
76
}
77
.weeklyagenda-cell .week-title {
78
        display: block;
79
        height: 3em;
80
        line-height: 3em;
81
}
82
.weeklyagenda-cell .no-activity {
83
        color: #888;
84
}
85
.weeklyagenda-cell li.day-title {
86
        margin: 1em 0 0.5em 0;
87
}
88
.weeklyagenda-cell ul ul {
89
        list-style: none;
90
}
91
.weeklyagenda-cell li.disabled {
92
        color: #888;
93
}
94
.weeklyagenda-cell li > span {
95
        position: relative;
96
}
97
.weeklyagenda-cell span > span {
98
        padding-left: 1.8em;
99
}
100
.weeklyagenda-cell span > span::before {
101
        display: block;
102
        content: '';
103
        position: absolute;
104
        margin: auto;
105
        height: calc(0.66rem + 2px);
106
        width: calc(0.66rem + 2px);
107
        background: transparent;
108
        top: 0.33rem;
109
        left: 0;
110
        border: 1px solid #aaa;
111
        border-radius: 2px;
112
}
113
.weeklyagenda-cell span > span::after {
114
        display: block;
115
        content: '';
116
        position: absolute;
117
        margin: auto;
118
        height: calc(0.66rem);
119
        width: calc(0.66rem);
120
        background: transparent;
121
        transition: background 0.1s linear;
122
        top: calc(0.33rem + 1px);
123
        left: 1px;
124
}
125
.weeklyagenda-cell [data-status=green] span > span::after {
126
        background: #3c3;
127
}
128

  
129
br.weekbreak { display: none; }
130

  
131
@media screen and (max-width: 500px) {
132

  
133
  br.weekbreak { display: block; }
134

  
135
  div.weeklyagenda-cell > ul {
136
    margin: 0 -4em;
137
  }
138
  div.weeklyagenda-cell > ul .week-title {
139
    margin: 0 4em;
140
    line-height: 150%;
141
    text-align: center;
142
  }
143

  
144
}
145
</style>
146
<script>
147
$('.weeklyagenda-cell-{{ cell.pk }} .previous-week').on('click', function() {
148
  var $cell = $(this).parents('.weeklyagenda-cell-{{ cell.pk }}');
149
  var $prev = $('li.shown', $cell).prev();
150
  if ($prev.length) { $('li.shown', $cell).removeClass('shown'); $prev.addClass('shown'); }
151
  return false;
152
});
153
$('.weeklyagenda-cell-{{ cell.pk }} .next-week').on('click', function() {
154
  var $cell = $(this).parents('.weeklyagenda-cell-{{ cell.pk }}');
155
  var $next = $('li.shown', $cell).next();
156
  if ($next.length) { $('li.shown', $cell).removeClass('shown'); $next.addClass('shown'); }
157
  return false;
158
});
159
$('.weeklyagenda-cell-{{ cell.pk }} li.day-title').each(function(idx, elem) {
160
  /* hide empty days */
161
  var $next = $(elem).next();
162
  if ($next.length == 0 || $next.is('.day-title')) {
163
    $(elem).hide();
164
  }
165
});
166
$('.weeklyagenda-cell-{{ cell.pk }} li.week').each(function(idx, elem) {
167
  /* hide no-activity message if not empty */
168
  if ($('.activity', $(elem)).length > 0) {
169
    $('.no-activity', $(elem)).hide();
170
  }
171
  /* hide booking button if all items are disabled */
172
  if ($('.activity', $(elem)).not('.disabled').length == 0) {
173
    $('.booking-btn', $(elem)).hide();
174
  }
175
});
176
$('.weeklyagenda-cell-{{ cell.pk }}').each(function(idx, elem) {
177
  /* init first week shown */
178
  var $cell = $(this);
179
  $('li', $cell).removeClass('shown');
180
  if ($('li.day-title.current', $cell).length) {
181
    $('li.day-title.current', $cell).parent().parent().addClass('shown');
182
  } else {
183
    $('li', $cell).first().addClass('shown');
184
  }
185
});
186

  
187
</script>
188

  
189
{% endif %}
190
{% endblock %}
combo/settings.py
366 366
# hide work-in-progress/experimental/broken/legacy/whatever cells for now
367 367
BOOKING_CALENDAR_CELL_ENABLED = False
368 368
LEGACY_CHART_CELL_ENABLED = False
369
PUBLIK_FAMILY_CELL_ENABLED = False
369 370

  
370 371

  
371 372
def debug_show_toolbar(request):
tests/test_family.py
1
import json
2
from unittest import mock
3

  
4
import pytest
5
from django.core.cache import cache
6
from django.test.client import RequestFactory
7

  
8
from combo.apps.family.models import WeeklyAgendaCell
9
from combo.data.models import Page
10
from combo.utils import NothingInCacheException
11

  
12
pytestmark = pytest.mark.django_db
13

  
14

  
15
@pytest.fixture
16
def context():
17
    ctx = {'request': RequestFactory().get('/')}
18
    ctx['request'].user = None
19
    ctx['request'].session = {}
20
    return ctx
21

  
22

  
23
class MockUser:
24
    email = 'foo@example.net'
25
    is_authenticated = True
26
    is_anonymous = False
27

  
28
    def get_name_id(self):
29
        return None
30

  
31

  
32
class MockUserWithNameId:
33
    email = 'foo@example.net'
34
    is_authenticated = True
35
    is_anonymous = False
36

  
37
    def get_name_id(self):
38
        return 'xyz'
39

  
40

  
41
class MockedRequestResponse(mock.Mock):
42
    status_code = 200
43

  
44
    def json(self):
45
        return json.loads(self.content)
46

  
47

  
48
def test_weeklyagenda_cell(settings, context):
49
    settings.PUBLIK_FAMILY_CELL_ENABLED = True
50

  
51
    page = Page.objects.create(title='Family', slug='index', template_name='standard')
52
    cell = WeeklyAgendaCell.objects.create(page=page, placeholder='content', order=0)
53

  
54
    context['request'].user = MockUser()
55

  
56
    # query should fail as nothing is cached
57
    cache.clear()
58
    with pytest.raises(NothingInCacheException):
59
        cell.render(context)
60

  
61
    context['synchronous'] = True  # to get fresh content
62

  
63
    data = {'data': []}
64
    with mock.patch('combo.utils.requests.get') as requests_get:
65
        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
66
        cell.render(context)
67
        # wrong url
68
        assert (
69
            requests_get.call_args_list[0][0][0]
70
            == 'http://chrono.example.org/api/agendas/datetimes/?agendas=&user_external_id=:'
71
        )
72

  
73
    cell.agenda_reference = 'some-agenda'
74
    cell.save()
75
    context['request'].user = MockUserWithNameId()
76
    with mock.patch('combo.utils.requests.get') as requests_get:
77
        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
78
        cell.render(context)
79
        assert (
80
            requests_get.call_args_list[0][0][0]
81
            == 'http://chrono.example.org/api/agendas/datetimes/?agendas=some-agenda&user_external_id=:xyz'
82
        )
83

  
84
    cell.user_external_id_key = 'some-key'
85
    cell.save()
86
    with mock.patch('combo.utils.requests.get') as requests_get:
87
        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
88
        cell.render(context)
89
        assert (
90
            requests_get.call_args_list[0][0][0]
91
            == 'http://chrono.example.org/api/agendas/datetimes/?agendas=some-agenda&user_external_id=some-key:xyz'
92
        )
tests/test_manager.py
880 880
    resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
881 881
    with CaptureQueriesContext(connection) as ctx:
882 882
        resp = resp.form.submit()
883
        assert len(ctx.captured_queries) in [298, 299]
883
        assert len(ctx.captured_queries) in [303, 304]
884 884
    assert Page.objects.count() == 4
885 885
    assert PageSnapshot.objects.all().count() == 4
886 886

  
......
891 891
    resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
892 892
    with CaptureQueriesContext(connection) as ctx:
893 893
        resp = resp.form.submit()
894
        assert len(ctx.captured_queries) == 268
894
        assert len(ctx.captured_queries) == 272
895 895
    assert set(Page.objects.get(slug='one').related_cells['cell_types']) == {'data_textcell', 'data_linkcell'}
896 896
    assert Page.objects.count() == 4
897 897
    assert LinkCell.objects.count() == 2
......
2222 2222

  
2223 2223
    with CaptureQueriesContext(connection) as ctx:
2224 2224
        resp2 = resp.click('view', index=1)
2225
        assert len(ctx.captured_queries) == 70
2225
        assert len(ctx.captured_queries) == 71
2226 2226
    assert Page.snapshots.latest('pk').related_cells == {'cell_types': ['data_textcell']}
2227 2227
    assert resp2.text.index('Hello world') < resp2.text.index('Foobar3')
2228 2228

  
......
2283 2283
    resp = resp.click('restore', index=6)
2284 2284
    with CaptureQueriesContext(connection) as ctx:
2285 2285
        resp = resp.form.submit().follow()
2286
        assert len(ctx.captured_queries) == 142
2286
        assert len(ctx.captured_queries) == 144
2287 2287

  
2288 2288
    resp2 = resp.click('See online')
2289 2289
    assert resp2.text.index('Foobar1') < resp2.text.index('Foobar2') < resp2.text.index('Foobar3')
2290
-