Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export notebook to pdf #1163

Merged
merged 3 commits into from
Dec 4, 2024
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ EMAIL_USE_TLS=false
ELASTICSEARCH_HOST=http://localhost:9200
ELASTICSEARCH_USERNAME=
ELASTICSEARCH_PASSWORD=
EROS_BASE_IMAGE_URL=
EROS_HTTP_TOKEN=
EUPHROSYNE_TOOLS_API_URL=http://localhost:8001
DEFAULT_FROM_EMAIL=alexandre.hajjar@beta.gouv.fr
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ env:
EMAIL_USE_TLS: false
DEFAULT_FROM_EMAIL: h@ll.o
SITE_URL: http://localhost:8000
PYTHON_VERSION: 3.11.2
PYTHON_VERSION: 3.12.7
EUPHROSYNE_TOOLS_API_URL: http://localhost:8001

jobs:
python-checks:
Expand Down
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ disable=raw-checker-failed,
R0801,
E5110,
consider-using-f-string,
too-many-positional-arguments
too-many-positional-arguments,
cyclic-import

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12.7
30 changes: 28 additions & 2 deletions euphro_tools/download_urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Everything related to getting signed download link from euphro tools."""

import os
import typing
from datetime import datetime
from typing import Literal

import requests

Expand All @@ -11,7 +11,7 @@
from .exceptions import EuphroToolsException
from .utils import get_run_data_path

DataType = Literal["raw_data", "processed_data"]
DataType = typing.Literal["raw_data", "processed_data"]


def generate_download_url(
Expand Down Expand Up @@ -54,3 +54,29 @@ def fetch_token_for_run_data(
except (requests.HTTPError, requests.ConnectionError) as error:
raise EuphroToolsException from error
return request.json()["token"]


class GetUrlAndTokenForProjectImagesResponse(typing.TypedDict):
base_url: str
token: str


def get_storage_info_for_project_images(
project_slug: str,
) -> GetUrlAndTokenForProjectImagesResponse:
"""Get a download URL and token for a project's images."""
url = (
os.environ["EUPHROSYNE_TOOLS_API_URL"]
+ f"/images/projects/{project_slug}/signed-url"
)
token = EuphroToolsAPIToken.for_euphrosyne().access_token
try:
request = requests.get(
url,
timeout=5,
headers={"Authorization": f"Bearer {token}"},
)
request.raise_for_status()
except (requests.HTTPError, requests.ConnectionError) as error:
raise EuphroToolsException from error
return request.json()
1 change: 1 addition & 0 deletions euphro_tools/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# pylint: disable=cyclic-import
class EuphroToolsException(Exception):
pass
3 changes: 3 additions & 0 deletions euphrosyne/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ def build_development_db_name(base_db_name):
},
}

EUPHROSYNE_TOOLS_API_URL = os.environ["EUPHROSYNE_TOOLS_API_URL"]
EROS_BASE_IMAGE_URL = os.getenv("EROS_BASE_IMAGE_URL")

S3_BUCKET_NAME = os.getenv("S3_BUCKET_NAME")
S3_BUCKET_REGION_NAME = os.getenv("S3_BUCKET_REGION_NAME")
S3_ENDPOINT_URL = os.getenv("S3_ENDPOINT_URL")
Expand Down
27 changes: 27 additions & 0 deletions lab/objects/c2rmf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any

import requests
from django.conf import settings

from lab.thesauri.models import Era

Expand Down Expand Up @@ -99,3 +100,29 @@ def fetch_full_objectgroup_from_eros(
updated_og.inventory = data.get("inv") or ""
updated_og.materials = (data.get("support") or "").split(" / ")
return updated_og


def construct_image_url_from_eros_path(path: str) -> str:
# pylint: disable=import-outside-toplevel
from euphro_auth.jwt.tokens import EuphroToolsAPIToken

c2rmf_id, image_id = path.split("/")
if c2rmf_id.startswith("C2RMF"):
image_category = f"pyr-{c2rmf_id[:6]}"
elif c2rmf_id.startswith("F"):
image_category = f"pyr-{c2rmf_id[:2]}"
else:
image_category = "pyr-FZ"

eros_base_url = (
settings.EROS_BASE_IMAGE_URL or f"{settings.EUPHROSYNE_TOOLS_API_URL}/eros"
)

url = f"{eros_base_url}/iiif/{image_category}/{c2rmf_id}/{image_id}.tif/full/500,/0/default.jpg" # pylint: disable=line-too-long

# Add token to the URL if using EROS direct URL. Else we use the EuphroTools API
# proxy which includes the token in the request headers.
if settings.EROS_BASE_IMAGE_URL:
token = EuphroToolsAPIToken.for_euphrosyne().access_token
return f"{url}?token={token}"
return url
29 changes: 29 additions & 0 deletions lab/objects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,32 @@ class Meta:
name="run_object_group_image_unique_path_transform_perrun_object_group",
),
]

@property
def file_name(self) -> str:
if (
self.path.startswith("C2RMF")
or self.path.startswith("FZ")
or self.path.startswith("F")
) and len(self.path.split("/")) == 2:
image_id = self.path.split("/")[1]
return f"{image_id}.tiff"
return self.path.rsplit("/", maxsplit=1)[-1].split("?")[0]


def construct_image_url_from_path(
path: str, storage_base_url: str, storage_token: str | None = None
) -> str:
# pylint: disable=import-outside-toplevel
from lab.objects.c2rmf import construct_image_url_from_eros_path

if (
path.startswith("C2RMF") or path.startswith("FZ") or path.startswith("F")
) and len(path.split("/")) == 2:
return construct_image_url_from_eros_path(path)

return (
f"{storage_base_url}{path}?{storage_token}"
if storage_token
else f"{storage_base_url}{path}"
)
30 changes: 30 additions & 0 deletions lab/objects/tests/test_c2mrf.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest import mock

from ..c2rmf import (
construct_image_url_from_eros_path,
fetch_full_objectgroup_from_eros,
fetch_partial_objectgroup_from_eros,
)
Expand Down Expand Up @@ -43,3 +44,32 @@ def test_fetch_full_objectgroup_from_eros(_):
assert og.dating_era.label == "1500"
assert og.inventory == "ODUT 01107"
assert og.materials == ["terre cuite"]


@mock.patch("lab.objects.c2rmf.settings")
def test_construct_image_url_from_eros_path(mock_settings):

mock_settings.EROS_BASE_IMAGE_URL = "http://example.com"
mock_settings.EUPHROSYNE_TOOLS_API_URL = "http://tools.example.com"

path = "C2RMF12345/67890"
expected_url = "http://example.com/iiif/pyr-C2RMF1/C2RMF12345/67890.tif/full/500,/0/default.jpg?token=" # pylint: disable=line-too-long
assert construct_image_url_from_eros_path(path).startswith(expected_url)

path = "F12345/67890"
expected_url = (
"http://example.com/iiif/pyr-F1/F12345/67890.tif/full/500,/0/default.jpg?token="
)
assert construct_image_url_from_eros_path(path).startswith(expected_url)

path = "Z12345/67890"
expected_url = (
"http://example.com/iiif/pyr-FZ/Z12345/67890.tif/full/500,/0/default.jpg?token="
)
assert construct_image_url_from_eros_path(path).startswith(expected_url)

# No token when EROS_BASE_IMAGE_URL is None
mock_settings.EROS_BASE_IMAGE_URL = None
path = "C2RMF12345/67890"
expected_url = "http://tools.example.com/eros/iiif/pyr-C2RMF1/C2RMF12345/67890.tif/full/500,/0/default.jpg" # pylint: disable=line-too-long
assert construct_image_url_from_eros_path(path) == expected_url
17 changes: 17 additions & 0 deletions lab/objects/tests/test_run_object_group_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.test import SimpleTestCase

from ..models import RunObjetGroupImage


class TestRunObjectGroupImage(SimpleTestCase):
def test_run_object_group_image_file_name(self):

assert RunObjetGroupImage(path="C2RMF77463/KOA68").file_name == "KOA68.tiff"
assert RunObjetGroupImage(path="FZ77463/KOA68").file_name == "KOA68.tiff"
assert RunObjetGroupImage(path="FBLABLA/KOA68").file_name == "KOA68.tiff"
assert (
RunObjetGroupImage(
path="/project-test/images/object-groups/163/abcdefghif.jpg"
).file_name
== "abcdefghif.jpg"
)
13 changes: 10 additions & 3 deletions lab/permissions.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import enum
import typing
from functools import wraps

from django.contrib.auth.models import AnonymousUser
from django.http import HttpResponse, JsonResponse
from django.http.request import HttpRequest

from euphro_auth.models import User
from lab.models import Project
from shared.view_mixins import StaffUserRequiredMixin

if typing.TYPE_CHECKING:
from lab.models import Project


class LabRole(enum.IntEnum):
ANONYMOUS = 0
Expand All @@ -18,7 +21,7 @@ class LabRole(enum.IntEnum):
LAB_ADMIN = 4


def get_user_permission_group(request: HttpRequest, project: Project) -> LabRole:
def get_user_permission_group(request: HttpRequest, project: "Project") -> LabRole:
if request.user.is_anonymous:
return LabRole.ANONYMOUS
user: User = request.user
Expand All @@ -41,7 +44,7 @@ def is_lab_admin(user: User | AnonymousUser) -> bool:
return bool(user.is_superuser or getattr(user, "is_lab_admin", None))


def is_project_leader(user: User, project: Project) -> bool:
def is_project_leader(user: User, project: "Project") -> bool:
return project.participation_set.filter(user=user, is_leader=True).exists()


Expand All @@ -50,6 +53,8 @@ class ProjectMembershipRequiredMixin(StaffUserRequiredMixin):
def dispatch(
self, request: HttpRequest, project_id: int, *args, **kwargs
) -> HttpResponse:
from lab.models import Project # pylint: disable=import-outside-toplevel

response = super().dispatch(request, *args, **kwargs)
if request.user.is_anonymous:
return self.handle_no_permission()
Expand All @@ -65,6 +70,8 @@ def dispatch(
def project_membership_required(view_func):
@wraps(view_func)
def _wrapped_view(request, project_id: int, *args, **kwargs):
from lab.models import Project # pylint: disable=import-outside-toplevel

try:
project = Project.objects.get(pk=project_id)
except Project.DoesNotExist:
Expand Down
Empty file.
Loading
Loading