forms: make anonymise attribute a string (#73203) #372

Merged
ecazenave merged 3 commits from wip/73203-anonymisation into main 2023-06-26 18:20:14 +02:00
11 changed files with 195 additions and 36 deletions

View File

@ -224,7 +224,7 @@ def test_restrict_to_anonymised_data(app, pub, role):
formdef.workflow_roles = {'_receiver': role.id}
formdef.fields = [
fields.StringField(id='1', label='foobar', varname='foobar'),
fields.StringField(id='2', label='foobar2', varname='foobar2', anonymise=False),
fields.StringField(id='2', label='foobar2', varname='foobar2', anonymise='no'),
]
formdef.workflow_id = workflow.id
formdef.store()

View File

@ -2483,7 +2483,7 @@ def test_api_access_restrict_to_anonymised_data(pub, local_user, auth):
formdef.workflow_roles = {'_receiver': role.id}
formdef.fields = [
fields.StringField(id='1', label='foobar', varname='foobar'),
fields.StringField(id='2', label='foobar2', varname='foobar2', anonymise=False),
fields.StringField(id='2', label='foobar2', varname='foobar2', anonymise='no'),
]
formdef.workflow_id = workflow.id
formdef.store()

View File

@ -78,7 +78,7 @@ def formdef(pub):
varname='block-items',
label='Block items',
items=['foo', 'bar', 'baz'],
anonymise=False,
anonymise='no',
display_locations=['statistics'],
),
]
@ -94,11 +94,11 @@ def formdef(pub):
varname='test-items',
label='Test items',
items=['foo', 'bar', 'baz'],
anonymise=False,
anonymise='no',
)
items_field.display_locations = ['statistics']
block_field = fields.BlockField(
id='4', label='Block Data', varname='blockdata', block_slug='foobar', anonymise=False
id='4', label='Block Data', varname='blockdata', block_slug='foobar', anonymise='no'
)
formdef.fields = [item_field, items_field, block_field]
formdef.store()

View File

@ -17,6 +17,7 @@ from wcs.api_access import ApiAccess
from wcs.blocks import BlockDef
from wcs.carddef import CardDef
from wcs.categories import Category
from wcs.fields import StringField
from wcs.formdef import FormDef
from wcs.qommon.http_request import HTTPRequest
from wcs.qommon.ident.password_accounts import PasswordAccount
@ -5697,3 +5698,130 @@ def test_webservice_call_error_handling_with_marker(http_requests, pub):
assert evolutions[2].status == 'wf-one'
# error handling of wscall
assert evolutions[3].status == 'wf-two'
def test_anonymise_action_intermediate(pub):
FormDef.wipe()
Workflow.wipe()
role = pub.role_class(name='xxx')
role.store()
user = create_user(pub, is_admin=True)
user.name = 'Foo Bar'
user.email = 'foo@example.com'
user.roles.append(role.id)
user.store()
wf = Workflow(name='anonymise-action-intermediate')
wf.id = '1'
wf.roles = {'_receiver': role.id}
anonymise_intermediate_status = wf.add_status('anonymise_intermediate', id='anonymise_intermediate')
anonymise_intermediate_status.visibility = ['_receiver']
anonymise_final_status = wf.add_status('anonymise_final', id='anonymise_final')
anonymise_final_status.visibility = ['_receiver']
anonymise_intermediate_action = anonymise_intermediate_status.add_action(
'anonymise', id='_anonymise', prepend=True
)
anonymise_intermediate_action.label = 'Intermediate anonymisation'
anonymise_intermediate_action.mode = 'intermediate'
jump_to_anonymise_final = anonymise_intermediate_status.add_action('choice', id='_to_anonymise_final')
jump_to_anonymise_final.by = ['_receiver']
jump_to_anonymise_final.status = anonymise_final_status.id
jump_to_anonymise_final.label = 'choice'
jump_to_anonymise_final.identifier = 'id1'
anonymise_final_action = anonymise_final_status.add_action('anonymise', id='_anonymise', prepend=True)
anonymise_final_action.label = 'Final anonymisation'
anonymise_final_action.mode = 'final'
wf.store()
formdef = FormDef()
formdef.id = '1'
formdef.workflow_roles = {'_receiver': role.id}
formdef.name = 'Foo'
formdef.workflow_id = wf.id
formdef.fields = [
StringField(id='0', label='string', anonymise='intermediate', varname='inter'),
StringField(id='1', label='string', anonymise='final', varname='final'),
]
formdef.store()
formdef.data_class().wipe()
formdata = formdef.data_class()()
formdata.data = {'0': 'Foo', '1': 'Bar'}
formdata.just_created()
formdata.store()
formdata.perform_workflow()
formdata = formdef.data_class().select()[0]
assert formdata.status == 'wf-anonymise_intermediate'
assert formdata.data == {'0': None, '1': 'Bar'}
app = login(get_app(pub))
resp = app.get('/backoffice/management/foo/%s/' % formdata.id)
resp = resp.form.submit('button_to_anonymise_final')
resp = resp.follow()
formdata = formdef.data_class().select()[0]
assert formdata.status == 'wf-anonymise_final'
assert formdata.data == {'0': None, '1': None}
def test_anonymise_action_final_also_deletes_fields_with_intermediate(pub):
FormDef.wipe()
Workflow.wipe()
role = pub.role_class(name='xxx')
role.store()
user = create_user(pub, is_admin=True)
user.name = 'Foo Bar'
user.email = 'foo@example.com'
user.roles.append(role.id)
user.store()
wf = Workflow(name='anonymise-action-final')
wf.id = '1'
wf.roles = {'_receiver': role.id}
received_status = wf.add_status('received', id='anonymise_received')
anonymise_final_status = wf.add_status('anonymise_final', id='anonymise_final')
anonymise_final_status.visibility = ['_receiver']
anonymise_final_action = anonymise_final_status.add_action('anonymise', id='_anonymise', prepend=True)
anonymise_final_action.label = 'Final anonymisation'
anonymise_final_action.mode = 'final'
jump_to_anonymise_final = received_status.add_action('choice', id='_to_anonymise_final')
jump_to_anonymise_final.by = ['_receiver']
jump_to_anonymise_final.status = anonymise_final_status.id
jump_to_anonymise_final.label = 'choice'
jump_to_anonymise_final.identifier = 'id1'
wf.store()
formdef = FormDef()
formdef.id = '1'
formdef.workflow_roles = {'_receiver': role.id}
formdef.name = 'Foo'
formdef.workflow_id = wf.id
formdef.fields = [
StringField(id='0', label='string', anonymise='intermediate', varname='inter'),
StringField(id='1', label='string', anonymise='final', varname='final'),
]
formdef.store()
formdef.data_class().wipe()
formdata = formdef.data_class()()
formdata.data = {'0': 'Foo', '1': 'Bar'}
formdata.just_created()
formdata.store()
formdata.perform_workflow()
formdata = formdef.data_class().select()[0]
assert formdata.status == 'wf-anonymise_received'
assert formdata.data == {'0': 'Foo', '1': 'Bar'}
app = login(get_app(pub))
resp = app.get('/backoffice/management/foo/%s/' % formdata.id)
resp = resp.form.submit('button_to_anonymise_final')
resp = resp.follow()
formdata = formdef.data_class().select()[0]
assert formdata.status == 'wf-anonymise_final'
assert formdata.data == {'0': None, '1': None}

View File

@ -156,7 +156,7 @@ def test_text_anonymise(pub):
formdata.anonymise()
assert not formdata.data.get('0')
formdef.fields[0].anonymise = False
formdef.fields[0].anonymise = 'no'
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()
@ -849,7 +849,7 @@ def test_date_anonymise(pub):
formdata.anonymise()
assert not formdata.data.get('0')
formdef.fields[0].anonymise = False
formdef.fields[0].anonymise = 'no'
formdef.store()
formdata = formdef.data_class()()
formdata.just_created()

View File

@ -1020,7 +1020,7 @@ def test_anonymise(pub):
assert formdata.evolution[1].parts is None
assert item.render_as_line() == 'Anonymisation'
item.unlink_user = True
item.mode = 'unlink_user'
assert item.render_as_line() == 'Anonymisation (only user unlinking)'
@ -1051,7 +1051,7 @@ def test_anonymise_custom_view_user_filtered(pub):
formdef = FormDef()
formdef.name = 'baz'
formdef.fields = [
ItemsField(id='1', label='list', data_source={'type': 'carddef:foo:card-view'}, anonymise=True),
ItemsField(id='1', label='list', data_source={'type': 'carddef:foo:card-view'}, anonymise='final'),
]
formdef.store()

View File

@ -1722,7 +1722,7 @@ def test_anonymise_action_unlink_user(pub, submitter_is_triggerer):
anonymise = wf.possible_status[1].add_action('anonymise', id='_anonymise', prepend=True)
anonymise.label = 'Unlink User'
anonymise.varname = 'mycard'
anonymise.unlink_user = True
anonymise.mode = 'unlink_user'
wf.store()
carddef = CardDef()
@ -1765,7 +1765,7 @@ def test_anonymise_action_unlink_user_no_request(pub):
anonymise = wf.possible_status[1].add_action('anonymise', id='_anonymise', prepend=True)
anonymise.label = 'Unlink User'
anonymise.varname = 'mycard'
anonymise.unlink_user = True
anonymise.mode = 'unlink_user'
wf.store()
carddef = CardDef()

View File

@ -447,7 +447,7 @@ def test_anonymise_action_unlink_user(pub, submitter_is_triggerer):
anonymise = wf.possible_status[1].add_action('anonymise', id='_anonymise', prepend=True)
anonymise.label = 'Unlink User'
anonymise.varname = 'mycard'
anonymise.unlink_user = True
anonymise.mode = 'unlink_user'
wf.store()
formdef = FormDef()

View File

@ -281,8 +281,9 @@ class Field:
condition = None
# flag a field for removal by AnonymiseWorkflowStatusItem
# possible values are final, intermediate, no.

Peut-être actualiser le commentaire qui précède, pour noter les valeurs possibles .

Peut-être actualiser le commentaire qui précède, pour noter les valeurs possibles .

Fait.

Fait.
# can be overriden in field' settings
anonymise = True
anonymise = 'final'
stats = None
# declarations for serialization, they are mostly for legacy files,
@ -623,6 +624,9 @@ class Field:
self.display_locations.append('listings')
changed = True
self.in_listing = None
if isinstance(self.anonymise, bool): # 2023-06-13
self.anonymise = 'final' if self.anonymise else 'no'
changed = True

Ce code va toujours mettre "final" comme valeur, ça devrait être soit if/else, soit en une seule ligne,

self.anonymise = 'final' if self.anonymise else 'no'
Ce code va toujours mettre "final" comme valeur, ça devrait être soit if/else, soit en une seule ligne, ``` self.anonymise = 'final' if self.anonymise else 'no' ```
return changed
@staticmethod
@ -949,6 +953,9 @@ class WidgetField(Field):
return options
def get_anonymise_options(self):
return [('final', _('Final')), ('intermediate', _('Intermediate')), ('no', _('No'))]
def fill_admin_form(self, form):
form.add(StringWidget, 'label', title=_('Label'), value=self.label, required=True, size=50)
form.add(
@ -1006,9 +1013,10 @@ class WidgetField(Field):
if 'anonymise' in self.get_admin_attributes():
# override anonymise flag default value
form.add(
CheckboxWidget,
RadiobuttonsWidget,
'anonymise',
title=_('Anonymise'),
title=_('Anonymisation'),
options=self.get_anonymise_options(),
value=self.anonymise,
advanced=True,
hint=_('Marks the field data for removal in the anonymisation processes.'),
@ -1559,7 +1567,7 @@ class BoolField(WidgetField):
widget_class = CheckboxWidget
required = False
anonymise = False
anonymise = 'no'
def perform_more_widget_changes(self, form, kwargs, edit=True):
if not edit:
@ -2276,7 +2284,7 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
items = []
show_as_radio = None
anonymise = False
anonymise = 'no'
widget_class = SingleSelectHintWidget
data_source = {}
in_filters = False
@ -3761,7 +3769,7 @@ class RankedItemsField(WidgetField):
items = []
randomize_items = False
widget_class = RankedItemsWidget
anonymise = False
anonymise = 'no'
def perform_more_widget_changes(self, form, kwargs, edit=True):
kwargs['elements'] = self.items or []

View File

@ -1255,11 +1255,17 @@ class FormData(StorableObject):
last_update_time = property(get_last_update_time, set_last_update_time)
def anonymise(self):
def anonymise(self, mode='final'):
for field in self.formdef.get_all_fields():
if field.anonymise:
if field.anonymise == 'no':
continue

Je pense que si kind == 'final' ça devrait anonymiser tout, y compris ce qui est marqué pour 'intermediate'.

Je pense que si kind == 'final' ça devrait anonymiser tout, y compris ce qui est marqué pour 'intermediate'.
if mode in ('final', field.anonymise):
field.set_value(self.data, None)

Je trouverais plus lisible (à la fois le diff et le code résultant) de moins déplacer, plutôt avoir un return supplémentaire, plus haut

if kind != 'final':
    self.store()
    return

self.anonymised = localtime()
self.user_id = None
...
Je trouverais plus lisible (à la fois le diff et le code résultant) de moins déplacer, plutôt avoir un return supplémentaire, plus haut ``` if kind != 'final': self.store() return self.anonymised = localtime() self.user_id = None ... ```
if mode != 'final':
self.store()
return
self.anonymised = localtime()
self.user_id = None
self.user_label = None
@ -1325,7 +1331,7 @@ class FormData(StorableObject):
new_data = {}
seen = set()
for field in fields:
if anonymise and field.anonymise:
if anonymise and field.anonymise == 'final':
continue
if not field.varname and not include_unnamed_fields:
continue

View File

@ -17,7 +17,7 @@
from quixote import get_request, get_session
from wcs.qommon import _
from wcs.qommon.form import CheckboxWidget
from wcs.qommon.form import RadiobuttonsWidget
from wcs.workflows import WorkflowStatusItem, register_item_class
@ -25,36 +25,53 @@ class AnonymiseWorkflowStatusItem(WorkflowStatusItem):
description = _('Anonymisation')
key = 'anonymise'
category = 'formdata-action'
unlink_user = False
mode = 'final'

Détail mais plutôt "mode" que "kind" (pour faire comme user_association_mode, operation_mode, target_mode, qui existent dans les autres actions).

Détail mais plutôt "mode" que "kind" (pour faire comme user_association_mode, operation_mode, target_mode, qui existent dans les autres actions).
def migrate(self):
changed = super().migrate()
if getattr(self, 'unlink_user', False): # 2023-06-13
self.mode = 'unlink_user'
self.unlink_user = None
changed = True
return changed
def get_line_details(self):
if self.unlink_user:
if self.mode == 'unlink_user':
return _('only user unlinking')
def perform(self, formdata):
if self.unlink_user is True:
if self.mode == 'unlink_user':
if get_request() and formdata.is_submitter(get_request().user):
get_session().mark_anonymous_formdata(formdata)
formdata.unlink_user()
formdata.remove_tracking_code()
else:
formdata.anonymise()
# self.mode is 'intermediate' or 'final'
formdata.anonymise(self.mode)
def get_parameters(self):
return ('unlink_user',)
return ('mode',)
def get_mode_options(self):
return [
('final', _('Final')),
('intermediate', _('Intermediate')),
(
'unlink_user',
_('Only unlink user from the form/card. If existing the tracking code will be deleted.'),
),
]
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
super().add_parameters_widgets(form, parameters, prefix, formdef, **kwargs)
if 'unlink_user' in parameters:
if 'mode' in parameters:
form.add(
CheckboxWidget,
'%sunlink_user' % prefix,
title=_('Only perform form/card user unlinking'),
hint=_(
'If checked, this action will only unlink user from the form/card. '
'If existing, the tracking code will be deleted.'
),
value=self.unlink_user,
RadiobuttonsWidget,
'%smode' % prefix,
title=_('Anonymisation type'),
options=self.get_mode_options(),
value=self.mode,
default_value=self.__class__.mode,
)