Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions kobo/apps/audit_log/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
get_client_ip,
get_human_readable_client_user_agent,
)
from kobo.apps.subsequences.constants import Action
from kpi.constants import (
ACCESS_LOG_LOGINAS_AUTH_TYPE,
ACCESS_LOG_SUBMISSION_AUTH_TYPE,
Expand Down Expand Up @@ -404,6 +405,8 @@ def create_from_request(cls, request: WSGIRequest):
'submissions-list': cls._create_from_submission_request,
'submission-detail': cls._create_from_submission_request,
'advanced-submission-post': cls._create_from_submission_extra_request,
'advanced-features-list': cls._create_from_question_advanced_action_request,
'advanced-features-detail': cls._create_from_question_advanced_action_request,
}
url_name = request.resolver_match.url_name
method = url_name_to_action.get(url_name, None)
Expand Down Expand Up @@ -787,6 +790,32 @@ def _create_from_permissions_request(cls, request):
)
ProjectHistoryLog.objects.bulk_create(logs)

@classmethod
def _create_from_question_advanced_action_request(cls, request):
initial_data = getattr(request, 'initial_data', None)
updated_data = getattr(request, 'updated_data', None)
source_data = updated_data if updated_data else initial_data
asset_uid = request.resolver_match.kwargs['parent_lookup_asset']
if not source_data:
return
if source_data.get('action') != Action.QUAL:
return
owner = source_data.pop('asset.owner.username')
object_id = source_data.pop('object_id')
metadata = {
'asset_uid': asset_uid,
'log_subtype': PROJECT_HISTORY_LOG_PROJECT_SUBTYPE,
'ip_address': get_client_ip(request),
'source': get_human_readable_client_user_agent(request),
'project_owner': owner,
}
action = AuditAction.UPDATE_QA
metadata['qa'] = {PROJECT_HISTORY_LOG_METADATA_FIELD_NEW: source_data['params']}
user = get_database_user(request.user)
ProjectHistoryLog.objects.create(
user=user, object_id=object_id, action=action, metadata=metadata
)

@classmethod
def _create_from_v1_export(cls, request):
updated_data = getattr(request, 'updated_data', None)
Expand Down
56 changes: 28 additions & 28 deletions kobo/apps/audit_log/tests/test_project_history_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
remove_uuid_prefix,
)
from kobo.apps.openrosa.libs.utils.logger_tools import dict2xform
from kobo.apps.subsequences.models import QuestionAdvancedAction
from kobo.apps.subsequences.constants import Action
from kpi.constants import (
ASSET_TYPE_TEMPLATE,
CLONE_ARG_NAME,
Expand Down Expand Up @@ -559,37 +561,35 @@ def test_update_content_creates_log(self, use_v2):
use_v2=use_v2,
)

def test_update_qa_creates_log(self):
request_data = {
'advanced_features': {
'qual': {
'qual_survey': [
{
'type': 'qual_note',
'uuid': '12345',
'scope': 'by_question#survey',
'xpath': 'q1',
'labels': {'_default': 'QA Question'},
# requests to remove a question just add this
# option rather than actually deleting anything
'options': {'deleted': True},
}
]
}
}
}

log_metadata = self._base_asset_detail_endpoint_test(
patch=True,
url_name=self.detail_url,
request_data=request_data,
expected_action=AuditAction.UPDATE_QA,
def test_add_qa_creates_log(self):
request_data = {"action":"qual", "question_xpath":"Audio", "params": [{"labels": {"_default":"wherefore?"},"uuid":"12345", "type":"qualText"}]}
metadata = self._base_project_history_log_test(
self.client.post,
reverse('api_v2:advanced-features-list', args=[self.asset.uid]),
request_data = request_data,
expected_action = AuditAction.UPDATE_QA,
expected_subtype=PROJECT_HISTORY_LOG_PROJECT_SUBTYPE
)
self.assertEqual(metadata['qa']['new'], request_data['params'])

self.assertEqual(
log_metadata['qa'][PROJECT_HISTORY_LOG_METADATA_FIELD_NEW],
request_data['advanced_features']['qual']['qual_survey'],
def test_update_qa_creates_log(self):
question_qual_action = QuestionAdvancedAction.objects.create(
asset=self.asset,
action=Action.QUAL,
question_xpath='q1',
params=[{"labels": {"_default": "why?"}, "uuid":"12345", "type":"qualText"}]
)
request_data = {"params": [{"labels": {"_default":"wherefore?"},"uuid":"12345", "type":"qualText"}]}
metadata = self._base_project_history_log_test(
self.client.patch,
reverse('api_v2:advanced-features-detail', args=[self.asset.uid, question_qual_action.uid]),
request_data = request_data,
expected_action = AuditAction.UPDATE_QA,
expected_subtype=PROJECT_HISTORY_LOG_PROJECT_SUBTYPE
)
self.assertEqual(metadata['qa']['new'], request_data['params'])



def test_failed_qa_update_does_not_create_log(self):
# badly formatted QA dict should result in an error before update
Expand Down
2 changes: 2 additions & 0 deletions kobo/apps/subsequences/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
from .automatic_google_translation import AutomaticGoogleTranslationAction
from .manual_transcription import ManualTranscriptionAction
from .manual_translation import ManualTranslationAction
from .qual import QualAction

# TODO, what about using a loader for every class in "actions" folder (except base.py)?
ACTIONS = (
AutomaticGoogleTranscriptionAction,
AutomaticGoogleTranslationAction,
ManualTranscriptionAction,
ManualTranslationAction,
QualAction,
)

ACTION_IDS_TO_CLASSES = {a.ID: a for a in ACTIONS}
18 changes: 18 additions & 0 deletions kobo/apps/subsequences/actions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,17 @@ def get_output_fields(self) -> list[dict]:
# raise NotImplementedError()
return []

def update_params(self, incoming_params):
"""
Returns the result of updating current params with incoming ones from
a request. May be overridden, eg, to prevent deletion of existing lanugages
for transcriptions/translations
Defaults to replacing the existing params with the new ones.
Should raise an error if the incoming params are not well-formatted
"""
self.validate_params(incoming_params)
self.params = incoming_params

def validate_external_data(self, data):
jsonschema.validate(data, self.external_data_schema)

Expand Down Expand Up @@ -594,6 +605,13 @@ def languages(self) -> list[str]:
languages.append(individual_params['language'])
return languages

def update_params(self, incoming_params):
self.validate_params(incoming_params)
current_languages = self.languages
for language_obj in incoming_params:
if language_obj['language'] not in current_languages:
self.params.append(language_obj)


class BaseAutomaticNLPAction(BaseManualNLPAction):
"""
Expand Down
15 changes: 15 additions & 0 deletions kobo/apps/subsequences/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from django.db import models

SUBMISSION_UUID_FIELD = 'meta/rootUuid' # FIXME: import from elsewhere
SUPPLEMENT_KEY = '_supplementalDetails' # leave unchanged for backwards compatibility

Expand All @@ -20,3 +22,16 @@
'20250820',
None
]


class Action(models.TextChoices):
MANUAL_TRANSCRIPTION = 'manual_transcription'
MANUAL_TRANSLATION = 'manual_translation'
AUTOMATIC_GOOGLE_TRANSLATION = 'automatic_google_translation'
AUTOMATIC_GOOGLE_TRANSCRIPTION = 'automatic_google_transcription'
QUAL = 'qual'


def set_version(schema: dict) -> dict:
schema['_version'] = SCHEMA_VERSIONS[0]
return schema
74 changes: 74 additions & 0 deletions kobo/apps/subsequences/docs/api/v2/subsequences/create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
## Add an advanced action to an asset

Enables a new type of advanced action on a question in the asset.
* `action`, `params`, and `question_xpath` are required
* `params` must match the expected param_schema of the `action`

Accepted `action`s include:
* `manual_transcription`
* `automatic_google_transcription`
* `manual_translation`
* `automatic_google_translation`
* `qual`

For all actions except `qual`, `params` must look like
> '[{"language": "es"}, {"language": "en"}, ...]'

For `qual`, `params` must look like
```
[
{
'type': 'qualInteger',
'uuid': '1a2c8eb0-e2ec-4b3c-942a-c1a5410c081a',
'labels': {'_default': 'How many characters appear in the story?'},
},
{
'type': 'qualSelectMultiple',
'uuid': '2e30bec7-4843-43c7-98bc-13114af230c5',
'labels': {'_default': "What themes were present in the story?"},
'choices': [
{
'uuid': '2e24e6b4-bc3b-4e8e-b0cd-d8d3b9ca15b6',
'labels': {'_default': 'Empathy'},
},
{
'uuid': 'cb82919d-2948-4ccf-a488-359c5d5ee53a',
'labels': {'_default': 'Competition'},
},
{
'uuid': '8effe3b1-619e-4ada-be45-ebcea5af0aaf',
'labels': {'_default': 'Apathy'},
},
],
},
{
'type': 'qualSelectOne',
'uuid': '1a8b748b-f470-4c40-bc09-ce2b1197f503',
'labels': {'_default': 'Was this a first-hand account?'},
'choices': [
{
'uuid': '3c7aacdc-8971-482a-9528-68e64730fc99',
'labels': {'_default': 'Yes'},
},
{
'uuid': '7e31c6a5-5eac-464c-970c-62c383546a94',
'labels': {'_default': 'No'},
},
],
},
{
'type': 'qualTags',
'uuid': 'e9b4e6d1-fdbb-4dc9-8b10-a9c3c388322f',
'labels': {'_default': 'Tag any landmarks mentioned in the story'},
},
{
'type': 'qualText',
'uuid': '83acf2a7-8edc-4fd8-8b9f-f832ca3f18ad',
'labels': {'_default': 'Add any further remarks'},
},
{
'type': 'qualNote',
'uuid': '5ef11d48-d7a3-432e-af83-8c2e9b1feb72',
'labels': {'_default': 'Thanks for your diligence'},
},
]```
3 changes: 3 additions & 0 deletions kobo/apps/subsequences/docs/api/v2/subsequences/list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## List all advanced features on an asset

Lists all advanced features on all questions in an asset
3 changes: 3 additions & 0 deletions kobo/apps/subsequences/docs/api/v2/subsequences/retrieve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Retrieve advanced feature configuration for a question on an asset

Gets the params for one advanced action for one question in an asset
67 changes: 67 additions & 0 deletions kobo/apps/subsequences/docs/api/v2/subsequences/update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
## Update an advanced action on an asset

Update the params of an advanced action on a question in the asset.
* `params` is required
* `params` must match the expected param_schema of the action being updated

For all actions except `qual`, `params` must look like
> '[{"language": "es"}, {"language": "en"}, ...]'

For `qual`, `params` must look like
```
[
{
'type': 'qualInteger',
'uuid': '1a2c8eb0-e2ec-4b3c-942a-c1a5410c081a',
'labels': {'_default': 'How many characters appear in the story?'},
},
{
'type': 'qualSelectMultiple',
'uuid': '2e30bec7-4843-43c7-98bc-13114af230c5',
'labels': {'_default': "What themes were present in the story?"},
'choices': [
{
'uuid': '2e24e6b4-bc3b-4e8e-b0cd-d8d3b9ca15b6',
'labels': {'_default': 'Empathy'},
},
{
'uuid': 'cb82919d-2948-4ccf-a488-359c5d5ee53a',
'labels': {'_default': 'Competition'},
},
{
'uuid': '8effe3b1-619e-4ada-be45-ebcea5af0aaf',
'labels': {'_default': 'Apathy'},
},
],
},
{
'type': 'qualSelectOne',
'uuid': '1a8b748b-f470-4c40-bc09-ce2b1197f503',
'labels': {'_default': 'Was this a first-hand account?'},
'choices': [
{
'uuid': '3c7aacdc-8971-482a-9528-68e64730fc99',
'labels': {'_default': 'Yes'},
},
{
'uuid': '7e31c6a5-5eac-464c-970c-62c383546a94',
'labels': {'_default': 'No'},
},
],
},
{
'type': 'qualTags',
'uuid': 'e9b4e6d1-fdbb-4dc9-8b10-a9c3c388322f',
'labels': {'_default': 'Tag any landmarks mentioned in the story'},
},
{
'type': 'qualText',
'uuid': '83acf2a7-8edc-4fd8-8b9f-f832ca3f18ad',
'labels': {'_default': 'Add any further remarks'},
},
{
'type': 'qualNote',
'uuid': '5ef11d48-d7a3-432e-af83-8c2e9b1feb72',
'labels': {'_default': 'Thanks for your diligence'},
},
]```
Loading
Loading