Skip to content

Commit

Permalink
Merge pull request #1640 from UUDigitalHumanitieslab/feature/expand-c…
Browse files Browse the repository at this point in the history
…orpus-edit-api

Edit corpus documention in API
  • Loading branch information
lukavdplas authored Aug 9, 2024
2 parents f025685 + bc2b63a commit 7655673
Show file tree
Hide file tree
Showing 24 changed files with 250 additions and 146 deletions.
7 changes: 7 additions & 0 deletions backend/addcorpus/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,13 @@ class PageType(models.TextChoices):
help_text='markdown contents of the documentation'
)

@property
def page_index(self):
'''Numerical index to determine the order in which pages should be displayed.
Based on the order in which `PageType` choices are declared.'''
indexed_values = enumerate(__class__.PageType.values)
return next((i for (i, value) in indexed_values if value == self.type), None)

def __str__(self):
return f'{self.corpus_configuration.corpus.name} - {self.type}'

Expand Down
45 changes: 27 additions & 18 deletions backend/addcorpus/permissions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from rest_framework import permissions
from rest_framework.exceptions import NotFound
from users.models import CustomUser
from typing import List
from rest_framework.request import Request
from addcorpus.models import Corpus

def corpus_name_from_request(request):
Expand All @@ -25,20 +24,7 @@ def corpus_name_from_request(request):
return corpus


def filter_user_corpora(corpora: List[Corpus], user: CustomUser) -> List[Corpus]:
'''
Filter all available corpora to only
include the ones the user has access to
'''

return [
corpus
for corpus in corpora
if user.has_access(corpus.name)
]


class CorpusAccessPermission(permissions.BasePermission):
class CanSearchCorpus(permissions.BasePermission):
message = 'You do not have permission to access this corpus'

def has_permission(self, request, view):
Expand All @@ -48,9 +34,32 @@ def has_permission(self, request, view):
# check if the corpus exists
try:
corpus = Corpus.objects.get(name=corpus_name)
assert corpus.active
except:
raise NotFound('Corpus does not exist')

# check if the user has access
return user.has_access(corpus)
return user.can_search(corpus)


class IsCurator(permissions.BasePermission):
'''
The user is permitted to use the corpus definition API.
'''

message = 'You do not have permission to manage corpus definitions'

def has_permission(self, request: Request, view):
return request.user.is_staff

class IsCuratorOrReadOnly(permissions.BasePermission):
'''
The user is permitted to edit the corpus, or it is a read-only request.
'''

message = 'You do not have permission to edit this corpus'

def has_permission(self, request: Request, view):
if request.method in permissions.SAFE_METHODS:
return True

return request.user.is_staff
21 changes: 19 additions & 2 deletions backend/addcorpus/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ def to_representation(self, value):
key = super().to_representation(value)
return self.choices[key]

def to_internal_value(self, data):
# If the data provides a display name, get the corresponding key.
# The browsable API sends keys instead of labels; use the original data if no
# matching label is found.
value = next(
(key for (key, label) in self.choices.items() if label == data),
data
)
return super().to_internal_value(value)

class CorpusConfigurationSerializer(serializers.ModelSerializer):
fields = FieldSerializer(many=True, read_only=True)
languages = serializers.ListField(child=LanguageField())
Expand Down Expand Up @@ -123,11 +133,18 @@ def to_representation(self, value):

class CorpusDocumentationPageSerializer(serializers.ModelSerializer):
type = PrettyChoiceField(choices = CorpusDocumentationPage.PageType.choices)
content = DocumentationTemplateField()
index = serializers.IntegerField(source='page_index', read_only=True)
content = DocumentationTemplateField(read_only=True)
content_template = serializers.CharField(source='content')
corpus = serializers.SlugRelatedField(
source='corpus_configuration',
queryset=CorpusConfiguration.objects.all(),
slug_field='corpus__name',
)

class Meta:
model = CorpusDocumentationPage
fields = ['corpus_configuration', 'type', 'content']
fields = ['id', 'corpus', 'type', 'content', 'content_template', 'index']


class JSONDefinitionField(serializers.Field):
Expand Down
21 changes: 15 additions & 6 deletions backend/addcorpus/tests/test_corpus_access.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
from users.models import CustomUser, CustomAnonymousUser
from addcorpus.models import Corpus

def test_access_through_group(db, basic_mock_corpus, group_with_access):
user = CustomUser.objects.create(username='nice-user', password='secret')
user.groups.add(group_with_access)
user.save()
assert user.has_access(basic_mock_corpus)
corpus = Corpus.objects.get(name=basic_mock_corpus)
assert user.can_search(corpus)
assert corpus in user.searchable_corpora()

def test_superuser_access(basic_mock_corpus, admin_user):
assert admin_user.has_access(basic_mock_corpus)
corpus = Corpus.objects.get(name=basic_mock_corpus)
assert admin_user.can_search(corpus)
assert corpus in admin_user.searchable_corpora()

def test_no_corpus_access(db, basic_mock_corpus):
user = CustomUser.objects.create(username='bad-user', password='secret')
assert not user.has_access(basic_mock_corpus)

corpus = Corpus.objects.get(name=basic_mock_corpus)
assert not user.can_search(corpus)
assert corpus not in user.searchable_corpora()

def test_public_corpus_access(db, basic_corpus_public):
user = CustomUser.objects.create(username='new-user', password='secret')
assert user.has_access(basic_corpus_public)
corpus = Corpus.objects.get(name=basic_corpus_public)
assert user.can_search(corpus)
assert corpus in user.searchable_corpora()
anon = CustomAnonymousUser()
assert anon.has_access(basic_corpus_public)
assert anon.can_search(corpus)
assert corpus in anon.searchable_corpora()

def test_api_access(db, basic_mock_corpus, group_with_access, auth_client, auth_user):
# default: no access
Expand Down
59 changes: 47 additions & 12 deletions backend/addcorpus/tests/test_corpus_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Dict

from users.models import CustomUser
from addcorpus.models import Corpus
from addcorpus.models import Corpus, CorpusDocumentationPage
from addcorpus.python_corpora.save_corpus import load_and_save_all_corpora

def test_no_corpora(db, settings, admin_client):
Expand All @@ -16,23 +16,58 @@ def test_no_corpora(db, settings, admin_client):
assert status.is_success(response.status_code)
assert response.data == []

def test_corpus_documentation_view(admin_client, basic_mock_corpus, settings):
response = admin_client.get(f'/api/corpus/documentation/{basic_mock_corpus}/')
def test_corpus_documentation_list_view(admin_client, basic_mock_corpus, settings):
response = admin_client.get(f'/api/corpus/documentation/')
assert response.status_code == 200
pages = response.data

# check that the pages are sorted in canonical order
page_types = [page['type'] for page in pages]
# check that the pages specify canonical order
sorted_and_filtered = sorted(
(page for page in pages if page['corpus'] == basic_mock_corpus),
key=lambda page: page['index']
)
page_types = [page['type'] for page in sorted_and_filtered]
assert page_types == ['General information', 'Citation', 'License']

# should contain citation guidelines
citation_page = next(page for page in pages if page['type'] == 'Citation')
match = { 'type': 'Citation', 'corpus': basic_mock_corpus }
citation_page = next(
page for page in response.data if match.items() <= page.items()
)

# check that the page template is rendered with context
content = citation_page['content']
assert '{{ frontend_url }}' not in content
assert settings.BASE_URL in content

def test_corpus_documentation_filter_list_view(admin_client, basic_mock_corpus):
response = admin_client.get(f'/api/corpus/documentation/?corpus={basic_mock_corpus}')
assert status.is_success(response.status_code)
pages = response.data
for page in pages:
assert page['corpus'] == basic_mock_corpus


def test_corpus_documentation_retrieve_view(admin_client: Client, basic_mock_corpus):
page = CorpusDocumentationPage.objects.first()
response = admin_client.get(f'/api/corpus/documentation/{page.pk}/')
assert status.is_success(response.status_code)


def test_corpus_documentation_create_view(admin_client, basic_mock_corpus):
request_data = {
'corpus': basic_mock_corpus,
'type': 'Terms of service',
'content_template': 'You can do whatever you want.'
}
response = admin_client.post(
'/api/corpus/documentation/',
request_data,
content_type='application/json'
)
assert status.is_success(response.status_code)


def test_corpus_image_view(admin_client, basic_mock_corpus):
corpus = Corpus.objects.get(name=basic_mock_corpus)
assert not corpus.configuration.image
Expand All @@ -47,26 +82,26 @@ def test_corpus_image_view(admin_client, basic_mock_corpus):
assert response.status_code == 200

def test_nonexistent_corpus(admin_client):
response = admin_client.get(f'/api/corpus/documentation/unknown-corpus/')
response = admin_client.get(f'/api/corpus/image/unknown-corpus')
assert response.status_code == 404

def test_no_corpus_access(db, client, basic_mock_corpus):
'''Test a request from a user that should not have access to the corpus'''

user = CustomUser.objects.create(username='bad-user', password='secret')
client.force_login(user)
response = client.get(f'/api/corpus/documentation/{basic_mock_corpus}/')
response = client.get(f'/api/corpus/image/{basic_mock_corpus}')
assert response.status_code == 403


def test_corpus_documentation_unauthenticated(db, client, basic_mock_corpus):
def test_private_corpus_image_unauthenticated(db, client, basic_mock_corpus):
response = client.get(
f'/api/corpus/documentation/{basic_mock_corpus}/')
f'/api/corpus/image/{basic_mock_corpus}')
assert response.status_code == 401

def test_public_corpus_documentation_unauthenticated(db, client, basic_corpus_public):
def test_public_corpus_image_unauthenticated(db, client, basic_corpus_public):
response = client.get(
f'/api/corpus/documentation/{basic_corpus_public}/')
f'/api/corpus/image/{basic_corpus_public}')
assert response.status_code == 200

def test_corpus_serialization(admin_client, basic_mock_corpus):
Expand Down
11 changes: 10 additions & 1 deletion backend/addcorpus/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from addcorpus.models import Corpus
from addcorpus.models import Corpus, CorpusDocumentationPage

def test_corpus_model(db):
corpus = Corpus(name = 'test_corpus')
Expand All @@ -9,3 +9,12 @@ def test_corpus_model(db):
corpus.delete()

assert not Corpus.objects.filter(name = corpus)

def test_corpus_documentation_page_model(db, basic_mock_corpus):
corpus = Corpus.objects.get(name=basic_mock_corpus)
page = CorpusDocumentationPage(
corpus_configuration=corpus.configuration,
type=CorpusDocumentationPage.PageType.LICENSE,
content='Do whatever you want.',
)
assert page.page_index == 2
3 changes: 1 addition & 2 deletions backend/addcorpus/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from addcorpus.views import CorpusImageView, CorpusView, CorpusDocumentationPageViewset, CorpusDocumentView

urlpatterns = [
path('', CorpusView.as_view()),
path('', CorpusView.as_view({'get': 'list'})),
path('image/<str:corpus>', CorpusImageView.as_view()),
path('documentation/<str:corpus>/', CorpusDocumentationPageViewset.as_view({'get': 'list'})),
path('document/<str:corpus>/<str:filename>', CorpusDocumentView.as_view()),
]
Loading

0 comments on commit 7655673

Please sign in to comment.