Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async delete #126

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
29 changes: 28 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ Add script to the template from #5 and bind the ``modalForm`` to the trigger ele
});
</script>

Async create/update with or without modal closing on submit
Async create/update/delete with or without modal closing on submit
===========================================================

Set ``asyncUpdate`` and ``asyncSettings`` settings to create or update objects without page redirection to ``successUrl`` and define whether a modal should close or stay opened after form submission. See comments in example below and paragraph **modalForm options** for explanation of ``asyncSettings``.
Expand All @@ -234,6 +234,10 @@ Set ``asyncUpdate`` and ``asyncSettings`` settings to create or update objects w
<button type="button" class="update-book btn btn-sm btn-primary" data-form-url="{% url 'update_book' book.pk %}">
<span class="fa fa-pencil"></span>
</button>
<!-- Delete book buttons -->
<button type="button" class="delete-book-async btn btn-sm btn-danger" data-form-url="{% url 'delete_book' book.pk %}">
<span class="fa fa-trash"></span>
</button>
...
</td>
</tr>
Expand Down Expand Up @@ -278,6 +282,26 @@ Set ``asyncUpdate`` and ``asyncSettings`` settings to create or update objects w
});
}
updateBookModalForm();

//checking for form validity on delete means submitting the delete form twice, causing an error
function deleteBookModalForm() {
$(".delete-book").each(function () {
$(this).modalForm({
formURL: $(this).data("form-url"),
asyncUpdate: true,
checkValidBeforeSubmit: false,
asyncSettings: {
closeOnSubmit: false,
successMessage: asyncSuccessMessage,
dataUrl: "books/",
dataElementId: "#books-table",
dataKey: "table",
addModalFormFunction: deleteBookModalForm
}
});
});
}
deleteBookModalForm();

...
});
Expand Down Expand Up @@ -338,6 +362,9 @@ errorClass
submitBtn
Sets the custom class for the button triggering form submission in modal. ``Default: ".submit-btn"``

checkValidBeforeSubmit
Sets whether to check form validity before submitting (set to false for async delete). ``Default: true``

asyncUpdate
Sets asynchronous content update after form submission. ``Default: false``

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ https://github.com/trco/django-bootstrap-modal-forms
var addEventHandlers = function (settings) {
// submitBtn click handler
$(settings.submitBtn).on("click", function (event) {
isFormValid(settings, submitForm);
if (settings.checkValidBeforeSubmit) {
isFormValid(settings, submitForm);
}
else {
submitForm(settings);
}
});
// Modal close handler
$(settings.modalID).on("hidden.bs.modal", function (event) {
Expand Down Expand Up @@ -144,6 +149,7 @@ https://github.com/trco/django-bootstrap-modal-forms
formURL: null,
errorClass: ".invalid",
submitBtn: ".submit-btn",
checkValidBeforeSubmit: true,
asyncUpdate: false,
asyncSettings: {
closeOnSubmit: false,
Expand Down
6 changes: 5 additions & 1 deletion examples/templates/_books_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<th class="text-center" scope="col">Publication date</th>
<th class="text-center" scope="col">Pages</th>
<th class="text-center" scope="col">Price (€)</th>
<th class="text-center" scope="col">Read / Update / Delete</th>
<th class="text-center" scope="col">Read / Update / Delete / Async Delete</th>
</tr>
</thead>
<tbody>
Expand All @@ -34,6 +34,10 @@
<button type="button" class="bs-modal delete-book btn btn-sm btn-danger" data-form-url="{% url 'delete_book' book.pk %}">
<span class="fa fa-trash"></span>
</button>
<!-- Async delete book button -->
<button type="button" class="delete-book-async btn btn-sm btn-danger" data-form-url="{% url 'delete_book_async' book.pk %}">
<span class="fa fa-trash"></span>
</button>
</td>
</tr>
{% endfor %}
Expand Down
22 changes: 22 additions & 0 deletions examples/templates/examples/delete_book_async.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% load widget_tweaks %}

<form method="post" action="">
{% csrf_token %}

<div class="modal-header">
<h3 class="modal-title">Delete Book</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>

<div class="modal-body">
<p class="delete-text">Are you sure you want to delete book with title
<strong>{{ book.title }}</strong>?</p>
</div>

<div class="modal-footer">
<button type="button" id="delete-async-btn" class="submit-btn btn btn-primary">Delete</button>
</div>

</form>
53 changes: 39 additions & 14 deletions examples/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,23 @@ <h4>
modalID: "#create-modal"
});

var asyncSuccessMessage = [
"<div ",
"style='position:fixed;top:0;z-index:10000;width:100%;border-radius:0;' ",
"class='alert alert-icon alert-success alert-dismissible fade show mb-0' role='alert'>",
"Success: Book was updated.",
"<button type='button' class='close' data-dismiss='alert' aria-label='Close'>",
"<span aria-hidden='true'>&times;</span>",
"</button>",
"</div>",
"<script>",
"$('.alert').fadeTo(2000, 500).slideUp(500, function () {$('.alert').slideUp(500).remove();});",
"<\/script>"
].join("");
function createAsyncSuccessMessage(msg) {
return [
"<div ",
"style='position:fixed;top:0;z-index:10000;width:100%;border-radius:0;' ",
"class='alert alert-icon alert-success alert-dismissible fade show mb-0' role='alert'>",
`${msg}`,
"<button type='button' class='close' data-dismiss='alert' aria-label='Close'>",
"<span aria-hidden='true'>&times;</span>",
"</button>",
"</div>",
"<script>",
"$('.alert').fadeTo(2000, 500).slideUp(500, function () {$('.alert').slideUp(500).remove();});",
"<\/script>"
].join("");
}

var asyncUpdateMessage = createAsyncSuccessMessage("Success: Book was updated.");

function updateBookModalForm() {
$(".update-book").each(function () {
Expand All @@ -103,7 +107,7 @@ <h4>
asyncUpdate: true,
asyncSettings: {
closeOnSubmit: false,
successMessage: asyncSuccessMessage,
successMessage: asyncUpdateMessage,
dataUrl: "books/",
dataElementId: "#books-table",
dataKey: "table",
Expand All @@ -114,6 +118,27 @@ <h4>
}
updateBookModalForm();

var asyncDeleteMessage = createAsyncSuccessMessage("Success: Book was asynchronously deleted.");

function deleteBookModalForm() {
$(".delete-book-async").each(function () {
$(this).modalForm({
formURL: $(this).data("form-url"),
asyncUpdate: true,
checkValidBeforeSubmit: false,
asyncSettings: {
closeOnSubmit: true,
successMessage: asyncDeleteMessage,
dataUrl: "books/",
dataElementId: "#books-table",
dataKey: "table",
addModalFormFunction: deleteBookModalForm
}
});
});
}
deleteBookModalForm();

// Read and Delete book buttons open modal with id="modal"
// The formURL is retrieved from the data of the element
$(".bs-modal").each(function () {
Expand Down
1 change: 1 addition & 0 deletions examples/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
path('update/<int:pk>', views.BookUpdateView.as_view(), name='update_book'),
path('read/<int:pk>', views.BookReadView.as_view(), name='read_book'),
path('delete/<int:pk>', views.BookDeleteView.as_view(), name='delete_book'),
path('delete/async/<int:pk>', views.BookDeleteAsyncView.as_view(), name='delete_book_async'),
path('books/', views.books, name='books'),
path('signup/', views.SignUpView.as_view(), name='signup'),
path('login/', views.CustomLoginView.as_view(), name='login'),
Expand Down
7 changes: 7 additions & 0 deletions examples/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ class BookDeleteView(BSModalDeleteView):
success_url = reverse_lazy('index')


class BookDeleteAsyncView(BSModalDeleteView):
model = Book
template_name = 'examples/delete_book_async.html'
success_message = 'Success: Book was deleted.'
success_url = reverse_lazy('index')


class SignUpView(BSModalCreateView):
form_class = CustomUserCreationForm
template_name = 'examples/signup.html'
Expand Down
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
Django==2.2.13
django-bootstrap-modal-forms==1.4.1
django-widget-tweaks==1.4.2
selenium==3.14.0
geckodriver-autoinstaller==0.1.0
pytz==2018.5
selenium==3.14.0
sqlparse==0.3.1
urllib3==1.25.10
43 changes: 43 additions & 0 deletions tests/tests_functional.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
from selenium.webdriver.support.select import Select

import geckodriver_autoinstaller
geckodriver_autoinstaller.install() # Check if the current version of geckodriver exists
# and if it doesn't exist, download it automatically,
# then add geckodriver to path

from examples.models import Book
from .base import FunctionalTest

Expand Down Expand Up @@ -292,3 +297,41 @@ def test_delete_object(self):
# There is no books in database anymore
books = Book.objects.all()
self.assertEqual(books.count(), 0)

def test_delete_async_object(self):
# User visits homepage
self.browser.get(self.live_server_url)

# User clicks Delete book button
self.browser.find_element_by_class_name('delete-book-async').click()

# Delete book modal opens
modal = self.wait_for(element_id='modal')

# User sees modal content
modal_body = modal.find_element_by_class_name('modal-body')
delete_text = modal_body.find_element_by_class_name('delete-text').text
self.assertEqual(
delete_text, 'Are you sure you want to delete book with title Life of John Doe?'
)

# User clicks delete button
delete_btn = modal.find_element_by_class_name('submit-btn')
delete_btn.click()

# User sees success message after page redirection
redirect_url = self.browser.current_url
self.assertRegex(redirect_url, '/')

# Slice removes '\nx' since alert is dismissible and contains 'times' button
success_msg = self.browser.find_element_by_class_name('alert').text[:-2]
self.assertEqual(success_msg, 'Success: Book was asynchronously deleted.')

# User sees an empty table
table_entries = self.wait_for_table_rows()
self.assertEqual(len(table_entries), 0)

# There is no books in database anymore
books = Book.objects.all()
self.assertEqual(books.count(), 0)