Skip to content

Commit

Permalink
records: add status field to community records
Browse files Browse the repository at this point in the history
* added "status" field to community records
* renamed "is_verified" property to "is_safelisted"
* updated mappings to support "is_safelisted"
* added tests for the new field
* added tests for safelisting feature
  • Loading branch information
alejandromumo committed Mar 6, 2024
1 parent df0ca54 commit 65da818
Show file tree
Hide file tree
Showing 13 changed files with 151 additions and 23 deletions.
10 changes: 6 additions & 4 deletions invenio_communities/communities/records/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
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,
from invenio_communities.communities.records.systemfields.is_safelisted import (
IsSafelistedField,
)

from ..dumpers.featured import FeaturedDumperExt
Expand Down Expand Up @@ -64,7 +64,7 @@ class Community(Record):
extensions=[
FeaturedDumperExt("featured"),
RelationDumperExt("relations"),
CalculatedFieldDumperExt("is_verified"),
CalculatedFieldDumperExt("is_safelisted"),
]
)

Expand Down Expand Up @@ -121,7 +121,9 @@ class Community(Record):
custom=CustomFieldsRelation("COMMUNITIES_CUSTOM_FIELDS"),
)

is_verified = IsVerifiedField("is_verified")
status = ModelField("status", dump_type=str)

is_safelisted = IsSafelistedField("is_safelisted")

deletion_status = CommunityDeletionStatusField()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@
"description": "Whether or not the tombstone page is publicly visible."
}
}
},
"status": {
"type": "string",
"enum": [
"new",
"moderated",
"verified"
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@
"id": {
"type": "keyword"
},
"is_verified": {
"is_safelisted": {
"type": "boolean"
},
"status": {
"type": "keyword"
},
"slug": {
"type": "keyword"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@
"id": {
"type": "keyword"
},
"is_verified": {
"is_safelisted": {
"type": "boolean"
},
"status": {
"type": "keyword"
},
"slug": {
"type": "keyword"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"id": {
"type": "keyword"
},
"is_verified": {
"is_safelisted": {
"type": "boolean"
},
"slug": {
Expand All @@ -47,6 +47,9 @@
"deletion_status": {
"type": "keyword"
},
"status": {
"type": "keyword"
},
"is_deleted": {
"type": "boolean"
},
Expand Down
30 changes: 29 additions & 1 deletion invenio_communities/communities/records/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# under the terms of the MIT License; see LICENSE file for more details.

"""Community database models."""

import enum
from datetime import datetime

from invenio_db import db
Expand All @@ -22,6 +22,28 @@
from .systemfields.deletion_status import CommunityDeletionStatusEnum


class CommunityStatusEnum(enum.Enum):
"""Community status enum."""

NEW = "N"
VERIFIED = "V"
MODERATED = "M"

def __str__(self):
"""Return its value."""
return self.value

def __eq__(self, __value) -> bool:
"""Check if the value is equal to the enum value.
Supports comparison with string values.
"""
if isinstance(__value, str):
return self.value == __value

return super().__eq__(__value)


class CommunityMetadata(db.Model, RecordMetadataBase):
"""Represent a community."""

Expand All @@ -39,6 +61,12 @@ class CommunityMetadata(db.Model, RecordMetadataBase):
default=CommunityDeletionStatusEnum.PUBLISHED.value,
)

status = db.Column(
ChoiceType(CommunityStatusEnum, impl=db.String(1)),
nullable=False,
default=CommunityStatusEnum.NEW.value,
)


class CommunityFileMetadata(db.Model, RecordMetadataBase, FileRecordModelMixin):
"""File associated with a community."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,29 @@

from invenio_records_resources.records.systemfields.calculated import CalculatedField

from ..models import CommunityStatusEnum

class IsVerifiedField(CalculatedField):

class IsSafelistedField(CalculatedField):
"""Systemfield for calculating whether or not the request is expired."""

def __init__(self, key=None):
"""Constructor."""
super().__init__(key=key, use_cache=False)

def calculate(self, record):
"""Calculate the ``is_verified`` property of the record."""
"""Calculate the ``is_safelisted`` property of the record."""
# import here due to circular import
from invenio_communities.members.records.api import Member

community_verified = False
owners = [m.dumps() for m in Member.get_members(record.id) if m.role == "owner"]
for owner in owners:
# community is considered verified if at least one owner is verified
if owner["user"]["verified_at"] is not None:
community_verified = True
break
community_verified = record.status == CommunityStatusEnum.VERIFIED
if not community_verified:
owners = [
m.dumps() for m in Member.get_members(record.id) if m.role == "owner"
]
for owner in owners:
# community is considered verified if at least one owner is verified
if owner["user"]["verified_at"] is not None:
community_verified = True
break
return community_verified
9 changes: 6 additions & 3 deletions invenio_communities/communities/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,9 @@ class Meta:
unknown = EXCLUDE

field_dump_permissions = {
# hide 'is_verified' behind a permission
"is_verified": "moderate",
# hide 'is_safelisted' behind a permission
"is_safelisted": "moderate",
"status": "moderate",
}

id = fields.String(dump_only=True)
Expand All @@ -229,7 +230,9 @@ class Meta:
partial(CustomFieldsSchema, fields_var="COMMUNITIES_CUSTOM_FIELDS")
)

is_verified = fields.Boolean(dump_only=True)
is_safelisted = fields.Boolean(dump_only=True)

status = fields.String(dump_only=True)

theme = fields.Nested(CommunityThemeSchema, allow_none=True)

Expand Down
2 changes: 1 addition & 1 deletion invenio_communities/communities/services/sort.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ def apply(self, identity, search, params):

if current_app.config["COMMUNITIES_SEARCH_SORT_BY_VERIFIED"]:
fields = self._compute_sort_fields(params)
return search.sort(*["-is_verified", *fields])
return search.sort(*["-is_safelisted", *fields])

return super(CommunitiesSortParam, self).apply(identity, search, params)
2 changes: 1 addition & 1 deletion invenio_communities/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class CommunityPermissionPolicy(BasePermissionPolicy):
can_featured_update = [Administration(), SystemProcess()]
can_featured_delete = [Administration(), SystemProcess()]

# Used to hide at the moment the `is_verified` field. It should be set to
# Used to hide at the moment the `is_safelisted` field. It should be set to
# correct permissions based on which the field will be exposed only to moderators
can_moderate = [Disable()]

Expand Down
35 changes: 35 additions & 0 deletions tests/communities/test_safelist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2024 CERN.
#
# Invenio-communities is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.
"""Test safelist feature for communities."""

from copy import deepcopy

from invenio_db import db

from invenio_communities.communities.records.models import CommunityStatusEnum


def test_safelist_computed_by_verified_status(
community_service, minimal_community, location, es_clear, unverified_user
):
"""
Test that the safelist feature for communities is computed correctly based on the verified status.
"""
# Create a comunity
# Flag it as "verified"
# Validate that the computed field "is_verified" is set to "True"
c_data = deepcopy(minimal_community)
c_data["slug"] = "test_status_perms"
c_item = community_service.create(unverified_user.identity, data=c_data)
assert c_item._record.status == CommunityStatusEnum.NEW
assert c_item._record.is_safelisted is False
community = community_service.record_cls.pid.resolve(c_item.id)
community.status = CommunityStatusEnum.VERIFIED
community.commit()
db.session.commit()
c_item = community_service.read(unverified_user.identity, c_item.id)
assert c_item._record.is_safelisted is True
24 changes: 24 additions & 0 deletions tests/communities/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from invenio_records_resources.services.errors import PermissionDeniedError
from marshmallow import ValidationError

from invenio_communities.communities.records.models import CommunityStatusEnum
from invenio_communities.communities.records.systemfields.deletion_status import (
CommunityDeletionStatusEnum,
)
Expand Down Expand Up @@ -762,3 +763,26 @@ def test_bulk_update_parent_overwrite(
for c_id in children:
c_comm = community_service.record_cls.pid.resolve(c_id)
assert str(c_comm.parent.id) == str(parent_community.id)


def test_status_new(community_service, minimal_community, location, es_clear, owner):
c_data = deepcopy(minimal_community)
c_data["slug"] = "test_status_new"
co = community_service.create(data=c_data, identity=owner.identity)
assert co._record.status == CommunityStatusEnum.NEW


def test_status_permissions(
community_service, minimal_community, users, location, es_clear, owner
):
"""Test that search does not return the 'status' field to any user."""
c_data = deepcopy(minimal_community)
c_data["slug"] = "test_status_perms"
co = community_service.create(data=c_data, identity=owner.identity)
community_service.record_cls.index.refresh()
assert co._record.status == CommunityStatusEnum.NEW

for uname, u in users.items():
search = community_service.search(u.identity)
assert search.total == 1
assert not any("status" in hit for hit in search.hits)
16 changes: 15 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from copy import deepcopy

import pytest
from flask_principal import AnonymousIdentity
from flask_principal import AnonymousIdentity, Need
from invenio_access.models import ActionRoles
from invenio_access.permissions import any_user as any_user_need
from invenio_access.permissions import superuser_access
Expand Down Expand Up @@ -251,6 +251,20 @@ def owner(users):
return users["owner"]


@pytest.fixture()
def unverified_user(UserFixture, app, db):
"""User meant to test 'verified' property of records."""
u = UserFixture(
email="unverified@inveniosoftware.org",
password="testuser",
)
u.create(app, db)
u.user.verified_at = None
# Dumping `is_verified` requires authenticated user in tests
u.identity.provides.add(Need(method="system_role", value="authenticated_user"))
return u


@pytest.fixture(scope="module")
def any_user(UserFixture, app, database):
"""A user without privileges or memberships."""
Expand Down

0 comments on commit 65da818

Please sign in to comment.