-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add back embargoed projects when querying public projects feat: elasticsearch catalog feat: add cron command to index catalog & cron job feat: add geonames_id constraint to Location fix: call to huma_num with no id during elasticsearch build feat: divide object dating field in two - era & period fix mypy issues fix(elasticsearch): from query param not passed correctly feat: catalog data request chore: move user related test factories to euphro_auth fix: permission view on data request run inline feat(es): add is_data_embargoed to RunDoc feat(data-request): prevent request data from embargoed runs feat: data request events feat; add lru cache to eros fetch call feat: fetch data from eros when building catalog
- Loading branch information
Showing
105 changed files
with
5,227 additions
and
648 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
from django.contrib import admin, messages | ||
from django.db.models import Model, QuerySet | ||
from django.http import HttpRequest | ||
from django.http.response import HttpResponse | ||
from django.utils import timezone | ||
from django.utils.safestring import mark_safe | ||
from django.utils.translation import gettext | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
from euphro_tools.exceptions import EuphroToolsException | ||
from lab.admin.mixins import LabAdminAllowedMixin | ||
from lab.permissions import is_lab_admin | ||
|
||
from .data_links import send_links | ||
from .models import DataAccessEvent, DataRequest | ||
|
||
|
||
class ReadonlyInlineMixin: | ||
|
||
def has_change_permission( | ||
self, | ||
request: HttpRequest, | ||
obj: Model | None = None, # pylint: disable=unused-argument | ||
) -> bool: | ||
return False | ||
|
||
def has_delete_permission( | ||
self, | ||
request: HttpRequest, | ||
obj: Model | None = None, # pylint: disable=unused-argument | ||
) -> bool: | ||
return is_lab_admin(request.user) | ||
|
||
def has_add_permission( | ||
self, | ||
request: HttpRequest, | ||
obj: Model | None = None, # pylint: disable=unused-argument | ||
) -> bool: | ||
return False | ||
|
||
def has_view_permission( | ||
self, | ||
request: HttpRequest, | ||
obj: Model | None = None, # pylint: disable=unused-argument | ||
) -> bool: | ||
return is_lab_admin(request.user) | ||
|
||
|
||
@admin.action(description=_("Accept request(s) (send download links)")) | ||
def action_send_links( | ||
# pylint: disable=unused-argument | ||
modeladmin: "DataRequestAdmin", | ||
request: HttpRequest, | ||
queryset: QuerySet[DataRequest], | ||
): | ||
for data_request in queryset: | ||
try: | ||
send_links(data_request) | ||
except EuphroToolsException as error: | ||
modeladmin.message_user( | ||
request, | ||
_("Error sending links to %(email)s for %(data_request)s: %(error)s") | ||
% { | ||
"data_request": data_request, | ||
"error": error, | ||
"email": data_request.user_email, | ||
}, | ||
level=messages.ERROR, | ||
) | ||
continue | ||
data_request.sent_at = timezone.now() | ||
if not data_request.request_viewed: | ||
data_request.request_viewed = True | ||
data_request.save() | ||
|
||
|
||
class BeenSeenListFilter(admin.SimpleListFilter): | ||
# Human-readable title which will be displayed in the | ||
# right admin sidebar just above the filter options. | ||
title = _("has been sent") | ||
|
||
# Parameter for the filter that will be used in the URL query. | ||
parameter_name = "been_sent" | ||
|
||
def lookups(self, request, model_admin): | ||
return [ | ||
("1", _("Yes")), | ||
("0", _("No")), | ||
] | ||
|
||
def queryset(self, request: HttpRequest, queryset: QuerySet[DataRequest]): | ||
if not self.value(): | ||
return queryset | ||
return queryset.filter(sent_at__isnull=self.value() == "0") | ||
|
||
|
||
class DataAccessEventInline(ReadonlyInlineMixin, admin.TabularInline): | ||
model = DataAccessEvent | ||
extra = 0 | ||
|
||
fields = ("path", "access_time") | ||
readonly_fields = ("path", "access_time") | ||
|
||
|
||
class RunInline(ReadonlyInlineMixin, admin.TabularInline): | ||
model = DataRequest.runs.through | ||
verbose_name = "Run" | ||
verbose_name_plural = "Runs" | ||
extra = 0 | ||
|
||
fields = ("run",) | ||
|
||
|
||
@admin.register(DataRequest) | ||
class DataRequestAdmin(LabAdminAllowedMixin, admin.ModelAdmin): | ||
actions = [action_send_links] | ||
list_filter = [BeenSeenListFilter] | ||
|
||
list_display = ( | ||
"created", | ||
"sent_at", | ||
"user_email", | ||
"user_first_name", | ||
"user_last_name", | ||
"display_viewed", | ||
) | ||
|
||
fields = ( | ||
"created", | ||
"sent_at", | ||
"user_email", | ||
"user_first_name", | ||
"user_last_name", | ||
"user_institution", | ||
"description", | ||
) | ||
readonly_fields = ( | ||
"created", | ||
"user_email", | ||
"user_first_name", | ||
"user_last_name", | ||
"user_institution", | ||
"description", | ||
) | ||
|
||
inlines = [RunInline, DataAccessEventInline] | ||
|
||
def has_change_permission(self, request: HttpRequest, obj: Model | None = None): | ||
return False | ||
|
||
def change_view( | ||
self, | ||
request: HttpRequest, | ||
object_id: str, | ||
form_url: str = "", | ||
extra_context: dict[str, bool] | None = None, | ||
) -> HttpResponse: | ||
obj = self.get_object(request, object_id) | ||
if obj and not obj.request_viewed: | ||
obj.request_viewed = True | ||
obj.save() | ||
response = super().change_view(request, object_id, form_url, extra_context) | ||
return response | ||
|
||
def changelist_view( | ||
self, request: HttpRequest, extra_context: dict[str, str] | None = None | ||
): | ||
extra_context = extra_context or {} | ||
extra_context["title"] = gettext("Data requests") | ||
return super().changelist_view(request, extra_context) | ||
|
||
@admin.display(description=_("Is sent"), boolean=True) | ||
def is_sent(self, obj: "DataRequest") -> bool: | ||
return obj.sent_at is not None | ||
|
||
@admin.display(description="") | ||
def display_viewed(self, obj: "DataRequest") -> str: | ||
if obj.request_viewed: | ||
return "" | ||
return mark_safe( | ||
f'<p class="fr-badge fr-badge--new fr-badge--sm">{_("New")}</p>' | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
from django.urls import path | ||
|
||
from . import api_views | ||
|
||
urlpatterns = ( | ||
path( | ||
"", | ||
api_views.DataRequestCreateAPIView.as_view(), | ||
name="create", | ||
), | ||
path( | ||
"access-event", | ||
api_views.DataAccessEventCreateAPIView.as_view(), | ||
name="create-access-event", | ||
), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from rest_framework import generics, serializers | ||
|
||
from data_request.emails import send_data_request_created_email | ||
from euphro_auth.jwt.authentication import EuphrosyneAdminJWTAuthentication | ||
from lab.runs.models import Run | ||
|
||
from .models import DataAccessEvent, DataRequest | ||
|
||
|
||
class DataRequestSerializer(serializers.ModelSerializer): | ||
runs = serializers.PrimaryKeyRelatedField( | ||
many=True, | ||
queryset=Run.objects.only_not_embargoed(), | ||
allow_empty=False, | ||
) | ||
|
||
class Meta: | ||
model = DataRequest | ||
fields = [ | ||
"user_email", | ||
"user_first_name", | ||
"user_last_name", | ||
"user_institution", | ||
"description", | ||
"runs", | ||
] | ||
|
||
|
||
class DataRequestCreateAPIView(generics.CreateAPIView): | ||
queryset = DataRequest.objects.all() | ||
serializer_class = DataRequestSerializer | ||
|
||
def perform_create(self, serializer: DataRequestSerializer): | ||
super().perform_create(serializer) | ||
send_data_request_created_email(serializer.instance.user_email) | ||
|
||
|
||
class DataAccessEventSerializer(serializers.ModelSerializer): | ||
data_request = serializers.PrimaryKeyRelatedField( | ||
queryset=DataRequest.objects.all(), | ||
allow_empty=False, | ||
) | ||
|
||
class Meta: | ||
model = DataAccessEvent | ||
fields = ["data_request", "path"] | ||
|
||
|
||
class DataAccessEventCreateAPIView(generics.CreateAPIView): | ||
queryset = DataAccessEvent.objects.all() | ||
serializer_class = DataAccessEventSerializer | ||
authentication_classes = [EuphrosyneAdminJWTAuthentication] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class DataRequestConfig(AppConfig): | ||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "data_request" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import datetime | ||
import typing | ||
|
||
from euphro_tools.download_urls import ( | ||
DataType, | ||
fetch_token_for_run_data, | ||
generate_download_url, | ||
) | ||
|
||
from .emails import LinkDict, send_data_email | ||
from .models import DataRequest | ||
|
||
NUM_DAYS_VALID = 7 | ||
|
||
|
||
def send_links(data_request: DataRequest): | ||
links: list[LinkDict] = [] | ||
expiration = datetime.datetime.now() + datetime.timedelta(days=NUM_DAYS_VALID) | ||
for run in data_request.runs.all(): | ||
for data_type in typing.get_args(DataType): | ||
project_name = run.project.name | ||
token = fetch_token_for_run_data( | ||
run.project.slug, | ||
run.label, | ||
data_type, | ||
expiration=expiration, | ||
data_request_id=str(data_request.id), | ||
) | ||
links.append( | ||
{ | ||
"name": f"{run.label} ({project_name})", | ||
"url": generate_download_url( | ||
data_type=data_type, | ||
project_slug=run.project.slug, | ||
run_label=run.label, | ||
token=token, | ||
), | ||
"data_type": data_type, | ||
} | ||
) | ||
send_data_email( | ||
context={"links": links, "expiration_date": expiration}, | ||
email=data_request.user_email, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import datetime | ||
import logging | ||
import smtplib | ||
import typing | ||
|
||
from django.core import mail | ||
from django.template.loader import render_to_string | ||
from django.utils.html import strip_tags | ||
from django.utils.translation import gettext as _ | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class LinkDict(typing.TypedDict): | ||
name: str | ||
url: str | ||
data_type: typing.Literal["raw_data", "processed_data"] | ||
|
||
|
||
class DataEmailContext(typing.TypedDict): | ||
links: list[LinkDict] | ||
expiration_date: datetime.datetime | ||
|
||
|
||
def send_data_request_created_email( | ||
email: str, | ||
): | ||
subject = _("[New AGLAE] Data request received") | ||
template_path = "data_request/email/data-request-created.html" | ||
_send_mail(subject, email, template_path) | ||
|
||
|
||
def send_data_email( | ||
email: str, | ||
context: DataEmailContext, | ||
): | ||
subject = _("Your New AGLAE data links") | ||
template_path = "data_request/email/data-links.html" | ||
_send_mail(subject, email, template_path, context) | ||
|
||
|
||
def _send_mail( | ||
subject: str, | ||
email: str, | ||
template_path: str, | ||
context: typing.Mapping[str, typing.Any] | None = None, | ||
): | ||
html_message = render_to_string(template_path, context=context) | ||
plain_message = strip_tags(html_message) | ||
|
||
try: | ||
mail.send_mail( | ||
subject, | ||
plain_message, | ||
from_email=None, | ||
recipient_list=[email], | ||
html_message=html_message, | ||
) | ||
except (smtplib.SMTPException, ConnectionError) as e: | ||
logger.error( | ||
"Error sending data request email to %s. Reason: %s", | ||
email, | ||
str(e), | ||
) | ||
raise e |
Oops, something went wrong.