ethyca-fides 2.67.2b2__py2.py3-none-any.whl → 2.67.2rc0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ethyca-fides might be problematic. Click here for more details.
- {ethyca_fides-2.67.2b2.dist-info → ethyca_fides-2.67.2rc0.dist-info}/METADATA +2 -2
- {ethyca_fides-2.67.2b2.dist-info → ethyca_fides-2.67.2rc0.dist-info}/RECORD +107 -107
- fides/_version.py +3 -3
- fides/api/db/crud.py +41 -24
- fides/api/graph/traversal.py +1 -1
- fides/api/main.py +1 -2
- fides/api/models/detection_discovery/core.py +20 -33
- fides/api/models/manual_task/manual_task.py +6 -3
- fides/api/models/privacy_request/privacy_request.py +0 -78
- fides/api/service/privacy_request/dsr_package/dsr_report_builder.py +19 -3
- fides/api/task/create_request_tasks.py +3 -5
- fides/api/task/filter_results.py +1 -1
- fides/api/task/manual/manual_task_address.py +2 -4
- fides/api/task/manual/manual_task_graph_task.py +83 -87
- fides/api/task/manual/manual_task_utils.py +201 -84
- fides/api/tasks/storage.py +0 -3
- fides/api/util/aws_util.py +13 -1
- fides/api/util/storage_util.py +0 -19
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/{gqCWjlCr8J6bJ_5t4lTGZ → Y7C7V1jdXK7rWXDIdtD13}/_buildManifest.js +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-330475705adbd36f.js → [id]-e0a755c69081fffa.js} +1 -1
- fides/ui-build/static/admin/add-systems/manual.html +1 -1
- fides/ui-build/static/admin/add-systems/multiple.html +1 -1
- fides/ui-build/static/admin/add-systems.html +1 -1
- fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
- fides/ui-build/static/admin/consent/configure.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
- fides/ui-build/static/admin/consent/properties.html +1 -1
- fides/ui-build/static/admin/consent/reporting.html +1 -1
- fides/ui-build/static/admin/consent.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
- fides/ui-build/static/admin/data-catalog.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
- fides/ui-build/static/admin/data-discovery/activity.html +1 -1
- fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/detection.html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
- fides/ui-build/static/admin/datamap.html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
- fides/ui-build/static/admin/dataset/new.html +1 -1
- fides/ui-build/static/admin/dataset.html +1 -1
- fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
- fides/ui-build/static/admin/datastore-connection/new.html +1 -1
- fides/ui-build/static/admin/datastore-connection.html +1 -1
- fides/ui-build/static/admin/index.html +1 -1
- fides/ui-build/static/admin/integrations/[id].html +1 -1
- fides/ui-build/static/admin/integrations.html +1 -1
- fides/ui-build/static/admin/lib/fides-preview.js +1 -1
- fides/ui-build/static/admin/login/[provider].html +1 -1
- fides/ui-build/static/admin/login.html +1 -1
- fides/ui-build/static/admin/messaging/[id].html +1 -1
- fides/ui-build/static/admin/messaging/add-template.html +1 -1
- fides/ui-build/static/admin/messaging.html +1 -1
- fides/ui-build/static/admin/poc/ant-components.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
- fides/ui-build/static/admin/poc/forms.html +1 -1
- fides/ui-build/static/admin/poc/table-migration.html +1 -1
- fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
- fides/ui-build/static/admin/privacy-requests.html +1 -1
- fides/ui-build/static/admin/properties/[id].html +1 -1
- fides/ui-build/static/admin/properties/add-property.html +1 -1
- fides/ui-build/static/admin/properties.html +1 -1
- fides/ui-build/static/admin/reporting/datamap.html +1 -1
- fides/ui-build/static/admin/settings/about/alpha.html +1 -1
- fides/ui-build/static/admin/settings/about.html +1 -1
- fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
- fides/ui-build/static/admin/settings/consent.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields.html +1 -1
- fides/ui-build/static/admin/settings/domain-records.html +1 -1
- fides/ui-build/static/admin/settings/domains.html +1 -1
- fides/ui-build/static/admin/settings/email-templates.html +1 -1
- fides/ui-build/static/admin/settings/locations.html +1 -1
- fides/ui-build/static/admin/settings/organization.html +1 -1
- fides/ui-build/static/admin/settings/regulations.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id].html +1 -1
- fides/ui-build/static/admin/systems.html +1 -1
- fides/ui-build/static/admin/taxonomy.html +1 -1
- fides/ui-build/static/admin/user-management/new.html +1 -1
- fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
- fides/ui-build/static/admin/user-management.html +1 -1
- {ethyca_fides-2.67.2b2.dist-info → ethyca_fides-2.67.2rc0.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.67.2b2.dist-info → ethyca_fides-2.67.2rc0.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.67.2b2.dist-info → ethyca_fides-2.67.2rc0.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.67.2b2.dist-info → ethyca_fides-2.67.2rc0.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{gqCWjlCr8J6bJ_5t4lTGZ → Y7C7V1jdXK7rWXDIdtD13}/_ssgManifest.js +0 -0
|
@@ -383,13 +383,15 @@ class ManualTaskInstance(Base):
|
|
|
383
383
|
|
|
384
384
|
@property
|
|
385
385
|
def incomplete_fields(self) -> list["ManualTaskConfigField"]:
|
|
386
|
-
"""Get all fields that
|
|
386
|
+
"""Get all fields that haven't been completed yet.
|
|
387
|
+
A field is considered incomplete if:
|
|
388
|
+
1. It's required and has no submission
|
|
387
389
|
Returns:
|
|
388
390
|
list[ManualTaskConfigField]: List of incomplete fields
|
|
389
391
|
"""
|
|
390
392
|
return [
|
|
391
393
|
field
|
|
392
|
-
for field in self.
|
|
394
|
+
for field in self.required_fields
|
|
393
395
|
if not self.get_submission_for_field(field.id)
|
|
394
396
|
]
|
|
395
397
|
|
|
@@ -399,7 +401,8 @@ class ManualTaskInstance(Base):
|
|
|
399
401
|
return [
|
|
400
402
|
field
|
|
401
403
|
for field in self.config.field_definitions
|
|
402
|
-
if
|
|
404
|
+
if field.field_metadata.get("required", False)
|
|
405
|
+
and self.get_submission_for_field(field.id)
|
|
403
406
|
]
|
|
404
407
|
|
|
405
408
|
def get_submission_for_field(
|
|
@@ -47,16 +47,8 @@ from fides.api.models.attachment import (
|
|
|
47
47
|
from fides.api.models.audit_log import AuditLog
|
|
48
48
|
from fides.api.models.client import ClientDetail
|
|
49
49
|
from fides.api.models.comment import Comment, CommentReference, CommentReferenceType
|
|
50
|
-
from fides.api.models.connectionconfig import ConnectionConfig
|
|
51
50
|
from fides.api.models.fides_user import FidesUser
|
|
52
51
|
from fides.api.models.field_types import EncryptedLargeDataDescriptor
|
|
53
|
-
from fides.api.models.manual_task import (
|
|
54
|
-
ManualTask,
|
|
55
|
-
ManualTaskConfig,
|
|
56
|
-
ManualTaskConfigurationType,
|
|
57
|
-
ManualTaskEntityType,
|
|
58
|
-
ManualTaskInstance,
|
|
59
|
-
)
|
|
60
52
|
from fides.api.models.manual_webhook import AccessManualWebhook
|
|
61
53
|
from fides.api.models.masking_secret import MaskingSecret
|
|
62
54
|
from fides.api.models.policy import (
|
|
@@ -208,14 +200,6 @@ class PrivacyRequest(
|
|
|
208
200
|
viewonly=True,
|
|
209
201
|
uselist=True,
|
|
210
202
|
)
|
|
211
|
-
manual_task_instances = relationship(
|
|
212
|
-
"ManualTaskInstance",
|
|
213
|
-
lazy="select",
|
|
214
|
-
passive_deletes="all",
|
|
215
|
-
primaryjoin="and_(ManualTaskInstance.entity_id==foreign(PrivacyRequest.id), "
|
|
216
|
-
"ManualTaskInstance.entity_type=='privacy_request')",
|
|
217
|
-
uselist=True,
|
|
218
|
-
)
|
|
219
203
|
property_id = Column(String, nullable=True)
|
|
220
204
|
|
|
221
205
|
cancel_reason = Column(String(200))
|
|
@@ -1191,68 +1175,6 @@ class PrivacyRequest(
|
|
|
1191
1175
|
db, manual_webhook_id, "erasure_manual_webhook"
|
|
1192
1176
|
)
|
|
1193
1177
|
|
|
1194
|
-
def create_manual_task_instances(
|
|
1195
|
-
self, db: Session, connection_configs_with_manual_tasks: list[ConnectionConfig]
|
|
1196
|
-
) -> list[ManualTaskInstance]:
|
|
1197
|
-
"""Create ManualTaskInstance entries for all active manual tasks relevant to a privacy request."""
|
|
1198
|
-
# Early return if no relevant policy rules
|
|
1199
|
-
policy_rules = {
|
|
1200
|
-
ActionType.access: bool(
|
|
1201
|
-
self.policy.get_rules_for_action(action_type=ActionType.access)
|
|
1202
|
-
),
|
|
1203
|
-
ActionType.erasure: bool(
|
|
1204
|
-
self.policy.get_rules_for_action(action_type=ActionType.erasure)
|
|
1205
|
-
),
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
if not any(policy_rules.values()):
|
|
1209
|
-
return []
|
|
1210
|
-
|
|
1211
|
-
# Build configuration types using list comprehension
|
|
1212
|
-
config_types = [
|
|
1213
|
-
(
|
|
1214
|
-
ManualTaskConfigurationType.access_privacy_request
|
|
1215
|
-
if action_type == ActionType.access
|
|
1216
|
-
else ManualTaskConfigurationType.erasure_privacy_request
|
|
1217
|
-
)
|
|
1218
|
-
for action_type, has_rules in policy_rules.items()
|
|
1219
|
-
if has_rules
|
|
1220
|
-
]
|
|
1221
|
-
|
|
1222
|
-
# Get all relevant manual tasks and configs in one query
|
|
1223
|
-
connection_config_ids = [cc.id for cc in connection_configs_with_manual_tasks]
|
|
1224
|
-
manual_tasks_with_configs = (
|
|
1225
|
-
db.query(ManualTask, ManualTaskConfig)
|
|
1226
|
-
.join(ManualTaskConfig, ManualTask.id == ManualTaskConfig.task_id)
|
|
1227
|
-
.filter(
|
|
1228
|
-
ManualTask.parent_entity_id.in_(connection_config_ids),
|
|
1229
|
-
ManualTask.parent_entity_type == "connection_config",
|
|
1230
|
-
ManualTaskConfig.is_current.is_(True),
|
|
1231
|
-
ManualTaskConfig.config_type.in_(config_types),
|
|
1232
|
-
)
|
|
1233
|
-
.all()
|
|
1234
|
-
)
|
|
1235
|
-
|
|
1236
|
-
# Get existing config IDs to avoid duplicates
|
|
1237
|
-
existing_config_ids = {
|
|
1238
|
-
instance.config_id for instance in self.manual_task_instances
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
# Create instances using list comprehension and filter out existing ones
|
|
1242
|
-
return [
|
|
1243
|
-
ManualTaskInstance.create(
|
|
1244
|
-
db=db,
|
|
1245
|
-
data={
|
|
1246
|
-
"entity_id": self.id,
|
|
1247
|
-
"entity_type": ManualTaskEntityType.privacy_request,
|
|
1248
|
-
"task_id": manual_task.id,
|
|
1249
|
-
"config_id": config.id,
|
|
1250
|
-
},
|
|
1251
|
-
)
|
|
1252
|
-
for manual_task, config in manual_tasks_with_configs
|
|
1253
|
-
if config.id not in existing_config_ids
|
|
1254
|
-
]
|
|
1255
|
-
|
|
1256
1178
|
def get_existing_request_task(
|
|
1257
1179
|
self,
|
|
1258
1180
|
db: Session,
|
|
@@ -13,7 +13,7 @@ from loguru import logger
|
|
|
13
13
|
|
|
14
14
|
from fides.api.models.privacy_request import PrivacyRequest
|
|
15
15
|
from fides.api.schemas.policy import ActionType
|
|
16
|
-
from fides.api.util.storage_util import StorageJSONEncoder
|
|
16
|
+
from fides.api.util.storage_util import StorageJSONEncoder
|
|
17
17
|
|
|
18
18
|
DSR_DIRECTORY = Path(__file__).parent.resolve()
|
|
19
19
|
|
|
@@ -204,7 +204,7 @@ class DsrReportBuilder:
|
|
|
204
204
|
|
|
205
205
|
file_size = attachment.get("file_size")
|
|
206
206
|
if isinstance(file_size, (int, float)):
|
|
207
|
-
file_size =
|
|
207
|
+
file_size = self._format_size(float(file_size))
|
|
208
208
|
else:
|
|
209
209
|
file_size = "Unknown"
|
|
210
210
|
|
|
@@ -321,6 +321,22 @@ class DsrReportBuilder:
|
|
|
321
321
|
|
|
322
322
|
return datasets
|
|
323
323
|
|
|
324
|
+
def _format_size(self, size_bytes: float) -> str:
|
|
325
|
+
"""
|
|
326
|
+
Format size in bytes to human readable format.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
size_bytes: Size in bytes
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Formatted string with appropriate unit (B, KB, MB, GB)
|
|
333
|
+
"""
|
|
334
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
|
335
|
+
if size_bytes < 1024.0:
|
|
336
|
+
return f"{size_bytes:.1f} {unit}"
|
|
337
|
+
size_bytes /= 1024.0
|
|
338
|
+
return f"{size_bytes:.1f} TB"
|
|
339
|
+
|
|
324
340
|
def generate(self) -> BytesIO:
|
|
325
341
|
"""
|
|
326
342
|
Processes the request and DSR data to build zip file containing the DSR report.
|
|
@@ -379,7 +395,7 @@ class DsrReportBuilder:
|
|
|
379
395
|
|
|
380
396
|
# Calculate time taken and file size
|
|
381
397
|
time_taken = time_module.time() - start_time
|
|
382
|
-
file_size =
|
|
398
|
+
file_size = self._format_size(float(len(self.baos.getvalue())))
|
|
383
399
|
|
|
384
400
|
logger.bind(time_to_generate=time_taken, dsr_package_size=file_size).info(
|
|
385
401
|
"DSR report generation complete."
|
|
@@ -35,7 +35,7 @@ from fides.api.task.deprecated_graph_task import format_data_use_map_for_caching
|
|
|
35
35
|
from fides.api.task.execute_request_tasks import log_task_queued, queue_request_task
|
|
36
36
|
from fides.api.task.manual.manual_task_address import ManualTaskAddress
|
|
37
37
|
from fides.api.task.manual.manual_task_utils import (
|
|
38
|
-
|
|
38
|
+
create_manual_task_instances_for_privacy_request,
|
|
39
39
|
)
|
|
40
40
|
from fides.api.util.logger_context_utils import log_context
|
|
41
41
|
|
|
@@ -92,7 +92,7 @@ def build_access_networkx_digraph(
|
|
|
92
92
|
manual_nodes = [
|
|
93
93
|
addr
|
|
94
94
|
for addr in traversal_nodes.keys()
|
|
95
|
-
if ManualTaskAddress.
|
|
95
|
+
if addr.collection == ManualTaskAddress.MANUAL_DATA_COLLECTION
|
|
96
96
|
]
|
|
97
97
|
for manual_node in manual_nodes:
|
|
98
98
|
networkx_graph.add_edge(ROOT_COLLECTION_ADDRESS, manual_node)
|
|
@@ -472,9 +472,7 @@ def run_access_request(
|
|
|
472
472
|
)
|
|
473
473
|
|
|
474
474
|
# Snapshot manual task field instances for this privacy request
|
|
475
|
-
privacy_request
|
|
476
|
-
session, get_connection_configs_with_manual_tasks(session)
|
|
477
|
-
)
|
|
475
|
+
create_manual_task_instances_for_privacy_request(session, privacy_request)
|
|
478
476
|
|
|
479
477
|
# Save Access Request Tasks to the database
|
|
480
478
|
ready_tasks = persist_new_access_request_tasks(
|
fides/api/task/filter_results.py
CHANGED
|
@@ -39,7 +39,7 @@ def filter_data_categories(
|
|
|
39
39
|
continue
|
|
40
40
|
|
|
41
41
|
# Skip manual task data - it doesn't need filtering since it's controlled by field definitions
|
|
42
|
-
if ManualTaskAddress.
|
|
42
|
+
if f":{ManualTaskAddress.MANUAL_DATA_COLLECTION}" in node_address:
|
|
43
43
|
filtered_access_results[node_address].extend(results)
|
|
44
44
|
continue
|
|
45
45
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from typing import Union
|
|
2
|
-
|
|
3
1
|
from fides.api.graph.config import CollectionAddress
|
|
4
2
|
|
|
5
3
|
|
|
@@ -22,7 +20,7 @@ class ManualTaskAddress:
|
|
|
22
20
|
return collection_name == ManualTaskAddress.MANUAL_DATA_COLLECTION
|
|
23
21
|
|
|
24
22
|
@staticmethod
|
|
25
|
-
def is_manual_task_address(address:
|
|
23
|
+
def is_manual_task_address(address: CollectionAddress) -> bool:
|
|
26
24
|
"""Check if address represents manual task data"""
|
|
27
25
|
if isinstance(address, str):
|
|
28
26
|
# Handle string format "connection_key:collection_name"
|
|
@@ -35,7 +33,7 @@ class ManualTaskAddress:
|
|
|
35
33
|
return ManualTaskAddress._is_manual_data_collection(address.collection)
|
|
36
34
|
|
|
37
35
|
@staticmethod
|
|
38
|
-
def get_connection_key(address:
|
|
36
|
+
def get_connection_key(address: CollectionAddress) -> str:
|
|
39
37
|
"""Extract connection config key from manual task address"""
|
|
40
38
|
if not ManualTaskAddress.is_manual_task_address(address):
|
|
41
39
|
raise ValueError(f"Not a manual task address: {address}")
|
|
@@ -7,11 +7,11 @@ from fides.api.common_exceptions import AwaitingAsyncTaskCallback
|
|
|
7
7
|
from fides.api.models.attachment import AttachmentType
|
|
8
8
|
from fides.api.models.manual_task import (
|
|
9
9
|
ManualTask,
|
|
10
|
+
ManualTaskConfig,
|
|
10
11
|
ManualTaskConfigurationType,
|
|
11
12
|
ManualTaskEntityType,
|
|
12
13
|
ManualTaskFieldType,
|
|
13
14
|
ManualTaskInstance,
|
|
14
|
-
ManualTaskSubmission,
|
|
15
15
|
StatusType,
|
|
16
16
|
)
|
|
17
17
|
from fides.api.models.privacy_request import PrivacyRequest
|
|
@@ -23,7 +23,6 @@ from fides.api.task.manual.manual_task_utils import (
|
|
|
23
23
|
get_manual_task_for_connection_config,
|
|
24
24
|
)
|
|
25
25
|
from fides.api.util.collection_util import Row
|
|
26
|
-
from fides.api.util.storage_util import format_size
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
class ManualTaskGraphTask(GraphTask):
|
|
@@ -123,36 +122,29 @@ class ManualTaskGraphTask(GraphTask):
|
|
|
123
122
|
# request has started, while allowing different config types (access vs erasure)
|
|
124
123
|
# to have separate instances.
|
|
125
124
|
# ------------------------------------------------------------------
|
|
126
|
-
existing_task_instance =
|
|
127
|
-
(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
existing_task_instance = (
|
|
126
|
+
db.query(ManualTaskInstance)
|
|
127
|
+
.join(ManualTaskInstance.config) # Join to access config information
|
|
128
|
+
.filter(
|
|
129
|
+
ManualTaskInstance.task_id == manual_task.id,
|
|
130
|
+
ManualTaskInstance.entity_id == privacy_request.id,
|
|
131
|
+
ManualTaskInstance.entity_type == ManualTaskEntityType.privacy_request,
|
|
132
|
+
# Only check for instances of the same config type
|
|
133
|
+
ManualTaskConfig.config_type == allowed_config_type,
|
|
134
|
+
)
|
|
135
|
+
.first()
|
|
134
136
|
)
|
|
135
137
|
if existing_task_instance:
|
|
136
138
|
# An instance already exists for this privacy request and config type – no need
|
|
137
139
|
# to create another one tied to a newer config version.
|
|
138
140
|
return
|
|
139
141
|
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
for config in sorted(
|
|
146
|
-
manual_task.configs,
|
|
147
|
-
key=lambda c: c.version if hasattr(c, "version") else 0,
|
|
148
|
-
reverse=True,
|
|
149
|
-
)
|
|
150
|
-
if config.is_current and config.config_type == allowed_config_type
|
|
151
|
-
),
|
|
152
|
-
None,
|
|
153
|
-
)
|
|
142
|
+
# Check each active config for instances (now we know none exist yet for this config type)
|
|
143
|
+
for config in manual_task.configs:
|
|
144
|
+
if not config.is_current or config.config_type != allowed_config_type:
|
|
145
|
+
# Skip configs that are not current or not relevant for this request type
|
|
146
|
+
continue
|
|
154
147
|
|
|
155
|
-
if config:
|
|
156
148
|
ManualTaskInstance.create(
|
|
157
149
|
db=db,
|
|
158
150
|
data={
|
|
@@ -164,6 +156,7 @@ class ManualTaskGraphTask(GraphTask):
|
|
|
164
156
|
},
|
|
165
157
|
)
|
|
166
158
|
|
|
159
|
+
# pylint: disable=too-many-branches,too-many-nested-blocks
|
|
167
160
|
def _get_submitted_data(
|
|
168
161
|
self,
|
|
169
162
|
db: Session,
|
|
@@ -175,90 +168,93 @@ class ManualTaskGraphTask(GraphTask):
|
|
|
175
168
|
Check if all manual task instances have submissions for ALL fields and return aggregated data
|
|
176
169
|
Returns None if any field submissions are missing (all fields must be completed or skipped)
|
|
177
170
|
"""
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
171
|
+
aggregated_data: dict[str, Any] = {}
|
|
172
|
+
|
|
173
|
+
def _format_size(size_bytes: int) -> str:
|
|
174
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
175
|
+
size = float(size_bytes)
|
|
176
|
+
for unit in units:
|
|
177
|
+
if size < 1024.0:
|
|
178
|
+
return f"{size:.1f} {unit}"
|
|
179
|
+
size /= 1024.0
|
|
180
|
+
return f"{size:.1f} PB"
|
|
181
|
+
|
|
182
|
+
candidate_instances: list[ManualTaskInstance] = (
|
|
183
|
+
db.query(ManualTaskInstance)
|
|
184
|
+
.filter(
|
|
185
|
+
ManualTaskInstance.task_id == manual_task.id,
|
|
186
|
+
ManualTaskInstance.entity_id == privacy_request.id,
|
|
187
|
+
ManualTaskInstance.entity_type == ManualTaskEntityType.privacy_request,
|
|
188
|
+
)
|
|
189
|
+
.all()
|
|
190
|
+
)
|
|
184
191
|
|
|
185
192
|
if not candidate_instances:
|
|
186
193
|
return None # No instance yet for this manual task
|
|
187
194
|
|
|
188
|
-
# Check for incomplete fields and update status in single pass
|
|
189
195
|
for inst in candidate_instances:
|
|
190
|
-
|
|
196
|
+
# Skip instances tied to other request types
|
|
197
|
+
if not inst.config or inst.config.config_type != allowed_config_type:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
all_fields = inst.config.field_definitions or []
|
|
201
|
+
|
|
202
|
+
# Every field must have a submission
|
|
203
|
+
if not all(inst.get_submission_for_field(f.id) for f in all_fields):
|
|
191
204
|
return None # At least one instance still incomplete
|
|
192
205
|
|
|
193
|
-
#
|
|
206
|
+
# Ensure status set
|
|
194
207
|
if inst.status != StatusType.completed:
|
|
195
208
|
inst.status = StatusType.completed
|
|
196
209
|
inst.save(db)
|
|
197
210
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def _aggregate_submission_data(
|
|
203
|
-
self, instances: list[ManualTaskInstance]
|
|
204
|
-
) -> dict[str, Any]:
|
|
205
|
-
"""Aggregate submission data from all instances into a single dictionary."""
|
|
206
|
-
aggregated_data: dict[str, Any] = {}
|
|
207
|
-
|
|
208
|
-
for inst in instances:
|
|
209
|
-
# Filter valid submissions and process them
|
|
210
|
-
valid_submissions = (
|
|
211
|
-
submission
|
|
212
|
-
for submission in inst.submissions
|
|
213
|
-
if (
|
|
214
|
-
submission.field
|
|
215
|
-
and submission.field.field_key
|
|
216
|
-
and isinstance(submission.data, dict)
|
|
217
|
-
)
|
|
218
|
-
)
|
|
211
|
+
# Aggregate submission data from this instance
|
|
212
|
+
for submission in inst.submissions:
|
|
213
|
+
if not submission.field or not submission.field.field_key:
|
|
214
|
+
continue
|
|
219
215
|
|
|
220
|
-
for submission in valid_submissions:
|
|
221
216
|
field_key = submission.field.field_key
|
|
222
|
-
# We already checked isinstance(submission.data, dict) in valid_submissions
|
|
223
|
-
data_dict: dict[str, Any] = submission.data # type: ignore[assignment]
|
|
224
|
-
field_type = data_dict.get("field_type")
|
|
225
217
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
self._process_attachment_field(submission)
|
|
229
|
-
if field_type == ManualTaskFieldType.attachment.value
|
|
230
|
-
else data_dict.get("value")
|
|
231
|
-
)
|
|
218
|
+
if not isinstance(submission.data, dict):
|
|
219
|
+
continue
|
|
232
220
|
|
|
233
|
-
|
|
221
|
+
data_dict: dict[str, Any] = submission.data
|
|
234
222
|
|
|
235
|
-
|
|
236
|
-
self, submission: ManualTaskSubmission
|
|
237
|
-
) -> Optional[dict[str, dict[str, Any]]]:
|
|
238
|
-
"""Process attachment field and return attachment map or None."""
|
|
239
|
-
attachment_map: dict[str, dict[str, Any]] = {}
|
|
223
|
+
field_type = data_dict.get("field_type")
|
|
240
224
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
225
|
+
if field_type == ManualTaskFieldType.attachment.value:
|
|
226
|
+
attachment_map: dict[str, dict[str, Any]] = {}
|
|
227
|
+
for attachment in submission.attachments or []:
|
|
228
|
+
if (
|
|
229
|
+
attachment.attachment_type
|
|
230
|
+
== AttachmentType.include_with_access_package
|
|
231
|
+
):
|
|
232
|
+
try:
|
|
233
|
+
size, url = attachment.retrieve_attachment()
|
|
234
|
+
attachment_map[attachment.file_name] = {
|
|
235
|
+
"url": str(url) if url else None,
|
|
236
|
+
"size": (_format_size(size) if size else "Unknown"),
|
|
237
|
+
}
|
|
238
|
+
except (
|
|
239
|
+
Exception
|
|
240
|
+
) as exc: # pylint: disable=broad-exception-caught
|
|
241
|
+
logger.warning(
|
|
242
|
+
"Error retrieving attachment {}: {}",
|
|
243
|
+
attachment.file_name,
|
|
244
|
+
str(exc),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
aggregated_data[field_key] = attachment_map or None
|
|
248
|
+
else:
|
|
249
|
+
aggregated_data[field_key] = data_dict.get("value")
|
|
250
|
+
|
|
251
|
+
return aggregated_data if aggregated_data else None
|
|
256
252
|
|
|
257
253
|
def dry_run_task(self) -> int:
|
|
258
254
|
"""Return estimated row count for dry run - manual tasks don't have predictable counts"""
|
|
259
255
|
return 1 # Placeholder - manual tasks generate variable data
|
|
260
256
|
|
|
261
|
-
# Provide erasure support for manual tasks
|
|
257
|
+
# NEW METHOD: Provide erasure support for manual tasks
|
|
262
258
|
@retry(action_type=ActionType.erasure, default_return=0)
|
|
263
259
|
def erasure_request(
|
|
264
260
|
self,
|