backoffice: edit related card in a popup (#73689)
gitea/wcs/pipeline/head This commit looks good Details

This commit is contained in:
Lauréline Guérin 2023-01-31 16:03:56 +01:00
parent 6354c452c0
commit 68726ff1ae
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
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')
with_edit_related_url = query and limit and edit_related
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
)
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);
}
$(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;
}
};
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();
}
});
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'));
}
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>