Skip to content

Commit

Permalink
add partialDelete to button actions
Browse files Browse the repository at this point in the history
  • Loading branch information
jrief committed Jun 1, 2024
1 parent fcd1ebf commit 8745e10
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 4 deletions.
3 changes: 3 additions & 0 deletions assets/tag-attributes.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ Function
= 'setFieldValue' _ '(' _ target:PATH _ ',' _ source:SOURCEARG _ ')' {
return { funcname: 'setFieldValue', args: [target.split('.'), source] };
}
/ 'deletePartial' _ '(' _ target:PATH _ ',' _ source:SOURCEARG _ ')' {
return { funcname: 'deletePartial', args: [target.split('.'), source] };
}
/ _ funcname:$keystring '(' args:arglist ')' _ {
return { funcname: funcname, args: args };
}
Expand Down
41 changes: 40 additions & 1 deletion client/django-formset/DjangoFormset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ class DjangoButton {
}

/**
* Action to activate a button so that it can be used
* Action to activate a button so that it can be used by dialogs.
*/
private activate(...args: any[]) {
return (response: Response) => {
Expand All @@ -903,6 +903,15 @@ class DjangoButton {
}
}

private deletePartial(target: Path, source: FieldValue) {
return (response: Response) => {
if (typeof source === 'string' && parseInt(source)) {
this.formset.deletePartial(target, source);
}
return Promise.resolve(response);
}
}

/**
* Dummy action to be called in case of empty actionsQueue.
*/
Expand Down Expand Up @@ -2201,6 +2210,36 @@ export class DjangoFormset {
}
}

async deletePartial(path: Path, pk: string) : Promise<Response|undefined> {
if (!this.endpoint)
throw new Error("<django-formset> requires attribute 'endpoint=\"server endpoint\"' for submission");
try {
const query = new URLSearchParams({pk, path: path.join('.')});
const headers = new Headers();
if (this.CSRFToken) {
headers.append('X-CSRFToken', this.CSRFToken);
}
const response = await fetch(`${this.endpoint}?${query.toString()}`, {
method: 'DELETE',
headers,
});
switch (response.status) {
case 204:
this.clearErrors();
return response;
default:
console.warn(`Unknown response status: ${response.status}`);
this.clearErrors();
this.buttons.forEach(button => button.restoreToInitial());
return response;
}
} catch (error) {
this.clearErrors();
this.buttons.forEach(button => button.restoreToInitial());
alert(error);
}
}

private reportErrors(body: any) {
console.info("Response from server:", body);
for (const form of this.forms) {
Expand Down
26 changes: 25 additions & 1 deletion formset/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.db import transaction
from django.db.models import QuerySet
from django.forms.fields import CallableChoiceIterator
from django.http.response import HttpResponseBadRequest, JsonResponse
from django.http.response import HttpResponse, HttpResponseForbidden, HttpResponseBadRequest, JsonResponse
from django.utils.encoding import force_str
from django.utils.functional import cached_property
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
Expand Down Expand Up @@ -206,6 +206,14 @@ def patch(self, request, **kwargs):
"""
return self.post(request, **kwargs)

def delete(self, request, **kwargs):
"""
Method `DELETE` is used for partial deletion.
"""
if set(['path', 'pk']).issubset(request.GET):
return self._delete_partial()
return HttpResponseForbidden("Method DELETE not supported in this context")

def _fetch_partial_data(self):
collection_class = self.get_collection_class()
empty_holder = collection_class
Expand All @@ -225,6 +233,22 @@ def _fetch_partial_data(self):
return JsonResponse(initial)
return HttpResponseBadRequest("Invalid path value")

def _delete_partial(self):
collection_class = self.get_collection_class()
empty_holder = collection_class
for part in self.request.GET['path'].split('.'):
if not (empty_holder := empty_holder.declared_holders.get(part)):
break
if empty_holder is not None:
try:
instance = empty_holder._meta.model.objects.get(pk=self.request.GET.get('pk'))
except empty_holder._meta.model.DoesNotExist:
pass
else:
instance.delete()
return HttpResponse(status=204)
return HttpResponseBadRequest("Invalid path value")

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form_collection'] = self.get_form_collection()
Expand Down
34 changes: 32 additions & 2 deletions testapp/tests/test_e2e_dialogmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,22 @@ class PageForm(ModelForm):
required=False,
)
edit_reporter = Activator(
label="Edit Reporter",
widget=Button(
action='activate("prefill", page.reporter)',
attrs={'df-disable': '!page.reporter'},
),
)
add_reporter = Activator(
label="Add Reporter",
widget=Button(
action='activate',
),
)
delete_reporter = Activator(
widget=Button(
action='deletePartial(change_reporter, page.reporter) -> setFieldValue(page.reporter, "")',
attrs={'df-disable': '!page.reporter'},
),
)

class Meta:
model = PageModel
Expand Down Expand Up @@ -196,3 +200,29 @@ def test_edit_reporter(page, mocker, viewname):
assert Reporter.objects.get(id=reporter.id).full_name == "Sarah Johnson"
expect(select_reporter).to_have_value(str(reporter.id))
expect(pseudo_input).to_have_text("Sarah Johnson")


@pytest.mark.urls(__name__)
@pytest.mark.parametrize('viewname', ['page'])
def test_delete_reporter(page, mocker, viewname):
form_collection = page.locator('django-formset > django-form-collection')
dialog = form_collection.nth(0).locator('> dialog')
expect(dialog).not_to_be_visible()
select_reporter = form_collection.locator('select[name="reporter"]')
expect(select_reporter).to_have_value('')
reporter, _ = Reporter.objects.get_or_create(full_name="Sarah Hemingway")
select_reporter.evaluate(f'el => el.value = {reporter.id}')
expect(select_reporter).to_have_value(str(reporter.id))
pseudo_input = form_collection.nth(1).locator(f'.ts-wrapper div.item[data-value=\"{reporter.id}\"]')
expect(pseudo_input).to_have_text("Sarah Hemingway")

# delete the current reporter remotely
spy = mocker.spy(PageCollectionView, 'delete')
form_collection.nth(1).locator('button[name="delete_reporter"]').click()

# check if the reporter was deleted from the database and from the selectize widget
sleep(1)
spy.assert_called()
assert not Reporter.objects.filter(id=reporter.id).exists()
expect(select_reporter).to_have_value('')
expect(pseudo_input).not_to_be_visible()

0 comments on commit 8745e10

Please sign in to comment.