Projet

Général

Profil

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

Lauréline Guérin, 03 septembre 2021 10:05

Télécharger (17,4 ko)

Voir les différences:

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

 .gitignore                                    |  1 +
 .../migrations/0006_weekly_agenda_cell.py     | 50 ++++++++++
 combo/apps/family/models.py                   | 48 +++++++++-
 .../static/css/combo.weekly_agenda.scss       | 95 +++++++++++++++++++
 .../family/static/js/combo.weekly_agenda.js   | 46 +++++++++
 .../templates/family/weekly_agenda.html       | 51 ++++++++++
 combo/settings.py                             |  1 +
 tests/test_family.py                          | 92 ++++++++++++++++++
 tests/test_manager.py                         |  8 +-
 9 files changed, 387 insertions(+), 5 deletions(-)
 create mode 100644 combo/apps/family/migrations/0006_weekly_agenda_cell.py
 create mode 100644 combo/apps/family/static/css/combo.weekly_agenda.scss
 create mode 100644 combo/apps/family/static/js/combo.weekly_agenda.js
 create mode 100644 combo/apps/family/templates/family/weekly_agenda.html
 create mode 100644 tests/test_family.py
.gitignore
13 13
combo/apps/maps/static/css/combo.map.css.map
14 14
combo/apps/pwa/static/css/combo.manager.pwa.css
15 15
combo/apps/pwa/static/css/combo.manager.pwa.css.map
16
combo/apps/family/static/css/combo.weekly_agenda.css
16 17
data/themes/gadjo/static/css/agent-portal.css
17 18
data/themes/gadjo/static/css/agent-portal.css.map
18 19
.cache
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
    force_async = True
65

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

  
69
    class Media:
70
        js = ('js/combo.weekly_agenda.js',)
71
        css = {'all': ('css/combo.weekly_agenda.css',)}
72

  
73
    @classmethod
74
    def is_enabled(cls):
75
        return settings.PUBLIK_FAMILY_CELL_ENABLED
76

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

  
89
    def is_visible(self, **kwargs):
90
        user = kwargs.get('user')
91
        if not user or user.is_anonymous:
92
            return False
93
        return super().is_visible(**kwargs)
94

  
95
    def get_cell_extra_context(self, context):
96
        if context.get('placeholder_search_mode'):
97
            return {}
98
        return super().get_cell_extra_context(context)
combo/apps/family/static/css/combo.weekly_agenda.scss
1
.weeklyagenda-cell {
2
	margin-top: 1em;
3
	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.next-week {
26
		margin-left: 1em;
27
		margin-right: 0;
28
	}
29
	.week-title {
30
		display: block;
31
		height: 3em;
32
		line-height: 3em;
33
	}
34
	.no-activity {
35
		color: #888;
36
	}
37
	li {
38
		&.day-title {
39
			margin: 1em 0 0.5em 0;
40
		}
41
		&.disabled {
42
			color: #888;
43
		}
44
		& > span {
45
			position: relative;
46
		}
47
	}
48
	span > span {
49
		padding-left: 1.8em;
50
		&::before {
51
			display: block;
52
			content: '';
53
			position: absolute;
54
			margin: auto;
55
			height: calc(0.66rem + 2px);
56
			width: calc(0.66rem + 2px);
57
			background: transparent;
58
			top: 0.33rem;
59
			left: 0;
60
			border: 1px solid #aaa;
61
			border-radius: 2px;
62
		}
63
		&::after {
64
			display: block;
65
			content: '';
66
			position: absolute;
67
			margin: auto;
68
			height: calc(0.66rem);
69
			width: calc(0.66rem);
70
			background: transparent;
71
			transition: background 0.1s linear;
72
			top: calc(0.33rem + 1px);
73
			left: 1px;
74
		}
75
	}
76
	& [data-status=green] span > span::after {
77
		background: #3c3;
78
	}
79
}
80

  
81
br.weekbreak {
82
	display: none;
83
}
84

  
85
@media screen and (max-width: 500px) {
86
	br.weekbreak { display: block; }
87
	div.weeklyagenda-cell > ul {
88
		margin: 0 -4em;
89
		& .week-title {
90
			margin: 0 4em;
91
			line-height: 150%;
92
			text-align: center;
93
		}
94
	}
95
}
combo/apps/family/static/js/combo.weekly_agenda.js
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();
20
      }
21
    });
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();
30
      }
31
    });
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');
40
      }
41
    });
42
  }
43
  $(document).on('combo:cell-loaded', function(ev, cell) {
44
    init_agenda($('.weeklyagenda-cell', $(cell)).data('cell-id'));
45
  });
46
});
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 }}" data-cell-id="{{ 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
</div>
50
{% endif %}
51
{% 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
-