diff --git a/help/fr/api-cards.page b/help/fr/api-cards.page index e80a7a64b..bece02065 100644 --- a/help/fr/api-cards.page +++ b/help/fr/api-cards.page @@ -386,6 +386,7 @@ Une API existe pour récupérer le schéma de données d’un modèle de fiches. "disabled_redirection" : null, "discussion" : false, "drafts_lifespan" : null, + "drafts_max_per_user" : null, "enable_tracking_codes" : false, "expiration_date" : null, "fields" : [ diff --git a/tests/admin_pages/test_form.py b/tests/admin_pages/test_form.py index fd0713334..81938d5e9 100644 --- a/tests/admin_pages/test_form.py +++ b/tests/admin_pages/test_form.py @@ -284,6 +284,7 @@ def test_forms_edit_tracking_code(pub, formdef): resp = resp.click('Form Tracking') assert resp.forms[0]['drafts_lifespan'].value == '' + assert resp.forms[0]['drafts_max_per_user'].value == '' resp = resp.forms[0].submit().follow() # check empty value is ok resp = resp.click('Form Tracking') @@ -297,6 +298,20 @@ def test_forms_edit_tracking_code(pub, formdef): resp = resp.forms[0].submit().follow() assert FormDef.get(1).drafts_lifespan == '5' + resp = resp.click('Form Tracking') + resp.forms[0]['drafts_max_per_user'].value = 'xxx' + resp = resp.forms[0].submit() + assert 'Maximum must be between 2 and 100 drafts.' in resp + resp.forms[0]['drafts_max_per_user'].value = '120' + resp = resp.forms[0].submit() + assert 'Maximum must be between 2 and 100 drafts.' in resp + resp.forms[0]['drafts_max_per_user'].value = '1' + resp = resp.forms[0].submit() + assert 'Maximum must be between 2 and 100 drafts.' in resp + resp.forms[0]['drafts_max_per_user'].value = '3' + resp = resp.forms[0].submit().follow() + assert FormDef.get(1).drafts_max_per_user == '3' + formdef.fields = [ fields.StringField(id='1', label='VerifyString'), fields.DateField(id='2', label='VerifyDate'), diff --git a/tests/form_pages/test_draft.py b/tests/form_pages/test_draft.py index b008d7da9..7f351cc6b 100644 --- a/tests/form_pages/test_draft.py +++ b/tests/form_pages/test_draft.py @@ -414,6 +414,14 @@ def test_form_max_drafts(pub): assert not formdef.data_class().has_key(drafts[0].id) # oldest draft was removed + formdef.drafts_max_per_user = '3' + formdef.store() + + resp = app.get('/test/') + resp.form['f0'] = 'hello2' + resp = resp.form.submit('submit') + assert formdef.data_class().count([Equal('status', 'draft')]) == 4 + def test_form_draft_temporary_access_url(pub): FormDef.wipe() diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py index e3ff8239f..b17b56bfb 100644 --- a/wcs/admin/forms.py +++ b/wcs/admin/forms.py @@ -30,6 +30,7 @@ from wcs.carddef import CardDef from wcs.categories import Category from wcs.formdef import ( DRAFTS_DEFAULT_LIFESPAN, + DRAFTS_DEFAULT_MAX_PER_USER, FormDef, FormdefImportError, FormdefImportRecoverableError, @@ -290,6 +291,23 @@ class OptionsDirectory(Directory): widget.validation_function = check_lifespan widget.validation_function_error_message = _('Lifespan must be between 2 and 100 days.') + widget = form.add( + WcsExtraStringWidget, + 'drafts_max_per_user', + title=_('Maximum number of drafts per user (between 2 and 100)'), + value=self.formdef.drafts_max_per_user, + hint=_('%s drafts per user by default') % DRAFTS_DEFAULT_MAX_PER_USER, + ) + + def check_max_per_user(value): + try: + return bool(int(value) >= 2 and int(value) <= 100) + except (ValueError, TypeError): + return False + + widget.validation_function = check_max_per_user + widget.validation_function_error_message = _('Maximum must be between 2 and 100 drafts.') + form.widgets.append(HtmlWidget(htmltext('

%s

') % _('Tracking Code'))) form.add( CheckboxWidget, @@ -495,6 +513,7 @@ class OptionsDirectory(Directory): 'id_template', 'submission_lateral_template', 'drafts_lifespan', + 'drafts_max_per_user', 'user_support', 'management_sidebar_items', ] diff --git a/wcs/formdef.py b/wcs/formdef.py index 0199ed79c..430405b24 100644 --- a/wcs/formdef.py +++ b/wcs/formdef.py @@ -61,6 +61,7 @@ from .qommon.upload_storage import PicklableUpload from .roles import logged_users_role DRAFTS_DEFAULT_LIFESPAN = 100 # days +DRAFTS_DEFAULT_MAX_PER_USER = 5 if not hasattr(types, 'ClassType'): types.ClassType = type @@ -190,6 +191,7 @@ class FormDef(StorableObject): submission_lateral_template = None id_template = None drafts_lifespan = None + drafts_max_per_user = None user_support = None geolocations = None @@ -220,6 +222,7 @@ class FormDef(StorableObject): 'submission_lateral_template', 'id_template', 'drafts_lifespan', + 'drafts_max_per_user', 'user_support', ] BOOLEAN_ATTRIBUTES = [ @@ -634,6 +637,9 @@ class FormDef(StorableObject): def get_drafts_lifespan(self): return int(self.drafts_lifespan or DRAFTS_DEFAULT_LIFESPAN) + def get_drafts_max_per_user(self): + return int(self.drafts_max_per_user or DRAFTS_DEFAULT_MAX_PER_USER) + _workflow = None def get_workflow(self): diff --git a/wcs/forms/root.py b/wcs/forms/root.py index 4671dabe3..67396e7aa 100644 --- a/wcs/forms/root.py +++ b/wcs/forms/root.py @@ -1778,11 +1778,11 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin): if get_session().mark_anonymous_formdata(filled): get_session().store() elif new_draft: - # keep at most 5 drafts per user + # keep at most "max_per_user" drafts per user data_class = self.formdef.data_class() for id in data_class.get_sorted_ids( '-last_update_time', [Equal('status', 'draft'), Equal('user_id', str(filled.user_id))] - )[5:]: + )[self.formdef.get_drafts_max_per_user() :]: data_class.remove_object(id) if new_draft: diff --git a/wcs/templates/wcs/backoffice/formdef-inspect.html b/wcs/templates/wcs/backoffice/formdef-inspect.html index a91be5f52..9ccfd4d63 100644 --- a/wcs/templates/wcs/backoffice/formdef-inspect.html +++ b/wcs/templates/wcs/backoffice/formdef-inspect.html @@ -67,6 +67,7 @@
  • {% trans "Fields to check after entering the tracking code" %}{% trans ":" %} {{ tracking_code_verify_fields_labels|default:"-" }}
  • {% endif %}
  • {% trans "Lifespan of drafts (in days)" %}{% trans ":" %} {{ formdef.get_drafts_lifespan }}
  • +
  • {% trans "Maximum number of drafts per user" %}{% trans ":" %} {{ formdef.get_drafts_max_per_user }}
  • {% trans "Templates" %}