édition d'une fiche dans une popup (#73689) #83

Merged
lguerin merged 1 commits from wip/73689-card-edit-popup into main 2023-02-28 10:46:04 +01:00
15 changed files with 289 additions and 46 deletions

View File

@ -1111,7 +1111,7 @@ def test_carddata_edit_user_selection(pub):
assert '/user-pending-forms' not in resp.text
def test_carddata_add_related(pub):
def test_carddata_add_edit_related(pub):
user = create_user(pub)
BlockDef.wipe()
@ -1160,11 +1160,13 @@ def test_carddata_add_related(pub):
id='1',
label='First name',
type='string',
varname='firstname',
),
fields.StringField(
id='2',
label='Last name',
type='string',
varname='lastname',
),
fields.ItemField(
id='3',
@ -1176,6 +1178,7 @@ def test_carddata_add_related(pub):
]
adult.backoffice_submission_roles = user.roles
adult.workflow_roles = {'_editor': user.roles[0]}
adult.digest_templates = {'default': '{{ form_var_firstname }} {{ form_var_lastname }}'}
adult.store()
adult.data_class().wipe()
@ -1186,15 +1189,18 @@ def test_carddata_add_related(pub):
id='1',
label='First name',
type='string',
varname='firstname',
),
fields.StringField(
id='2',
label='Last name',
type='string',
varname='lastname',
),
]
child.backoffice_submission_roles = user.roles
child.workflow_roles = {'_editor': user.roles[0]}
child.digest_templates = {'default': '{{ form_var_firstname }} {{ form_var_lastname }}'}
child.store()
child.data_class().wipe()
@ -1205,6 +1211,12 @@ def test_carddata_add_related(pub):
assert 'Add another Child' in resp
assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 2
assert '/backoffice/data/child/add/?_popup=1' in resp
assert resp.pyquery('select#form_f1').attr('data-initial-edit-related-url') == ''
assert resp.pyquery('#edit_form_f1.edit-related')
assert resp.pyquery('select#form_f2').attr('data-initial-edit-related-url') == ''
assert resp.pyquery('#edit_form_f2.edit-related')
assert resp.pyquery('select#form_f3__element0__f1').attr('data-initial-edit-related-url') == ''
assert resp.pyquery('#edit_form_f3__element0__f1.edit-related')
resp_popup = app.get('/backoffice/data/adult/add/?_popup=1')
assert 'select2.min.js' in resp_popup.text
@ -1217,24 +1229,135 @@ def test_carddata_add_related(pub):
assert 'Add another Child' in resp
assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 1
assert '/backoffice/data/child/add/?_popup=1' in resp
assert resp.pyquery('select#form_f1').attr('data-initial-edit-related-url') is None
assert not resp.pyquery('#edit_form_f1.edit-related')
assert resp.pyquery('select#form_f2').attr('data-initial-edit-related-url') == ''
assert resp.pyquery('#edit_form_f2.edit-related')
assert resp.pyquery('select#form_f3__element0__f1').attr('data-initial-edit-related-url') == ''
assert resp.pyquery('#edit_form_f3__element0__f1.edit-related')
# check creation and edition in popup
resp = app.get('/backoffice/data/child/add/?_popup=1')
resp.form['f1'] = 'foo'
resp.form['f2'] = 'bar'
resp = resp.form.submit('submit')
carddata = child.data_class().select()[0]
assert carddata.get_workflow_traces()
childdata = child.data_class().select()[0]
assert len(childdata.get_workflow_traces()) == 1
# user ha no creation rights on child
child.backoffice_submission_roles = None
child.store()
resp = app.get('/backoffice/data/family/add/')
resp = app.get('/backoffice/data/child/%s/wfedit-_editable?_popup=1' % childdata.id)
assert resp.form['f1'].value == 'foo'
assert resp.form['f2'].value == 'bar'
resp.form['f1'] = 'foo2'
resp.form['f2'] = 'bar2'
resp = resp.form.submit('submit')
childdata.refresh_from_storage()
assert len(childdata.get_workflow_traces()) == 2
# create some data
adultdata1 = adult.data_class()()
adultdata1.data = {
'1': 'foo',
'2': 'bar 1',
'3': 'Foo',
}
adultdata1.just_created()
adultdata1.store()
adultdata2 = adult.data_class()()
adultdata2.data = {
'1': 'foo',
'2': 'bar 2',
'3': 'Foo',
}
adultdata2.just_created()
adultdata2.store()
familydata = family.data_class()()
familydata.data = {
'1': str(adultdata1.id),
'2': str(adultdata2.id),
'3': {
'data': [{'1': str(childdata.id), '1_display': childdata.default_digest}],
'schema': {}, # not important here
},
'3_display': 'blah',
}
familydata.just_created()
familydata.store()
# check initial values
resp = app.get('/backoffice/data/family/%s/wfedit-_editable' % familydata.id)
assert 'Add another RL1' not in resp
assert 'Add another RL2' in resp
assert 'Add another Child' not in resp
assert 'Add another Child' in resp
assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 1
assert '/backoffice/data/child/add/?_popup=1' in resp
assert resp.pyquery('select#form_f1').attr('data-initial-edit-related-url') is None
assert not resp.pyquery('#edit_form_f1.edit-related')
assert (
resp.pyquery('select#form_f2').attr('data-initial-edit-related-url')
== 'http://example.net/backoffice/data/adult/%s/wfedit-_editable' % adultdata2.id
)
assert resp.pyquery('#edit_form_f2.edit-related')
assert (
resp.pyquery('select#form_f3__element0__f1').attr('data-initial-edit-related-url')
== 'http://example.net/backoffice/data/child/%s/wfedit-_editable' % childdata.id
)
assert resp.pyquery('#edit_form_f3__element0__f1.edit-related')
# check autocomplete result
# no query, no edit url
autocomplete_resp = app.get(resp.pyquery('select#form_f2').attr('data-select2-url') + '?page_limit=10')
assert autocomplete_resp.json == {
'data': [{'id': 1, 'text': 'foo bar 1'}, {'id': 2, 'text': 'foo bar 2'}]
}
# no limit, no edit url
autocomplete_resp = app.get(resp.pyquery('select#form_f2').attr('data-select2-url') + '?q=foo')
assert autocomplete_resp.json == {
'data': [{'id': 1, 'text': 'foo bar 1'}, {'id': 2, 'text': 'foo bar 2'}]
}
# ok
autocomplete_resp = app.get(
resp.pyquery('select#form_f2').attr('data-select2-url') + '?q=foo&page_limit=10'
)
assert autocomplete_resp.json == {
'data': [
{
'id': 1,
'text': 'foo bar 1',
'edit_related_url': 'http://example.net/backoffice/data/adult/1/wfedit-_editable',
},
{
'id': 2,
'text': 'foo bar 2',
'edit_related_url': 'http://example.net/backoffice/data/adult/2/wfedit-_editable',
},
]
}
# user has no creation rights on child
child.backoffice_submission_roles = None
child.store()
resp = app.get('/backoffice/data/family/%s/wfedit-_editable' % familydata.id)
assert 'Add another Child' not in resp
assert '/backoffice/data/child/add/?_popup=1' not in resp
# user has no edit rights on adult
adult.workflow_roles = {}
adult.store()
resp = app.get('/backoffice/data/family/%s/wfedit-_editable' % familydata.id)
assert resp.pyquery('select#form_f2').attr('data-initial-edit-related-url') == ''
assert resp.pyquery('#edit_form_f2.edit-related')
autocomplete_resp = app.get(
resp.pyquery('select#form_f2').attr('data-select2-url') + '?q=foo&page_limit=10'
)
assert autocomplete_resp.json == {
'data': [
{'id': 1, 'text': 'foo bar 1', 'edit_related_url': ''},
{'id': 2, 'text': 'foo bar 2', 'edit_related_url': ''},
]
}
def test_backoffice_card_global_interactive_action(pub):
user = create_user(pub)

View File

@ -1214,13 +1214,21 @@ class AutocompleteDirectory(Directory):
if 'dynamic_custom_view' in info:
custom_view = get_publisher().custom_view_class.get(info['dynamic_custom_view'])
custom_view.filters = info['dynamic_custom_view_filters']
query = get_request().form.get('q', '')
limit = get_request().form.get('page_limit')
edit_related = info.get('edit_related')

(edit_related est passé dans le context du token)

(edit_related est passé dans le context du token)
with_edit_related_url = query and limit and edit_related

pour l'autocomplete, on a toujours un param q et une pagination; par précaution, si l'un est manquant, pas de vérification des droits du user pour éviter de jouer ça sur toutes les fiches

pour l'autocomplete, on a toujours un param q et une pagination; par précaution, si l'un est manquant, pas de vérification des droits du user pour éviter de jouer ça sur toutes les fiches
values = CardDef.get_data_source_items(
carddef_ref,
custom_view=custom_view,
query=get_request().form.get('q', ''),
limit=get_request().form.get('page_limit'),
query=query,
limit=limit,
with_edit_related_url=with_edit_related_url,
)
return json.dumps({'data': [{'id': x['id'], 'text': x['text']} for x in values]})
keys = ['id', 'text']
if with_edit_related_url:
keys.append('edit_related_url')
return json.dumps({'data': [{key: x.get(key, '') for key in keys} for x in values]})
class GeoJsonDirectory(Directory):

View File

@ -355,6 +355,7 @@ class CardFillPage(FormFillPage):
{
'value': str(filled.id),
'obj': str(filled.default_digest),
'edit_related_url': filled.get_edit_related_url() or '',
}
)
return template.QommonTemplateResponse(

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
from quixote import get_publisher
from quixote import get_publisher, get_request
from wcs.formdata import FormData
@ -36,7 +36,7 @@ class CardData(FormData):
formdef = property(get_formdef)
def get_data_source_structured_item(self, digest_key='default'):
def get_data_source_structured_item(self, digest_key='default', with_edit_related_url=False):
if self.digests is None:
if digest_key == 'default':
summary = _('Digest (default) not defined')
@ -48,6 +48,10 @@ class CardData(FormData):
'id': self.id,
'text': (self.digests or {}).get(digest_key) or '',
}
if with_edit_related_url:
edit_related_url = self.get_edit_related_url()
if edit_related_url:
item['edit_related_url'] = edit_related_url
for field in self.formdef.get_all_fields():
if not field.varname or field.varname in ('id', 'text'):
continue
@ -56,6 +60,22 @@ class CardData(FormData):
item[field.varname] = value
return item
def get_edit_related_url(self):
wf_status = self.get_status()
for _item in wf_status.items:
if not _item.key == 'editable':
continue
if not _item.check_auth(self, get_request().user):
continue
return (
self.get_url(
backoffice=get_request().is_in_backoffice(),
include_category=True,
language=get_publisher().current_language,
)
+ 'wfedit-%s' % _item.id

(j'ai repris la même url que dans la méthode submit_form de l'action editable)

(j'ai repris la même url que dans la méthode submit_form de l'action editable)
)
def get_display_label(self, digest_key='default'):
return (self.digests or {}).get(digest_key) or self.get_display_name()

View File

@ -183,7 +183,14 @@ class CardDef(FormDef):
@classmethod
def get_data_source_items(
cls, data_source_id, query=None, limit=None, custom_view=None, get_by_id=None, get_by_text=None
cls,
data_source_id,
query=None,
limit=None,
custom_view=None,
get_by_id=None,
get_by_text=None,
with_edit_related_url=False,
):
assert data_source_id.startswith('carddef:')
parts = data_source_id.split(':')
@ -244,7 +251,9 @@ class CardDef(FormDef):
criterias.append(ElementEqual('digests', digest_key, get_by_text))
items = [
x.get_data_source_structured_item(digest_key=digest_key)
x.get_data_source_structured_item(
digest_key=digest_key, with_edit_related_url=with_edit_related_url
)
for x in carddef.data_class().select(clause=criterias, order_by=order_by, limit=limit)
]
if order_by is None:

View File

@ -823,7 +823,7 @@ class NamedDataSource(XmlStorableObject):
url += '&' + self.query_parameter + '='
return url
def get_jsonp_url(self):
def get_jsonp_url(self, **kwargs):
if self.type == 'jsonp':
return self.data_source.get('value')
@ -833,7 +833,7 @@ class NamedDataSource(XmlStorableObject):
token_context = {'url': json_url, 'data_source': self.id}
elif self.type and self.type.startswith('carddef:'):
token_context = {'carddef_ref': self.type}
token_context = {'carddef_ref': self.type, **kwargs}
parts = self.type.split(':')
if len(parts) > 2:
@ -858,11 +858,12 @@ class NamedDataSource(XmlStorableObject):
had_template = True
if had_template:
# keep altered custom view in token
token_context = {
'carddef_ref': self.type,
'dynamic_custom_view': custom_view.id,
'dynamic_custom_view_filters': custom_view.filters,
}
token_context.update(
{
'dynamic_custom_view': custom_view.id,
'dynamic_custom_view_filters': custom_view.filters,
}
)
if token_context:
token_context['session_id'] = get_session().id

View File

@ -2219,8 +2219,12 @@ class ItemFieldMixin:
and data_source
and data_source.can_jsonp()
):
carddef = self.get_carddef()
url_kwargs = {}
if self.key == 'item' and get_request().is_in_backoffice() and carddef:
url_kwargs['edit_related'] = True
# store display value in session to be used by select2
url = data_source.get_jsonp_url()
url = data_source.get_jsonp_url(**url_kwargs)
if not session.jsonp_display_values:
session.jsonp_display_values = {}
session.jsonp_display_values['%s_%s' % (url, value)] = display_value
@ -2302,14 +2306,14 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
display_mode = self.get_display_mode(data_source)
if display_mode == 'autocomplete' and data_source and data_source.can_jsonp():
self.url = kwargs['url'] = data_source.get_jsonp_url()
carddef = self.get_carddef()
if (
get_request().is_in_backoffice()
and carddef
and carddef.can_user_add_cards(get_request().user)
):
kwargs['add_related_url'] = carddef.get_backoffice_submission_url()
url_kwargs = {}
if get_request().is_in_backoffice() and carddef:
if carddef.can_user_add_cards(get_request().user):
kwargs['add_related_url'] = carddef.get_backoffice_submission_url()
kwargs['edit_related'] = True
url_kwargs['edit_related'] = True
self.url = kwargs['url'] = data_source.get_jsonp_url(**url_kwargs)
self.widget_class = JsonpSingleSelectWidget
return

View File

@ -1077,7 +1077,8 @@ class FormPage(FormdefDirectoryBase, FormTemplateMixin):
existing_formdata = None
if self.edit_mode:
existing_formdata = self.edited_data.data
if not get_request().form:
request_data = {k: v for k, v in get_request().form.items() if k != '_popup'}
if not request_data:
# on the initial visit editing the form (i.e. not after
# clicking for previous or next page), we need to load the
# existing data into the session
@ -1771,6 +1772,21 @@ class FormPage(FormdefDirectoryBase, FormTemplateMixin):
self.edited_data.evolution.append(evo)
self.edited_data.store()
break
if get_request().form.get('_popup'):
popup_response_data = json.dumps(
{
'value': str(self.edited_data.id),
'obj': str(self.edited_data.default_digest),
'edit_related_url': self.edited_data.get_edit_related_url() or '',
}
)
return template.QommonTemplateResponse(
templates=['wcs/backoffice/popup_response.html'],
context={'popup_response_data': popup_response_data},
is_django_native=True,
)
return redirect(url or '.')
def validating(self, data, page_error_messages=None):

View File

@ -2878,10 +2878,11 @@ class RankedItemsWidget(CompositeWidget):
class JsonpSingleSelectWidget(Widget):
template_name = 'qommon/forms/widgets/select_jsonp.html'
def __init__(self, name, value=None, url=None, add_related_url=None, **kwargs):
def __init__(self, name, value=None, url=None, add_related_url=None, edit_related=False, **kwargs):
super().__init__(name, value=value, **kwargs)
self.url = url
self.add_related_url = add_related_url
self.edit_related = edit_related
def add_media(self):
get_response().add_javascript(['select2.js'])
@ -2905,6 +2906,27 @@ class JsonpSingleSelectWidget(Widget):
return get_session().jsonp_display_values.get(key)
def get_edit_related_url(self):
if not self.edit_related:
return
if self.value is None:
value = None
else:
value = htmlescape(self.value)
if not value:
return None
field = getattr(self, 'field', None)
if not field:
return
carddef = field.get_carddef()
if not carddef:
return
try:
carddata = carddef.data_class().get(value)
except KeyError:
return
return carddata.get_edit_related_url()
def get_select2_url(self):
if Template.is_template_string(self.url):
vars = get_publisher().substitutions.get_context_variables(mode='lazy')

View File

@ -2368,10 +2368,21 @@ div.timetable-widget {
.wcs-widget-select2-container {
display: flex;
.add-related {
.add-related, .edit-related {
margin-top: 2px; // == margin-top of span.select2-container
margin-left: 3px;
}
a.edit-related {
text-indent: -10000px;
overflow: hidden;
width: 12px;
background: white url(/static/css/icons/action-edit.small.#{$string-color}.png) center center no-repeat;
background-size: 16px;
&:hover {
background-color: #386ede;
background-image: url(/static/css/icons/action-edit.small.white.png);
}
}
}
.wcs-block-with-remove-button {

View File

@ -1,4 +1,4 @@
(function() {
var initData = JSON.parse(document.getElementById('popup-response-constants').dataset.popupResponse);
opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj);
opener.dismissRelatedObjectPopup(window, initData.value, initData.obj, initData.edit_related_url);
})();

View File

@ -387,36 +387,42 @@ $(function() {
return text;
}
function showAddRelatedObjectPopup(triggeringLink) {
var name = triggeringLink.id.replace(/^add_/, '');
function showPopup(triggeringLink, name_regexp) {
var name = triggeringLink.id.replace(name_regexp, '');
name = id_to_windowname(name);
console.log(name)
var href = triggeringLink.href;
console.log(href)
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
win.focus();
return false;
}
function dismissAddRelatedObjectPopup(win, newId, newRepr) {
function showRelatedObjectPopup(triggeringLink) {
return showPopup(triggeringLink, /^(edit|add)_/);
}
function dismissRelatedObjectPopup(win, newId, newRepr, edit_related_url) {
var name = windowname_to_id(win.name);
var elem = document.getElementById(name);
if (elem) {
var elemName = elem.nodeName.toUpperCase();
if (elemName === 'SELECT') {
elem.options[elem.options.length] = new Option(newRepr, newId, true, true);
var $option = $('<option />').val(newId).html(newRepr).prop('selected', true);
if (edit_related_url) {
$option.attr('data-edit-related-url', edit_related_url);

ajout d'un data attribute sur l'option sélectionnée à la fermeture de la popup

ajout d'un data attribute sur l'option sélectionnée à la fermeture de la popup
}
$(elem).append($option);
}
// Trigger a change event to update related links if required.
$(elem).trigger('change');
}
win.close();
}
window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
window.dismissRelatedObjectPopup = dismissRelatedObjectPopup;
$('body').on('click', '.add-related', function(e) {
$('body').on('click', '.add-related, .edit-related', function(e) {
e.preventDefault();
if (this.href) {
showAddRelatedObjectPopup(this);
showRelatedObjectPopup(this);
}
});
});

View File

@ -331,6 +331,12 @@ $(function() {
inputTooShort: function (input, min) { return WCS_I18N.s2_tooshort; },
loadingMore: function () { return WCS_I18N.s2_loadmore; },
searching: function () { return WCS_I18N.s2_searching; }
},
templateSelection: function(data, container) {
if (data.edit_related_url) {
$(data.element).attr('data-edit-related-url', data.edit_related_url);
}
return data.text;

je me suis un peu perdue dans le js, ça marche comme ça mais ça éparpille pas mal les choses ...
là c'est pour ajouter un data attribute sur les options du select à l'autocompletion

je me suis un peu perdue dans le js, ça marche comme ça mais ça éparpille pas mal les choses ... là c'est pour ajouter un data attribute sur les options du select à l'autocompletion
}
};
if (!required) {
@ -368,8 +374,14 @@ $(function() {
var select2 = $(elem).select2(options);
$(elem).on('change', function() {
// update _display hidden field with selected text
var text = $(elem).find(':selected').first().text();
var $selected = $(elem).find(':selected').first();
var text = $selected.text();
$input_display_value.val(text);
// update edit-related button href
$(elem).siblings('.edit-related').attr('href', '').hide();
if ($selected.attr('data-edit-related-url')) {
$(elem).siblings('.edit-related').attr('href', $selected.attr('data-edit-related-url') + '?_popup=1').show();

affichage du lien d'édition si on a un data attribute qui va bien sur l'option sélectionnée

affichage du lien d'édition si on a un data attribute qui va bien sur l'option sélectionnée
}
});
if ($input_display_value.val()) {
// if the _display hidden field was created with an initial value take it
@ -378,6 +390,9 @@ $(function() {
var option = $('<option></option>', {value: $(elem).data('value')});
option.appendTo($(elem));
option.text($input_display_value.val());
if ($(elem).data('initial-edit-related-url')) {
option.attr('data-edit-related-url', $(elem).data('initial-edit-related-url'));

pour gérer la valeur initiale du champ, et avoir le bouton d'édition

pour gérer la valeur initiale du champ, et avoir le bouton d'édition
}
select2.val($(elem).data('value')).trigger('change');
$(elem).select2('data', {id: $(elem).data('value'), text: $(elem).data('initial-display-value')});
}

View File

@ -6,12 +6,19 @@
data-select2-url="{{widget.get_select2_url}}"
{% if widget.value %}data-value="{{ widget.value }}"{% endif %}
data-required="{% if widget.is_required %}true{% endif %}"
data-initial-display-value="{{widget.get_display_value|default_if_none:''}}">
data-initial-display-value="{{widget.get_display_value|default_if_none:''}}"
{% if widget.edit_related %}data-initial-edit-related-url="{{widget.get_edit_related_url|default_if_none:''}}"{% endif %}>
</select>
{% if widget.add_related_url %}
<a class="add-related pk-button" id="add_form_{{ widget.get_name_for_id }}"
href="{{ widget.add_related_url }}?_popup=1"
title="{% blocktrans with card=widget.get_title %}Add another {{ card }}{% endblocktrans %}">+</a>
{% endif %}
{% if widget.edit_related %}
<a class="edit-related pk-button" id="edit_form_{{ widget.get_name_for_id }}"
style="display: none"
title="{% blocktrans with card=widget.get_title %}Edit selected {{ card }}{% endblocktrans %}"
>{% blocktrans with card=widget.get_title %}Edit selected {{ card }}{% endblocktrans %}</a>
{% endif %}
</div>
{% endblock %}

View File

@ -4,7 +4,7 @@
<body>
<script type="text/javascript"
id="popup-response-constants"
src="{% static "/js/popup_response.js" %}"
src="{% static "js/popup_response.js" %}"
data-popup-response="{{ popup_response_data }}">
</script>
</body>