Skip to content

Commit

Permalink
Merge pull request #7685 from pdelboca/add-htmx
Browse files Browse the repository at this point in the history
Adding htmx to CKAN 馃殌  (With an example)
  • Loading branch information
amercader committed Oct 11, 2023
2 parents 70f1ee5 + eba5356 commit 07a4b36
Show file tree
Hide file tree
Showing 38 changed files with 4,642 additions and 852 deletions.
3 changes: 3 additions & 0 deletions changes/7685.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Start using `htmx <https://htmx.org/>` to modernize the CKAN frontend. For more information check the :doc:`documentation <extensions/htmx>`_.

`follow_*` and `unfollow_*` APIs will no longer return an error if the user is already following or not following the entity.
1 change: 1 addition & 0 deletions changes/7685.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`snippet/organization.html` has been moved to `organization/snippets/info.html` for consistency with Groups/Packages/Users.
54 changes: 51 additions & 3 deletions ckan/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,61 @@ def _get_request():
return flask.request


class HtmxDetails(object):
"""Object to access htmx properties from the request headers.
This object will be added to the CKAN `request` object
as `request.htmx`. It adds properties to easily access
htmx's request headers defined in
https://htmx.org/reference/#headers.
"""

def __init__(self, request: Any):
self.request = request

def __bool__(self) -> bool:
return self.request.headers.get("HX-Request") == "true"

@property
def boosted(self) -> bool:
return self.request.headers.get("HX-Boosted") == "true"

@property
def current_url(self) -> str | None:
return self.request.headers.get("HX-Current-URL")

@property
def history_restore_request(self) -> bool:
return self.request.headers.get("HX-History-Restore-Request") == "true"

@property
def prompt(self) -> str | None:
return self.request.headers.get("HX-Prompt")

@property
def target(self) -> str | None:
return self.request.headers.get("HX-Target")

@property
def trigger(self) -> str | None:
return self.request.headers.get("HX-Trigger")

@property
def trigger_name(self) -> str | None:
return self.request.headers.get("HX-Trigger-Name")


class CKANRequest(LocalProxy[Request]):
u'''Common request object
'''CKAN request class.
This is just a wrapper around LocalProxy so we can handle some special
cases for backwards compatibility.
This is a subclass of the Flask request object. It adds a new
`htmx` property to access htmx properties from the request headers.
'''

@property
def htmx(self) -> HtmxDetails:
return HtmxDetails(self)

@property
@maintain.deprecated('Use `request.args` instead of `request.params`',
since="2.10.0")
Expand Down
99 changes: 40 additions & 59 deletions ckan/logic/action/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@

import six

import ckan.common

import ckan.lib.plugins as lib_plugins
import ckan.logic as logic
import ckan.plugins as plugins
Expand All @@ -33,7 +31,7 @@
import ckan.authz as authz
import ckan.model

from ckan.common import _
from ckan.common import _, asbool
from ckan.types import Context, DataDict, ErrorDict, Schema

# FIXME this looks nasty and should be shared better
Expand Down Expand Up @@ -458,7 +456,7 @@ def resource_create_default_resource_views(

dataset_dict = data_dict.get('package')

create_datastore_views = ckan.common.asbool(
create_datastore_views = asbool(
data_dict.get('create_datastore_views', False))

return ckan.lib.datapreview.add_views_to_resource(
Expand Down Expand Up @@ -497,7 +495,7 @@ def package_create_default_resource_views(

_check_access('package_create_default_resource_views', context, data_dict)

create_datastore_views = ckan.common.asbool(
create_datastore_views = asbool(
data_dict.get('create_datastore_views', False))

return ckan.lib.datapreview.add_views_to_dataset_resources(
Expand Down Expand Up @@ -1227,36 +1225,34 @@ def follow_user(context: Context,
if not userobj:
raise NotAuthorized(_("You must be logged in to follow users"))

schema = context.get(
'schema') or ckan.logic.schema.default_follow_user_schema()
schema = context.get('schema',
ckan.logic.schema.default_follow_user_schema())

validated_data_dict, errors = _validate(data_dict, schema, context)

if errors:
model.Session.rollback()
raise ValidationError(errors)
msg = _('Error validating the schema of the user to follow.')
raise ValidationError(msg)

# Don't let a user follow herself.
if userobj.id == validated_data_dict['id']:
message = _('You cannot follow yourself')
raise ValidationError({'message': message})

# Don't let a user follow someone she is already following.
if model.UserFollowingUser.is_following(userobj.id,
validated_data_dict['id']):
followeduserobj = model.User.get(validated_data_dict['id'])
assert followeduserobj
name = followeduserobj.display_name
message = _('You are already following {0}').format(name)
raise ValidationError({'message': message})
# If the user is already following, return the existing follower object
follower = model.UserFollowingUser.get(
userobj.id, validated_data_dict['id']
)
if follower:
return model_dictize.user_following_user_dictize(follower, context)

follower = model_save.follower_dict_save(
validated_data_dict, context, model.UserFollowingUser)

if not context.get('defer_commit'):
model.repo.commit()

log.debug(u'User {follower} started following user {object}'.format(
log.debug('User {follower} started following user {object}'.format(
follower=follower.follower_id, object=follower.object_id))

return model_dictize.user_following_user_dictize(follower, context)
Expand All @@ -1276,41 +1272,32 @@ def follow_dataset(context: Context,
:rtype: dictionary
'''

if not context.get('user'):
raise NotAuthorized(
_("You must be logged in to follow a dataset."))
raise NotAuthorized(_("You must be logged in to follow users"))

model = context['model']

userobj = model.User.get(context['user'])
if not userobj:
raise NotAuthorized(
_("You must be logged in to follow a dataset."))

schema = context.get(
'schema') or ckan.logic.schema.default_follow_dataset_schema()
raise NotAuthorized(_("You must be logged in to follow users"))

schema = (context.get('schema') or
ckan.logic.schema.default_follow_dataset_schema())
validated_data_dict, errors = _validate(data_dict, schema, context)

if errors:
model.Session.rollback()
raise ValidationError(errors)

# Don't let a user follow a dataset she is already following.
if model.UserFollowingDataset.is_following(userobj.id,
validated_data_dict['id']):
# FIXME really package model should have this logic and provide
# 'display_name' like users and groups
pkgobj = model.Package.get(validated_data_dict['id'])
assert pkgobj
name = pkgobj.title or pkgobj.name or pkgobj.id
message = _(
'You are already following {0}').format(name)
message = _('Error validating the schema of the dataset to follow.')
raise ValidationError({'message': message})

follower = model_save.follower_dict_save(validated_data_dict, context,
model.UserFollowingDataset)
# If the user is already following, return the existing follower object.
follower = model.UserFollowingDataset.get(
userobj.id, validated_data_dict['id']
)
if follower:
return model_dictize.user_following_dataset_dictize(follower, context)

follower = model_save.follower_dict_save(
validated_data_dict, context, model.UserFollowingDataset
)

if not context.get('defer_commit'):
model.repo.commit()
Expand Down Expand Up @@ -1423,42 +1410,36 @@ def follow_group(context: Context,
'''
if not context.get('user'):
raise NotAuthorized(
_("You must be logged in to follow a group."))
raise NotAuthorized(_("You must be logged in to follow users"))

model = context['model']

userobj = model.User.get(context['user'])
if not userobj:
raise NotAuthorized(
_("You must be logged in to follow a group."))
raise NotAuthorized(_("You must be logged in to follow users"))

schema = context.get('schema',
ckan.logic.schema.default_follow_group_schema())

validated_data_dict, errors = _validate(data_dict, schema, context)

if errors:
model.Session.rollback()
raise ValidationError(errors)

# Don't let a user follow a group she is already following.
if model.UserFollowingGroup.is_following(userobj.id,
validated_data_dict['id']):
groupobj = model.Group.get(validated_data_dict['id'])
assert groupobj
name = groupobj.display_name
message = _(
'You are already following {0}').format(name)
message = _('Error validating the schema of the group to follow.')
raise ValidationError({'message': message})

# If it's already following, return the existing follower object.
follower = model.UserFollowingGroup.get(
userobj.id, validated_data_dict['id']
)
if follower:
return model_dictize.user_following_group_dictize(follower, context)

follower = model_save.follower_dict_save(validated_data_dict, context,
model.UserFollowingGroup)

if not context.get('defer_commit'):
model.repo.commit()

log.debug(u'User {follower} started following group {object}'.format(
log.debug('User {follower} started following group {object}'.format(
follower=follower.follower_id, object=follower.object_id))

return model_dictize.user_following_group_dictize(follower, context)
Expand Down
12 changes: 7 additions & 5 deletions ckan/logic/action/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,10 +645,10 @@ def tag_delete(context: Context, data_dict: DataDict) -> None:
tag_obj.delete()
model.repo.commit()


def _unfollow(
context: Context, data_dict: DataDict, schema: Schema,
FollowerClass: Type['ModelFollowingModel[Any, Any]']):

model = context['model']

if not context.get('user'):
Expand All @@ -662,15 +662,17 @@ def _unfollow(

validated_data_dict, errors = validate(data_dict, schema, context)
if errors:
raise ValidationError(errors)
object_id = validated_data_dict.get('id')
msg = _("Error validating the schema of the object to unfollow.")
raise ValidationError(msg)

object_id = validated_data_dict.get('id')
follower_id = userobj.id
follower_obj = FollowerClass.get(follower_id, object_id)
if follower_obj is None:
raise NotFound(
_('You are not following {0}.').format(data_dict.get('id')))
return

follower_obj.delete()

model.repo.commit()

def unfollow_user(context: Context, data_dict: DataDict) -> None:
Expand Down
23 changes: 3 additions & 20 deletions ckan/public/base/css/main-rtl.css
Original file line number Diff line number Diff line change
Expand Up @@ -13307,9 +13307,10 @@ td.diff_header {
margin-left: 0;
}
.context-info .nums {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding-top: 10px;
padding-bottom: 0;
border-top: 1px dotted #DDD;
}
.context-info .nums::after {
Expand All @@ -13318,34 +13319,16 @@ td.diff_header {
content: "";
}
.context-info .nums dl {
float: left;
width: 50%;
margin: 5px 0 0 0;
color: #444;
}
.context-info .nums dl dt {
display: block;
font-size: 13px;
font-weight: 300;
}
.context-info .nums dl dd {
display: block;
font-size: 30px;
font-weight: 700;
line-height: 36px;
margin-left: 0;
}
.context-info .nums dl dd .smallest {
font-size: 13px;
}
.context-info .nums dl dd .smaller {
font-size: 16px;
}
.context-info .nums dl dd .small {
font-size: 21px;
}
.context-info .follow_button {
margin: 0.75rem 0;
}
.context-info.editing .module-content {
margin-top: 0;
Expand Down
23 changes: 3 additions & 20 deletions ckan/public/base/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -13307,9 +13307,10 @@ td.diff_header {
margin-left: 0;
}
.context-info .nums {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding-top: 10px;
padding-bottom: 0;
border-top: 1px dotted #DDD;
}
.context-info .nums::after {
Expand All @@ -13318,34 +13319,16 @@ td.diff_header {
content: "";
}
.context-info .nums dl {
float: left;
width: 50%;
margin: 5px 0 0 0;
color: #444;
}
.context-info .nums dl dt {
display: block;
font-size: 13px;
font-weight: 300;
}
.context-info .nums dl dd {
display: block;
font-size: 30px;
font-weight: 700;
line-height: 36px;
margin-left: 0;
}
.context-info .nums dl dd .smallest {
font-size: 13px;
}
.context-info .nums dl dd .smaller {
font-size: 16px;
}
.context-info .nums dl dd .small {
font-size: 21px;
}
.context-info .follow_button {
margin: 0.75rem 0;
}
.context-info.editing .module-content {
margin-top: 0;
Expand Down

0 comments on commit 07a4b36

Please sign in to comment.