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 (4)
Showing
with 75 additions and 42 deletions
...@@ -440,7 +440,7 @@ class ManageClassificationsSelection(SelectionMixin, CorpusACLMixin, CreateAPIVi ...@@ -440,7 +440,7 @@ class ManageClassificationsSelection(SelectionMixin, CorpusACLMixin, CreateAPIVi
mode = serializer.validated_data["mode"] mode = serializer.validated_data["mode"]
if mode == ClassificationMode.Create: if mode == ClassificationMode.Create:
return self.create(corpus, request, *args, **kwargs) return self.create(corpus, request, *args, **kwargs)
elif mode == ClassificationMode.Validate: if mode == ClassificationMode.Validate:
elements = self.get_selection(corpus.id) elements = self.get_selection(corpus.id)
Classification.objects.filter( Classification.objects.filter(
element__in=elements, element__in=elements,
......
...@@ -45,7 +45,7 @@ class InterpretedDate(object): ...@@ -45,7 +45,7 @@ class InterpretedDate(object):
""" """
if self.month and self.day: if self.month and self.day:
return DatePrecision.Day return DatePrecision.Day
elif self.month: if self.month:
return DatePrecision.Month return DatePrecision.Month
return DatePrecision.Year return DatePrecision.Year
......
...@@ -667,8 +667,7 @@ class ElementSerializer(ElementTinySerializer): ...@@ -667,8 +667,7 @@ class ElementSerializer(ElementTinySerializer):
validated_data.update(image=image, polygon=polygon) validated_data.update(image=image, polygon=polygon)
instance = super().update(instance, validated_data) return super().update(instance, validated_data)
return instance
class ElementNeighborsSerializer(serializers.ModelSerializer): class ElementNeighborsSerializer(serializers.ModelSerializer):
......
...@@ -32,8 +32,7 @@ class EntityTypeSerializer(serializers.ModelSerializer): ...@@ -32,8 +32,7 @@ class EntityTypeSerializer(serializers.ModelSerializer):
corpus = data.get("corpus") corpus = data.get("corpus")
if self.instance and corpus: if self.instance and corpus:
raise ValidationError({"corpus": ["It is not possible to update an Entity Type's corpus."]}) raise ValidationError({"corpus": ["It is not possible to update an Entity Type's corpus."]})
data = super().to_internal_value(data) return super().to_internal_value(data)
return data
def validate(self, data): def validate(self, data):
name = data.get("name") if "name" in data else self.instance.name name = data.get("name") if "name" in data else self.instance.name
......
...@@ -21,7 +21,7 @@ from arkindex.documents.models import ( ...@@ -21,7 +21,7 @@ from arkindex.documents.models import (
Transcription, Transcription,
TranscriptionEntity, TranscriptionEntity,
) )
from arkindex.ponos.models import Task from arkindex.ponos.models import Artifact, Task
from arkindex.process.models import Process, ProcessDatasetSet, ProcessElement, WorkerActivity, WorkerRun from arkindex.process.models import Process, ProcessDatasetSet, ProcessElement, WorkerActivity, WorkerRun
from arkindex.training.models import DatasetElement, DatasetSet from arkindex.training.models import DatasetElement, DatasetSet
from arkindex.users.models import User from arkindex.users.models import User
...@@ -41,6 +41,10 @@ def corpus_delete(corpus_id: str) -> None: ...@@ -41,6 +41,10 @@ def corpus_delete(corpus_id: str) -> None:
corpus.top_level_type_id = None corpus.top_level_type_id = None
corpus.save(update_fields=["top_level_type_id"]) corpus.save(update_fields=["top_level_type_id"])
# Set process.element to NULL in all processes, to avoid IntegrityErrors when deleting elements, as
# elements are deleted before processes.
Process.objects.filter(corpus_id=corpus_id).exclude(element_id=None).update(element_id=None)
# Delete all related objects, bypassing RESTRICTs deletion rules # Delete all related objects, bypassing RESTRICTs deletion rules
# and Django's way of loading everything into memory before deleting. # and Django's way of loading everything into memory before deleting.
querysets = [ querysets = [
...@@ -67,6 +71,8 @@ def corpus_delete(corpus_id: str) -> None: ...@@ -67,6 +71,8 @@ def corpus_delete(corpus_id: str) -> None:
Selection.objects.filter(element__corpus_id=corpus_id), Selection.objects.filter(element__corpus_id=corpus_id),
corpus.memberships.all(), corpus.memberships.all(),
corpus.exports.all(), corpus.exports.all(),
# Delete task artifacts
Artifact.objects.filter(task__process__corpus_id=corpus_id),
# ProcessDatasetSet M2M # ProcessDatasetSet M2M
ProcessDatasetSet.objects.filter(set__dataset__corpus_id=corpus_id), ProcessDatasetSet.objects.filter(set__dataset__corpus_id=corpus_id),
ProcessDatasetSet.objects.filter(process__corpus_id=corpus_id), ProcessDatasetSet.objects.filter(process__corpus_id=corpus_id),
......
...@@ -363,7 +363,7 @@ class TestEditElementPath(FixtureTestCase): ...@@ -363,7 +363,7 @@ class TestEditElementPath(FixtureTestCase):
path_id = elements["B"].paths.get().id path_id = elements["B"].paths.get().id
if path1.id == path_id: if path1.id == path_id:
return str(path1.path[0]) return str(path1.path[0])
elif path2.id == path_id: if path2.id == path_id:
return str(path2.path[0]) return str(path2.path[0])
raise AssertionError("Unexpected top-level path ID") raise AssertionError("Unexpected top-level path ID")
......
...@@ -47,7 +47,7 @@ class ImageServerManager(models.Manager): ...@@ -47,7 +47,7 @@ class ImageServerManager(models.Manager):
f'The URL "{url}" does not match any existing image server. ' f'The URL "{url}" does not match any existing image server. '
"Please ask an instance administrator to register the IIIF server for this image." "Please ask an instance administrator to register the IIIF server for this image."
) )
elif servers_count > 1: if servers_count > 1:
raise self.model.MultipleObjectsReturned( raise self.model.MultipleObjectsReturned(
f'The URL "{url}" matched multiple existing image servers' f'The URL "{url}" matched multiple existing image servers'
) )
......
...@@ -108,7 +108,7 @@ class ImageServer(models.Model): ...@@ -108,7 +108,7 @@ class ImageServer(models.Model):
src_url, dest_url = map(urllib.parse.urlsplit, (self.url, other.url)) src_url, dest_url = map(urllib.parse.urlsplit, (self.url, other.url))
if src_url.scheme != dest_url.scheme: if src_url.scheme != dest_url.scheme:
raise ValueError("Cannot merge into an image server of a different protocol") raise ValueError("Cannot merge into an image server of a different protocol")
elif src_url.netloc != dest_url.netloc: if src_url.netloc != dest_url.netloc:
raise ValueError("Cannot merge into an image server on a different domain") raise ValueError("Cannot merge into an image server on a different domain")
# Check paths # Check paths
......
...@@ -68,7 +68,7 @@ class ImageSerializer(serializers.ModelSerializer): ...@@ -68,7 +68,7 @@ class ImageSerializer(serializers.ModelSerializer):
if not self.instance: if not self.instance:
# Force the Unchecked status when creating a new image # Force the Unchecked status when creating a new image
return S3FileStatus.Unchecked return S3FileStatus.Unchecked
elif value == S3FileStatus.Checked: if value == S3FileStatus.Checked:
# Perform image validation if we are updating an existing image to Checked # Perform image validation if we are updating an existing image to Checked
try: try:
if self.instance.server.s3_bucket and not self.instance.server.s3_read_only_bucket: if self.instance.server.s3_bucket and not self.instance.server.s3_read_only_bucket:
......
...@@ -402,8 +402,9 @@ class ProcessDetails(ProcessACLMixin, ProcessQuerysetMixin, RetrieveUpdateDestro ...@@ -402,8 +402,9 @@ class ProcessDetails(ProcessACLMixin, ProcessQuerysetMixin, RetrieveUpdateDestro
class ProcessRetry(ProcessACLMixin, ProcessQuerysetMixin, GenericAPIView): class ProcessRetry(ProcessACLMixin, ProcessQuerysetMixin, GenericAPIView):
""" r"""
Retry a process. Can only be used on processes with Error, Failed, Stopped or Completed states.\n\n Retry a process. Can only be used on processes with Error, Failed, Stopped or Completed states.
Requires an **admin** access to the process and **guest** access to the process' farm. Requires an **admin** access to the process and **guest** access to the process' farm.
""" """
permission_classes = (IsVerified, ) permission_classes = (IsVerified, )
...@@ -427,9 +428,9 @@ class ProcessRetry(ProcessACLMixin, ProcessQuerysetMixin, GenericAPIView): ...@@ -427,9 +428,9 @@ class ProcessRetry(ProcessACLMixin, ProcessQuerysetMixin, GenericAPIView):
# Allow 'retrying' a process that has no Ponos tasks (that has never been started) # Allow 'retrying' a process that has no Ponos tasks (that has never been started)
if len(process.tasks.all()) and state in (State.Unscheduled, State.Pending): if len(process.tasks.all()) and state in (State.Unscheduled, State.Pending):
raise ValidationError({"__all__": ["This process is already pending"]}) raise ValidationError({"__all__": ["This process is already pending"]})
elif state == State.Running: if state == State.Running:
raise ValidationError({"__all__": ["This process is already running"]}) raise ValidationError({"__all__": ["This process is already running"]})
elif state == State.Stopping: if state == State.Stopping:
raise ValidationError({"__all__": ["This process is stopping"]}) raise ValidationError({"__all__": ["This process is stopping"]})
@extend_schema( @extend_schema(
...@@ -476,8 +477,9 @@ class FilesProcess(CreateAPIView): ...@@ -476,8 +477,9 @@ class FilesProcess(CreateAPIView):
) )
) )
class CorpusProcess(SelectionMixin, CorpusACLMixin, CreateAPIView): class CorpusProcess(SelectionMixin, CorpusACLMixin, CreateAPIView):
""" r"""
Create a distributed process from elements of an Arkindex corpus.\n\n Create a distributed process from elements of an Arkindex corpus.
Requires an **admin** access to the corpus. Requires an **admin** access to the corpus.
""" """
permission_classes = (IsVerified, ) permission_classes = (IsVerified, )
...@@ -542,8 +544,9 @@ class CorpusProcess(SelectionMixin, CorpusACLMixin, CreateAPIView): ...@@ -542,8 +544,9 @@ class CorpusProcess(SelectionMixin, CorpusACLMixin, CreateAPIView):
), ),
) )
class StartProcess(CorpusACLMixin, CreateAPIView): class StartProcess(CorpusACLMixin, CreateAPIView):
""" r"""
Start a process, used to build a Workflow with Workers.\n\n Start a process, used to build a Workflow with Workers.
Requires an **admin** access to the corpus of this process. Requires an **admin** access to the corpus of this process.
""" """
permission_classes = (IsVerified, ) permission_classes = (IsVerified, )
...@@ -1555,8 +1558,9 @@ class WorkerRunDetails(ProcessACLMixin, RetrieveUpdateDestroyAPIView): ...@@ -1555,8 +1558,9 @@ class WorkerRunDetails(ProcessACLMixin, RetrieveUpdateDestroyAPIView):
tags=["process"] tags=["process"]
)) ))
class ListProcessElements(CorpusACLMixin, ListAPIView): class ListProcessElements(CorpusACLMixin, ListAPIView):
""" r"""
List all elements for a process with workers.\n\n List all elements for a process with workers.
Requires an **admin** access to the process corpus. Requires an **admin** access to the process corpus.
""" """
pagination_class = CountCursorPagination pagination_class = CountCursorPagination
...@@ -1579,8 +1583,7 @@ class ListProcessElements(CorpusACLMixin, ListAPIView): ...@@ -1579,8 +1583,7 @@ class ListProcessElements(CorpusACLMixin, ListAPIView):
def get_serializer_class(self): def get_serializer_class(self):
if self.with_image: if self.with_image:
return ProcessElementSerializer return ProcessElementSerializer
else: return ProcessElementLightSerializer
return ProcessElementLightSerializer
def get_queryset(self): def get_queryset(self):
if not self.has_admin_access(self.process.corpus): if not self.has_admin_access(self.process.corpus):
...@@ -1829,8 +1832,9 @@ class WorkerActivityBase(ListAPIView): ...@@ -1829,8 +1832,9 @@ class WorkerActivityBase(ListAPIView):
) )
) )
class CorpusWorkersActivity(CorpusACLMixin, WorkerActivityBase): class CorpusWorkersActivity(CorpusACLMixin, WorkerActivityBase):
""" r"""
Retrieve corpus wise statistics about the activity of all its worker processes.\n Retrieve corpus wise statistics about the activity of all its worker processes.
Requires a **guest** access. Requires a **guest** access.
""" """
...@@ -1846,8 +1850,9 @@ class CorpusWorkersActivity(CorpusACLMixin, WorkerActivityBase): ...@@ -1846,8 +1850,9 @@ class CorpusWorkersActivity(CorpusACLMixin, WorkerActivityBase):
) )
) )
class ProcessWorkersActivity(ProcessACLMixin, WorkerActivityBase): class ProcessWorkersActivity(ProcessACLMixin, WorkerActivityBase):
""" r"""
Retrieve process statistics about the activity of its workers.\n Retrieve process statistics about the activity of its workers.
Requires a **guest** access. Requires a **guest** access.
""" """
......
...@@ -232,8 +232,7 @@ class Process(IndexableModel): ...@@ -232,8 +232,7 @@ class Process(IndexableModel):
""" """
if self.has_prefetched_tasks: if self.has_prefetched_tasks:
return max((t.expiry for t in self.tasks.all()), default=None) return max((t.expiry for t in self.tasks.all()), default=None)
else: return self.tasks.aggregate(models.Max("expiry"))["expiry__max"]
return self.tasks.aggregate(models.Max("expiry"))["expiry__max"]
def get_last_run(self) -> int: def get_last_run(self) -> int:
""" """
......
...@@ -545,7 +545,7 @@ class ApplyProcessTemplateSerializer(ProcessACLMixin, serializers.Serializer): ...@@ -545,7 +545,7 @@ class ApplyProcessTemplateSerializer(ProcessACLMixin, serializers.Serializer):
access_level = self.process_access_level(process) access_level = self.process_access_level(process)
if not access_level: if not access_level:
raise ValidationError(detail="Process with this ID does not exist.") raise ValidationError(detail="Process with this ID does not exist.")
elif access_level < Role.Contributor.value: if access_level < Role.Contributor.value:
raise PermissionDenied(detail="You do not have a contributor access to this process.") raise PermissionDenied(detail="You do not have a contributor access to this process.")
errors = [] errors = []
...@@ -627,7 +627,7 @@ class CorpusProcessSerializer(serializers.Serializer): ...@@ -627,7 +627,7 @@ class CorpusProcessSerializer(serializers.Serializer):
access_level = get_max_level(self.context["request"].user, corpus) access_level = get_max_level(self.context["request"].user, corpus)
if not access_level: if not access_level:
raise ValidationError(["Corpus with this ID does not exist."]) raise ValidationError(["Corpus with this ID does not exist."])
elif access_level < Role.Admin.value: if access_level < Role.Admin.value:
raise ValidationError(["You do not have an admin access to this corpus."]) raise ValidationError(["You do not have an admin access to this corpus."])
return corpus return corpus
......
...@@ -37,8 +37,7 @@ class ModelArgument(object): ...@@ -37,8 +37,7 @@ class ModelArgument(object):
text_filter = {f"{self.text_search_field}__{self.text_search_lookup}": arg} text_filter = {f"{self.text_search_field}__{self.text_search_lookup}": arg}
if self.many: if self.many:
return qs.filter(**text_filter) return qs.filter(**text_filter)
else: return qs.get(**text_filter)
return qs.get(**text_filter)
class CorpusArgument(ModelArgument): class CorpusArgument(ModelArgument):
......
...@@ -304,4 +304,4 @@ class NullField(serializers.CharField): ...@@ -304,4 +304,4 @@ class NullField(serializers.CharField):
def to_internal_value(self, data): def to_internal_value(self, data):
if data is not None: if data is not None:
self.fail("invalid") self.fail("invalid")
return None return
...@@ -26,7 +26,7 @@ class TestConfig(TestCase): ...@@ -26,7 +26,7 @@ class TestConfig(TestCase):
data = data.value data = data.value
if data is None: if data is None:
return self.represent_none(data) return self.represent_none(data)
elif isinstance(data, (bool, int, float, bytes, str)): if isinstance(data, (bool, int, float, bytes, str)):
return self.represent_data(data) return self.represent_data(data)
return self.represent_str(str(data)) return self.represent_str(str(data))
......
...@@ -10,6 +10,11 @@ FROM "documents_corpus" ...@@ -10,6 +10,11 @@ FROM "documents_corpus"
WHERE "documents_corpus"."id" = '{corpus_id}'::uuid WHERE "documents_corpus"."id" = '{corpus_id}'::uuid
LIMIT 21; LIMIT 21;
UPDATE "process_process"
SET "element_id" = NULL
WHERE ("process_process"."corpus_id" = '{corpus_id}'::uuid
AND NOT ("process_process"."element_id" IS NULL));
SELECT "django_content_type"."id", SELECT "django_content_type"."id",
"django_content_type"."app_label", "django_content_type"."app_label",
"django_content_type"."model" "django_content_type"."model"
...@@ -152,6 +157,15 @@ DELETE ...@@ -152,6 +157,15 @@ DELETE
FROM "documents_corpusexport" FROM "documents_corpusexport"
WHERE "documents_corpusexport"."corpus_id" = '{corpus_id}'::uuid; WHERE "documents_corpusexport"."corpus_id" = '{corpus_id}'::uuid;
DELETE
FROM "ponos_artifact"
WHERE "ponos_artifact"."id" IN
(SELECT U0."id"
FROM "ponos_artifact" U0
INNER JOIN "ponos_task" U1 ON (U0."task_id" = U1."id")
INNER JOIN "process_process" U2 ON (U1."process_id" = U2."id")
WHERE U2."corpus_id" = '{corpus_id}'::uuid);
DELETE DELETE
FROM "process_processdatasetset" FROM "process_processdatasetset"
WHERE "process_processdatasetset"."id" IN WHERE "process_processdatasetset"."id" IN
......
...@@ -14,6 +14,11 @@ UPDATE "documents_corpus" ...@@ -14,6 +14,11 @@ UPDATE "documents_corpus"
SET "top_level_type_id" = NULL SET "top_level_type_id" = NULL
WHERE "documents_corpus"."id" = '{corpus_id}'::uuid; WHERE "documents_corpus"."id" = '{corpus_id}'::uuid;
UPDATE "process_process"
SET "element_id" = NULL
WHERE ("process_process"."corpus_id" = '{corpus_id}'::uuid
AND NOT ("process_process"."element_id" IS NULL));
SELECT "django_content_type"."id", SELECT "django_content_type"."id",
"django_content_type"."app_label", "django_content_type"."app_label",
"django_content_type"."model" "django_content_type"."model"
...@@ -156,6 +161,15 @@ DELETE ...@@ -156,6 +161,15 @@ DELETE
FROM "documents_corpusexport" FROM "documents_corpusexport"
WHERE "documents_corpusexport"."corpus_id" = '{corpus_id}'::uuid; WHERE "documents_corpusexport"."corpus_id" = '{corpus_id}'::uuid;
DELETE
FROM "ponos_artifact"
WHERE "ponos_artifact"."id" IN
(SELECT U0."id"
FROM "ponos_artifact" U0
INNER JOIN "ponos_task" U1 ON (U0."task_id" = U1."id")
INNER JOIN "process_process" U2 ON (U1."process_id" = U2."id")
WHERE U2."corpus_id" = '{corpus_id}'::uuid);
DELETE DELETE
FROM "process_processdatasetset" FROM "process_processdatasetset"
WHERE "process_processdatasetset"."id" IN WHERE "process_processdatasetset"."id" IN
......
...@@ -48,8 +48,7 @@ class DatasetAdmin(admin.ModelAdmin): ...@@ -48,8 +48,7 @@ class DatasetAdmin(admin.ModelAdmin):
self.readonly_fields = self.__class__.readonly_fields self.readonly_fields = self.__class__.readonly_fields
if obj is not None: if obj is not None:
self.readonly_fields += ("unique_elements",) self.readonly_fields += ("unique_elements",)
form = super().get_form(request, obj=None, **kwargs) return super().get_form(request, obj=None, **kwargs)
return form
admin.site.register(Model, ModelAdmin) admin.site.register(Model, ModelAdmin)
......
...@@ -417,8 +417,7 @@ class ModelsList(TrainingModelMixin, ListCreateAPIView): ...@@ -417,8 +417,7 @@ class ModelsList(TrainingModelMixin, ListCreateAPIView):
"id": str(existing_model.id), "id": str(existing_model.id),
"name": "A model with this name already exists", "name": "A model with this name already exists",
}) })
else: raise PermissionDenied()
raise PermissionDenied()
return serializer.save() return serializer.save()
...@@ -753,7 +752,7 @@ class DatasetSetBase(): ...@@ -753,7 +752,7 @@ class DatasetSetBase():
) )
if self.request.method == "DELETE" and not Corpus.objects.admin(self.request.user).filter(pk=dataset.corpus_id).exists(): if self.request.method == "DELETE" and not Corpus.objects.admin(self.request.user).filter(pk=dataset.corpus_id).exists():
raise PermissionDenied(detail="You do not have admin access to this dataset.") raise PermissionDenied(detail="You do not have admin access to this dataset.")
elif self.request.method != "DELETE" and not Corpus.objects.writable(self.request.user).filter(pk=dataset.corpus_id).exists(): if self.request.method != "DELETE" and not Corpus.objects.writable(self.request.user).filter(pk=dataset.corpus_id).exists():
raise PermissionDenied(detail="You do not have contributor access to this dataset.") raise PermissionDenied(detail="You do not have contributor access to this dataset.")
if dataset.state != DatasetState.Open: if dataset.state != DatasetState.Open:
raise ValidationError(detail="You can only add or update sets from a dataset in an open state.") raise ValidationError(detail="You can only add or update sets from a dataset in an open state.")
......
...@@ -23,7 +23,7 @@ def ask(phrase): ...@@ -23,7 +23,7 @@ def ask(phrase):
choice = input(phrase + " [y/n]: ") choice = input(phrase + " [y/n]: ")
if choice.lower() == "y": if choice.lower() == "y":
return True return True
elif choice.lower() == "n": if choice.lower() == "n":
return False return False
...@@ -33,7 +33,7 @@ def choose(instances, name_field="name", title="Pick one item", allow_skip=False ...@@ -33,7 +33,7 @@ def choose(instances, name_field="name", title="Pick one item", allow_skip=False
nb = instances.count() nb = instances.count()
if nb == 0: if nb == 0:
return return
elif nb == 1: if nb == 1:
return instances.first() return instances.first()
# Build internal representation # Build internal representation
......