ethyca-fides 2.64.6b0__py2.py3-none-any.whl → 2.64.6b1__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.64.6b0.dist-info → ethyca_fides-2.64.6b1.dist-info}/METADATA +1 -1
- {ethyca_fides-2.64.6b0.dist-info → ethyca_fides-2.64.6b1.dist-info}/RECORD +112 -108
- fides/_version.py +3 -3
- fides/api/graph/traversal.py +18 -0
- fides/api/service/connectors/__init__.py +4 -0
- fides/api/service/connectors/manual_task_connector.py +96 -0
- fides/api/service/privacy_request/dsr_package/templates/collection_index.html +9 -1
- fides/api/service/privacy_request/dsr_package/templates/main.css +6 -2
- fides/api/service/privacy_request/request_runner_service.py +7 -0
- fides/api/task/create_request_tasks.py +16 -0
- fides/api/task/execute_request_tasks.py +10 -1
- fides/api/task/filter_results.py +6 -0
- fides/api/task/graph_task.py +1 -0
- fides/api/task/manual/__init__.py +0 -0
- fides/api/task/manual/manual_task_graph_task.py +300 -0
- fides/api/task/manual/manual_task_utils.py +322 -0
- fides/api/task/task_resources.py +3 -0
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/{_app-1650bbc3cb8c2299.js → _app-7430e1499432b029.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/[id]-00fb442c4adb7371.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/{privacy-requests-7d625b170911a072.js → privacy-requests-c8b02ae92dd7e45b.js} +1 -1
- fides/ui-build/static/admin/_next/static/css/399d4757862a3982.css +1 -0
- fides/ui-build/static/admin/_next/static/{7-nocO64klVotMrKmugnq → z7GzAZhoGiO521zm7WPYX}/_buildManifest.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-headless.js +1 -1
- fides/ui-build/static/admin/lib/fides-preview.js +1 -1
- fides/ui-build/static/admin/lib/fides-tcf.js +1 -1
- fides/ui-build/static/admin/lib/fides.js +2 -2
- 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
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/[id]-ee8d27e2f17563a0.js +0 -1
- fides/ui-build/static/admin/_next/static/css/5ded47c57dae5baf.css +0 -1
- {ethyca_fides-2.64.6b0.dist-info → ethyca_fides-2.64.6b1.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.64.6b0.dist-info → ethyca_fides-2.64.6b1.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.64.6b0.dist-info → ethyca_fides-2.64.6b1.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.64.6b0.dist-info → ethyca_fides-2.64.6b1.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{7-nocO64klVotMrKmugnq → z7GzAZhoGiO521zm7WPYX}/_ssgManifest.js +0 -0
|
@@ -33,6 +33,10 @@ from fides.api.models.worker_task import ExecutionLogStatus
|
|
|
33
33
|
from fides.api.schemas.policy import ActionType
|
|
34
34
|
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
|
+
from fides.api.task.manual.manual_task_utils import (
|
|
37
|
+
ManualTaskAddress,
|
|
38
|
+
create_manual_task_instances_for_privacy_request,
|
|
39
|
+
)
|
|
36
40
|
from fides.api.util.logger_context_utils import log_context
|
|
37
41
|
|
|
38
42
|
|
|
@@ -85,6 +89,14 @@ def build_access_networkx_digraph(
|
|
|
85
89
|
# Connect the end nodes, those that have no downstream dependencies, to the terminator node
|
|
86
90
|
networkx_graph.add_edge(node, TERMINATOR_ADDRESS)
|
|
87
91
|
|
|
92
|
+
manual_nodes = [
|
|
93
|
+
addr
|
|
94
|
+
for addr in traversal_nodes.keys()
|
|
95
|
+
if addr.collection == ManualTaskAddress.MANUAL_DATA_COLLECTION
|
|
96
|
+
]
|
|
97
|
+
for manual_node in manual_nodes:
|
|
98
|
+
networkx_graph.add_edge(ROOT_COLLECTION_ADDRESS, manual_node)
|
|
99
|
+
|
|
88
100
|
_add_edge_if_no_nodes(traversal_nodes, networkx_graph)
|
|
89
101
|
return networkx_graph
|
|
90
102
|
|
|
@@ -458,6 +470,10 @@ def run_access_request(
|
|
|
458
470
|
end_nodes: List[CollectionAddress] = traversal.traverse(
|
|
459
471
|
traversal_nodes, collect_tasks_fn
|
|
460
472
|
)
|
|
473
|
+
|
|
474
|
+
# Snapshot manual task field instances for this privacy request
|
|
475
|
+
create_manual_task_instances_for_privacy_request(session, privacy_request)
|
|
476
|
+
|
|
461
477
|
# Save Access Request Tasks to the database
|
|
462
478
|
ready_tasks = persist_new_access_request_tasks(
|
|
463
479
|
session, privacy_request, traversal, traversal_nodes, end_nodes, graph
|
|
@@ -29,6 +29,8 @@ from fides.api.task.graph_task import (
|
|
|
29
29
|
GraphTask,
|
|
30
30
|
mark_current_and_downstream_nodes_as_failed,
|
|
31
31
|
)
|
|
32
|
+
from fides.api.task.manual.manual_task_graph_task import ManualTaskGraphTask
|
|
33
|
+
from fides.api.task.manual.manual_task_utils import ManualTaskAddress
|
|
32
34
|
from fides.api.task.task_resources import TaskResources
|
|
33
35
|
from fides.api.tasks import DSR_QUEUE_NAME, DatabaseTask, celery_app
|
|
34
36
|
from fides.api.util.cache import cache_task_tracking_key
|
|
@@ -108,7 +110,14 @@ def create_graph_task(
|
|
|
108
110
|
to begin with - this may be unrecoverable and a new Privacy Request should be created.
|
|
109
111
|
"""
|
|
110
112
|
try:
|
|
111
|
-
|
|
113
|
+
collection_address = request_task.request_task_address
|
|
114
|
+
|
|
115
|
+
# Check if this is a manual task address
|
|
116
|
+
graph_task: GraphTask
|
|
117
|
+
if ManualTaskAddress.is_manual_task_address(collection_address):
|
|
118
|
+
graph_task = ManualTaskGraphTask(resources)
|
|
119
|
+
else:
|
|
120
|
+
graph_task = GraphTask(resources)
|
|
112
121
|
|
|
113
122
|
except Exception as exc:
|
|
114
123
|
logger.debug(
|
fides/api/task/filter_results.py
CHANGED
|
@@ -6,6 +6,7 @@ from loguru import logger
|
|
|
6
6
|
|
|
7
7
|
from fides.api.graph.config import CollectionAddress, FieldPath
|
|
8
8
|
from fides.api.graph.graph import DatasetGraph
|
|
9
|
+
from fides.api.task.manual.manual_task_utils import ManualTaskAddress
|
|
9
10
|
from fides.api.util.collection_util import Row
|
|
10
11
|
|
|
11
12
|
|
|
@@ -37,6 +38,11 @@ def filter_data_categories(
|
|
|
37
38
|
if not results:
|
|
38
39
|
continue
|
|
39
40
|
|
|
41
|
+
# Skip manual task data - it doesn't need filtering since it's controlled by field definitions
|
|
42
|
+
if f":{ManualTaskAddress.MANUAL_DATA_COLLECTION}" in node_address:
|
|
43
|
+
filtered_access_results[node_address].extend(results)
|
|
44
|
+
continue
|
|
45
|
+
|
|
40
46
|
# Results from fides connectors are a special case:
|
|
41
47
|
# they've already been filtered and stored in a dict keyed by rule key.
|
|
42
48
|
# So here, we simply find the results corresponding to our current rule
|
fides/api/task/graph_task.py
CHANGED
|
@@ -109,6 +109,7 @@ def retry(
|
|
|
109
109
|
method_name,
|
|
110
110
|
self.execution_node.address,
|
|
111
111
|
)
|
|
112
|
+
# Log the awaiting processing status and exit without retrying.
|
|
112
113
|
self.log_awaiting_processing(action_type, ex)
|
|
113
114
|
# Request Task put in "awaiting_processing" status and exited, awaiting Async Callback
|
|
114
115
|
return None
|
|
File without changes
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
from fides.api.common_exceptions import AwaitingAsyncTaskCallback
|
|
7
|
+
from fides.api.models.attachment import AttachmentType
|
|
8
|
+
from fides.api.models.manual_task import (
|
|
9
|
+
ManualTask,
|
|
10
|
+
ManualTaskConfigurationType,
|
|
11
|
+
ManualTaskEntityType,
|
|
12
|
+
ManualTaskFieldType,
|
|
13
|
+
ManualTaskInstance,
|
|
14
|
+
StatusType,
|
|
15
|
+
)
|
|
16
|
+
from fides.api.models.privacy_request import PrivacyRequest
|
|
17
|
+
from fides.api.schemas.policy import ActionType
|
|
18
|
+
from fides.api.schemas.privacy_request import PrivacyRequestStatus
|
|
19
|
+
from fides.api.task.graph_task import GraphTask, retry
|
|
20
|
+
from fides.api.task.manual.manual_task_utils import (
|
|
21
|
+
ManualTaskAddress,
|
|
22
|
+
get_manual_tasks_for_connection_config,
|
|
23
|
+
)
|
|
24
|
+
from fides.api.util.collection_util import Row
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ManualTaskGraphTask(GraphTask):
|
|
28
|
+
"""GraphTask implementation for ManualTask execution"""
|
|
29
|
+
|
|
30
|
+
@retry(action_type=ActionType.access, default_return=[])
|
|
31
|
+
def access_request(self, *inputs: List[Row]) -> List[Row]:
|
|
32
|
+
"""
|
|
33
|
+
Execute manual task logic following the standard GraphTask pattern:
|
|
34
|
+
1. Create ManualTaskInstances if they don't exist
|
|
35
|
+
2. Check for submissions
|
|
36
|
+
3. Return data if submitted, raise AwaitingAsyncTaskCallback if not
|
|
37
|
+
"""
|
|
38
|
+
db = self.resources.session
|
|
39
|
+
collection_address = self.execution_node.address
|
|
40
|
+
|
|
41
|
+
# Verify this is a manual task address
|
|
42
|
+
if not ManualTaskAddress.is_manual_task_address(collection_address):
|
|
43
|
+
raise ValueError(f"Invalid manual task address: {collection_address}")
|
|
44
|
+
|
|
45
|
+
connection_key = ManualTaskAddress.get_connection_key(collection_address)
|
|
46
|
+
|
|
47
|
+
# Get manual tasks for this connection
|
|
48
|
+
manual_tasks = get_manual_tasks_for_connection_config(db, connection_key)
|
|
49
|
+
|
|
50
|
+
if not manual_tasks:
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
# Check/create manual task instances for ACCESS configs only
|
|
54
|
+
self._ensure_manual_task_instances(
|
|
55
|
+
db,
|
|
56
|
+
manual_tasks,
|
|
57
|
+
self.resources.request,
|
|
58
|
+
ManualTaskConfigurationType.access_privacy_request,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Check if all manual task instances have submissions for ACCESS configs only
|
|
62
|
+
submitted_data = self._get_submitted_data(
|
|
63
|
+
db,
|
|
64
|
+
manual_tasks,
|
|
65
|
+
self.resources.request,
|
|
66
|
+
ManualTaskConfigurationType.access_privacy_request,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if submitted_data is not None:
|
|
70
|
+
result: List[Row] = [submitted_data] if submitted_data else []
|
|
71
|
+
self.request_task.access_data = result
|
|
72
|
+
|
|
73
|
+
# Mark request task as complete and write execution log
|
|
74
|
+
self.log_end(ActionType.access)
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
# Set privacy request status to requires_input if not already set
|
|
78
|
+
if self.resources.request.status != PrivacyRequestStatus.requires_input:
|
|
79
|
+
self.resources.request.status = PrivacyRequestStatus.requires_input
|
|
80
|
+
self.resources.request.save(db)
|
|
81
|
+
|
|
82
|
+
# This should trigger log_awaiting_processing via the @retry decorator
|
|
83
|
+
raise AwaitingAsyncTaskCallback(
|
|
84
|
+
f"Manual task for {connection_key} requires user input"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def _ensure_manual_task_instances(
|
|
88
|
+
self,
|
|
89
|
+
db: Session,
|
|
90
|
+
manual_tasks: List[ManualTask],
|
|
91
|
+
privacy_request: PrivacyRequest,
|
|
92
|
+
allowed_config_type: "ManualTaskConfigurationType",
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Create ManualTaskInstances for configs matching `allowed_config_type` if they don't exist."""
|
|
95
|
+
|
|
96
|
+
for manual_task in manual_tasks:
|
|
97
|
+
# ------------------------------------------------------------------
|
|
98
|
+
# Short-circuit: if instances already exist for this task & entity
|
|
99
|
+
# (no matter what config version they were created for) we should reuse
|
|
100
|
+
# them instead of creating a brand-new one that would result in
|
|
101
|
+
# duplicates when configurations are versioned after the privacy
|
|
102
|
+
# request has started.
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
existing_task_instance = (
|
|
105
|
+
db.query(ManualTaskInstance)
|
|
106
|
+
.filter(
|
|
107
|
+
ManualTaskInstance.task_id == manual_task.id,
|
|
108
|
+
ManualTaskInstance.entity_id == privacy_request.id,
|
|
109
|
+
ManualTaskInstance.entity_type
|
|
110
|
+
== ManualTaskEntityType.privacy_request,
|
|
111
|
+
)
|
|
112
|
+
.first()
|
|
113
|
+
)
|
|
114
|
+
if existing_task_instance:
|
|
115
|
+
# An instance already exists for this privacy request – no need
|
|
116
|
+
# to create another one tied to a newer config version.
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Check each active config for instances (now we know none exist yet)
|
|
120
|
+
for config in manual_task.configs:
|
|
121
|
+
if not config.is_current or config.config_type != allowed_config_type:
|
|
122
|
+
# Skip configs that are not current or not relevant for this request type
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
ManualTaskInstance.create(
|
|
126
|
+
db=db,
|
|
127
|
+
data={
|
|
128
|
+
"task_id": manual_task.id,
|
|
129
|
+
"config_id": config.id,
|
|
130
|
+
"entity_id": privacy_request.id,
|
|
131
|
+
"entity_type": ManualTaskEntityType.privacy_request.value,
|
|
132
|
+
"status": StatusType.pending.value,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# pylint: disable=too-many-branches,too-many-nested-blocks
|
|
137
|
+
def _get_submitted_data(
|
|
138
|
+
self,
|
|
139
|
+
db: Session,
|
|
140
|
+
manual_tasks: List[ManualTask],
|
|
141
|
+
privacy_request: PrivacyRequest,
|
|
142
|
+
allowed_config_type: "ManualTaskConfigurationType",
|
|
143
|
+
) -> Optional[Dict[str, Any]]:
|
|
144
|
+
"""
|
|
145
|
+
Check if all manual task instances have submissions for ALL fields and return aggregated data
|
|
146
|
+
Returns None if any field submissions are missing (all fields must be completed or skipped)
|
|
147
|
+
"""
|
|
148
|
+
aggregated_data: Dict[str, Any] = {}
|
|
149
|
+
|
|
150
|
+
def _format_size(size_bytes: int) -> str:
|
|
151
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
152
|
+
size = float(size_bytes)
|
|
153
|
+
for unit in units:
|
|
154
|
+
if size < 1024.0:
|
|
155
|
+
return f"{size:.1f} {unit}"
|
|
156
|
+
size /= 1024.0
|
|
157
|
+
return f"{size:.1f} PB"
|
|
158
|
+
|
|
159
|
+
for manual_task in manual_tasks:
|
|
160
|
+
|
|
161
|
+
candidate_instances: list[ManualTaskInstance] = (
|
|
162
|
+
db.query(ManualTaskInstance)
|
|
163
|
+
.filter(
|
|
164
|
+
ManualTaskInstance.task_id == manual_task.id,
|
|
165
|
+
ManualTaskInstance.entity_id == privacy_request.id,
|
|
166
|
+
ManualTaskInstance.entity_type
|
|
167
|
+
== ManualTaskEntityType.privacy_request,
|
|
168
|
+
)
|
|
169
|
+
.all()
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if not candidate_instances:
|
|
173
|
+
return None # No instance yet for this manual task
|
|
174
|
+
|
|
175
|
+
for inst in candidate_instances:
|
|
176
|
+
# Skip instances tied to other request types
|
|
177
|
+
if not inst.config or inst.config.config_type != allowed_config_type:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
all_fields = inst.config.field_definitions or []
|
|
181
|
+
|
|
182
|
+
# Every field must have a submission
|
|
183
|
+
if not all(inst.get_submission_for_field(f.id) for f in all_fields):
|
|
184
|
+
return None # At least one instance still incomplete
|
|
185
|
+
|
|
186
|
+
# Ensure status set
|
|
187
|
+
if inst.status != StatusType.completed:
|
|
188
|
+
inst.status = StatusType.completed
|
|
189
|
+
inst.save(db)
|
|
190
|
+
|
|
191
|
+
# Aggregate submission data from this instance
|
|
192
|
+
for submission in inst.submissions:
|
|
193
|
+
if not submission.field or not submission.field.field_key:
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
field_key = submission.field.field_key
|
|
197
|
+
|
|
198
|
+
if not isinstance(submission.data, dict):
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
data_dict: Dict[str, Any] = submission.data
|
|
202
|
+
|
|
203
|
+
field_type = data_dict.get("field_type")
|
|
204
|
+
|
|
205
|
+
if field_type == ManualTaskFieldType.attachment.value:
|
|
206
|
+
attachment_map: Dict[str, Dict[str, Any]] = {}
|
|
207
|
+
for attachment in submission.attachments or []:
|
|
208
|
+
if (
|
|
209
|
+
attachment.attachment_type
|
|
210
|
+
== AttachmentType.include_with_access_package
|
|
211
|
+
):
|
|
212
|
+
try:
|
|
213
|
+
size, url = attachment.retrieve_attachment()
|
|
214
|
+
attachment_map[attachment.file_name] = {
|
|
215
|
+
"url": str(url) if url else None,
|
|
216
|
+
"size": (
|
|
217
|
+
_format_size(size) if size else "Unknown"
|
|
218
|
+
),
|
|
219
|
+
}
|
|
220
|
+
except (
|
|
221
|
+
Exception
|
|
222
|
+
) as exc: # pylint: disable=broad-exception-caught
|
|
223
|
+
logger.warning(
|
|
224
|
+
"Error retrieving attachment {}: {}",
|
|
225
|
+
attachment.file_name,
|
|
226
|
+
str(exc),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
aggregated_data[field_key] = attachment_map or None
|
|
230
|
+
else:
|
|
231
|
+
aggregated_data[field_key] = data_dict.get("value")
|
|
232
|
+
|
|
233
|
+
return aggregated_data if aggregated_data else None
|
|
234
|
+
|
|
235
|
+
def dry_run_task(self) -> int:
|
|
236
|
+
"""Return estimated row count for dry run - manual tasks don't have predictable counts"""
|
|
237
|
+
return 1 # Placeholder - manual tasks generate variable data
|
|
238
|
+
|
|
239
|
+
# NEW METHOD: Provide erasure support for manual tasks
|
|
240
|
+
@retry(action_type=ActionType.erasure, default_return=0)
|
|
241
|
+
def erasure_request(
|
|
242
|
+
self,
|
|
243
|
+
retrieved_data: List[Row],
|
|
244
|
+
*erasure_prereqs: int, # noqa: D401, pylint: disable=unused-argument
|
|
245
|
+
) -> int:
|
|
246
|
+
"""Execute manual-task-driven erasure logic.
|
|
247
|
+
|
|
248
|
+
Mirrors access_request behaviour but returns the number of rows masked (always 0)
|
|
249
|
+
once all required manual task submissions are present. If submissions are
|
|
250
|
+
incomplete the privacy request is paused awaiting user input.
|
|
251
|
+
"""
|
|
252
|
+
db = self.resources.session
|
|
253
|
+
collection_address = self.execution_node.address
|
|
254
|
+
|
|
255
|
+
# Validate manual task address
|
|
256
|
+
if not ManualTaskAddress.is_manual_task_address(collection_address):
|
|
257
|
+
raise ValueError(f"Invalid manual task address: {collection_address}")
|
|
258
|
+
|
|
259
|
+
connection_key = ManualTaskAddress.get_connection_key(collection_address)
|
|
260
|
+
|
|
261
|
+
# Fetch relevant manual tasks for this connection
|
|
262
|
+
manual_tasks = get_manual_tasks_for_connection_config(db, connection_key)
|
|
263
|
+
if not manual_tasks:
|
|
264
|
+
# No manual tasks defined – nothing to erase
|
|
265
|
+
self.log_end(ActionType.erasure)
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
# Create ManualTaskInstances for ERASURE configs only
|
|
269
|
+
self._ensure_manual_task_instances(
|
|
270
|
+
db,
|
|
271
|
+
manual_tasks,
|
|
272
|
+
self.resources.request,
|
|
273
|
+
ManualTaskConfigurationType.erasure_privacy_request,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Check for full submissions – reuse helper used by access flow, filtering ERASURE configs
|
|
277
|
+
submissions_complete = self._get_submitted_data(
|
|
278
|
+
db,
|
|
279
|
+
manual_tasks,
|
|
280
|
+
self.resources.request,
|
|
281
|
+
ManualTaskConfigurationType.erasure_privacy_request,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# If any field submissions are missing, pause processing
|
|
285
|
+
if submissions_complete is None:
|
|
286
|
+
if self.resources.request.status != PrivacyRequestStatus.requires_input:
|
|
287
|
+
self.resources.request.status = PrivacyRequestStatus.requires_input
|
|
288
|
+
self.resources.request.save(db)
|
|
289
|
+
raise AwaitingAsyncTaskCallback(
|
|
290
|
+
f"Manual erasure task for {connection_key} requires user input"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Mark rows_masked = 0 (manual tasks do not mask data directly)
|
|
294
|
+
if self.request_task.id:
|
|
295
|
+
# Storing result for DSR 3.0; SQLAlchemy column typing triggers mypy warning
|
|
296
|
+
self.request_task.rows_masked = 0 # type: ignore[assignment]
|
|
297
|
+
|
|
298
|
+
# Mark successful completion
|
|
299
|
+
self.log_end(ActionType.erasure)
|
|
300
|
+
return 0
|