Skip to content
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
17 changes: 10 additions & 7 deletions invenio_requests/customizations/request_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,19 +192,22 @@ def _create_marshmallow_schema(cls):
RefBaseSchema.create_from_dict(cls.allowed_receiver_ref_types),
allow_none=cls.receiver_can_be_none,
),
"reviewers": ma.fields.List(
"topic": ma.fields.Nested(
RefBaseSchema.create_from_dict(cls.allowed_topic_ref_types),
allow_none=cls.topic_can_be_none,
),
}

if cls.reviewers_can_be_none():
# if the config is enabled, add the reviewers field to the schema
additional_fields["reviewers"] = ma.fields.List(
ma.fields.Nested(
MultipleEntityReferenceBaseSchema.create_from_dict(
cls.allowed_reviewers_ref_types()
),
allow_none=cls.reviewers_can_be_none(),
)
),
"topic": ma.fields.Nested(
RefBaseSchema.create_from_dict(cls.allowed_topic_ref_types),
allow_none=cls.topic_can_be_none,
),
}
)

# If a payload schema is defined, add it to the request schema
if cls.payload_schema is not None:
Expand Down
43 changes: 43 additions & 0 deletions invenio_requests/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

"""Custom exceptions used in the Invenio-Requests module."""

from invenio_i18n import lazy_gettext as _
from invenio_records_resources.services.errors import PermissionDeniedError


class ActionError(Exception):
"""Exception indicating an error related to the action."""
Expand Down Expand Up @@ -47,3 +50,43 @@ def __init__(self, action, reason=None):
"""
reason = reason or "Could not execute the action"
super().__init__(action, reason)


class RequestLockedError(PermissionDeniedError):
"""Exception indicating that the request is locked."""

def __init__(self, description=_("The request is locked.")):
"""Constructor.

:param message: The message to display.
"""
self._description = description

@property
def description(self):
"""Return the description."""
return self._description

def __str__(self):
"""Return str(self)."""
return self.description


class RequestEventPermissionError(PermissionDeniedError):
"""Exception indicating that the request event permission is denied."""

def __init__(self, description=None):
"""Constructor.

:param message: The message to display.
"""
self._description = description

@property
def description(self):
"""Return the description."""
return self._description

def __str__(self):
"""Return str(self)."""
return self.description
3 changes: 3 additions & 0 deletions invenio_requests/records/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,6 @@ class Request(Record):

last_activity_at = LastActivity()
"""The last activity (derived from other fields)."""

is_locked = DictField("is_locked")
"""Whether or not the request is locked."""
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
"type": {
"type": "string"
},
"is_locked": {
"type": "boolean"
},
"title": {
"type": "string"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@
},
"last_activity_at": {
"type": "date"
},
"is_locked": {
"type": "boolean"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@
},
"last_activity_at": {
"type": "date"
},
"is_locked": {
"type": "boolean"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@
},
"last_activity_at": {
"type": "date"
},
"is_locked": {
"type": "boolean"
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions invenio_requests/resources/events/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@

"""RequestEvent Resource Configuration."""

from flask_resources import HTTPJSONException, create_error_handler
from invenio_records_resources.resources import (
RecordResourceConfig,
SearchRequestArgsSchema,
)
from marshmallow import fields

from ...errors import RequestEventPermissionError, RequestLockedError


class RequestCommentsSearchRequestArgsSchema(SearchRequestArgsSchema):
"""Add parameter to parse tags."""
Expand Down Expand Up @@ -55,3 +58,19 @@ class RequestCommentsResourceConfig(RecordResourceConfig):
],
**RecordResourceConfig.response_handlers,
}

error_handlers = {
**RecordResourceConfig.error_handlers,
RequestLockedError: create_error_handler(
lambda e: HTTPJSONException(
code=403,
description=e.description,
)
),
RequestEventPermissionError: create_error_handler(
lambda e: HTTPJSONException(
code=403,
description=e.description,
)
),
}
10 changes: 9 additions & 1 deletion invenio_requests/resources/requests/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from invenio_records_resources.services.base.config import ConfiguratorMixin, FromConfig
from marshmallow import fields

from ...errors import CannotExecuteActionError, NoSuchActionError
from ...errors import CannotExecuteActionError, NoSuchActionError, RequestLockedError
from .fields import ReferenceString


Expand Down Expand Up @@ -50,6 +50,12 @@ class RequestSearchRequestArgsSchema(SearchRequestArgsSchema):
description=str(e),
)
),
RequestLockedError: create_error_handler(
lambda e: HTTPJSONException(
code=403,
description=e.description,
)
),
}


Expand All @@ -66,6 +72,8 @@ class RequestsResourceConfig(RecordResourceConfig, ConfiguratorMixin):
"user-prefix": "/user",
"item": "/<id>",
"action": "/<id>/actions/<action>",
"lock": "/<id>/lock",
"unlock": "/<id>/unlock",
}

request_view_args = {
Expand Down
22 changes: 22 additions & 0 deletions invenio_requests/resources/requests/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ def s(route):
route("DELETE", p(routes["item"]), self.delete),
route("POST", p(routes["action"]), self.execute_action),
route("GET", s(routes["user-prefix"]), self.search_user_requests),
route("GET", p(routes["lock"]), self.lock_request),
route("GET", p(routes["unlock"]), self.unlock_request),
]

@request_extra_args
Expand Down Expand Up @@ -143,3 +145,23 @@ def execute_action(self):
expand=resource_requestctx.args.get("expand", False),
)
return item.to_dict(), 200

@request_view_args
@request_headers
def lock_request(self):
"""Lock a request."""
self.service.lock_request(
identity=g.identity,
id_=resource_requestctx.view_args["id"],
)
return "", 204

@request_view_args
@request_headers
def unlock_request(self):
"""Unlock a request."""
self.service.unlock_request(
identity=g.identity,
id_=resource_requestctx.view_args["id"],
)
return "", 204
13 changes: 12 additions & 1 deletion invenio_requests/services/events/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
)
from invenio_records_resources.services.base.config import ConfiguratorMixin, FromConfig
from invenio_records_resources.services.records.links import pagination_links
from invenio_records_resources.services.records.results import RecordItem, RecordList
from invenio_records_resources.services.records.results import (
FieldsResolver,
RecordItem,
RecordList,
)

from invenio_requests.proxies import (
current_request_type_registry,
Expand All @@ -39,6 +43,12 @@ def id(self):
class RequestEventList(RecordList):
"""RequestEvent result item."""

def __init__(self, *args, **kwargs):
"""Constructor."""
request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
self._request = request

@property
def hits(self):
"""Iterator over the hits."""
Expand All @@ -55,6 +65,7 @@ def hits(self):
context=dict(
identity=self._identity,
record=record,
request=self._request, # Need to pass the request to the schema to get the permissions to check if locked
meta=hit.meta,
),
)
Expand Down
45 changes: 36 additions & 9 deletions invenio_requests/services/events/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import sqlalchemy.exc
from flask_principal import AnonymousIdentity
from invenio_i18n import _
from invenio_i18n import lazy_gettext as _
from invenio_notifications.services.uow import NotificationOp
from invenio_records_resources.services import RecordService, ServiceSchemaWrapper
from invenio_records_resources.services.base.links import LinksTemplate
Expand All @@ -31,6 +31,7 @@
from invenio_requests.records.api import RequestEventFormat
from invenio_requests.services.results import EntityResolverExpandableField

from ...errors import RequestEventPermissionError, RequestLockedError
from ...resolvers.registry import ResolverRegistry


Expand Down Expand Up @@ -72,7 +73,20 @@ def create(
:param dict data: Input data according to the data schema.
"""
request = self._get_request(request_id)
self.require_permission(identity, "create_comment", request=request)
self.require_permission(identity, "read", request=request)
try:
# If the event is a log, we don't check for permissions to not block logs creation
if event_type.type_id != LogEventType.type_id:
self.require_permission(identity, "create_comment", request=request)
except PermissionDeniedError:
if request.get("is_locked", False):
raise RequestLockedError(
description=_("Commenting is now locked for this conversation.")
)
else:
raise RequestEventPermissionError(
_("You do not have permission to comment on this conversation.")
)

# Validate data (if there are errors, .load() raises)
schema = self._wrap_schema(event_type.marshmallow_schema())
Expand Down Expand Up @@ -159,16 +173,26 @@ def update(self, identity, id_, data, revision_id=None, uow=None, expand=False):
"""Update a comment (only comments can be updated)."""
event = self._get_event(id_)
request = self._get_request(event.request.id)
self.require_permission(
identity, "update_comment", request=request, event=event
)
try:
self.require_permission(
identity, "update_comment", request=request, event=event
)
except PermissionDeniedError:
if request.get("is_locked", False):
raise RequestLockedError(
description=_("Updating is now locked for this comment.")
)
else:
raise RequestEventPermissionError(
_("You do not have permission to update this comment.")
)
self.check_revision_id(event, revision_id)

if event.type != CommentEventType:
raise PermissionError("You cannot update this event.")
raise RequestEventPermissionError(_("You cannot update this event."))

schema = self._wrap_schema(event.type.marshmallow_schema())
data, _ = schema.load(
data, errors = schema.load(
data,
context=dict(identity=identity, record=event, event_type=event.type),
)
Expand Down Expand Up @@ -217,15 +241,15 @@ def delete(self, identity, id_, revision_id=None, uow=None):
self.check_revision_id(event, revision_id)

if event.type != CommentEventType:
raise PermissionError("You cannot delete this event.")
raise RequestEventPermissionError(_("You cannot delete this event."))

# update the event for the deleted comment with a LogEvent
event.type = LogEventType
schema = self._wrap_schema(event.type.marshmallow_schema())
data = dict(
payload=dict(
event="comment_deleted",
content=_("deleted a comment"),
content="deleted a comment",
format=RequestEventFormat.HTML.value,
)
)
Expand Down Expand Up @@ -289,6 +313,7 @@ def search(
),
expandable_fields=self.expandable_fields,
expand=expand,
request=request,
)

def focused_list(
Expand Down Expand Up @@ -355,6 +380,7 @@ def focused_list(
),
expandable_fields=self.expandable_fields,
expand=expand,
request=request,
)

def scan(
Expand Down Expand Up @@ -390,6 +416,7 @@ def scan(
),
expandable_fields=self.expandable_fields,
expand=expand,
request=request,
)

# Utilities
Expand Down
Loading
Loading