Skip to content

Commit 97a3896

Browse files
committed
Merge branch 'main' into themes
* main: Update Django to 4.2.3 and also update other python packages. (#4582) Handle rare event of file uploads having data without id (#4581) Create a json output version of the open calls on the front page. (#4554) Correct project template urls. (#4575) Fix submission filters queryset issue (#4572)
2 parents 878ca57 + e4f3f7f commit 97a3896

17 files changed

Lines changed: 1137 additions & 945 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Open calls API endpoint
2+
3+
At `/api/v2/open-calls.json` that same lists and order of open calls that appear on the front page are available in JSON format.
4+
5+
```json
6+
[
7+
{
8+
"description": "The application description if any.",
9+
"image": "/path/to/application/image.jpeg",
10+
"next_deadline": "1970-01-01",
11+
"title": "Title of the application",
12+
"weight": 1
13+
}
14+
]
15+
```
16+
17+
This should make it easy to add a list of open calls to the organisations main web site.

hypha/api/__init__.py

Whitespace-only changes.

hypha/api/urls.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.urls import include, path
2+
3+
from .v2 import urls as v2_urls
4+
5+
app_name = "api"
6+
7+
urlpatterns = [
8+
path("v2/", include(v2_urls)),
9+
]

hypha/api/v2/__init__.py

Whitespace-only changes.

hypha/api/v2/urls.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.urls import path
2+
3+
from .views import open_calls_json
4+
5+
app_name = "v2"
6+
7+
urlpatterns = [
8+
path("open-calls.json", open_calls_json),
9+
]

hypha/api/v2/views.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from django.conf import settings
2+
from django.http import JsonResponse
3+
from django_ratelimit.decorators import ratelimit
4+
5+
from hypha.apply.funds.models import ApplicationBase, LabBase
6+
7+
8+
@ratelimit(key="ip", rate=settings.DEFAULT_RATE_LIMIT)
9+
def open_calls_json(request):
10+
"""Open calls in JSON format.
11+
12+
List open calls in JSON format, useful when you want to list open calls on an external site.
13+
"""
14+
rounds = ApplicationBase.objects.order_by_end_date().specific()
15+
labs = LabBase.objects.public().live().specific()
16+
all_funds = list(rounds) + list(labs)
17+
18+
# Only pass rounds/labs that are open & visible for the front page
19+
data = [
20+
fund.as_json
21+
for fund in all_funds
22+
if fund.open_round and fund.list_on_front_page
23+
]
24+
return JsonResponse(data, safe=False)

hypha/apply/funds/models/applications.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import date
22
from typing import Optional
33

4+
import nh3
45
from django import forms
56
from django.conf import settings
67
from django.contrib.postgres.fields import ArrayField
@@ -73,10 +74,40 @@ def order_by_end_date(self):
7374
return qs.order_by("end_date")
7475

7576

77+
class AsJsonMixin:
78+
@cached_property
79+
def as_json(self):
80+
# Clean the strings in title and description.
81+
title = nh3.clean(self.title, tags=set())
82+
description = nh3.clean(self.description, tags=set())
83+
# If image exist scale it down and convert to webp.
84+
image = (
85+
self.image.get_rendition("max-1200x1200|format-webp|webpquality-60").url
86+
if self.image
87+
else ""
88+
)
89+
# Make sure weight is an int.
90+
weight = int(self.weight)
91+
# If next deadline exist and is set to show, format it as standard iso date.
92+
try:
93+
next_deadline = (
94+
self.next_deadline().isoformat() if self.show_deadline else ""
95+
)
96+
except AttributeError:
97+
next_deadline = ""
98+
return {
99+
"title": title,
100+
"description": description,
101+
"image": image,
102+
"weight": weight,
103+
"next_deadline": next_deadline,
104+
}
105+
106+
76107
@method_decorator(
77108
ratelimit(key="ip", rate=settings.DEFAULT_RATE_LIMIT, method="POST"), name="serve"
78109
)
79-
class ApplicationBase(EmailForm, WorkflowStreamForm): # type: ignore
110+
class ApplicationBase(EmailForm, WorkflowStreamForm, AsJsonMixin): # type: ignore
80111
is_creatable = False
81112

82113
# Adds validation around forms & workflows. Isn't on Workflow class due to not displaying workflow field on Round
@@ -555,7 +586,7 @@ def serve(self, request, *args, **kwargs):
555586
@method_decorator(
556587
ratelimit(key="ip", rate=settings.DEFAULT_RATE_LIMIT, method="POST"), name="serve"
557588
)
558-
class LabBase(EmailForm, WorkflowStreamForm, SubmittableStreamForm): # type: ignore
589+
class LabBase(EmailForm, WorkflowStreamForm, SubmittableStreamForm, AsJsonMixin): # type: ignore
559590
is_creatable = False
560591
submission_class = ApplicationSubmission
561592

hypha/apply/funds/models/mixins.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def process_file_data(self, data, latest_existing_data=None):
110110
new_file_upload = False
111111
for file_data in uploads_data:
112112
# id can be a path or a uuid, where path can exist only for existing files so uuid means a new file
113-
if self._is_valid_uuid(file_data["id"]):
113+
if self._is_valid_uuid(file_data.get("id", None)):
114114
new_file_upload = True # if any new file is uploaded we have to process and save the files
115115

116116
# get existing files from instance
@@ -153,7 +153,7 @@ def _is_valid_uuid(self, value):
153153
try:
154154
uuid.UUID(value)
155155
return True
156-
except ValueError:
156+
except (ValueError, TypeError):
157157
return False
158158

159159
def extract_files(self):

hypha/apply/funds/tables.py

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -235,32 +235,15 @@ def get_used_rounds(request):
235235
return Round.objects.filter(submissions__isnull=False).distinct()
236236

237237

238-
def get_used_rounds_from_dataset(dataset):
239-
return Round.objects.filter(id__in=dataset.values("round")).distinct()
240-
241-
242238
def get_used_funds(request):
243239
# Use page to pick up on both Labs and Funds
244240
return Page.objects.filter(applicationsubmission__isnull=False).distinct()
245241

246242

247-
def get_used_funds_from_dataset(dataset):
248-
return Page.objects.filter(id__in=dataset.values("page")).distinct()
249-
250-
251243
def get_round_leads(request):
252244
return User.objects.filter(submission_lead__isnull=False).distinct()
253245

254246

255-
def get_round_leads_from_dataset(dataset):
256-
return User.objects.filter(id__in=dataset.values("lead")).distinct()
257-
258-
259-
def get_reviewers_from_dataset(dataset):
260-
"""All assigned reviewers, not including Staff and Admin because we want a list of reviewers only"""
261-
return User.objects.filter(id__in=dataset.values("reviewers")).distinct()
262-
263-
264247
def get_screening_statuses(request):
265248
return ScreeningStatus.objects.filter(
266249
id__in=ApplicationSubmission.objects.all()
@@ -269,12 +252,6 @@ def get_screening_statuses(request):
269252
)
270253

271254

272-
def get_screening_statuses_from_dataset(dataset):
273-
return ScreeningStatus.objects.filter(
274-
id__in=dataset.values("screening_statuses__id")
275-
).distinct()
276-
277-
278255
def get_meta_terms(request):
279256
return MetaTerm.objects.filter(
280257
filter_on_dashboard=True,
@@ -284,12 +261,6 @@ def get_meta_terms(request):
284261
)
285262

286263

287-
def get_meta_terms_from_dataset(dataset):
288-
return MetaTerm.objects.filter(
289-
filter_on_dashboard=True, id__in=dataset.values("meta_terms__id")
290-
).distinct()
291-
292-
293264
class MultiCheckboxesMixin(filters.Filter):
294265
def __init__(self, *args, **kwargs):
295266
label = kwargs.get("label")
@@ -359,32 +330,8 @@ def __init__(self, *args, exclude=None, limit_statuses=None, **kwargs):
359330
if exclude is None:
360331
exclude = []
361332

362-
qs = kwargs.get("queryset")
363-
364-
archived = kwargs.pop("archived") if "archived" in kwargs.keys() else None
365-
if archived is not None:
366-
archived = int(archived) if archived else None
367-
368333
super().__init__(*args, **kwargs)
369334

370-
reviewers_qs = get_reviewers_from_dataset(
371-
dataset=qs.exclude(reviewers__isnull=True)
372-
)
373-
if archived is not None and archived == 0:
374-
reviewers_qs = get_reviewers_from_dataset(
375-
dataset=qs.filter(is_archive=archived).exclude(reviewers__isnull=True)
376-
)
377-
qs = qs.filter(is_archive=archived)
378-
379-
self.filters["fund"].queryset = get_used_funds_from_dataset(dataset=qs)
380-
self.filters["round"].queryset = get_used_rounds_from_dataset(dataset=qs)
381-
self.filters["lead"].queryset = get_round_leads_from_dataset(dataset=qs)
382-
self.filters[
383-
"screening_statuses"
384-
].queryset = get_screening_statuses_from_dataset(dataset=qs)
385-
self.filters["reviewers"].queryset = reviewers_qs
386-
self.filters["meta_terms"].queryset = get_meta_terms_from_dataset(dataset=qs)
387-
388335
self.filters["status"] = StatusMultipleChoiceFilter(limit_to=limit_statuses)
389336
self.filters["category_options"].extra["choices"] = [
390337
(option.id, option.value)

hypha/apply/projects/templates/application_projects/partials/contracting_category_documents.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
{% heroicon_mini "information-circle" class="inline align-middle fill-light-blue" aria_hidden=true %}
3232
<a
3333
class="font-semibold border-b-2 border-dashed"
34-
href="{% url 'apply:projects:category_template' pk=object.pk type='contract_document' category_pk=document_category.pk %}"
34+
href="{% url 'apply:projects:category_template' pk=object.submission.pk type='contract_document' category_pk=document_category.pk %}"
3535
target="_blank"
3636
>
3737
{% trans "View template" %}

0 commit comments

Comments
 (0)