Skip to content

Commit c9a09d5

Browse files
committed
Merge branch 'development' into IVS-556-task-pasta-refactoring
2 parents e68e2bb + 8f70d9e commit c9a09d5

30 files changed

+1130
-100
lines changed

.github/workflows/ci_cd.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ on:
1010
- development
1111
paths-ignore:
1212
- 'README.md'
13-
pull_request:
14-
branches:
15-
- development
1613
workflow_dispatch:
1714

1815
jobs:

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ fetch-modules:
7575
git submodule foreach git reset --hard
7676
git submodule update --remote --recursive
7777

78+
# runs end-to-end tests against a local instance of the Validation Service DB
79+
e2e-test: start-infra
80+
cd e2e && npm install && npm run install-playwright && npm run test
81+
82+
e2e-test-report: start-infra
83+
cd e2e && npm install && npm run install-playwright && npm run test:html && npm run test:report
7884

7985
BRANCH ?= main
8086
SUBTREES := \
@@ -97,4 +103,4 @@ checkout:
97103

98104
@echo "==> signatures/store (always on main)"
99105
@( cd backend/apps/ifc_validation/checks/signatures/store && \
100-
git checkout -q main && git pull )
106+
git checkout -q main && git pull )

backend/apps/ifc_validation/admin.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from apps.ifc_validation_models.models import set_user_context
2727

2828
from .tasks import ifc_file_validation_task
29+
from .filters import ProducedByAdvancedFilter, ModelProducedByAdvancedFilter, CreatedByAdvancedFilter
2930

3031
from core import utils
3132
from core.filters import AdvancedDateFilter
@@ -65,7 +66,15 @@ class ValidationRequestAdmin(BaseAdmin, NonAdminAddable):
6566
readonly_fields = ["id", "public_id", "deleted", "file_name", "file", "file_size_text", "duration_text", "started", "completed", "channel", "created", "created_by", "updated", "updated_by"]
6667
date_hierarchy = "created"
6768

68-
list_filter = ["status", "deleted", "model__produced_by", "channel", "created_by", "created_by__useradditionalinfo__is_vendor", "created_by__useradditionalinfo__is_vendor_self_declared", ('created', AdvancedDateFilter)]
69+
list_filter = [
70+
"status",
71+
"deleted",
72+
ModelProducedByAdvancedFilter,
73+
"channel",
74+
CreatedByAdvancedFilter,
75+
"created_by__useradditionalinfo__is_vendor",
76+
"created_by__useradditionalinfo__is_vendor_self_declared",
77+
('created', AdvancedDateFilter)]
6978
search_fields = ('file_name', 'status', 'model__produced_by__name', 'created_by__username', 'updated_by__username')
7079

7180
actions = ["soft_delete_action", "soft_restore_action", "mark_as_failed_action", "restart_processing_action", "hard_delete_action"]
@@ -363,7 +372,12 @@ class ModelAdmin(BaseAdmin, NonAdminAddable):
363372
date_hierarchy = "created"
364373

365374
search_fields = ('file_name', 'schema', 'mvd', 'produced_by__name', 'produced_by__version')
366-
list_filter = ['schema', 'produced_by', ('date', AdvancedDateFilter), ('created', AdvancedDateFilter)]
375+
list_filter = [
376+
'schema',
377+
ProducedByAdvancedFilter,
378+
('date', AdvancedDateFilter),
379+
('created', AdvancedDateFilter)
380+
]
367381

368382
@admin.display(description="File Size", ordering='size')
369383
def size_text(self, obj):
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from django.db.models import Q
2+
from django.utils.translation import gettext_lazy as _
3+
4+
from core.filters import AdvancedInputFilter
5+
6+
7+
class ModelProducedByAdvancedFilter(AdvancedInputFilter):
8+
9+
parameter_name = 'model__produced_by__contains'
10+
title = _('Produced By')
11+
12+
def queryset(self, request, queryset):
13+
14+
term = self.value()
15+
16+
if term is None:
17+
return
18+
19+
any_name = Q()
20+
for bit in term.split():
21+
any_name &= (
22+
Q(model__produced_by__name__icontains=bit) |
23+
Q(model__produced_by__version__icontains=bit) |
24+
Q(model__produced_by__company__name__icontains=bit)
25+
)
26+
27+
return queryset.filter(any_name)
28+
29+
30+
class ProducedByAdvancedFilter(AdvancedInputFilter):
31+
32+
parameter_name = 'produced_by__contains'
33+
title = _('Produced By')
34+
35+
def queryset(self, request, queryset):
36+
37+
term = self.value()
38+
39+
if term is None:
40+
return
41+
42+
any_name = Q()
43+
for bit in term.split():
44+
any_name &= (
45+
Q(produced_by__name__icontains=bit) |
46+
Q(produced_by__version__icontains=bit) |
47+
Q(produced_by__company__name__icontains=bit)
48+
)
49+
50+
return queryset.filter(any_name)
51+
52+
53+
class CreatedByAdvancedFilter(AdvancedInputFilter):
54+
55+
parameter_name = 'created_by__contains'
56+
title = _('Created By')
57+
58+
def queryset(self, request, queryset):
59+
60+
term = self.value()
61+
62+
if term is None:
63+
return
64+
65+
any = Q()
66+
for bit in term.split():
67+
any &= (
68+
Q(created_by__username=bit) |
69+
Q(created_by__last_name__icontains=bit) |
70+
Q(created_by__first_name__icontains=bit)
71+
)
72+
73+
return queryset.filter(any)

backend/apps/ifc_validation/tasks/email_tasks.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,12 @@ def send_acknowledgement_admin_email_task(id, file_name):
5555
'USER_FULL_NAME': user.get_full_name(),
5656
'USER_EMAIL': user.email,
5757
'PUBLIC_URL': PUBLIC_URL,
58-
'ENVIRONMENT': ENVIRONMENT
58+
'ENVIRONMENT': ENVIRONMENT,
59+
'CHANNEL': request.channel
5960
}
6061
to = ADMIN_EMAIL
6162
body_html = render_to_string("validation_ack_admin_email.html", merge_data)
62-
body_text = f"User uploaded {{merge_data.NUMBER_OF_FILES}} file(s) in {{merge_data.ENVIRONMENT}}."
63+
body_text = f"User submitted {{merge_data.NUMBER_OF_FILES}} file(s) in {{merge_data.ENVIRONMENT}}."
6364
subject = get_title_from_html(body_html)
6465

6566
# queue for sending
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<html>
22
<head>
3-
<title>Validation Service - User uploaded {{NUMBER_OF_FILES}} file(s)</title>
3+
<title>Validation Service - User submitted {{NUMBER_OF_FILES}} file(s)</title>
44
</head>
55
<body>
66
<div>
7-
{{NUMBER_OF_FILES}} file(s) were uploaded by {{USER_FULL_NAME}} ({{USER_EMAIL}}): {{FILE_NAMES}} in {{ENVIRONMENT}}.
7+
{{NUMBER_OF_FILES}} file(s) were submitted by {{USER_FULL_NAME}} ({{USER_EMAIL}}): {{FILE_NAMES}} in {{ENVIRONMENT}} via {{CHANNEL}}.
88
</div>
99
</body>
1010
</html>

backend/apps/ifc_validation/templates/validation_failed_admin_email.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
</head>
55
<body>
66
<div>
7-
{{NUMBER_OF_FILES}} file(s) uploaded by {{USER_FULL_NAME}} ({{USER_EMAIL}}) failed to process: {{FILE_NAMES}} in {{ENVIRONMENT}}.
7+
{{NUMBER_OF_FILES}} file(s) submitted by {{USER_FULL_NAME}} ({{USER_EMAIL}}) failed to process: {{FILE_NAMES}} in {{ENVIRONMENT}}.
88
</div>
99
</body>
1010
</html>

backend/apps/ifc_validation/urls.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from django.urls import path
1+
from django.urls import path, re_path
22

33
from .views import ValidationRequestListAPIView, ValidationRequestDetailAPIView
44
from .views import ValidationTaskListAPIView, ValidationTaskDetailAPIView
@@ -11,14 +11,15 @@
1111
urlpatterns = [
1212

1313
# REST API
14-
path('validationrequest/', ValidationRequestListAPIView.as_view()),
15-
path('validationrequest/<str:id>/', ValidationRequestDetailAPIView.as_view()),
16-
path('validationtask/', ValidationTaskListAPIView.as_view()),
17-
path('validationtask/<str:id>/', ValidationTaskDetailAPIView.as_view()),
18-
path('validationoutcome/', ValidationOutcomeListAPIView.as_view()),
19-
path('validationoutcome/<str:id>/', ValidationOutcomeDetailAPIView.as_view()),
20-
path('model/', ModelListAPIView.as_view()),
21-
path('model/<str:id>/', ModelDetailAPIView.as_view()),
14+
# using re_path to make trailing slashes optional
15+
re_path(r'validationrequest/?$', ValidationRequestListAPIView.as_view()),
16+
re_path(r'validationrequest/(?P<id>[\w-]+)/?$', ValidationRequestDetailAPIView.as_view()),
17+
re_path(r'validationtask/?$', ValidationTaskListAPIView.as_view()),
18+
re_path(r'validationtask/(?P<id>[\w-]+)/?$', ValidationTaskDetailAPIView.as_view()),
19+
re_path(r'validationoutcome/?$', ValidationOutcomeListAPIView.as_view()),
20+
re_path(r'validationoutcome/(?P<id>[\w-]+)/?$', ValidationOutcomeDetailAPIView.as_view()),
21+
re_path(r'model/?$', ModelListAPIView.as_view()),
22+
re_path(r'model/(?P<id>[\w-]+)/?$', ModelDetailAPIView.as_view()),
2223

2324
# Django Admin charts
2425
path("chart/filter-options/", charts.get_filter_options),

backend/apps/ifc_validation/views.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44

55
from django.db import transaction
66
from core.utils import get_client_ip_address
7-
from core.settings import MAX_FILES_PER_UPLOAD
7+
from core.settings import MAX_FILES_PER_UPLOAD, MAX_FILE_SIZE_IN_MB
88

99
from rest_framework import status
1010
from rest_framework.parsers import FormParser, MultiPartParser
1111
from rest_framework.response import Response
1212
from rest_framework.views import APIView
1313
from rest_framework.exceptions import APIException
1414
from rest_framework.permissions import IsAuthenticated
15-
from rest_framework.authentication import SessionAuthentication, BasicAuthentication, TokenAuthentication
1615
from rest_framework.throttling import UserRateThrottle
1716
from rest_framework.throttling import ScopedRateThrottle
1817
from rest_framework.decorators import throttle_classes
@@ -33,7 +32,6 @@
3332
class ValidationRequestDetailAPIView(APIView):
3433

3534
queryset = ValidationRequest.objects.all()
36-
authentication_classes = [SessionAuthentication, TokenAuthentication, BasicAuthentication]
3735
permission_classes = [IsAuthenticated]
3836
parser_classes = (MultiPartParser, FormParser)
3937
serializer_class = ValidationRequestSerializer
@@ -78,7 +76,6 @@ def delete(self, request, id, *args, **kwargs):
7876
class ValidationRequestListAPIView(APIView):
7977

8078
queryset = ValidationRequest.objects.all()
81-
authentication_classes = [SessionAuthentication, TokenAuthentication, BasicAuthentication]
8279
permission_classes = [IsAuthenticated]
8380
parser_classes = (MultiPartParser, FormParser)
8481
serializer_class = ValidationRequestSerializer
@@ -90,7 +87,6 @@ def get_throttles(self):
9087
"""
9188
Applies scoped throttling only for POST requests (aka submitting a new Validation Request).
9289
"""
93-
logger.info('*** ' + self.request.method)
9490
return [ScopedRateThrottle()] if self.request.method == 'POST' else [UserRateThrottle()]
9591

9692
@extend_schema(operation_id='validationrequest_list')
@@ -103,6 +99,10 @@ def get(self, request, *args, **kwargs):
10399
logger.info('API request - User IP: %s Request Method: %s Request URL: %s Content-Length: %s' % (get_client_ip_address(request), request.method, request.path, request.META.get('CONTENT_LENGTH')))
104100

105101
user_requests = ValidationRequest.objects.filter(created_by__id=request.user.id, deleted=False)
102+
public_id = self.request.query_params.get('public_id', None)
103+
if public_id:
104+
user_requests = user_requests.filter(id=ValidationRequest.to_private_id(public_id))
105+
106106
serializer = self.serializer_class(user_requests, many=True)
107107
return Response(serializer.data, status=status.HTTP_200_OK)
108108

@@ -132,14 +132,29 @@ def post(self, request, *args, **kwargs):
132132
if file_i is not None: files += file_i
133133
logger.info(f"Received {len(files)} file(s) - files: {files}")
134134

135+
# only accept one file (for now)
136+
if len(files) != 1:
137+
data = {'message': f"Only one file can be uploaded at a time."}
138+
return Response(data, status=status.HTTP_400_BAD_REQUEST)
139+
135140
# retrieve file size and save
136141
uploaded_file = serializer.validated_data
137142
logger.info(f'uploaded_file = {uploaded_file}')
138143
f = uploaded_file['file']
139144
f.seek(0, 2)
140145
file_length = f.tell()
141146
file_name = uploaded_file['file_name']
142-
logger.info(f"file_length for uploaded file {file_name} = {file_length}")
147+
logger.info(f"file_length for uploaded file {file_name} = {file_length} ({file_length / (1024*1024)} MB)")
148+
149+
# check if file name ends with .ifc
150+
if not file_name.lower().endswith('.ifc'):
151+
data = {'file_name': "File name must end with '.ifc'."}
152+
return Response(data, status=status.HTTP_400_BAD_REQUEST)
153+
154+
# apply file size limit
155+
if file_length > MAX_FILE_SIZE_IN_MB * 1024 * 1024:
156+
data = {'message': f"File size exceeds allowed file size limit ({MAX_FILE_SIZE_IN_MB} MB)."}
157+
return Response(data, status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE)
143158

144159
# can't use this, file hasn't been saved yet
145160
#file = os.path.join(MEDIA_ROOT, uploaded_file['file_name'])
@@ -167,7 +182,6 @@ def submit_task(instance):
167182
class ValidationTaskDetailAPIView(APIView):
168183

169184
queryset = ValidationTask.objects.all()
170-
authentication_classes = [SessionAuthentication, BasicAuthentication, TokenAuthentication]
171185
permission_classes = [IsAuthenticated]
172186
serializer_class = ValidationTaskSerializer
173187
throttle_classes = [UserRateThrottle]
@@ -193,7 +207,6 @@ def get(self, request, id, *args, **kwargs):
193207
class ValidationTaskListAPIView(APIView):
194208

195209
queryset = ValidationTask.objects.all()
196-
authentication_classes = [SessionAuthentication, BasicAuthentication, TokenAuthentication]
197210
permission_classes = [IsAuthenticated]
198211
serializer_class = ValidationTaskSerializer
199212
throttle_classes = [UserRateThrottle]
@@ -224,7 +237,6 @@ def get(self, request, *args, **kwargs):
224237
class ValidationOutcomeDetailAPIView(APIView):
225238

226239
queryset = ValidationOutcome.objects.all()
227-
authentication_classes = [SessionAuthentication, BasicAuthentication, TokenAuthentication]
228240
permission_classes = [IsAuthenticated]
229241
serializer_class = ValidationOutcomeSerializer
230242
throttle_classes = [UserRateThrottle]
@@ -250,7 +262,6 @@ def get(self, request, id, *args, **kwargs):
250262
class ValidationOutcomeListAPIView(APIView):
251263

252264
queryset = ValidationOutcome.objects.all()
253-
authentication_classes = [SessionAuthentication, BasicAuthentication, TokenAuthentication]
254265
permission_classes = [IsAuthenticated]
255266
serializer_class = ValidationOutcomeSerializer
256267
throttle_classes = [UserRateThrottle]
@@ -285,7 +296,6 @@ def get(self, request, *args, **kwargs):
285296
class ModelDetailAPIView(APIView):
286297

287298
queryset = Model.objects.all()
288-
authentication_classes = [SessionAuthentication, BasicAuthentication, TokenAuthentication]
289299
permission_classes = [IsAuthenticated]
290300
serializer_class = ModelSerializer
291301
throttle_classes = [UserRateThrottle]
@@ -311,7 +321,6 @@ def get(self, request, id, *args, **kwargs):
311321
class ModelListAPIView(APIView):
312322

313323
queryset = Model.objects.all()
314-
authentication_classes = [SessionAuthentication, BasicAuthentication, TokenAuthentication]
315324
permission_classes = [IsAuthenticated]
316325
serializer_class = ModelSerializer
317326
throttle_classes = [UserRateThrottle]

0 commit comments

Comments
 (0)