Skip to content
Open
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
3 changes: 3 additions & 0 deletions examples/sqla/admin/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ class PostAdmin(ModelView):
column_default_sort = ("date", True)
create_modal = True
edit_modal = True
can_view_details = True
details_modal = True

column_sortable_list = [
"id",
"title",
Expand Down
20 changes: 19 additions & 1 deletion flask_admin/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2446,8 +2446,19 @@ def edit_view(self) -> T_RESPONSE | str:
else:
template = self.edit_template

if self.can_delete:
delete_form = self.delete_form()
else:
delete_form = None

return self.render(
template, model=model, form=form, form_opts=form_opts, return_url=return_url
template,
model=model,
form=form,
form_opts=form_opts,
return_url=return_url,
delete_form=delete_form,
get_pk_value=self.get_pk_value,
)

@expose("/details/")
Expand Down Expand Up @@ -2475,12 +2486,19 @@ def details_view(self) -> T_RESPONSE | str:
else:
template = self.details_template

if self.can_delete:
delete_form = self.delete_form()
else:
delete_form = None

return self.render(
template,
model=model,
details_columns=self._details_columns,
get_value=self.get_detail_value,
return_url=return_url,
delete_form=delete_form,
get_pk_value=self.get_pk_value,
)

@expose("/delete/", methods=("POST",))
Expand Down
110 changes: 94 additions & 16 deletions flask_admin/templates/bootstrap4/admin/lib.html
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ <h3>{{ text }}</h3>
{% endif %}
{% endmacro %}

{% macro form_tag(form=None, action=None) %}
``{% macro form_tag(form=None, action=None) %}

<form action="{{ action or '' }}" method="POST" role="form" class="admin-form" enctype="multipart/form-data">
<fieldset>
{{ caller() }}
Expand All @@ -213,29 +214,106 @@ <h3>{{ text }}</h3>

{% macro render_form_buttons(cancel_url, extra=None, is_modal=False) %}
{% if is_modal %}
<input type="submit" class="btn btn-primary" value="{{ _gettext('Save') }}" />
{% if extra %}
{{ extra }}
{% endif %}
{% if cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-danger" role="button" {% if is_modal %}data-dismiss="modal"{% endif %}>{{ _gettext('Cancel') }}</a>
{% endif %}
<input type="submit" class="btn btn-primary" value="{{ _gettext('Save') }}" />
{% if extra %}
{{ extra }}
{% endif %}
{% if cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-danger" role="button" {% if is_modal %}data-dismiss="modal"{% endif %}>{{ _gettext('Cancel') }}</a>
{% endif %}
<div>
{{ render_delete_form() }}
</div>

{% else %}
<hr>
<div class="form-group">
<div class="col-md-offset-2 col-md-10 submit-row">
<input type="submit" class="btn btn-primary" value="{{ _gettext('Save') }}" />
{% if extra %}
{{ extra }}
{% endif %}
{% if cancel_url is defined and cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-danger" role="button" {% if is_modal %}data-dismiss="modal"{% endif %}>{{ _gettext('Cancel') }}</a>
{% endif %}
<div class="d-flex justify-content-between">
<div class="col-md-offset-2 col-md-10 submit-row">
<input type="submit" class="btn btn-primary" value="{{ _gettext('Save') }}" />
{% if extra %}
{{ extra }}
{% endif %}
{% if cancel_url is defined and cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-danger" role="button" {% if is_modal %}data-dismiss="modal"{% endif %}>{{ _gettext('Cancel') }}</a>
{% endif %}

</div>
<div>
{{ render_delete_form() }}
</div>
</div>
</div>
{% endif %}
{% endmacro %}


{% macro render_delete_form() -%}

{% if delete_form is defined %}

{{ delete_form.id(id='delete-id', value=get_pk_value(model)) }}
{{ delete_form.url(id='delete-url', value=return_url) }}
{% if delete_form.csrf_token is defined and delete_form.csrf_token %}
{{ delete_form.csrf_token(id='delete-csrf') }}
{% elif csrf_token is defined and csrf_token %}
<input type="hidden" name="csrf_token" id="delete-csrf" value="{{ csrf_token() }}"/>
{% endif %}

<button class="btn btn-outline-danger" id="btn-delete"
data-msg="{{ _gettext('Are you sure you want to delete this record?') }}"
title="{{ _gettext('Delete Record') }}">
<span class="fa fa-trash glyphicon glyphicon-trash"></span>
{{ _gettext('Delete') }}
</button>

<script {{ admin_csp_nonce_attribute }} >

function deleteButtonHandler() {
$('#btn-delete').on('click', function (e)
{
e.preventDefault();
const msg = $(this).data('msg');
const confirmed = faHelpers.safeConfirm(msg);
if (!confirmed) {
return false;
}
const formData = new FormData();
formData.append('id', document.getElementById('delete-id').value);
formData.append('url', document.getElementById('delete-url').value);
formData.append('csrf_token', document.getElementById('delete-csrf')?.value);

fetch(`{{ url_for('.delete_view') }}`,
{
method: 'POST',
body: formData
}).then(res => {
console.log(res);
if (res.ok)
window.location.href = document.getElementById('delete-url').value;
});
});
}

document.addEventListener('DOMContentLoaded', function()
{
//console.log("DOM is fully loaded and parsed");
deleteButtonHandler();
});

// If the delete button is inside a modal, we need to re-attach the event handler every time the modal is shown
$('#fa_modal_window').on('shown.bs.modal', function (e)
{
//console.log("Modal shown");
deleteButtonHandler();
});

</script>

{% endif %}

{% endmacro %}

{% macro render_form(form, cancel_url, extra=None, form_opts=None, action=None, is_modal=False) -%}
{% call form_tag(action=action) %}
{{ render_form_fields(form, form_opts=form_opts) }}
Expand Down
18 changes: 12 additions & 6 deletions flask_admin/templates/bootstrap4/admin/model/details.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@
</ul>
{% endblock %}

{% block details_search %}
<div class="form-inline fa_filter_container col-lg-6">
<label for="fa_filter">{{ _gettext('Filter') }}</label>
<input id="fa_filter" type="text" class="ml-3 form-control">
</div>
{% block details_actions %}
<div class="form-inline fa_filter_container d-flex justify-content-between">
{% block details_search %}
<div class="form-inline">
<label for="fa_filter">{{ _gettext('Filter') }}</label>
<input id="fa_filter" type="text" class="ml-3 form-control">
</div>
{% endblock %}
{% block details_delete %}
{{ lib.render_delete_form() }}
{% endblock %}
</div>
{% endblock %}

{% block details_table %}
<table class="table table-hover table-bordered searchable">
{% for c, name in details_columns %}
Expand Down
16 changes: 12 additions & 4 deletions flask_admin/templates/bootstrap4/admin/model/modals/details.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ <h5 class="modal-title">{{ _gettext('View Record') + ' #' + request.args.get('id
</div>

<div class="modal-body">
{% block details_search %}
<div class="form-inline fa_filter_container col-lg-6">
<label for="fa_filter">{{ _gettext('Filter') }}</label>
<input id="fa_filter" type="text" class="ml-3 form-control">

{% block details_actions %}
<div class="form-inline fa_filter_container col d-flex justify-content-between">
{% block details_search %}
<div class="form-inline">
<label for="fa_filter">{{ _gettext('Filter') }}</label>
<input id="fa_filter" type="text" class="ml-3 form-control">
</div>
{% endblock %}
{% block details_delete %}
{{ lib.render_delete_form() }}
{% endblock %}
</div>
{% endblock %}

Expand Down
55 changes: 55 additions & 0 deletions flask_admin/tests/sqla/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3812,6 +3812,61 @@ class MyModelView(CustomModelView):
)


@pytest.mark.parametrize(
"with_delete, modal", [(True, True), (True, False), (False, True), (False, False)]
)
def test_del_btn_in_edit_and_details(
app: Flask,
sqla_db_ext: T_ANY_SQLA_PROVIDER,
admin: Admin,
session_or_db: T_LITERAL_SESSION_OR_DB,
with_delete: bool,
modal: bool,
) -> None:
with app.app_context():
Model1, Model2 = create_models(sqla_db_ext)
param = skip_or_return_session_or_db(sqla_db_ext, session_or_db)
sqla_db_ext.db.session.add_all(
[
Model1(test1="record-1"),
Model1(test1="record-2"),
]
)
sqla_db_ext.db.session.commit()

class MyModelView(CustomModelView):
can_edit = True
can_view_details = True

can_delete = with_delete
edit_modal = modal
details_modal = modal

# test column_list with a list of strings
view = MyModelView(
Model1,
param,
name="Without Modal",
)
admin.add_view(view)

client = app.test_client()

rv = client.get("/admin/model1/details/?id=2")
data = rv.data.decode("utf-8")
if with_delete:
assert "btn-delete" in data
else:
assert "btn-delete" not in data

rv = client.get("/admin/model1/edit/?id=2")
data = rv.data.decode("utf-8")
if with_delete:
assert "btn-delete" in data
else:
assert "btn-delete" not in data


@pytest.mark.xfail(
reason="SQLALiteProvider does not support passing db.session directly",
raises=TypeError,
Expand Down