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
14 changes: 9 additions & 5 deletions core/collections/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1475,11 +1475,15 @@ def _get_resolved_valueset_versions(self, valueset_version_cache=None):
versions.append(valueset_version_cache[cache_key])
return versions

def get_resolved_repo_versions_serialized(
self, system_version_cache=None, valueset_version_cache=None, expansion_cache=None):
"""Serialize repository versions resolved from this reference's system and valuesets."""
system_versions = self._get_resolved_system_versions(system_version_cache, expansion_cache)
valueset_versions = self._get_resolved_valueset_versions(valueset_version_cache)
def get_resolved_repo_versions_serialized( # pylint: disable=too-many-arguments
self, system_version_cache=None, valueset_version_cache=None, expansion_cache=None, user=None):
"""Serialize repository versions resolved from this reference's system and valuesets that `user` can view."""
system_versions = [
version for version in self._get_resolved_system_versions(system_version_cache, expansion_cache)
if version.has_view_access(user)]
valueset_versions = [
version for version in self._get_resolved_valueset_versions(valueset_version_cache)
if version.has_view_access(user)]
data = []
if system_versions:
from core.sources.serializers import SourceVersionListSerializer
Expand Down
3 changes: 2 additions & 1 deletion core/collections/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,8 @@ def get_resolved_repo_versions(self, obj):
if not self.include_resolved_repo_versions:
return None
return obj.get_resolved_repo_versions_serialized(
self.resolved_system_version_cache, self.resolved_valueset_version_cache, self.expansion_cache
self.resolved_system_version_cache, self.resolved_valueset_version_cache, self.expansion_cache,
get(self, 'context.request.user')
)


Expand Down
2 changes: 1 addition & 1 deletion core/collections/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ def get_serializer_class(self):

def get(self, request, *args, **kwargs):
reference = self.get_queryset()
data = reference.get_resolved_repo_versions_serialized()
data = reference.get_resolved_repo_versions_serialized(user=request.user)
return Response(data)


Expand Down
30 changes: 15 additions & 15 deletions core/common/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
LIST_DEFAULT_LIMIT, HTTP_COMPRESS_HEADER, CSV_DEFAULT_LIMIT, FACETS_ONLY, INCLUDE_RETIRED_PARAM, \
SEARCH_STATS_ONLY, INCLUDE_SEARCH_STATS, UPDATED_BY_USERNAME_PARAM, CHECKSUM_STANDARD_HEADER, \
CHECKSUM_SMART_HEADER, SEARCH_LATEST_REPO_VERSION, SAME_STANDARD_CHECKSUM_ERROR, ACCESS_TYPE_VIEW, \
INCLUDE_PARENT_CONCEPTS, INCLUDE_CHILD_CONCEPTS
INCLUDE_PARENT_CONCEPTS, INCLUDE_CHILD_CONCEPTS, INCLUDE_PARENT_CONCEPT_URLS, INCLUDE_CHILD_CONCEPT_URLS, \
INCLUDE_HIERARCHY_PATH, INCLUDE_SUMMARY
from core.common.permissions import HasPrivateAccess, HasOwnership, CanViewConceptDictionary, \
CanViewConceptDictionaryVersion, CanEditConceptDictionary
from .checksums import ChecksumModel
Expand All @@ -40,6 +41,9 @@

logger = logging.getLogger('oclapi')
TRUTHY = get_truthy_values()
PER_REQUESTER_LIST_PARAMS = (
INCLUDE_PARENT_CONCEPTS, INCLUDE_CHILD_CONCEPTS, INCLUDE_PARENT_CONCEPT_URLS, INCLUDE_CHILD_CONCEPT_URLS,
INCLUDE_HIERARCHY_PATH, INCLUDE_SUMMARY)
MAX_UNPAGINATED_RESULTS = 1000 # `Compress` lists; above this, users.list_unpaginated (ocl_online#230)


Expand Down Expand Up @@ -185,12 +189,11 @@ def __get_cached_data_if_any(self, request):

def __can_cache(self):
# facets/search-stats are header driven and change the response shape, but are not part of the cache key;
# included parent/child concepts are filtered per requester, so they can't be shared
# hierarchy concepts, URLs, paths and summary counts are filtered per requester, so they can't be shared
params = self.request.query_params
return (self.is_repo_version_children_request_without_any_search() and
not self.should_include_facets() and not self.should_include_search_stats() and
params.get(INCLUDE_PARENT_CONCEPTS) not in TRUTHY and
params.get(INCLUDE_CHILD_CONCEPTS) not in TRUTHY and
all(params.get(param) not in TRUTHY for param in PER_REQUESTER_LIST_PARAMS) and
get(self, 'parent_resource.is_latest_version', False))

def list(self, request, *args, **kwargs):
Expand Down Expand Up @@ -232,13 +235,9 @@ def _list(self, request): # pylint:disable=too-many-locals,too-many-branches,to
if not data:
search_string = query_params.get('type', None)
search_term = query_params.get('q', None)
if is_csv:
pattern = search_term
if pattern:
query_params._mutable = True # pylint: disable=protected-access
query_params['q'] = "*" + search_term + "*"

if is_csv and not search_string:
# q alone takes the search path only where it's an Elasticsearch search; other lists apply it themselves
is_csv_search = is_csv and bool(search_string or (search_term and get(self, 'is_searchable', False)))
if is_csv and not is_csv_search:
return self.get_csv(request)

if self.only_facets():
Expand All @@ -256,10 +255,11 @@ def _list(self, request): # pylint:disable=too-many-locals,too-many-branches,to
if self.object_list is None:
self.object_list = self.filter_queryset()

if is_csv and search_string:
klass = type(self.object_list[0])
queryset = klass.objects.filter(id__in=self.get_object_ids())
return self.get_csv(request, queryset)
if is_csv_search: # the search's hits; none gives an empty export where the document's model is known
ids = list(self.get_object_ids())
klass = get(self, 'document_model.Django.model') if not ids else None
klass = klass or type(self.object_list[0])
return self.get_csv(request, klass.objects.filter(id__in=ids))

# Skip pagination if compressed results are requested
compress = self.should_compress()
Expand Down
11 changes: 3 additions & 8 deletions core/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from django_elasticsearch_dsl.signals import RealTimeSignalProcessor
from elasticsearch import ApiError, TransportError
from elasticsearch.helpers import BulkIndexError, parallel_bulk, streaming_bulk
from pydash import get, compact
from pydash import get

from core.celery import app as celery_app
from core.common.tasks import update_collection_active_concepts_count, update_collection_active_mappings_count, \
Expand Down Expand Up @@ -1212,10 +1212,6 @@ def seed_references(self):
def should_auto_expand(self):
return True

@property
def identity_uris(self):
return compact([self.uri, self.canonical_url])

@classmethod
def persist_new(cls, obj, created_by, **kwargs):
errors = {}
Expand Down Expand Up @@ -1717,9 +1713,8 @@ def has_export(self):
def can_view_all_content(self, user):
if get(user, 'is_anonymous'):
return False
return get(
user, 'is_staff'
) or self.public_can_view or self.user_id == user.id or self.organization.members.filter(id=user.id).exists()
return bool(get(user, 'is_staff') or self.is_publicly_viewable or (user and (self.user_id == user.id or (
self.organization_id and self.organization.members.filter(id=user.id).exists()))))

@classmethod
def resolve_expression_to_version(cls, expression, namespace=None):
Expand Down
7 changes: 5 additions & 2 deletions core/common/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ def filter_queryset(self, queryset=None):
return _queryset

def filter_queryset_by_owner(self, queryset):
if self.user_is_self:
return queryset.filter(user=self.request.user)
if 'user' in self.kwargs:
return queryset.filter(user__username=self.kwargs['user'])
if 'org' in self.kwargs:
Expand Down Expand Up @@ -816,8 +818,9 @@ def __apply_common_search_filters(self, ignore_retired_filter=False, force=False
if ((self.is_concept_container_document_model() or self.is_repo_document_model()) and
self.request.query_params.get('allVersions', None) in TRUTHY):
default_filters.pop('version', None)
if self.is_user_document() and self.should_include_inactive():
default_filters.pop('is_active', None)
if self.is_user_document() and not self.should_include_inactive():
# excludes rather than requires, so a profile indexed without the field yet still shows
results = results.exclude('term', is_active=False)

updated_by = self.request.query_params.get(UPDATED_BY_USERNAME_PARAM, None)
if updated_by:
Expand Down
26 changes: 21 additions & 5 deletions core/concepts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pydash import get, compact, has

from core.common.checksums import ChecksumModel
from core.common.constants import ISO_639_1, LATEST, HEAD, ALL
from core.common.constants import ISO_639_1, LATEST, HEAD, ALL, ACCESS_TYPE_NONE
from core.common.db_functions import ImmutableUnaccent
from core.common.mixins import SourceChildMixin
from core.common.models import VersionedModel, ConceptContainerModel
Expand Down Expand Up @@ -1348,7 +1348,7 @@ def update_mappings(self):
# Updates mappings where mapping.to_concept or mapping.from_concepts matches concept's mnemonic and parent
# only mappings whose last saver can view this concept's repo; others keep the external code, as on save
from core.mappings.models import Mapping
parent_uris = self.parent.identity_uris
parent_uris = self.parent.get_mapping_link_uris()
can_view = self.parent.get_view_access_criteria('updated_by')

Mapping.objects.filter(
Expand Down Expand Up @@ -1382,6 +1382,20 @@ def index_mapped_codes(cls, queryset):
from core.concepts.documents import ConceptDocument
return cls.batch_index_partial_by_ids(queryset, ConceptDocument, cls.get_mapped_codes_actions, refresh=False)

@staticmethod
def get_viewable(queryset, user):
"""The concepts of `queryset` in repos `user` (None or anonymous for the public) can view."""
if get(user, 'is_staff'):
return queryset
if not get(user, 'is_authenticated'):
return queryset.exclude(public_access=ACCESS_TYPE_NONE)
return Concept.apply_user_criteria(queryset, user)

def get_viewable_hierarchy_concept_urls(self, relation, user):
"""parent_concept_urls or child_concept_urls (`relation`), of concepts in repos `user` can view."""
return list(self.get_viewable(
self.get_hierarchy_related_queryset(relation), user).values_list('uri', flat=True))

@property
def parent_concept_urls(self):
return self.get_hierarchy_concept_urls('parent_concepts')
Expand Down Expand Up @@ -1456,12 +1470,14 @@ def parent_concepts_count(self):
def parent_concept_queryset(self):
return self.get_hierarchy_related_queryset('parent_concepts')

def get_hierarchy_path(self):
def get_hierarchy_path(self, user=None):
"""The URLs from the top of the hierarchy down to this concept's parent, through parents in repos `user`
(None for the public) can view."""
result = []
parent_concept = self.parent_concepts.first()
parent_concept = self.get_viewable(self.parent_concepts.all(), user).first()
while parent_concept is not None:
result.append(drop_version(parent_concept.uri))
parent_concept = parent_concept.parent_concepts.first()
parent_concept = self.get_viewable(parent_concept.parent_concepts.all(), user).first()

result.reverse()
return result
Expand Down
75 changes: 47 additions & 28 deletions core/concepts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,21 @@ def get_collection_uri_from_view(view, request):

def get_viewable_concepts(queryset, request):
"""Concepts from repos the requester can view."""
user = get(request, 'user')
if get(user, 'is_staff'):
return queryset
if not get(user, 'is_authenticated'):
return queryset.exclude(public_access=ACCESS_TYPE_NONE)
return Concept.apply_user_criteria(queryset, user)
return Concept.get_viewable(queryset, get(request, 'user'))


class ViewableHierarchyUrlsField(ListField):
"""
A concept's parent or child concept URLs (`relation`), of concepts in repos the requester can view: the context's
`user`, else its request's user.
"""
def __init__(self, relation, **kwargs):
self.relation = relation
super().__init__(**kwargs)

def get_attribute(self, instance):
user = self.context['user'] if 'user' in self.context else get(self.context, 'request.user')
return instance.get_viewable_hierarchy_concept_urls(self.relation, user)


class LocalizedNameSerializer(ModelSerializer):
Expand Down Expand Up @@ -153,8 +162,9 @@ class ConceptAbstractSerializer(SourceVersionFieldMixin, AbstractResourceSeriali
parent_concepts = SerializerMethodField()
child_concepts = SerializerMethodField()
hierarchy_path = SerializerMethodField()
parent_concept_urls = ListField(allow_null=True, required=False, allow_empty=True)
child_concept_urls = ListField(read_only=True)
parent_concept_urls = ViewableHierarchyUrlsField(
'parent_concepts', allow_null=True, required=False, allow_empty=True)
child_concept_urls = ViewableHierarchyUrlsField('child_concepts', read_only=True)
summary = SerializerMethodField()
references = SerializerMethodField()
checksums = SerializerMethodField()
Expand Down Expand Up @@ -325,12 +335,12 @@ def get_parent_concepts(self, obj):

def get_hierarchy_path(self, obj):
if self.include_hierarchy_path:
return obj.get_hierarchy_path()
return obj.get_hierarchy_path(get(self, 'context.request.user'))
return None

def get_summary(self, obj):
if self.include_summary:
return ConceptSummarySerializer(obj).data
return ConceptSummarySerializer(obj, context=self.context).data
return None


Expand Down Expand Up @@ -430,13 +440,20 @@ class ConceptSummarySerializer(ModelSerializer):
names = SerializerMethodField()
descriptions = SerializerMethodField()
versions = SerializerMethodField()
parents = IntegerField(source='parent_concepts_count', read_only=True)
children = IntegerField(source='children_concepts_count', read_only=True)
parents = SerializerMethodField()
children = SerializerMethodField()

class Meta:
model = Concept
fields = ('children', 'parents', 'names', 'descriptions', 'versions', 'id', 'uuid', 'versioned_object_id')

def get_parents(self, obj):
"""Only parents in repos the requester can view, as the lists show."""
return Concept.get_viewable(obj.parent_concept_queryset(), get(self, 'context.request.user')).count()

def get_children(self, obj):
return Concept.get_viewable(obj.child_concept_queryset(), get(self, 'context.request.user')).count()

@staticmethod
def get_names(obj):
return obj.names.count()
Expand Down Expand Up @@ -613,20 +630,22 @@ def get_previous_version_url(self, obj):
return obj.prev_version_uri

@staticmethod
def get_parent_concept_urls(obj):
uris = set(c.uri for c in obj.parent_concepts.all())
def get_shared_hierarchy_urls(obj, relation):
"""An export is served to anyone who can view its repo version, so it links only concepts of the same source
and public ones. Reads the export's prefetched relations."""
concepts = list(getattr(obj, relation).all())
versioned = getattr(obj, 'versioned_object', None)
if versioned:
uris.update(c.uri for c in versioned.parent_concepts.all())
return list(uris)
concepts += getattr(versioned, relation).all()
return list({
concept.uri for concept in concepts
if concept.parent_id == obj.parent_id or concept.public_access != ACCESS_TYPE_NONE})

@staticmethod
def get_child_concept_urls(obj):
uris = set(c.uri for c in obj.child_concepts.all())
versioned = getattr(obj, 'versioned_object', None)
if versioned:
uris.update(c.uri for c in versioned.child_concepts.all())
return list(uris)
def get_parent_concept_urls(self, obj):
return self.get_shared_hierarchy_urls(obj, 'parent_concepts')

def get_child_concept_urls(self, obj):
return self.get_shared_hierarchy_urls(obj, 'child_concepts')


class ConceptVersionDetailSerializer(CollectionVersionsMixin, SourceVersionFieldMixin, ModelSerializer):
Expand All @@ -651,8 +670,8 @@ class ConceptVersionDetailSerializer(CollectionVersionsMixin, SourceVersionField
update_comment = CharField(source='comment', required=False, allow_null=True, allow_blank=True)
parent_concepts = SerializerMethodField()
child_concepts = SerializerMethodField()
parent_concept_urls = ListField(read_only=True)
child_concept_urls = ListField(read_only=True)
parent_concept_urls = ViewableHierarchyUrlsField('parent_concepts', read_only=True)
child_concept_urls = ViewableHierarchyUrlsField('child_concepts', read_only=True)
source_versions = ListField(read_only=True)
collection_versions = SerializerMethodField()
references = SerializerMethodField()
Expand Down Expand Up @@ -751,7 +770,7 @@ class ConceptHierarchySerializer(ModelSerializer):
uuid = CharField(source='id')
id = EncodedDecodedCharField(source='mnemonic')
url = CharField(source='uri')
children = ListField(source='child_concept_urls')
children = ViewableHierarchyUrlsField('child_concepts')
name = CharField(source='display_name')

class Meta:
Expand Down Expand Up @@ -785,7 +804,7 @@ def __init__(self, *args, **kwargs):

def get_children(self, obj):
if self.include_child_concepts:
return obj.child_concept_urls
return obj.get_viewable_hierarchy_concept_urls('child_concepts', get(self, 'context.request.user'))
return None


Expand Down Expand Up @@ -816,5 +835,5 @@ def __init__(self, *args, **kwargs):

def get_parents(self, obj):
if self.include_parent_concepts:
return obj.parent_concept_urls
return obj.get_viewable_hierarchy_concept_urls('parent_concepts', get(self, 'context.request.user'))
return None
Loading
Loading