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
2 changes: 1 addition & 1 deletion isic/core/api/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def collection_populate_from_search(request, id: int, payload: SearchQueryIn):
if collection.locked:
return 409, {"error": "Collection is locked"}

if collection.public and payload.to_queryset(request.user).private().exists(): # type: ignore[attr-defined]
if collection.public and payload.to_queryset(request.user).private().exists():
return 409, {"error": "Collection is public and cannot contain private images."}

# Pass data instead of validated_data because the celery task is going to revalidate.
Expand Down
4 changes: 2 additions & 2 deletions isic/core/dsl.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def es_query(s, loc, toks):


def es_query_and(s, loc, toks):
ret = {"bool": {"filter": []}}
ret: dict[str, Any] = {"bool": {"filter": []}}
if isinstance(toks, ParseResults):
# Explicit ANDs come in as parse results
q_objects = toks.asList()
Expand All @@ -204,7 +204,7 @@ def es_query_and(s, loc, toks):


def es_query_or(s, loc, toks):
ret = {"bool": {"should": []}}
ret: dict[str, Any] = {"bool": {"should": []}}
for tok in toks[0]:
ret["bool"]["should"].append(tok)
toks[0] = ret
Expand Down
24 changes: 18 additions & 6 deletions isic/core/management/commands/anonymize_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
metadata. All users are given the password "password".
"""

from collections.abc import Callable
import hashlib
import logging
import random
Expand All @@ -16,6 +17,7 @@
from django.contrib.auth.models import User
from django.contrib.sessions.models import Session
from django.db import connection, transaction
from django.db.models import Model, QuerySet
import djclick as click
from faker import Faker
from oauth2_provider.models import AccessToken, RefreshToken
Expand Down Expand Up @@ -124,17 +126,27 @@ def anonymize_hash_id(salt: str, hash_id: str, *, seen: set) -> str:
raise RuntimeError(f"Failed to generate unique hash_id after 100 attempts: {hash_id}")


def _batch_anonymize(queryset, fields, transform, *, label, dry_run, batch_size): # noqa: PLR0913
def _batch_anonymize[M: Model]( # noqa: PLR0913
queryset: QuerySet[M],
fields: list[str],
transform: Callable[[M], None],
*,
label: str,
dry_run: bool,
batch_size: int,
) -> int:
model_class = queryset.model
total = queryset.count()
objects_to_update = []
objects_to_update: list[M] = []

if dry_run:
label = f"[DRY RUN] {label}"

def _flush():
if objects_to_update and not dry_run:
model_class.objects.bulk_update(objects_to_update, fields, batch_size=batch_size)
model_class._default_manager.bulk_update(
objects_to_update, fields, batch_size=batch_size
)

with click.progressbar(
queryset.iterator(), length=total, label=label, show_eta=True, show_percent=True
Expand All @@ -154,8 +166,8 @@ def _flush():

def _anonymize_users(faker, salt, *, dry_run, batch_size):
password_hash = make_password("password")
names_cache = {}
emails_cache = {}
names_cache: dict[str, str] = {}
emails_cache: dict[str, str] = {}

def transform(user):
user.username = anonymize_username(salt, user.username)
Expand Down Expand Up @@ -244,7 +256,7 @@ def _anonymize_private_ids(salt, *, dry_run, batch_size):

counts = {}
for model_class, field_name, id_type in models_to_process:
cache = {}
cache: dict[str, str] = {}

def transform(obj, _field=field_name, _type=id_type, _cache=cache):
current_value = getattr(obj, _field)
Expand Down
2 changes: 1 addition & 1 deletion isic/core/management/commands/set_isic_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def add_staff_group():
User,
ZipUpload,
]:
content_type = ContentType.objects.get_for_model(model) # type: ignore[arg-type]
content_type = ContentType.objects.get_for_model(model)
for permission in ["view", "change"]:
group.permissions.add(
Permission.objects.get(
Expand Down
20 changes: 14 additions & 6 deletions isic/core/models/collection.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from django.contrib.auth.models import User
from django.contrib.postgres.indexes import GinIndex, OpClass
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.constraints import CheckConstraint, UniqueConstraint
from django.db.models.expressions import F
from django.db.models.functions import Upper
from django.db.models.query import QuerySet
from django.db.models.query_utils import Q
from django.urls import reverse
from django_extensions.db.models import TimeStampedModel

from .image import Image

if TYPE_CHECKING:
from django.db.models.query import QuerySet


class CollectionQuerySet(models.QuerySet["Collection"]):
def pinned(self):
Expand Down Expand Up @@ -62,7 +68,9 @@ class Meta(TimeStampedModel.Meta):

creator = models.ForeignKey(User, on_delete=models.PROTECT, related_name="collections")

images = models.ManyToManyField(Image, related_name="collections", through="CollectionImage")
images: models.ManyToManyField[Image, CollectionImage] = models.ManyToManyField(
Image, related_name="collections", through="CollectionImage"
)

# unique per user. names of pinned collections can't be used.
name = models.CharField(max_length=200)
Expand All @@ -73,7 +81,7 @@ class Meta(TimeStampedModel.Meta):

public = models.BooleanField(default=False)

shares = models.ManyToManyField(
shares: models.ManyToManyField[User, CollectionShare] = models.ManyToManyField(
User,
through="CollectionShare",
through_fields=("collection", "grantee"),
Expand Down Expand Up @@ -135,11 +143,11 @@ def counts(self):
}
return self.cached_counts

def full_clean(self, exclude=None, validate_unique=True): # noqa: FBT002
if self.pk and self.public and self.images.private().exists(): # type: ignore[attr-defined]
def full_clean(self, *args, **kwargs):
if self.pk and self.public and self.images.private().exists():
raise ValidationError("Can't make collection public, it contains private images.")

return super().full_clean(exclude=exclude, validate_unique=validate_unique)
return super().full_clean(*args, **kwargs)


class CollectionImage(models.Model):
Expand Down
4 changes: 3 additions & 1 deletion isic/core/models/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ class Meta(CreationSortedTimeStampedModel.Meta):
# index is used because public is filtered in every permissions check
public = models.BooleanField(default=False, db_index=True)

shares = models.ManyToManyField(User, through="ImageShare", through_fields=("image", "grantee"))
shares: models.ManyToManyField[User, ImageShare] = models.ManyToManyField(
User, through="ImageShare", through_fields=("image", "grantee")
)

objects = ImageManager()

Expand Down
22 changes: 11 additions & 11 deletions isic/core/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def _clamp(val: int, min_: int, max_: int) -> int:
return max(min_, min(val, max_))


def _reverse_order(order: tuple):
def _reverse_order(order: Sequence[str]):
"""
Reverse the ordering specification for a Django ORM query.

Expand Down Expand Up @@ -83,8 +83,8 @@ def decode_cursor(cls, encoded_cursor: str | None) -> Cursor:
offset = int(tokens.get("o", ["0"])[0])
offset = _clamp(offset, 0, CursorPagination._offset_cutoff)

reverse = tokens.get("r", ["0"])[0]
reverse = bool(int(reverse))
reverse_str = tokens.get("r", ["0"])[0]
reverse = bool(int(reverse_str))

position = tokens.get("p", [None])[0]
except (TypeError, ValueError) as e:
Expand Down Expand Up @@ -117,7 +117,7 @@ def paginate_queryset(
if not queryset.query.order_by:
queryset = queryset.order_by(*self.ordering)

order = queryset.query.order_by
order: tuple[str, ...] = queryset.query.order_by

total_count = (
# let the queryset define a custom_count attribute in the event that computing
Expand Down Expand Up @@ -227,11 +227,11 @@ def next_link( # noqa: PLR0913
base_url: str,
page: list,
cursor: Cursor,
order: tuple,
order: Sequence[str],
has_previous: bool,
limit: int,
next_position: str,
previous_position: str,
next_position: str | None,
previous_position: str | None,
) -> str:
if page and cursor.reverse and cursor.offset:
# If we're reversing direction and we have an offset cursor
Expand Down Expand Up @@ -287,12 +287,12 @@ def previous_link( # noqa: PLR0913
base_url: str,
page: list,
cursor: Cursor,
order: tuple,
order: Sequence[str],
has_next: bool,
limit: int,
next_position: str,
previous_position: str,
):
next_position: str | None,
previous_position: str | None,
) -> str:
if page and not cursor.reverse and cursor.offset:
# If we're reversing direction and we have an offset cursor
# then we cannot use the first position we find as a marker.
Expand Down
3 changes: 2 additions & 1 deletion isic/core/permissions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from functools import wraps
from typing import Any
from urllib.parse import urlparse

import django.apps
Expand Down Expand Up @@ -27,7 +28,7 @@ def authenticate(self, request: HttpRequest, key: str | None) -> User | None:
class UserPermissions:
model = User
perms = ["view_staff"]
filters = {}
filters: dict[str, Any] = {}

@staticmethod
def view_staff(user_obj, _=None):
Expand Down
4 changes: 2 additions & 2 deletions isic/core/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

logger = logging.getLogger(__name__)

IMAGE_INDEX_MAPPINGS = {"properties": {}}
IMAGE_INDEX_MAPPINGS: dict[str, Any] = {"properties": {}}
DEFAULT_SEARCH_AGGREGATES = {}
COUNTS_AGGREGATES = {}

Expand Down Expand Up @@ -170,7 +170,7 @@ def add_to_search_index(image: Image) -> None:
image = Image.objects.with_elasticsearch_properties().get(pk=image.pk)
get_elasticsearch_client().index(
index=settings.ISIC_ELASTICSEARCH_IMAGES_INDEX,
id=image.pk,
id=str(image.pk),
document=image.to_elasticsearch_document(source_only=True),
)

Expand Down
2 changes: 1 addition & 1 deletion isic/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def to_queryset(
qs = qs.from_search_query(self.query) # type: ignore[attr-defined]

if self.collections:
qs = qs.filter( # type: ignore[union-attr]
qs = qs.filter(
collections__in=get_visible_objects(
user,
"core.view_collection",
Expand Down
2 changes: 1 addition & 1 deletion isic/core/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def staff_image_metadata_csv(*, qs: QuerySet[Image]) -> Generator[list[str] | di
.distinct()
)

remapped_keys = reduce(
remapped_keys: list[str] = reduce(
operator.iadd,
[
[field.internal_id_name, field.csv_field_name]
Expand Down
10 changes: 5 additions & 5 deletions isic/core/services/collection/doi.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from isic.ingest.services.publish import unembargo_image

if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Sequence
from urllib.parse import ParseResult

from django.contrib.auth.models import User
Expand Down Expand Up @@ -120,8 +120,8 @@ def check_create_draft_doi_allowed(
*,
user: User,
collection: Collection,
supplemental_files: Iterable[dict[str, str]] | None = None,
related_identifiers: Iterable[RelatedIdentifierIn] | None = None,
supplemental_files: Sequence[dict[str, str]] | None = None,
related_identifiers: Sequence[RelatedIdentifierIn] | None = None,
) -> None:
if not user.has_perm("core.create_doi", collection):
raise ValidationError("You don't have permissions to do that.")
Expand Down Expand Up @@ -190,8 +190,8 @@ def create_collection_draft_doi(
user: User,
collection: Collection,
description: str,
supplemental_files: Iterable[dict[str, str]] | None = None,
related_identifiers: Iterable[RelatedIdentifierIn] | None = None,
supplemental_files: Sequence[dict[str, str]] | None = None,
related_identifiers: Sequence[RelatedIdentifierIn] | None = None,
) -> DraftDoi:
check_create_draft_doi_allowed(
user=user,
Expand Down
2 changes: 1 addition & 1 deletion isic/core/services/collection/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def move_collection_images(
if not ignore_lock and (src_collection.locked or dest_collection.locked):
raise ValidationError("Can't move images to/from a locked collection.")

if dest_collection.public and src_collection.images.private().exists(): # type: ignore[attr-defined]
if dest_collection.public and src_collection.images.private().exists():
raise ValidationError("Can't move private images to a public collection.")

with transaction.atomic():
Expand Down
12 changes: 8 additions & 4 deletions isic/core/tests/test_doi.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,11 @@ def test_doi_files(
doi.refresh_from_db()

assert doi.bundle is not None
assert doi.bundle_size is not None
assert doi.bundle_size > 0

assert doi.metadata is not None
assert doi.metadata_size is not None
assert doi.metadata_size > 0

with tempfile.TemporaryDirectory() as temp_dir, zipfile.ZipFile(doi.bundle) as zf:
Expand Down Expand Up @@ -322,7 +324,7 @@ def test_api_doi_creation(

draft_doi = DraftDoi.objects.get(collection=public_collection_with_public_images)
assert draft_doi.supplemental_files.count() == 1
assert draft_doi.supplemental_files.first().description == "test supplemental file"
assert draft_doi.supplemental_files.get().description == "test supplemental file"

assert draft_doi.related_identifiers.count() == 2

Expand Down Expand Up @@ -368,7 +370,7 @@ def test_doi_bundle_includes_supplemental_files(

assert doi.bundle is not None
assert doi.supplemental_files.count() == 1
supplemental_file = doi.supplemental_files.first()
supplemental_file = doi.supplemental_files.get()

with tempfile.TemporaryDirectory() as temp_dir, zipfile.ZipFile(doi.bundle) as zf:
zf.extractall(temp_dir)
Expand Down Expand Up @@ -553,7 +555,7 @@ def test_draft_doi_complete_lifecycle( # noqa: PLR0915
assert draft_doi.collection == collection

assert draft_doi.supplemental_files.count() == 1
supplemental_file = draft_doi.supplemental_files.first()
supplemental_file = draft_doi.supplemental_files.get()
assert supplemental_file.description == "Test supplemental file"
assert storages["default"].exists(supplemental_file.blob.name)

Expand Down Expand Up @@ -611,7 +613,7 @@ def test_draft_doi_complete_lifecycle( # noqa: PLR0915
}

assert final_doi.supplemental_files.count() == 1
final_supplemental_file = final_doi.supplemental_files.first()
final_supplemental_file = final_doi.supplemental_files.get()
assert final_supplemental_file.description == "Test supplemental file"

assert storages["sponsored"].exists(final_supplemental_file.blob.name)
Expand All @@ -627,8 +629,10 @@ def test_draft_doi_complete_lifecycle( # noqa: PLR0915
assert mock_datacite_promote_draft_doi_to_findable.call_count == 1

assert final_doi.bundle is not None
assert final_doi.bundle_size is not None
assert final_doi.bundle_size > 0
assert final_doi.metadata is not None
assert final_doi.metadata_size is not None
assert final_doi.metadata_size > 0


Expand Down
Loading