Created
October 4, 2022 00:34
-
-
Save lb-/276ac8b60565e106f5e94ca6d608c80c to your computer and use it in GitHub Desktop.
Creating an interactive event budgeting tool within Wagtail (Tutorial)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div | |
data-controller="budget" | |
data-action="change->budget#updateTotals focusout->budget#updateTotals" | |
data-budget-per-price-value="PP" | |
> | |
{% include "wagtailadmin/panels/multi_field_panel.html" %} | |
<output for="{{ field_ids|join:' ' }}"> | |
<h3>Budget summary</h3> | |
<dl> | |
<dt>Total price per</dt> | |
<dd data-budget-target="totalPricePer">-</dd> | |
<dt>Total fixed</dt> | |
<dd data-budget-target="totalFixed">-</dd> | |
<dt>Break even qty</dt> | |
<dd data-budget-target="breakEven">-</dd> | |
</dl> | |
</output> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Application, | |
Controller, | |
} from "https://unpkg.com/@hotwired/[email protected]/dist/stimulus.js"; | |
class BudgetController extends Controller { | |
static targets = ["breakEven", "ticketPrice", "totalFixed", "totalPricePer"]; | |
connect() { | |
this.updateTotals(); | |
} | |
/** | |
* Parse the inline panel children that are not hidden and read the inner field | |
* values, parsing the values into usable JS results. | |
*/ | |
get items() { | |
const inlinePanelChildren = this.element.querySelectorAll( | |
"[data-inline-panel-child]:not(.deleted)" | |
); | |
return [...inlinePanelChildren].map((element) => ({ | |
amount: parseFloat(element.querySelector("[data-amount]").value || "0"), | |
description: element.querySelector("[data-description]").value || "", | |
type: element.querySelector("[data-type]").value, | |
})); | |
} | |
/** | |
* parse ticket price and prepare the totals object to show a summary of | |
* totals in the items and the break even quantity required. | |
*/ | |
get totals() { | |
const perPriceValue = "PP"; | |
const items = this.items; | |
const ticketPrice = parseFloat(this.ticketPriceTarget.value || "0"); | |
const { totalPricePer, totalFixed } = items.reduce( | |
({ totalPricePer: pp = 0, totalFixed: pf = 0 }, { amount, type }) => ({ | |
totalPricePer: type === perPriceValue ? pp + amount : pp, | |
totalFixed: type === perPriceValue ? pf : pf + amount, | |
}), | |
{} | |
); | |
const totals = { | |
breakEven: null, | |
ticketPrice, | |
totalFixed, | |
totalPricePer, | |
}; | |
// do not attempt to show a break even if there is no ticket price | |
if (ticketPrice <= 0) return totals; | |
const ticketMargin = ticketPrice - totalPricePer; | |
// do not attempt to show a break even if ticket price does not cover price per | |
if (ticketMargin <= 0) return totals; | |
totals.breakEven = Math.ceil(totalFixed / ticketMargin); | |
return totals; | |
} | |
/** | |
* Update the DOM targets with the calculated totals. | |
*/ | |
updateTotals() { | |
const { breakEven, totalFixed, totalPricePer } = this.totals; | |
this.totalPricePerTarget.innerText = `${totalPricePer || "-"}`; | |
this.totalFixedTarget.innerText = `${totalFixed || "-"}`; | |
this.breakEvenTarget.innerText = `${breakEven || "-"}`; | |
} | |
} | |
const Stimulus = Application.start(); | |
Stimulus.register("budget", BudgetController); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django import forms | |
from django.db import models | |
from modelcluster.fields import ParentalKey | |
from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel | |
from wagtail.models import Orderable, Page | |
from .panels import BudgetGroupPanel | |
NUMBER_FIELD_ATTRS = { | |
"inputmode": "numeric", | |
"pattern": "[0-9.]*", | |
"type": "text", | |
} | |
class AbstractBudgetItem(models.Model): | |
""" | |
The abstract model for the budget item, complete with panels. | |
""" | |
class PriceType(models.TextChoices): | |
PRICE_PER = "PP", "Price per" | |
FIXED_PRICE = "FP", "Fixed price" | |
description = models.CharField( | |
"Description", | |
max_length=255, | |
) | |
price_type = models.CharField( | |
"Price type", | |
max_length=2, | |
choices=PriceType.choices, | |
default=PriceType.FIXED_PRICE, | |
) | |
amount = models.DecimalField( | |
"Amount", | |
default=0, | |
max_digits=6, | |
decimal_places=2, | |
) | |
panels = [ | |
FieldRowPanel( | |
[ | |
FieldPanel( | |
"description", | |
widget=forms.TextInput(attrs={"data-description": ""}), | |
), | |
FieldPanel("price_type", widget=forms.Select(attrs={"data-type": ""})), | |
FieldPanel( | |
"amount", | |
widget=forms.TextInput( | |
attrs={"data-amount": "", **NUMBER_FIELD_ATTRS} | |
), | |
), | |
] | |
) | |
] | |
class Meta: | |
abstract = True | |
class EventPageBudgetItem(Orderable, AbstractBudgetItem): | |
""" | |
The real model which combines the abstract model, an | |
Orderable helper class, and what amounts to a ForeignKey link | |
to the model we want to add related links to (EventPage) | |
""" | |
page = ParentalKey( | |
"events.EventPage", | |
on_delete=models.CASCADE, | |
related_name="related_budget_items", | |
) | |
class EventPage(Page): | |
ticket_price = models.DecimalField( | |
"Price", | |
default=0, | |
max_digits=6, | |
decimal_places=2, | |
) | |
content_panels = Page.content_panels + [ | |
BudgetGroupPanel( | |
[ | |
InlinePanel("related_budget_items"), | |
FieldPanel( | |
"ticket_price", | |
widget=forms.TextInput( | |
attrs={ | |
"data-budget-target": "ticketPrice", | |
**NUMBER_FIELD_ATTRS, | |
} | |
), | |
), | |
], | |
"Budget", | |
), | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.forms import MultiValueField | |
from wagtail.admin.panels import MultiFieldPanel | |
class BudgetGroupPanel(MultiFieldPanel): | |
class BoundPanel(MultiFieldPanel.BoundPanel): | |
template_name = "events/budget_group_panel.html" | |
def get_context_data(self, parent_context=None): | |
""" | |
Prepare a list of ids so that we can reference them in the | |
output. | |
""" | |
context = super().get_context_data(parent_context) | |
context["field_ids"] = filter( | |
None, [child.id_for_label() for child in self.visible_children] | |
) | |
return context |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.templatetags.static import static | |
from django.utils.html import format_html | |
from wagtail import hooks | |
@hooks.register("insert_editor_js") | |
def editor_css(): | |
return format_html( | |
'<script type="module" src="{}"></script>', | |
static("js/events.js"), | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment