From e451097beff65a41d4a39fada9efd1e84fce9e35 Mon Sep 17 00:00:00 2001 From: Nicolas ROCHE Date: Fri, 31 Jan 2020 16:45:27 +0100 Subject: [PATCH] kb: add cell to display last page updates (#39091) --- combo/apps/kb/__init__.py | 26 ++++ combo/apps/kb/migrations/0001_initial.py | 39 +++++ combo/apps/kb/migrations/__init__.py | 0 combo/apps/kb/models.py | 46 ++++++ .../combo/latest-page-updates-cell.html | 22 +++ combo/settings.py | 1 + tests/test_kb.py | 137 ++++++++++++++++++ 7 files changed, 271 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/latest-page-updates-cell.html create mode 100644 tests/test_kb.py diff --git a/combo/apps/kb/__init__.py b/combo/apps/kb/__init__.py new file mode 100644 index 00000000..6336152c --- /dev/null +++ b/combo/apps/kb/__init__.py @@ -0,0 +1,26 @@ +# combo - content management system +# Copyright (C) 2020 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import django.apps +from django.utils.translation import ugettext_lazy as _ + + +class AppConfig(django.apps.AppConfig): + name = 'combo.apps.kb' + verbose_name = _('Knowledge Base') + + +default_app_config = 'combo.apps.kb.AppConfig' diff --git a/combo/apps/kb/migrations/0001_initial.py b/combo/apps/kb/migrations/0001_initial.py new file mode 100644 index 00000000..f84ff4bd --- /dev/null +++ b/combo/apps/kb/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2020-01-31 15:40 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('data', '0042_page_creation_timestamp'), + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='LatestPageUpdatesCell', + 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')), + ('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)), + ('limit', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Maximum number of entries')), + ('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')), + ('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')), + ('root_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='kb_latest_page_updates_cell_root_page', to='data.Page', verbose_name='Root Page')), + ], + options={ + 'verbose_name': 'Latest Page Updates', + }, + ), + ] diff --git a/combo/apps/kb/migrations/__init__.py b/combo/apps/kb/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/combo/apps/kb/models.py b/combo/apps/kb/models.py new file mode 100644 index 00000000..2714e063 --- /dev/null +++ b/combo/apps/kb/models.py @@ -0,0 +1,46 @@ +# combo - content management system +# Copyright (C) 2020 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import datetime + +from django.db import models +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + +from combo.data.models import CellBase, Page +from combo.data.library import register_cell_class + +@register_cell_class +class LatestPageUpdatesCell(CellBase): + root_page = models.ForeignKey( + Page, on_delete=models.SET_NULL, null=True, blank=True, + verbose_name=_('Root Page'), related_name='kb_latest_page_updates_cell_root_page') + limit = models.PositiveSmallIntegerField( + _('Maximum number of entries'), null=True, blank=True) + + template_name = 'combo/latest-page-updates-cell.html' + + class Meta: + verbose_name = _('Latest Page Updates') + + def get_cell_extra_context(self, context): + extra_context = super(LatestPageUpdatesCell, self).get_cell_extra_context(context) + if self.root_page: + pages = self.root_page.get_descendants_and_me() + else: + pages = Page.objects.all() + extra_context['pages'] = pages.order_by('-last_update_timestamp')[:self.limit] + return extra_context diff --git a/combo/apps/kb/templates/combo/latest-page-updates-cell.html b/combo/apps/kb/templates/combo/latest-page-updates-cell.html new file mode 100644 index 00000000..ae71080f --- /dev/null +++ b/combo/apps/kb/templates/combo/latest-page-updates-cell.html @@ -0,0 +1,22 @@ +{% load i18n %} +{% block cell-content %} +

{% trans "Latest Page Updates" %}

+ +{% endblock %} diff --git a/combo/settings.py b/combo/settings.py index 7f63e3f7..8d71475e 100644 --- a/combo/settings.py +++ b/combo/settings.py @@ -76,6 +76,7 @@ INSTALLED_APPS = ( 'combo.apps.calendar', 'combo.apps.pwa', 'combo.apps.gallery', + 'combo.apps.kb', 'haystack', 'xstatic.pkg.josefinsans', 'xstatic.pkg.leaflet', diff --git a/tests/test_kb.py b/tests/test_kb.py new file mode 100644 index 00000000..4e126d72 --- /dev/null +++ b/tests/test_kb.py @@ -0,0 +1,137 @@ +# combo - content management system +# Copyright (C) 2020 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see .import pytest + +import pytest +import re + +from combo.data.models import CellBase, Page, TextCell +from combo.apps.kb.models import LatestPageUpdatesCell + +pytestmark = pytest.mark.django_db + + +def login(app, username='admin', password='admin'): + login_page = app.get('/login/') + login_form = login_page.forms[0] + login_form['username'] = username + login_form['password'] = password + resp = login_form.submit() + assert resp.status_int == 302 + return app + + +@pytest.mark.freeze_time('2020-01-01') +def test_manage_updated_pages_cell(app, admin_user): + page = Page(title='example page', slug='example-page') + page.save() + app = login(app) + resp = app.get('/manage/pages/%s/' % page.id, status=200) + + optgroup = resp.html.find('optgroup', attrs={'label': 'Knowledge Base'}) + add_cell_url = optgroup.findChild().attrs['data-add-url'] + assert 'kb_latestpageupdatescell' in add_cell_url + resp = app.get(add_cell_url, status=302) + resp = resp.follow() + cells = CellBase.get_cells(page_id=page.id) + assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text + resp = resp.forms[0].submit() + assert resp.status_int == 302 + + resp = app.get('/example-page/', status=200) + div = resp.html.find('div', attrs={'class': 'latest-page-updates-cell'}) + assert 'on Jan. 1, 2020' in div.findChild('time').text + +def test_updated_pages_cell_new_page(freezer): + freezer.move_to('2020-01-01') + page = Page(title='example page', slug='example-page') + page.save() + cell = LatestPageUpdatesCell(page=page, order=0) + cell.save() + ctx = {} + assert 'on Jan. 1, 2020' in cell.render(ctx) + assert page.snapshot is None + assert '(new page)' in cell.render(ctx) + + freezer.move_to('2020-01-08') + assert '(new page)' not in cell.render(ctx) + +def test_updated_pages_cell_limit(freezer): + for number in range(1, 4): + page = Page(title='page %s' % number, slug='page-%i' % number) + page.save() + cell = LatestPageUpdatesCell(page=page, order=0) + cell.save() + ctx = {} + assert cell.render(ctx).count(']*>([^<]*)', cell.render(ctx)) + assert 'on Jan. 30, 2020' in next(times).group(0) + assert 'on Jan. 22, 2020' in next(times).group(0) + assert 'on Jan. 11, 2020' in next(times).group(0) + + # update contained cell + freezer.move_to('2020-01-31') + text_cell = TextCell.objects.get(slug='cell-2') + text_cell.text = 'bar' + text_cell.save() + assert len(re.findall(']*>([^<]*)', cell.render(ctx)) + assert 'on Jan. 31, 2020' in next(times).group(0) + assert 'on Jan. 30, 2020' in next(times).group(0) + assert 'on Jan. 22, 2020' in next(times).group(0) + +def test_updated_pages_cell_root_page(): + ''' 1 3 + | + 2 + ''' + page1 = Page(title='page-1', slug='page-1') + page1.save() + page2 = Page(title='page-2', slug='page-2') + page2.parent_id = page1.id + page2.save() + page3 = Page(title='page-3', slug='page-3') + page3.save() + cell = LatestPageUpdatesCell(slug='me') + cell.page = page3 + cell.order = 0 + cell.save() + ctx = {} + assert cell.render(ctx).count('