diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 17b5e05b..d88cad64 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,13 +19,19 @@ jobs: needs: lint strategy: matrix: - ckan-version: ["2.11", "2.10", 2.9] + include: + - ckan-version: "2.11" + ckan-image: "ckan/ckan-dev:2.11-py3.10" + - ckan-version: "2.10" + ckan-image: "ckan/ckan-dev:2.10-py3.10" + - ckan-version: "2.9" + ckan-image: "ckan/ckan-dev:2.9-py3.9" fail-fast: false name: CKAN ${{ matrix.ckan-version }} runs-on: ubuntu-latest container: - image: ckan/ckan-dev:${{ matrix.ckan-version }} + image: ${{ matrix.ckan-image }} services: solr: image: ckan/ckan-solr:${{ matrix.ckan-version }}-solr9 @@ -47,6 +53,10 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install requirements (2.9) + run: | + pip install -U pytest-rerunfailures + if: ${{ matrix.ckan-version == '2.9' }} - name: Install requirements run: | pip install -r requirements.txt diff --git a/README.md b/README.md index ec41f680..457b5bc7 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,18 @@ ckanext.pages.editor = ckeditor ``` This enables either the [medium](https://jakiestfu.github.io/Medium.js/docs/) or [ckeditor](http://ckeditor.com/) +``` +ckanext.pages.revisions_limit = 3 +``` + +By default the value is set to `3` revisions to be stored. While adding this option with a higher number, the amount of stored revisions will be increased. + +``` +ckanext.pages.revisions_force_limit = true +``` + +By default is set to `False`. Needed when the `ckanext.pages.revisions_limit` number is decresed from the original (e.g. from 5 to 2) and we want to make sure that all Pages after update will have only specified number of Revisions instead of the old setting number. Without it, if Page had previously 5 Revisions, the page will continue to have 5 Revisions as it removes only the last one, so the new number limit will effect only new Pages, while setting this option to `true`, will force old Pages after update to have the spcific amount of last Revisions. + ## Extending ckanext-pages schema This extension defines an `IPagesSchema` interface that allows other extensions to update the pages schema and add custom fields. diff --git a/ckanext/pages/actions.py b/ckanext/pages/actions.py index 9778f520..336101c6 100644 --- a/ckanext/pages/actions.py +++ b/ckanext/pages/actions.py @@ -1,6 +1,7 @@ import datetime import json +from ckan.model.types import make_uuid from ckan import model import ckan.plugins as p import ckan.lib.navl.dictization_functions as df @@ -104,6 +105,10 @@ def _pages_update(context, data_dict): context['group_id'] = org_id schema = update_pages_schema() + # +1 is the Current state by default while ckanext.pages.revisions_limit is the amounf of previous states + revisions_limit = tk.asint(tk.config.get('ckanext.pages.revisions_limit', '3')) + 1 + force_revisions_limit = tk.asbool(tk.config.get('ckanext.pages.revisions_force_limit', False)) + data, errors = df.validate(data_dict, schema, context) if errors: @@ -129,15 +134,52 @@ def _pages_update(context, data_dict): extras[key] = data.get(key) out.extras = json.dumps(extras) - out.modified = datetime.datetime.utcnow() + out.modified = datetime.datetime.now(datetime.timezone.utc) user = model.User.get(context['user']) out.user_id = user.id + + revisions = out.revisions + + new_revision = { + make_uuid(): { + "content": out.content, + "user_id": user.id, + "created": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "current": True + } + } + if not revisions: + out.revisions = new_revision + else: + if (len(revisions) >= revisions_limit): + revisions = out.get_ordered_revisions() + + if not force_revisions_limit: + revisions.popitem() + else: + # Remove all previous revisions if there any to match revisions_limit + # Need to add +1 to the length to include the Active state as done for revisions_limit + for i in range((len(revisions) + 1) - revisions_limit): + revisions.popitem() + + # Remove the current key from all past revisions before merging + revisions = _remove_keys_revision_from_dict(revisions) + out.revisions = {**new_revision, **revisions} + out.save() session = context['session'] session.add(out) session.commit() +def _remove_keys_revision_from_dict(data_dict, keys=['current']): + return { + id: { + key: data_dict[id][key] for key in data_dict[id] if key not in keys + } for id in data_dict + } + + def pages_upload(context, data_dict): """ Upload a file to the CKAN server. @@ -194,6 +236,25 @@ def pages_update(context, data_dict): return _pages_update(context, data_dict) +def pages_revision_restore(context, data_dict): + p.toolkit.check_access('ckanext_pages_update', context, data_dict) + name = data_dict.get('page') + rev = data_dict.get('revision') + page = db.Page.get(name=name) + + if page and page.revisions: + page.revisions = _remove_keys_revision_from_dict(page.revisions) + revision = page.revisions.get(rev) + + try: + revision['current'] = True + page.content = revision['content'] + page.save() + return revision + except TypeError: + raise TypeError("Unexpected value.") + + def pages_delete(context, data_dict): try: p.toolkit.check_access('ckanext_pages_delete', context, data_dict) diff --git a/ckanext/pages/blueprint.py b/ckanext/pages/blueprint.py index 792535bf..53b3f0d4 100644 --- a/ckanext/pages/blueprint.py +++ b/ckanext/pages/blueprint.py @@ -13,6 +13,18 @@ def show(page): return utils.pages_show(page, page_type='page') +def pages_revisions(page): + return utils.pages_revisions(page, page_type='page') + + +def pages_revisions_preview(page, revision): + return utils.pages_revisions_preview(page, revision, page_type='page') + + +def pages_revision_restore(page, revision): + return utils.pages_revision_restore(page, revision, page_type='page') + + def pages_edit(page=None, data=None, errors=None, error_summary=None): return utils.pages_edit(page, data, errors, error_summary, 'page') @@ -37,6 +49,18 @@ def blog_edit(page=None, data=None, errors=None, error_summary=None): return utils.pages_edit(page, data, errors, error_summary, 'blog') +def blog_revisions(page): + return utils.pages_revisions(page, page_type='blog') + + +def blog_revisions_preview(page, revision): + return utils.pages_revisions_preview(page, revision, page_type='blog') + + +def blog_revision_restore(page, revision): + return utils.pages_revision_restore(page, revision, page_type='blog') + + def blog_delete(page): return utils.pages_delete(page, page_type='blog') @@ -67,6 +91,9 @@ def group_edit(id, page=None, data=None, errors=None, error_summary=None): pages.add_url_rule("/pages", view_func=index, endpoint="pages_index") pages.add_url_rule("/pages/", view_func=show) +pages.add_url_rule("/pages//revisions", view_func=pages_revisions) +pages.add_url_rule("/pages//revisions/", view_func=pages_revisions_preview) +pages.add_url_rule("/pages//revisions//restore", view_func=pages_revision_restore, methods=['GET']) pages.add_url_rule("/pages_edit", view_func=pages_edit, endpoint='new', methods=['GET', 'POST']) pages.add_url_rule("/pages_edit/", view_func=pages_edit, endpoint='new', methods=['GET', 'POST']) pages.add_url_rule("/pages_edit/", view_func=pages_edit, endpoint='edit', methods=['GET', 'POST']) @@ -77,6 +104,9 @@ def group_edit(id, page=None, data=None, errors=None, error_summary=None): pages.add_url_rule("/blog", view_func=blog_index) pages.add_url_rule("/blog/", view_func=blog_show) +pages.add_url_rule("/blog//revisions", view_func=blog_revisions) +pages.add_url_rule("/blog//revisions/", view_func=blog_revisions_preview) +pages.add_url_rule("/blog//revisions//restore", view_func=blog_revision_restore, methods=['GET']) pages.add_url_rule("/blog_edit", view_func=blog_edit, endpoint='blog_new', methods=['GET', 'POST']) pages.add_url_rule("/blog_edit/", view_func=blog_edit, endpoint='blog_new', methods=['GET', 'POST']) pages.add_url_rule("/blog_edit/", view_func=blog_edit, endpoint='blog_edit', methods=['GET', 'POST']) diff --git a/ckanext/pages/db.py b/ckanext/pages/db.py index b8e83bd6..d9232807 100644 --- a/ckanext/pages/db.py +++ b/ckanext/pages/db.py @@ -2,10 +2,13 @@ import uuid import json +from collections import OrderedDict from six import text_type import sqlalchemy as sa from sqlalchemy import Column, types from sqlalchemy.orm import class_mapper +from sqlalchemy.ext.mutable import MutableDict +from sqlalchemy.dialects.postgresql import JSONB try: from sqlalchemy.engine import Row @@ -52,6 +55,7 @@ class Page(DomainObject, BaseModel): created = Column(types.DateTime, default=datetime.datetime.utcnow) modified = Column(types.DateTime, default=datetime.datetime.utcnow) extras = Column(types.UnicodeText, default=u'{}') + revisions = Column(MutableDict.as_mutable(JSONB), default=u'{}') @classmethod def get(cls, **kw): @@ -75,6 +79,15 @@ def pages(cls, **kw): query = query.order_by(cls.created.desc()) return query.all() + def get_ordered_revisions(self): + # Compare timestamps to avoid different datetime formats error + return OrderedDict(reversed(sorted( + self.revisions.items(), + key=lambda x: datetime.datetime.timestamp( + datetime.datetime.fromisoformat(x[1]['created']) + ) + ))) + def table_dictize(obj, context, **kw): '''Get any model object and represent it as a dict''' diff --git a/ckanext/pages/migration/pages/versions/1725892d1d94_create_revisions_table.py b/ckanext/pages/migration/pages/versions/1725892d1d94_create_revisions_table.py new file mode 100644 index 00000000..2fbbfd56 --- /dev/null +++ b/ckanext/pages/migration/pages/versions/1725892d1d94_create_revisions_table.py @@ -0,0 +1,31 @@ +"""Create revisions column + +Revision ID: 1725892d1d94 +Revises: a756dbd73ead +Create Date: 2024-10-13 12:09:25.372524 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision = '1725892d1d94' +down_revision = 'a756dbd73ead' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + 'ckanext_pages', + sa.Column( + u'revisions', + postgresql.JSONB(astext_type=sa.Text()), + nullable=True) + ) + + +def downgrade(): + op.drop_column(u'ckanext_pages', u'revisions') diff --git a/ckanext/pages/plugin.py b/ckanext/pages/plugin.py index a5ae54f1..52fa11d4 100644 --- a/ckanext/pages/plugin.py +++ b/ckanext/pages/plugin.py @@ -128,6 +128,7 @@ def get_actions(self): actions_dict = { 'ckanext_pages_show': actions.pages_show, 'ckanext_pages_update': actions.pages_update, + 'ckanext_pages_revision_restore': actions.pages_revision_restore, 'ckanext_pages_delete': actions.pages_delete, 'ckanext_pages_list': actions.pages_list, 'ckanext_pages_upload': actions.pages_upload, diff --git a/ckanext/pages/tests/test_action.py b/ckanext/pages/tests/test_action.py index 99edf43a..a791bfcd 100644 --- a/ckanext/pages/tests/test_action.py +++ b/ckanext/pages/tests/test_action.py @@ -1,4 +1,6 @@ import pytest +import datetime +from collections import OrderedDict from ckan.tests import factories, helpers @@ -54,6 +56,54 @@ def test_pages_update_action(self, app): assert page["title"] == "New Page Updated" assert page["content"] == "This is a test content updated" + def test_pages_revision_restore_action(self, app): + user = factories.User() + helpers.call_action( + "ckanext_pages_update", + {"user": user["name"]}, + name="page_name", + title="First Revision Title", + content="First Revision Content", + ) + + helpers.call_action( + "ckanext_pages_update", + {"user": user["name"]}, + name="page_name", + title="Page Updated", + content="This is a test content updated", + page="page_name", + ) + + page = helpers.call_action("ckanext_pages_show", {}, page="page_name") + + revisions = page.get('revisions') + + assert len(revisions) == 2 + assert page['content'] == "This is a test content updated" + + sorted_revisions = OrderedDict(reversed(sorted( + revisions.items(), + key=lambda x: datetime.datetime.timestamp( + datetime.datetime.fromisoformat(x[1]['created']) + ) + ))) + + last_revision = sorted_revisions.popitem() + + helpers.call_action( + "ckanext_pages_revision_restore", + {"user": user["name"]}, + page="page_name", + revision=last_revision[0] + ) + + page = helpers.call_action("ckanext_pages_show", {}, page="page_name") + + assert page['title'] == "Page Updated" + assert page['content'] == "First Revision Content" + assert page['revisions'][last_revision[0]]['current'] + def test_pages_list(self, app): sysadmin = factories.Sysadmin() helpers.call_action( diff --git a/ckanext/pages/tests/test_logic.py b/ckanext/pages/tests/test_logic.py index f0124f21..015d76d9 100644 --- a/ckanext/pages/tests/test_logic.py +++ b/ckanext/pages/tests/test_logic.py @@ -5,6 +5,8 @@ except ImportError: import mock import pytest +from collections import OrderedDict +import datetime from ckan.plugins import toolkit from ckan.tests import factories, helpers @@ -214,3 +216,333 @@ def test_cannot_create_page_with_same_name(self, app): assert '
' in response.body assert 'Page name already exists' in response.body + + def test_revisions_page(self, app): + user = factories.Sysadmin() + env = {'REMOTE_USER': user['name'].encode('ascii')} + + helpers.call_action( + "ckanext_pages_update", + {"user": user["name"]}, + name="page_name", + title="New Page", + content="This is a test content", + ) + + response = app.get( + toolkit.url_for('pages.pages_revisions', page="page_name"), + status=200, extra_environ=env) + + assert 'Active Revision' in response.body + + response = app.get( + toolkit.url_for('pages.pages_revisions', page="page_name1"), + status=404, extra_environ=env) + + assert '404 Not Found' in response.body + + if toolkit.check_ckan_version(min_version="2.10.0"): + response = app.get( + toolkit.url_for('pages.pages_revisions', page="page_name"), + status=401) + + assert '

401 Unauthorized

' in response.body + else: + response = app.get( + toolkit.url_for('pages.pages_revisions', page="page_name") + ) + assert '

Login

' in response.body + + def test_revision_preview_page(self, app): + user = factories.Sysadmin() + env = {'REMOTE_USER': user['name'].encode('ascii')} + + helpers.call_action( + "ckanext_pages_update", + {"user": user["name"]}, + name="page_name", + title="New Page", + content="This is a test content", + ) + + page = helpers.call_action("ckanext_pages_show", {}, page="page_name") + + revision_id = [i for i in page['revisions']][0] + + response = app.get( + toolkit.url_for( + 'pages.pages_revisions_preview', + page="page_name", + revision=revision_id), + status=200, extra_environ=env) + + assert '

This is a test content

' in response.body + + response = app.get( + toolkit.url_for( + 'pages.pages_revisions_preview', + page="page_name", + revision=revision_id + '1'), + status=404, extra_environ=env) + + assert '404 Not Found' in response.body + + if toolkit.check_ckan_version(min_version="2.10.0"): + response = app.get( + toolkit.url_for( + 'pages.pages_revisions_preview', + page="page_name", + revision=revision_id), + status=401) + + assert '

401 Unauthorized

' in response.body + else: + response = app.get( + toolkit.url_for( + 'pages.pages_revisions_preview', + page="page_name", + revision=revision_id), + ) + assert '

Login

' in response.body + + def test_revision_restore_page(self, app): + user = factories.Sysadmin() + env = {'REMOTE_USER': user['name'].encode('ascii')} + + helpers.call_action( + "ckanext_pages_update", + {"user": user["name"]}, + name="page_name", + title="New Page", + content="This is a test content", + ) + + helpers.call_action( + "ckanext_pages_update", + {"user": user["name"]}, + name="page_name", + title="New Page Updated", + content="This is a test content updated", + page="page_name", + ) + + page = helpers.call_action("ckanext_pages_show", {}, page="page_name") + + assert page['content'] == 'This is a test content updated' + + revisions = page['revisions'] + + sorted_revisions = OrderedDict(reversed(sorted( + revisions.items(), + key=lambda x: datetime.datetime.timestamp( + datetime.datetime.fromisoformat(x[1]['created']) + ) + ))) + + last_revision = sorted_revisions.popitem() + response = app.get( + toolkit.url_for( + 'pages.pages_revision_restore', + page="page_name", + revision=last_revision[0]), + status=200, extra_environ=env) + + assert 'Content from revision created on' in response.body + + response = app.get( + toolkit.url_for( + 'pages.pages_revision_restore', + page="page_name", + revision=last_revision[0] + '1'), + status=200, extra_environ=env) + + assert 'Bad values, please make sure that provided values exist' in response.body + + if toolkit.check_ckan_version(min_version="2.10.0"): + response = app.get( + toolkit.url_for( + 'pages.pages_revision_restore', + page="page_name", + revision=last_revision[0]), + status=401) + + assert '

401 Unauthorized

' in response.body + else: + response = app.get( + toolkit.url_for( + 'pages.pages_revision_restore', + page="page_name", + revision=last_revision[0]), + ) + + assert '

Login

' in response.body + + def test_revisions_blog(self, app): + user = factories.Sysadmin() + env = {'REMOTE_USER': user['name'].encode('ascii')} + + helpers.call_action( + "ckanext_pages_update", + {"user": user["name"]}, + name="blog_name", + title="New Blog", + content="This is a test content", + page_type="blog", + publish_date="2024-10-15" + ) + + response = app.get( + toolkit.url_for('pages.blog_revisions', page="blog_name"), + status=200, extra_environ=env) + + assert 'Active Revision' in response.body + + response = app.get( + toolkit.url_for('pages.blog_revisions', page="blog_name1"), + status=404, extra_environ=env) + + assert '404 Not Found' in response.body + + if toolkit.check_ckan_version(min_version="2.10.0"): + response = app.get( + toolkit.url_for('pages.blog_revisions', page="blog_name"), + status=401) + + assert '

401 Unauthorized

' in response.body + else: + response = app.get( + toolkit.url_for('pages.blog_revisions', page="blog_name"), + ) + + assert '

Login

' in response.body + + def test_revision_preview_blog(self, app): + user = factories.Sysadmin() + env = {'REMOTE_USER': user['name'].encode('ascii')} + + helpers.call_action( + "ckanext_pages_update", + {"user": user["name"]}, + name="blog_name", + title="New Blog", + content="This is a test content", + page_type="blog", + publish_date="2024-10-15" + ) + + page = helpers.call_action("ckanext_pages_show", {}, page="blog_name") + + revision_id = [i for i in page['revisions']][0] + + response = app.get( + toolkit.url_for( + 'pages.blog_revisions_preview', + page="blog_name", + revision=revision_id), + status=200, extra_environ=env) + + assert '

This is a test content

' in response.body + + response = app.get( + toolkit.url_for( + 'pages.blog_revisions_preview', + page="blog_name", + revision=revision_id + '1'), + status=404, extra_environ=env) + + assert '404 Not Found' in response.body + + if toolkit.check_ckan_version(min_version="2.10.0"): + response = app.get( + toolkit.url_for( + 'pages.blog_revisions_preview', + page="blog_name", + revision=revision_id), + status=401) + + assert '

401 Unauthorized

' in response.body + else: + response = app.get( + toolkit.url_for( + 'pages.blog_revisions_preview', + page="blog_name", + revision=revision_id), + ) + assert '

Login

' in response.body + + def test_revision_restore_blog(self, app): + user = factories.Sysadmin() + env = {'REMOTE_USER': user['name'].encode('ascii')} + + helpers.call_action( + "ckanext_pages_update", + {"user": user["name"]}, + name="blog_name", + title="New Blog", + content="This is a test content", + page_type="blog", + publish_date="2024-10-15" + ) + + helpers.call_action( + "ckanext_pages_update", + {"user": user["name"]}, + name="blog_name", + title="New Blog Updated", + content="This is a test content updated", + page="blog_name", + page_type="blog", + publish_date="2024-10-15" + ) + + page = helpers.call_action("ckanext_pages_show", {}, page="blog_name") + + assert page['content'] == 'This is a test content updated' + + revisions = page['revisions'] + + sorted_revisions = OrderedDict(reversed(sorted( + revisions.items(), + key=lambda x: datetime.datetime.timestamp( + datetime.datetime.fromisoformat(x[1]['created']) + ) + ))) + + last_revision = sorted_revisions.popitem() + + response = app.get( + toolkit.url_for( + 'pages.blog_revision_restore', + page="blog_name", + revision=last_revision[0]), + status=200, extra_environ=env) + + assert 'Content from revision created on' in response.body + + response = app.get( + toolkit.url_for( + 'pages.blog_revision_restore', + page="blog_name", + revision=last_revision[0] + '1'), + status=200, extra_environ=env) + + assert 'Bad values, please make sure that provided values exist' in response.body + + if toolkit.check_ckan_version(min_version="2.10.0"): + response = app.get( + toolkit.url_for( + 'pages.blog_revision_restore', + page="blog_name", + revision=last_revision[0]), + status=401) + + assert '

401 Unauthorized

' in response.body + else: + response = app.get( + toolkit.url_for( + 'pages.blog_revision_restore', + page="blog_name", + revision=last_revision[0]), + ) + + assert '

Login

' in response.body diff --git a/ckanext/pages/theme/templates_main/ckanext_pages/blog.html b/ckanext/pages/theme/templates_main/ckanext_pages/blog.html index f5a85028..9609cc31 100644 --- a/ckanext/pages/theme/templates_main/ckanext_pages/blog.html +++ b/ckanext/pages/theme/templates_main/ckanext_pages/blog.html @@ -4,26 +4,31 @@ {% block primary_content %}
- {% if h.check_access('ckanext_pages_update') %} - {% asset 'pages/main-css' %} - {% link_for _('Edit'), named_route='pages.blog_edit', page=c.page.name, class_='btn btn-primary pull-right', icon='plus-square' %} - {% endif %} + {% block ckanext_pages_actions %} + {% if h.check_access('ckanext_pages_update') %} + {% asset 'pages/main-css' %} + {% link_for _('Edit'), named_route='pages.blog_edit', page=c.page.name, class_='btn btn-primary pull-right', icon='plus-square' %} + {% link_for _('Revisions'), named_route='pages.blog_revisions', page=c.page.name, class_='btn btn-primary pull-right me-2', icon='eye' %} + {% endif %} + {% endblock %}

{{ c.page.title }}

{% if c.page.publish_date %} {{ h.render_datetime(c.page.publish_date) }} {% endif %} - {% if c.page.content %} - {% set editor = h.pages_get_wysiwyg_editor() %} - {% if editor %} -
- {{c.page.content|safe}} -
+ {% block ckanext_pages_content %} + {% if c.page.content %} + {% set editor = h.pages_get_wysiwyg_editor() %} + {% if editor %} +
+ {{c.page.content|safe}} +
+ {% else %} + {{ h.render_content(c.page.content) }} + {% endif %} {% else %} - {{ h.render_content(c.page.content) }} +

{{ _('This page currently has no content') }}

{% endif %} - {% else %} -

{{ _('This page currently has no content') }}

- {% endif %} + {% endblock %}
{% endblock %} diff --git a/ckanext/pages/theme/templates_main/ckanext_pages/blog_revisions.html b/ckanext/pages/theme/templates_main/ckanext_pages/blog_revisions.html new file mode 100644 index 00000000..a0887335 --- /dev/null +++ b/ckanext/pages/theme/templates_main/ckanext_pages/blog_revisions.html @@ -0,0 +1,53 @@ +{% extends 'page.html' %} + +{% macro actor(revision) %} + + {{ h.linked_user(revision.user_id, 0, 30) }} + +{% endmacro %} + +{% block bodytag %}{{ super() }} class="blog"{% endblock %} +{% block subtitle %}{{ c.page.title }}{% endblock %} + +{% block primary %} +
+ {% if h.check_access('ckanext_pages_update') %} + {% asset 'pages/main-css' %} + {% link_for _('View'), named_route='pages.blog_show', page=c.page.name, class_='btn btn-primary pull-right', icon='eye' %} + {% endif %} +

{{ c.page.title }}

+ {% if c.page.revisions %} +
+
    + {% for key, revision in c.page.get_ordered_revisions().items() %} +
  • + + + + + {{ _('{actor} updated page at {date}').format( + actor=actor(revision), date=h.render_datetime(revision.created, with_hours=True) + )|safe }} + + {% link_for _('Preview'), named_route='pages.blog_revisions_preview', page=c.page.name, revision=key, class_='btn btn-default' %} + {% if not revision.current %} + {% link_for _('Restore'), named_route='pages.blog_revision_restore', page=c.page.name, revision=key, class_='btn btn-primary' %} + {% else %} + {{ _('Active Revision') }} + {% endif %} +
    + + {{ h.time_ago_from_timestamp(revision.created) }} + + +
  • + {% endfor %} +
+
+ {% else %} +

{{ _('This blog currently has no revisions') }}

+ {% endif %} +
+{% endblock %} + +{% block secondary %}{% endblock %} \ No newline at end of file diff --git a/ckanext/pages/theme/templates_main/ckanext_pages/blog_revisions_preview.html b/ckanext/pages/theme/templates_main/ckanext_pages/blog_revisions_preview.html new file mode 100644 index 00000000..43a4f177 --- /dev/null +++ b/ckanext/pages/theme/templates_main/ckanext_pages/blog_revisions_preview.html @@ -0,0 +1,25 @@ +{% extends 'ckanext_pages/blog.html' %} + +{% block ckanext_pages_actions %} + {% if h.check_access('ckanext_pages_update') %} + {% asset 'pages/main-css' %} + {% link_for _('Revisions'), named_route='pages.blog_revisions', page=c.page.name, class_='btn btn-primary pull-right', icon='eye' %} + {% endif %} +{% endblock %} +{% block ckanext_pages_content %} + {% if revision.content %} +
+ {% set editor = h.pages_get_wysiwyg_editor() %} + + {% if editor %} +
+ {{revision.content|safe}} +
+ {% else %} + {{ h.render_content(revision.content) }} + {% endif %} +
+ {% else %} +

{{ _('This page currently has no content') }}

+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/ckanext/pages/theme/templates_main/ckanext_pages/page.html b/ckanext/pages/theme/templates_main/ckanext_pages/page.html index 8d632756..149b85c1 100644 --- a/ckanext/pages/theme/templates_main/ckanext_pages/page.html +++ b/ckanext/pages/theme/templates_main/ckanext_pages/page.html @@ -9,6 +9,7 @@ {% if h.check_access('ckanext_pages_update') %} {% asset 'pages/main-css' %} {% link_for _('Edit'), named_route='pages.edit', page=c.page.name, class_='btn btn-primary pull-right', icon='edit' %} + {% link_for _('Revisions'), named_route='pages.pages_revisions', page=c.page.name, class_='btn btn-primary pull-right me-2', icon='eye' %} {% endif %} {% endblock %}

{{ c.page.title }}

diff --git a/ckanext/pages/theme/templates_main/ckanext_pages/page_revisions.html b/ckanext/pages/theme/templates_main/ckanext_pages/page_revisions.html new file mode 100644 index 00000000..64d47ab8 --- /dev/null +++ b/ckanext/pages/theme/templates_main/ckanext_pages/page_revisions.html @@ -0,0 +1,58 @@ +{% extends 'page.html' %} + +{% macro actor(revision) %} + + {{ h.linked_user(revision.user_id, 0, 30) }} + +{% endmacro %} + +{% block subtitle %}{{ c.page.title }}{% endblock %} + +{% block primary %} +
+ + {% block ckanext_pages_actions %} + {% if h.check_access('ckanext_pages_update') %} + {% asset 'pages/main-css' %} + {% link_for _('View Page'), named_route='pages.show', page=c.page.name, class_='btn btn-primary pull-right', icon='eye' %} + {% endif %} + {% endblock %} +

{{ c.page.title }}

+ {% block ckanext_pages_content %} + {% if c.page.revisions %} +
+
    + {% for key, revision in c.page.get_ordered_revisions().items() %} +
  • + + + + + {{ _('{actor} updated page at {date}').format( + actor=actor(revision), date=h.render_datetime(revision.created, with_hours=True) + )|safe }} + + {% link_for _('Preview'), named_route='pages.pages_revisions_preview', page=c.page.name, revision=key, class_='btn btn-default' %} + {% if not revision.current %} + {% link_for _('Restore'), named_route='pages.pages_revision_restore', page=c.page.name, revision=key, class_='btn btn-primary' %} + {% else %} + {{ _('Active Revision') }} + {% endif %} +
    + + {{ h.time_ago_from_timestamp(revision.created) }} + + +
  • + {% endfor %} +
+
+ {% else %} +

{{ _('This page currently has no revisions') }}

+ {% endif %} + + {% endblock %} +
+{% endblock %} + +{% block secondary %}{% endblock %} diff --git a/ckanext/pages/theme/templates_main/ckanext_pages/page_revisions_preview.html b/ckanext/pages/theme/templates_main/ckanext_pages/page_revisions_preview.html new file mode 100644 index 00000000..55be6056 --- /dev/null +++ b/ckanext/pages/theme/templates_main/ckanext_pages/page_revisions_preview.html @@ -0,0 +1,25 @@ +{% extends 'ckanext_pages/page.html' %} + +{% block ckanext_pages_actions %} + {% if h.check_access('ckanext_pages_update') %} + {% asset 'pages/main-css' %} + {% link_for _('Revisions'), named_route='pages.pages_revisions', page=c.page.name, class_='btn btn-primary pull-right', icon='eye' %} + {% endif %} +{% endblock %} +{% block ckanext_pages_content %} + {% if revision.content %} +
+ {% set editor = h.pages_get_wysiwyg_editor() %} + + {% if editor %} +
+ {{revision.content|safe}} +
+ {% else %} + {{ h.render_content(revision.content) }} + {% endif %} +
+ {% else %} +

{{ _('This page currently has no content') }}

+ {% endif %} +{% endblock %} diff --git a/ckanext/pages/utils.py b/ckanext/pages/utils.py index 9d8a67fd..2f246409 100644 --- a/ckanext/pages/utils.py +++ b/ckanext/pages/utils.py @@ -6,6 +6,8 @@ import ckan.logic as logic import ckan.lib.helpers as helpers +from ckanext.pages.db import Page + config = tk.config _ = tk._ @@ -187,6 +189,60 @@ def pages_show(page=None, page_type='page'): return tk.render('ckanext_pages/%s.html' % page_type) +def pages_revisions(page, page_type='page'): + try: + tk.check_access('ckanext_pages_update', {'user': tk.g.user}) + except tk.NotAuthorized: + return tk.abort(401, _('Unauthorized to view this page')) + + _page = Page.get(name=page) + + if not _page: + return tk.abort(404, _('Page Not Found')) + tk.c.page_type = page_type + tk.c.page = _page + return tk.render('ckanext_pages/%s_revisions.html' % page_type) + + +def pages_revisions_preview(page, revision, page_type='page'): + try: + tk.check_access('ckanext_pages_update', {'user': tk.g.user}) + except tk.NotAuthorized: + return tk.abort(401, _('Unauthorized to view this page')) + + _page = Page.get(name=page) + tk.c.page_type = page_type + tk.c.page = _page + try: + return tk.render('ckanext_pages/%s_revisions_preview.html' % page_type, extra_vars={ + "revision": _page.revisions[revision] + }) + except KeyError: + return tk.abort(404, _('Revision not found')) + + +def pages_revision_restore(page, revision, page_type='page'): + try: + tk.check_access('ckanext_pages_update', {'user': tk.g.user}) + except tk.NotAuthorized: + return tk.abort(401, _('Unauthorized to view this page')) + + try: + tk.get_action('ckanext_pages_revision_restore')( + context={}, data_dict={"page": page, "revision": revision} + ) + _page = Page.get(name=page) + timestamp = helpers.render_datetime(_page.revisions[revision]["created"], with_hours=True) + tk.h.flash_success(f"Content from revision created on {timestamp} set.") + except TypeError: + tk.h.flash_error( + """Bad values, please make sure that provided values exist: + Page name - '{name}', Revision version - '{rev}'""".format(name=page, rev=revision)) + + endpoint = 'show' if page_type in ('pages', 'page') else '%s_show' % page_type + return tk.redirect_to('pages.%s' % endpoint, page=page) + + def pages_delete(page, page_type='pages'): if page.startswith('/'): page = page[1:]