Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pages revisions feature #139

Merged
merged 4 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
63 changes: 62 additions & 1 deletion ckanext/pages/actions.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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.

Expand Down Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions ckanext/pages/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -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')

Expand Down Expand Up @@ -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/<page>", view_func=show)
pages.add_url_rule("/pages/<page>/revisions", view_func=pages_revisions)
pages.add_url_rule("/pages/<page>/revisions/<revision>", view_func=pages_revisions_preview)
pages.add_url_rule("/pages/<page>/revisions/<revision>/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/<page>", view_func=pages_edit, endpoint='edit', methods=['GET', 'POST'])
Expand All @@ -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/<page>", view_func=blog_show)
pages.add_url_rule("/blog/<page>/revisions", view_func=blog_revisions)
pages.add_url_rule("/blog/<page>/revisions/<revision>", view_func=blog_revisions_preview)
pages.add_url_rule("/blog/<page>/revisions/<revision>/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/<page>", view_func=blog_edit, endpoint='blog_edit', methods=['GET', 'POST'])
Expand Down
13 changes: 13 additions & 0 deletions ckanext/pages/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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'''
Expand Down
Original file line number Diff line number Diff line change
@@ -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')
1 change: 1 addition & 0 deletions ckanext/pages/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
50 changes: 50 additions & 0 deletions ckanext/pages/tests/test_action.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import pytest
import datetime
from collections import OrderedDict

from ckan.tests import factories, helpers

Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading