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

parent_community: use OS instead of DB queries for search result #1091

Merged
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
2 changes: 2 additions & 0 deletions invenio_communities/communities/records/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from invenio_vocabularies.records.api import Vocabulary
from invenio_vocabularies.records.systemfields.relations import CustomFieldsRelation

from invenio_communities.communities.records.systemfields.children import ChildrenField
from invenio_communities.communities.records.systemfields.is_verified import (
IsVerifiedField,
)
Expand Down Expand Up @@ -53,6 +54,7 @@ class Community(Record):
slug = ModelField()
pid = PIDSlugField("id", "slug")
parent = ParentCommunityField()
children = ChildrenField()

schema = ConstantField("$schema", "local://communities/communities-v1.0.0.json")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@
}
}
},
"children":{
"type": "object",
"description": "Children communities.",
"properties": {
"allow": {
"type": "boolean",
"description": "Whether or not the community allows children."
}
}
},
"parent": {
"type": "object",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,33 @@
}
}
},
"children": {
"type": "object",
"properties": {
"allow": {
"type": "boolean"
}
}
},
"parent": {
"type": "object",
"properties": {
"@v": {
"type": "keyword"
},
"uuid": {
"type": "keyword",
"index": false
},
"created": {
"type": "date"
},
"updated": {
"type": "date"
},
"version_id": {
"type": "long"
},
"id": {
"type": "keyword"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,33 @@
}
}
},
"children": {
"type": "object",
"properties": {
"allow": {
"type": "boolean"
}
}
},
"parent": {
"type": "object",
"properties": {
"@v": {
"type": "keyword"
},
"uuid": {
"type": "keyword",
"index": false
},
"created": {
"type": "date"
},
"updated": {
"type": "date"
},
"version_id": {
"type": "long"
},
"id": {
"type": "keyword"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,33 @@
}
}
},
"children": {
"type": "object",
"properties": {
"allow": {
"type": "boolean"
}
}
},
"parent": {
"type": "object",
"properties": {
"@v": {
"type": "keyword"
},
"uuid": {
"type": "keyword",
"index": false
},
"created": {
"type": "date"
},
"updated": {
"type": "date"
},
"version_id": {
"type": "long"
},
"id": {
"type": "keyword"
},
Expand Down
99 changes: 99 additions & 0 deletions invenio_communities/communities/records/systemfields/children.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2023 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Children system field."""

from invenio_records.systemfields import SystemField


class Children:
slint marked this conversation as resolved.
Show resolved Hide resolved
"""Children object."""

def __init__(self, allow=None):
"""Create a Children object."""
self._allow = allow or False

@property
def allow(self):
"""Get the allow."""
return self._allow

@allow.setter
def allow(self, value):
"""Set the allow."""
if not isinstance(value, bool):
raise ValueError("Invalid value for allow, it must be a boolean.")
self._allow = value

@classmethod
def from_dict(cls, data):
"""Create a Children object from a dictionary."""
return cls(allow=data.get("allow"))

def dump(self):
"""Dump the Children object to a dictionary."""
return {"allow": self.allow}


class ChildrenField(SystemField):
"""System field for children of a community."""

children_obj_class = Children

def __init__(self, key="children", children_obj_class=None):
"""Create a new ChildrenField instance."""
self._children_obj_class = children_obj_class or self.children_obj_class
super().__init__(key=key)

def obj(self, instance):
"""Get the Children object."""
obj = self._get_cache(instance)
if obj is not None:
return obj

data = self.get_dictkey(instance)
if data:
obj = self._children_obj_class.from_dict(data)
else:
obj = self._children_obj_class()

self._set_cache(instance, obj)
return obj

def set_obj(self, record, obj):
"""Set the Children object."""
# We accept both dicts and Children class objects.
if isinstance(obj, dict):
obj = self._children_obj_class.from_dict(obj)

if not isinstance(obj, self._children_obj_class):
raise ValueError("Invalid children object.")

# We do not dump the object until the pre_commit hook
self._set_cache(record, obj)

def __get__(self, record, owner=None):
"""Get the record's Children object."""
if record is None:
# access by class
return self

# access by object
return self.obj(record)

def __set__(self, record, obj):
"""Set the records Children object."""
self.set_obj(record, obj)

def pre_commit(self, record):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment/shelve: we should have probably done the same with the parent field, i.e. have a pre_commit to manage how/when the field is persisted in the record

"""Dump the configured values before the record is committed."""
obj = self.obj(record)
if obj is not None:
# only set the 'children' property if one was present in the
# record dict
record[self.key] = obj.dump()
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def set_obj(self, record, obj):
# Check if obj is None and remove 'parent' key from record
if obj is None:
record.pop("parent", None)
self._set_cache(record, None)
slint marked this conversation as resolved.
Show resolved Hide resolved
return

if isinstance(obj, type(record)):
Expand Down Expand Up @@ -82,8 +83,8 @@ def __set__(self, record, obj):
"""Set the records access object."""
self.set_obj(record, obj)

def pre_dump(self, record, data, dumper=None):
"""Before dumping, dereference the parent community."""
def post_dump(self, record, data, dumper=None):
"""After dumping, dereference the parent community."""
parent_community = getattr(record, self.attr_name)
if parent_community:
dump = parent_community.dumps()
Expand All @@ -104,3 +105,8 @@ def pre_dump(self, record, data, dumper=None):
"metadata.funding",
],
)

def post_load(self, record, data, loader=None):
"""Laod the parent community using the OS data (preventing a DB query)."""
if data.get("parent"):
record.parent = record.loads(data["parent"])
15 changes: 15 additions & 0 deletions invenio_communities/communities/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ class CommunityThemeSchema(Schema):
enabled = fields.Boolean()


class ChildrenSchema(Schema):
"""Children schema."""

allow = fields.Boolean()


class BaseCommunitySchema(BaseRecordSchema, FieldPermissionsMixin):
"""Base schema for the community metadata."""

Expand Down Expand Up @@ -223,6 +229,8 @@ class Meta:

deletion_status = fields.Nested(DeletionStatusSchema, dump_only=True)

children = NestedAttribute(ChildrenSchema)

@post_dump
def post_dump(self, data, many, **kwargs):
"""Hide tombstone info if the record isn't deleted and metadata if it is."""
Expand Down Expand Up @@ -274,6 +282,13 @@ def post_dump(self, data, many, **kwargs):
data.pop("parent", None)
return data

@post_load
def filter_parent_id(self, in_data, **kwargs):
"""Simply keep the parent id."""
if in_data.get("parent"):
in_data["parent"] = dict(id=in_data.get("parent", {}).get("id"))
Comment on lines +288 to +289
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: there are two input cases we have to handle:

  • {"parent": {"id": "<some_id>"} - we filter and just keep the parent.id key
  • {"parent": None} - We keep the data as is so that we can "unset" the community

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that @yashlamba is working on the service method that allows to set/update the parent, maybe you can improve it as you can test it

return in_data


class CommunityFeaturedSchema(Schema):
"""Community Featured schema."""
Expand Down
18 changes: 18 additions & 0 deletions invenio_communities/communities/services/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,23 @@ def update(self, identity, data=None, record=None, errors=None, **kwargs):
record["theme"] = data["theme"]


class ChildrenComponent(ServiceComponent):
"""Service component for children integration."""

def create(self, identity, data=None, record=None, **kwargs):
"""Create handler."""
if "children" in data:
self.service.require_permission(identity, "manage_children", record=record)
record.children = record.children.from_dict(data["children"])

def update(self, identity, data=None, record=None, **kwargs):
"""Update handler."""
# We check if the children field is passed and it's different to the values stored
if "children" in data and data["children"] != record.get("children", {}):
self.service.require_permission(identity, "manage_children", record=record)
record.children = record.children.from_dict(data["children"])


DefaultCommunityComponents = [
MetadataComponent,
CommunityThemeComponent,
Expand All @@ -311,4 +328,5 @@ def update(self, identity, data=None, record=None, errors=None, **kwargs):
FeaturedCommunityComponent,
OAISetComponent,
CommunityDeletionComponent,
ChildrenComponent,
]
3 changes: 3 additions & 0 deletions invenio_communities/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ class CommunityPermissionPolicy(BasePermissionPolicy):
can_set_theme = [SystemProcess()]
can_delete_theme = can_set_theme

# Permissions to set if communities can have children
can_manage_children = [SystemProcess()]


def can_perform_action(community, context):
"""Check if the given action is available on the request."""
Expand Down
14 changes: 14 additions & 0 deletions tests/communities/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,17 @@ def test_oai_set_update(community_service, comm, comm_restricted):
assert comm.data.get("slug") in oaiset.spec
assert new_data.get("metadata", {}).get("title") in oaiset.description
assert new_data.get("metadata", {}).get("title") in oaiset.name


def test_children_component(community_service, parent_community, db):
"""Test children component."""
# By default it's set to False
assert parent_community.children.allow is False

parent_community.children.allow = True
parent_community.commit()
db.session.commit()

comm = community_service.record_cls.get_record(str(parent_community.id))

assert comm.children.allow is True
35 changes: 35 additions & 0 deletions tests/communities/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,3 +592,38 @@ def test_theme_updates(
community_service.update(system_identity, community.id, community_data)
community_item = community_service.read(system_identity, community.id)
assert community_item.data.get("theme") is None


def test_children_updates(
app,
db,
search_clear,
location,
community_service,
community,
members,
):
current_cache.clear()
owner = members["owner"]

# Update children
community_data = deepcopy(community.data)
community_data.update(dict(children=dict(allow=True)))

# check if owner can update the children
with pytest.raises(PermissionDeniedError):
community_service.update(owner.identity, community.id, community_data)

# only system can update the children
community_service.update(system_identity, community.id, community_data)
community_item = community_service.read(system_identity, community.id)
assert community_item.data["children"]["allow"] == True

# Update community data without passing children should keep the stored values
community_data = deepcopy(community_item.data)
community_data.pop("children")

# Owner should be able to perfrom this operation since is not changing the children
community_service.update(owner.identity, community.id, community_data)
# Refresh index
community_service.record_cls.index.refresh()
Loading