Skip to content

Commit

Permalink
Merge branch 'DataShades-pages-revisions-feature'
Browse files Browse the repository at this point in the history
  • Loading branch information
amercader committed Oct 22, 2024
2 parents 81912bc + 063f03c commit 2a92bb8
Show file tree
Hide file tree
Showing 16 changed files with 780 additions and 17 deletions.
14 changes: 12 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
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

0 comments on commit 2a92bb8

Please sign in to comment.