Projet

Général

Profil

0001-kb-add-cell-to-display-last-updated-pages-39091.patch

Nicolas Roche, 21 janvier 2020 18:44

Télécharger (14,4 ko)

Voir les différences:

Subject: [PATCH] kb: add cell to display last updated pages (#39091)

 combo/apps/kb/__init__.py                     |  29 ++++
 combo/apps/kb/migrations/0001_initial.py      |  39 +++++
 combo/apps/kb/migrations/__init__.py          |   0
 combo/apps/kb/models.py                       |  61 ++++++++
 .../kb/templates/combo/last-updated-cell.html |  20 +++
 combo/apps/kb/urls.py                         |  20 +++
 combo/settings.py                             |   1 +
 tests/test_kb.py                              | 148 ++++++++++++++++++
 8 files changed, 318 insertions(+)
 create mode 100644 combo/apps/kb/__init__.py
 create mode 100644 combo/apps/kb/migrations/0001_initial.py
 create mode 100644 combo/apps/kb/migrations/__init__.py
 create mode 100644 combo/apps/kb/models.py
 create mode 100644 combo/apps/kb/templates/combo/last-updated-cell.html
 create mode 100644 combo/apps/kb/urls.py
 create mode 100644 tests/test_kb.py
combo/apps/kb/__init__.py
1
# combo - content management system
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import django.apps
18
from django.utils.translation import ugettext_lazy as _
19

  
20

  
21
class AppConfig(django.apps.AppConfig):
22
    name = 'combo.apps.kb'
23
    verbose_name = _('Knowledge base')
24

  
25
    def get_before_urls(self):
26
        from . import urls
27
        return urls.urlpatterns
28

  
29
default_app_config = 'combo.apps.kb.AppConfig'
combo/apps/kb/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-01-21 14:30
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    initial = True
12

  
13
    dependencies = [
14
        ('auth', '0008_alter_user_username_max_length'),
15
        ('data', '0038_increase_jsoncell_url_max_length'),
16
    ]
17

  
18
    operations = [
19
        migrations.CreateModel(
20
            name='LastUpdatedCell',
21
            fields=[
22
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23
                ('placeholder', models.CharField(max_length=20)),
24
                ('order', models.PositiveIntegerField()),
25
                ('slug', models.SlugField(blank=True, verbose_name='Slug')),
26
                ('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')),
27
                ('public', models.BooleanField(default=True, verbose_name='Public')),
28
                ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
29
                ('last_update_timestamp', models.DateTimeField(auto_now=True)),
30
                ('limit', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Maximum number of entries')),
31
                ('follow_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lastupdated_cell', to='data.Page', verbose_name='Followed parent page')),
32
                ('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')),
33
                ('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')),
34
            ],
35
            options={
36
                'verbose_name': 'Last updated content',
37
            },
38
        ),
39
    ]
combo/apps/kb/models.py
1
# combo - content management system
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

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

  
20
from combo.data.models import CellBase, Page, PageSnapshot
21
from combo.data.library import register_cell_class
22

  
23
@register_cell_class
24
class LastUpdatedCell(CellBase):
25
    follow_page = models.ForeignKey(
26
        'data.Page', blank=True, null=True, on_delete=models.CASCADE,
27
        related_name='lastupdated_cell',verbose_name=_('Followed parent page'))
28
    limit = models.PositiveSmallIntegerField(_('Maximum number of entries'),
29
            null=True, blank=True)
30

  
31
    template_name = 'combo/last-updated-cell.html'
32

  
33
    class Meta:
34
        verbose_name = _('Last updated pages')
35

  
36
    def get_cell_extra_context(self, context):
37
        extra_context = super(LastUpdatedCell, self).get_cell_extra_context(context)
38

  
39
        def get_descendants(page):
40
            descendants = [page]
41
            for item in page.get_children():
42
                descendants.extend(get_descendants(item))
43
            return descendants
44

  
45
        if self.follow_page:
46
            followed_page_ids = [x.id for x in get_descendants(self.follow_page)]
47
            followed_pages = Page.objects.filter(id__in=followed_page_ids)
48
        else:
49
            followed_pages = Page.objects.all()
50
        last_update_times = [(x.get_last_update_time(), x) for x in followed_pages]
51
        last_update_times.sort(key=lambda ts: ts[0], reverse=True)
52
        data = []
53
        for (last_update_time, page) in last_update_times[:self.limit]:
54
            item = {}
55
            item['url'] = page.get_online_url()
56
            item['title'] = page.title
57
            item['last_update_time'] = last_update_time
58
            item['is_new'] = not PageSnapshot.objects.filter(page=page)
59
            data.append(item)
60
        extra_context['pages'] = data
61
        return extra_context
combo/apps/kb/templates/combo/last-updated-cell.html
1
{% load i18n %}
2
<h2>{% trans "Last updated pages" %}</h2>
3
<div class="links-list">
4
  <ul>
5
    {% for page in pages %}
6
    <li>
7
      <a href="{{ page.url }}">
8
        {{ page.title }}
9
        <em>
10
          &nbsp;
11
          {% trans "on"%} {{ page.last_update_time }}
12
          {% if page.is_new %}
13
          {% trans "(new page)"%}
14
          {% endif %}
15
        </em>
16
      </a>
17
    </li>
18
    {% endfor %}
19
  </ul>
20
</div>
combo/apps/kb/urls.py
1
# combo - content management system
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import url
18

  
19
urlpatterns = [
20
]
combo/settings.py
76 76
    'combo.apps.calendar',
77 77
    'combo.apps.pwa',
78 78
    'combo.apps.gallery',
79
    'combo.apps.kb',
79 80
    'haystack',
80 81
    'xstatic.pkg.josefinsans',
81 82
    'xstatic.pkg.leaflet',
tests/test_kb.py
1
# combo - content management system
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.import pytest
16

  
17
import pytest
18

  
19
import freezegun
20
from bs4 import BeautifulSoup
21

  
22
from combo.data.models import CellBase, Page, PageSnapshot, TextCell
23
from combo.apps.kb.models import LastUpdatedCell
24

  
25
pytestmark = pytest.mark.django_db
26

  
27

  
28
def login(app, username='admin', password='admin'):
29
    login_page = app.get('/login/')
30
    login_form = login_page.forms[0]
31
    login_form['username'] = username
32
    login_form['password'] = password
33
    resp = login_form.submit()
34
    assert resp.status_int == 302
35
    return app
36

  
37

  
38
@pytest.mark.freeze_time('2020-01-01')
39
def test_manage_last_updated_content_cell(app, admin_user):
40
    page = Page(title='example page', slug='example-page')
41
    page.save()
42
    app = login(app)
43
    resp = app.get('/manage/pages/%s/' % page.id, status=200)
44

  
45
    optgroup = resp.html.find('optgroup', attrs={'label': 'Knowledge base'})
46
    add_cell_url = optgroup.findChild().attrs['data-add-url']
47
    assert 'kb_lastupdatedcell' in add_cell_url
48
    resp = app.get(add_cell_url, status=302)
49
    resp = resp.follow()
50
    cells = CellBase.get_cells(page_id=page.id)
51
    assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text
52
    resp = resp.forms[0].submit()
53
    assert resp.status_int == 302
54

  
55
    resp = app.get('/example-page/', status=200)
56
    div = resp.html.find('div', attrs={'class': 'links-list'})
57
    assert 'on Jan. 1, 2020' in div.findChild('em').text
58

  
59
def test_last_updated_content_cell_new_page(freezer):
60
    freezer.move_to('2020-01-01')
61
    page = Page(title='example page', slug='example-page')
62
    page.save()
63
    cell = LastUpdatedCell(page=page, order=0)
64
    cell.save()
65
    ctx = {}
66
    assert 'Jan. 1, 2020' in cell.render(ctx)
67
    assert page.snapshot is None
68
    assert '(new page)' in cell.render(ctx)
69

  
70
    # modified page
71
    PageSnapshot.take(page)
72
    assert '(new page)' not in cell.render(ctx)
73

  
74
def test_last_updated_content_cell_limit(freezer):
75
    for i in range(1, 4):
76
        page = Page(title='page %s' %i, slug='page-%i' %i)
77
        page.save()
78
    cell = LastUpdatedCell(page=page, order = 0)
79
    cell.save()
80
    ctx = {}
81
    html = BeautifulSoup(cell.render(ctx))
82
    assert len(html.find_all('li')) == 3
83

  
84
    cell.limit = 2
85
    cell.save()
86
    html = BeautifulSoup(cell.render(ctx))
87
    assert len(html.find_all('li')) == 2
88

  
89
def test_last_updated_content_cell_sort(freezer):
90
    for i in [30, 11, 2, 22]:
91
        freezer.move_to('2020-01-%02i' %i)
92
        page = Page(title='page %s' %i, slug='page-%i' %i)
93
        page.save()
94
        cell = TextCell(page=page, order=0, slug='cell-%i' %i, text='foo')
95
        cell.save()
96
    cell = LastUpdatedCell(page=page, order=0, limit=3)
97
    cell.save()
98
    ctx = {}
99
    html = BeautifulSoup(cell.render(ctx))
100
    ems = html.find_all('em')
101
    assert 'on Jan. 30, 2020' in ems[0].text
102
    assert 'on Jan. 22, 2020' in ems[1].text
103
    assert 'on Jan. 11, 2020' in ems[2].text
104

  
105
    # update contained cell
106
    freezer.move_to('2020-01-31')
107
    text_cell = TextCell.objects.get(slug='cell-2')
108
    text_cell.text='bar'
109
    text_cell.save()
110
    html = BeautifulSoup(cell.render(ctx))
111
    ems = html.find_all('em')
112
    assert 'on Jan. 31, 2020' in ems[0].text
113
    assert 'on Jan. 30, 2020' in ems[1].text
114
    assert 'on Jan. 22, 2020' in ems[2].text
115

  
116
def test_last_updated_content_cell_follow_page():
117
    ''' 1  6  11
118
       /   |   \
119
     2 4  7 9  12 14
120
     | |  | |   | |
121
     3 5  8 10 13 15
122
    '''
123
    def add_pages(depth=0, parent_id=None):
124
        page = Page(title='page %s' % add_pages.num, slug='page-%s' % add_pages.num)
125
        page.parent_id = parent_id
126
        page.save()
127
        add_pages.num += 1
128
        for m in range(0, 2-depth):
129
            add_pages(depth+1, page.id)
130

  
131
    add_pages.num = 1
132
    for i in range(1, 4):
133
        add_pages()
134
    cell = LastUpdatedCell()
135
    cell.page = Page.objects.get(slug='page-15')
136
    cell.order = 0
137
    cell.save()
138
    ctx = {}
139
    html = BeautifulSoup(cell.render(ctx))
140
    assert len(html.find_all('li')) == 15
141

  
142
    cell.follow_page = Page.objects.get(slug='page-1')
143
    html = BeautifulSoup(cell.render(ctx))
144
    assert len(html.find_all('li')) == 5
145

  
146
    cell.follow_page = Page.objects.get(slug='page-3')
147
    html = BeautifulSoup(cell.render(ctx))
148
    assert len(html.find_all('li')) == 1
0
-