Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • arkindex/backend
1 result
Show changes
Commits on Source (8)
Showing with 233 additions and 125 deletions
......@@ -5,7 +5,7 @@ from datetime import datetime, timezone
from django.conf import settings
from django.db import connection, transaction
from django.db.models import CharField, Max, Prefetch, Q, QuerySet
from django.db.models import CharField, Count, Max, Prefetch, Q, QuerySet
from django.db.models.functions import Cast
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
......@@ -17,7 +17,6 @@ from rest_framework.generics import (
DestroyAPIView,
ListAPIView,
ListCreateAPIView,
RetrieveAPIView,
RetrieveUpdateDestroyAPIView,
UpdateAPIView,
)
......@@ -52,7 +51,7 @@ from arkindex.documents.serializers.elements import (
from arkindex.documents.serializers.light import CorpusAllowedMetaDataSerializer, ElementTypeLightSerializer
from arkindex.documents.serializers.ml import ElementTranscriptionSerializer
from arkindex.images.models import Zone
from arkindex.project.mixins import CorpusACLMixin, DeprecatedMixin, SelectionMixin
from arkindex.project.mixins import CorpusACLMixin, SelectionMixin
from arkindex.project.openapi import AutoSchema
from arkindex.project.pagination import PageNumberPagination
from arkindex.project.permissions import IsAuthenticated, IsVerified, IsVerifiedOrReadOnly
......@@ -204,6 +203,20 @@ class ElementsListMixin(object):
},
'x-methods': ['get'],
},
{
'name': 'with_zone',
'in': 'query',
'description': (
'Returns zone attribute for each element. '
'If not set, elements zone field will not be returned.'
),
'required': False,
'schema': {
'type': 'boolean',
'default': False
},
'x-methods': ['get'],
},
{
'name': 'If-Modified-Since',
# Name for APIStar because dashes in Python kwargs would not go well
......@@ -339,6 +352,12 @@ class ElementsListMixin(object):
# By default, use all best classifications
return best_classifications
def get_serializer_context(self):
context = super().get_serializer_context()
if self.request:
context['with_zone'] = self.request.query_params.get('with_zone', '').lower() not in ('', 'false', '0')
return context
def get_prefetch(self):
prefetch = {'zone__image__server'}
with_best_classes = self.clean_params.get('with_best_classes')
......@@ -419,23 +438,6 @@ class ElementsListMixin(object):
return Response(status=status.HTTP_204_NO_CONTENT)
class DeprecatedElementsList(DeprecatedMixin, RetrieveAPIView):
"""
This endpoint has moved to `/corpus/{corpus}/elements/` (`ListElements`).
It is no longer possible to list elements on all corpora at once.
"""
openapi_overrides = {
'operationId': 'DeprecatedListElements',
'security': [],
'tags': ['elements'],
}
deprecation_message = (
'This endpoint has moved to `/corpus/{corpus}/elements/` (`ListElements`). '
'It is no longer possible to list elements on all corpora at once.'
)
class CorpusElements(ElementsListMixin, CorpusACLMixin, DestroyModelMixin, ListAPIView):
"""
List elements in a corpus and filter by type, name, ML class
......@@ -600,11 +602,8 @@ class ElementRetrieve(RetrieveUpdateDestroyAPIView):
'type',
'zone__image__server',
) \
.prefetch_related(
'metadatas__revision__repo',
'metadatas__entity',
Prefetch('classifications', queryset=classifications_queryset)
)
.prefetch_related(Prefetch('classifications', queryset=classifications_queryset)) \
.annotate(metadata_count=Count('metadatas', filter=Q(metadatas__entity_id=None)))
def check_object_permissions(self, request, obj):
super().check_object_permissions(request, obj)
......@@ -932,8 +931,11 @@ class ElementsCreate(CreateAPIView):
}
class ElementMetadata(CreateAPIView):
class ElementMetadata(ListCreateAPIView):
"""
List all metadata linked to an element.
Create a metadata on an existing element.
The `date` type allows dates to be parsed and indexed for search APIs. The supported date formats are as follows:
......@@ -948,20 +950,26 @@ class ElementMetadata(CreateAPIView):
known identifiers or references, or any arbitrary string.
"""
permission_classes = (IsVerified, )
pagination_class = None
serializer_class = MetaDataUpdateSerializer
openapi_overrides = {
'operationId': 'CreateMetaData',
'tags': ['elements']
}
def get_queryset(self):
if self.request and self.request.method == 'GET':
element = get_object_or_404(Element, id=self.kwargs['pk'], corpus__in=Corpus.objects.readable(self.request.user))
return element.metadatas.filter(entity_id=None).prefetch_related('revision__repo')
# We need to use the default database to avoid stale read
# when a metadata is created immediately after an element creation
return Element.objects.using('default').filter(corpus__in=Corpus.objects.writable(self.request.user))
def get_serializer_context(self):
context = super().get_serializer_context()
if self.request: # Ignore this step when generating the schema with OpenAPI
if self.request and self.request.method == 'GET':
context['list'] = True
elif self.request and self.request.method != 'GET': # Ignore this step when generating the schema with OpenAPI
context['element'] = self.get_object()
return context
......
......@@ -54,6 +54,11 @@ class MetaDataUpdateSerializer(MetaDataLightSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# We don't want to return the entity field when listing metadata
if self.context.get('list'):
self.fields.pop('entity')
return
if self.context.get('element'):
corpus = self.context['element'].corpus
elif self.instance:
......@@ -176,6 +181,17 @@ class ElementListSerializer(ElementTinySerializer):
best_classes = ClassificationSerializer(many=True, default=None, read_only=True)
has_children = serializers.BooleanField(default=None, read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'with_zone' in self.context and not self.context['with_zone']:
self.fields.pop('zone')
def get_thumbnail_url(self, element):
# Do not return full element zone when with_zone query parameter is false, instead just return a thumbnail
if 'with_zone' in self.context and not self.context['with_zone'] and not element.type.folder and element.zone:
return element.zone.thumbnail_url
return super().get_thumbnail_url(element)
class Meta(ElementTinySerializer.Meta):
model = Element
fields = ElementTinySerializer.Meta.fields + ('best_classes', 'has_children', 'worker_version_id')
......@@ -313,10 +329,8 @@ class ElementSerializer(ElementSlimSerializer):
return instance
def get_metadata(self, obj):
return MetaDataLightSerializer(
list(obj.metadatas.all().filter(entity__isnull=True)),
many=True
).data
if hasattr(self.instance, 'metadata_count'):
return self.instance.metadata_count
class ElementNeighborsSerializer(serializers.Serializer):
......
......@@ -89,7 +89,6 @@ class TestChildrenElements(FixtureAPITestCase):
"name": nested_volume.name,
"type": nested_volume.type.slug,
"worker_version_id": None,
"zone": None,
"corpus": {
"id": str(self.corpus.id),
"name": self.corpus.name,
......@@ -110,7 +109,6 @@ class TestChildrenElements(FixtureAPITestCase):
"name": nested_volume.name,
"type": nested_volume.type.slug,
"worker_version_id": str(self.worker_version.id),
"zone": None,
"corpus": {
"id": str(self.corpus.id),
"name": self.corpus.name,
......@@ -122,6 +120,74 @@ class TestChildrenElements(FixtureAPITestCase):
}
])
def test_element_children_with_zone_true(self):
lonely_element = Element.objects.get(name='Volume 1, page 2r')
response = self.client.get(
reverse('api:elements-children', kwargs={'pk': str(self.vol.id)})
+ '?with_zone=true&type=page&name=2'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertListEqual(response.json()['results'], [
{
"id": str(lonely_element.id),
"name": lonely_element.name,
"type": lonely_element.type.slug,
"worker_version_id": None,
"zone": {
"id": str(lonely_element.zone.id),
"image" : {
"id": str(lonely_element.zone.image.id),
"height": lonely_element.zone.image.height,
"width": lonely_element.zone.image.width,
"path": lonely_element.zone.image.path,
"s3_url": None,
"server": {
"display_name": lonely_element.zone.image.server.display_name,
"max_height": None,
"max_width": None,
"url": lonely_element.zone.image.server.url,
},
"status": lonely_element.zone.image.status.value,
"url": lonely_element.zone.image.url
},
"polygon": [[0, 0], [0, 1000], [1000, 1000], [1000, 0], [0, 0]],
"url": lonely_element.zone.url,
},
"corpus": {
"id": str(self.corpus.id),
"name": self.corpus.name,
"public": self.corpus.public,
},
"best_classes": None,
"has_children": None,
"thumbnail_url": None,
}
])
def test_element_children_with_zone_false(self):
lonely_element = Element.objects.get(name='Volume 1, page 2r')
response = self.client.get(
reverse('api:elements-children', kwargs={'pk': str(self.vol.id)})
+ '?with_zone=false&type=page&name=2'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertListEqual(response.json()['results'], [
{
"id": str(lonely_element.id),
"name": lonely_element.name,
"type": lonely_element.type.slug,
"worker_version_id": None,
"corpus": {
"id": str(self.corpus.id),
"name": self.corpus.name,
"public": self.corpus.public,
},
"best_classes": None,
"has_children": None,
"thumbnail_url": lonely_element.zone.thumbnail_url,
}
])
def test_element_children_worker_version(self):
self.corpus.elements.filter(name__contains='page 1r').update(worker_version=self.worker_version)
......
......@@ -50,7 +50,7 @@ class TestCreateElements(FixtureAPITestCase):
# Create a Volume
self.client.force_login(self.user)
request = self.make_create_request('my new volume')
with self.assertNumQueries(7):
with self.assertNumQueries(6):
response = self.client.post(**request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json())
data = response.json()
......@@ -68,7 +68,7 @@ class TestCreateElements(FixtureAPITestCase):
'source': None,
'worker_version': None,
'classifications': [],
'metadata': [],
'metadata': None,
'corpus': {
'id': str(volume.corpus.id),
'name': volume.corpus.name,
......@@ -89,7 +89,7 @@ class TestCreateElements(FixtureAPITestCase):
name='The castle of my dreams',
image=str(self.image.id),
)
with self.assertNumQueries(16):
with self.assertNumQueries(15):
response = self.client.post(**request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
data = response.json()
......@@ -107,7 +107,7 @@ class TestCreateElements(FixtureAPITestCase):
'source': None,
'worker_version': None,
'classifications': [],
'metadata': [],
'metadata': None,
'corpus': {
'id': str(page.corpus.id),
'name': page.corpus.name,
......@@ -126,7 +126,7 @@ class TestCreateElements(FixtureAPITestCase):
name='Castle story',
elt_type='act'
)
with self.assertNumQueries(13):
with self.assertNumQueries(12):
response = self.client.post(**request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
act = Element.objects.get(id=response.json()['id'])
......@@ -141,7 +141,7 @@ class TestCreateElements(FixtureAPITestCase):
elt_type='act',
worker_version=str(self.worker_version.id),
)
with self.assertNumQueries(8):
with self.assertNumQueries(7):
response = self.client.post(**request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
act = Element.objects.get(id=response.json()['id'])
......@@ -161,7 +161,7 @@ class TestCreateElements(FixtureAPITestCase):
image=str(self.image.id),
polygon=polygon
)
with self.assertNumQueries(16):
with self.assertNumQueries(15):
response = self.client.post(**request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
data = response.json()
......@@ -179,7 +179,7 @@ class TestCreateElements(FixtureAPITestCase):
'source': None,
'worker_version': None,
'classifications': [],
'metadata': [],
'metadata': None,
'corpus': {
'id': str(page.corpus.id),
'name': page.corpus.name,
......@@ -226,7 +226,7 @@ class TestCreateElements(FixtureAPITestCase):
elt_type='volume',
name='something',
)
with self.assertNumQueries(7):
with self.assertNumQueries(6):
response = self.client.post(**request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
new_vol = new_corpus.elements.get()
......
......@@ -90,7 +90,7 @@ class TestMetaData(FixtureAPITestCase):
def test_create_metadata_methods(self):
self.client.force_login(self.user)
methods = (self.client.get, self.client.patch, self.client.put)
methods = (self.client.patch, self.client.put)
for method in methods:
response = method(reverse('api:element-metadata', kwargs={'pk': str(self.vol.id)}))
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
......
......@@ -82,7 +82,7 @@ class TestPatchElements(FixtureAPITestCase):
def test_patch_element(self):
self.client.force_login(self.user)
with self.assertNumQueries(9):
with self.assertNumQueries(7):
response = self.client.patch(
reverse('api:element-retrieve', kwargs={'pk': str(self.vol.id)}),
data={'name': 'Untitled (2)'},
......
......@@ -45,7 +45,7 @@ class TestRetrieveElements(FixtureAPITestCase):
'source': None,
'worker_version': None,
'zone': None,
'metadata': [],
'metadata': 0,
'classifications': [
{
'id': str(classification.id),
......@@ -128,11 +128,11 @@ class TestRetrieveElements(FixtureAPITestCase):
self.assertIsNone(response.json()['thumbnail_put_url'])
def test_list_element_metadata(self):
with self.assertNumQueries(6):
response = self.client.get(reverse('api:element-retrieve', kwargs={'pk': str(self.page.id)}))
self.client.force_login(self.user)
with self.assertNumQueries(5):
response = self.client.get(reverse('api:element-metadata', kwargs={'pk': str(self.page.id)}))
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.json()['metadata']
self.assertListEqual(results, [
self.assertListEqual(response.json(), [
{
'dates': [],
'id': str(self.folio_metadata.id),
......@@ -161,15 +161,30 @@ class TestRetrieveElements(FixtureAPITestCase):
)
self.metadata.entity = entity
self.metadata.save()
with self.assertNumQueries(6):
response = self.client.get(reverse('api:element-retrieve', kwargs={'pk': str(self.page.id)}))
self.client.force_login(self.user)
with self.assertNumQueries(5):
response = self.client.get(reverse('api:element-metadata', kwargs={'pk': str(self.page.id)}))
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.json()['metadata']
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], 'folio')
self.assertEqual(len(response.json()), 1)
self.assertListEqual(response.json(), [
{
'dates': [],
'id': str(self.folio_metadata.id),
'name': 'folio',
'revision': None,
'type': 'text',
'value': '1r',
},
])
def test_list_element_metadata_wrong_acl(self):
self.page.corpus = self.private_corpus
self.page.save()
response = self.client.get(reverse('api:element-retrieve', kwargs={'pk': str(self.page.id)}))
self.client.force_login(self.user)
response = self.client.get(reverse('api:element-metadata', kwargs={'pk': str(self.page.id)}))
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_list_element_metadata_wrong_element_id(self):
self.client.force_login(self.user)
response = self.client.get(reverse('api:element-metadata', kwargs={'pk': '12341234-1234-1234-1234-123412341234'}))
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
......@@ -399,3 +399,7 @@ class Zone(IndexableModel):
self.image.url + '/',
f'{x},{y},{width},{height}/full/0/default.jpg'
)
@property
def thumbnail_url(self):
return self.url.replace('full', ',400')
......@@ -39,7 +39,6 @@ from arkindex.documents.api.elements import (
CorpusElements,
CorpusList,
CorpusRetrieve,
DeprecatedElementsList,
ElementBulkCreate,
ElementChildren,
ElementMetadata,
......@@ -89,7 +88,7 @@ from arkindex.users.api import (
CredentialsRetrieve,
GroupDetails,
GroupMembershipsList,
GroupsList,
GroupsCreate,
JobList,
JobRetrieve,
MembershipDetails,
......@@ -113,7 +112,6 @@ schema_view = cache_page(86400, key_prefix=settings.VERSION)(get_schema_view(gen
api = [
# Elements
path('elements/', DeprecatedElementsList.as_view(), name='elements'),
path('elements/<uuid:pk>/neighbors/', ElementNeighbors.as_view(), name='elements-neighbors'),
path('elements/<uuid:pk>/parents/', ElementParents.as_view(), name='elements-parents'),
path('elements/<uuid:pk>/children/', ElementChildren.as_view(), name='elements-children'),
......@@ -253,7 +251,7 @@ api = [
path('user/transkribus/', UserTranskribus.as_view(), name='user-transkribus'),
# Rights management
path('groups/', GroupsList.as_view(), name='groups-list'),
path('groups/', GroupsCreate.as_view(), name='groups-create'),
path('group/<uuid:pk>/', GroupDetails.as_view(), name='group-details'),
path('group/<uuid:pk>/members/', GroupMembershipsList.as_view(), name='group-members'),
path('user/memberships/', UserMemberships.as_view(), name='user-memberships'),
......
......@@ -120,6 +120,25 @@ paths:
# Will need https://gitlab.com/arkindex/backend/-/issues/86 to be removed
operationId: DestroyElementMLResults
description: Delete machine learning results on an element and its direct children.
/api/v1/element/{id}/metadata/:
get:
operationId: ListElementMetaData
description: List all metadata linked to an element.
post:
operationId: CreateMetaData
description: >-
Create a metadata on an existing element.
The `date` type allows dates to be parsed and indexed for search APIs. The supported date formats are as follows:
* `YYYY`: `1995` (recommended)
* `YYYY-MM`: `1995-02` (recommended)
* `YYYY-MM-DD`: `1995-02-16` (recommended)
* `/YYYY(.*)month/`: `1995, february`, with months in French or English (case-insensitive)
* `YYYY-YYYY`: `1995-1996` (yields a date interval)
The `reference` type is also indexed in search APIs and allows searching by
known identifiers or references, or any arbitrary string.
/api/v1/elements/{id}/children/:
delete:
operationId: DestroyElementChildren
......@@ -447,10 +466,6 @@ paths:
security: []
tags:
- ponos
/api/v1/groups/:
post:
operationId: CreateGroup
description: Create a new group. The request user will be added as a member of this group.
/api/v1/group/{id}/:
patch:
description: >-
......
......@@ -452,25 +452,6 @@ class OAuthCallback(UserPassesTestMixin, RedirectView):
return super().get(request)
class GroupsList(ListCreateAPIView):
"""
List existing groups either if they are public or the request user is a member.
"""
serializer_class = GroupSerializer
permission_classes = (IsVerified, )
openapi_overrides = {
'security': [],
'tags': ['users'],
}
def get_queryset(self):
# List public groups and ones to which user belongs
return Group.objects \
.annotate(members_count=Count('memberships')) \
.filter(Q(public=True) | Q(memberships__user=self.request.user)) \
.order_by('name', 'id')
class JobList(ListAPIView):
"""
List asynchronous jobs linked to the current user.
......@@ -514,6 +495,17 @@ class JobRetrieve(RetrieveDestroyAPIView):
instance.delete()
class GroupsCreate(CreateAPIView):
"""
Create a new group. The request user will be added as a member of this group.
"""
serializer_class = GroupSerializer
permission_classes = (IsVerified, )
openapi_overrides = {
'tags': ['users'],
}
class GroupMembershipsList(ListCreateAPIView):
"""
List members of a group to which user belongs with their privileges.
......@@ -563,14 +555,21 @@ class GroupDetails(RetrieveUpdateDestroyAPIView):
'tags': ['users'],
}
def get_membership(self, group):
return get_object_or_404(group.memberships, user_id=self.request.user.id)
def get_object(self):
if not hasattr(self, '_group'):
self._group = super().get_object()
if not hasattr(self, '_member'):
self._member = self.get_membership(self._group)
# Add request member level to the group
self._group.level = self._member.level
return self._group
def check_object_permissions(self, request, obj):
if not hasattr(self, '_member'):
self._member = obj.memberships.get(user=self.request.user)
self._member = self.get_membership(obj)
# Check the user has the right to delete or update a group
super().check_object_permissions(request, obj)
if request.method not in SAFE_METHODS and self._member.level < Group.ADMIN_LEVEL:
......
......@@ -237,14 +237,15 @@ class JobSerializer(serializers.Serializer):
return instance.get_status(refresh=False)
class GroupSerializer(serializers.ModelSerializer):
class SimpleGroupSerializer(serializers.ModelSerializer):
"""
A light group serializer with properties and members count
A light group serializer with its members count
"""
members_count = serializers.IntegerField(default=None, read_only=True)
class Meta:
model = Group
read_only_fields = ('id', 'members_count')
fields = (
'id',
'name',
......@@ -252,13 +253,24 @@ class GroupSerializer(serializers.ModelSerializer):
'members_count'
)
class GroupSerializer(SimpleGroupSerializer):
"""
A group serializer with the request member level
"""
level = serializers.IntegerField(default=None, read_only=True)
class Meta(SimpleGroupSerializer.Meta):
fields = SimpleGroupSerializer.Meta.fields + ('level', )
@transaction.atomic
def create(self, validated_data):
group = super().create(validated_data)
# Associate the creator to the group
group.add_member(self.context['request'].user, 100)
# Set members_count field manually after creation
group.add_member(self.context['request'].user, Group.ADMIN_LEVEL)
# Manually set fields required by the serializer
group.members_count = 1
group.level = Group.ADMIN_LEVEL
return group
......@@ -291,7 +303,7 @@ class MemberGroupSerializer(MembershipSerializer):
"""
Serialize a group for a specific member with its privilege level
"""
group = GroupSerializer(source='content_object')
group = SimpleGroupSerializer(source='content_object')
class Meta(MembershipSerializer.Meta):
fields = MembershipSerializer.Meta.fields + ('group', )
......
......@@ -36,11 +36,11 @@ class TestMembership(FixtureAPITestCase):
"""
self.client.force_login(self.unverified)
with self.assertNumQueries(2):
response = self.client.post(reverse('api:groups-list'), {'name': 'new group'})
response = self.client.post(reverse('api:groups-create'), {'name': 'new group'})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.client.logout()
with self.assertNumQueries(0):
response = self.client.post(reverse('api:groups-list'), {'name': 'new group'})
response = self.client.post(reverse('api:groups-create'), {'name': 'new group'})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_create_group_wrong_fields(self):
......@@ -50,7 +50,7 @@ class TestMembership(FixtureAPITestCase):
)
self.client.force_login(self.user)
for payload, error in checks:
response = self.client.post(reverse('api:groups-list'), payload)
response = self.client.post(reverse('api:groups-create'), payload)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), error)
......@@ -60,7 +60,7 @@ class TestMembership(FixtureAPITestCase):
"""
self.client.force_login(self.user)
with self.assertNumQueries(6):
response = self.client.post(reverse('api:groups-list'), {'name': 'new group', 'public': True})
response = self.client.post(reverse('api:groups-create'), {'name': 'new group', 'public': True})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
group = Group.objects.get(name='new group')
......@@ -68,7 +68,8 @@ class TestMembership(FixtureAPITestCase):
'id': str(group.id),
'name': 'new group',
'public': True,
'members_count': 1
'members_count': 1,
'level': 100
})
self.assertCountEqual(
group.memberships.values_list('user', 'level'),
......@@ -81,36 +82,9 @@ class TestMembership(FixtureAPITestCase):
"""
self.client.force_login(self.user)
with self.assertNumQueries(6):
response = self.client.post(reverse('api:groups-list'), {'name': self.group.name})
response = self.client.post(reverse('api:groups-create'), {'name': self.group.name})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_list_groups(self):
"""
List groups which are public or for which the user is a member
"""
self.client.force_login(self.user)
with self.assertNumQueries(4):
response = self.client.get(reverse('api:groups-list'))
self.assertDictEqual(response.json(), {
'count': 2,
'number': 1,
'next': None,
'previous': None,
'results': [
{
'id': str(self.admin_group.id),
'members_count': 1,
'name': 'Admin group',
'public': True
}, {
'id': str(self.group.id),
'members_count': 3,
'name': 'User group',
'public': False
}
]
})
def test_list_members_requires_member_user(self):
"""
Only users that belong to a group have the ability to list the members
......@@ -174,7 +148,8 @@ class TestMembership(FixtureAPITestCase):
'id': str(self.group.id),
'members_count': 3,
'name': self.group.name,
'public': self.group.public
'public': self.group.public,
'level': Group.ADMIN_LEVEL
})
def test_update_group_no_admin(self):
......@@ -197,7 +172,8 @@ class TestMembership(FixtureAPITestCase):
'name': 'Renamed group',
'public': True,
'id': uuid.uuid4(),
'members_count': 42
'members_count': 42,
'level': 42
}
self.client.force_login(self.user)
with self.assertNumQueries(5):
......@@ -210,7 +186,8 @@ class TestMembership(FixtureAPITestCase):
'id': str(self.group.id),
'members_count': 3,
'name': 'Renamed group',
'public': True
'public': True,
'level': Group.ADMIN_LEVEL
})
def test_delete_group_no_admin(self):
......