def render(self, context): formset = self.form[self.fieldname].formset # management form yield from Form.from_django_form(formset.management_form, standalone=False).render(context) # detect internal fields like the delete-checkbox or the order-widget etc. and add them declared_fields = [ f.fieldname for f in self.filter(lambda e, ancestors: isinstance(e, FormField)) ] internal_fields = [ field for field in formset.empty_form.fields if field not in declared_fields ] for field in internal_fields: self.append(FormField(field)) # wrapping things with the div is a bit ugly but the quickest way to do it now yield f'<div id="formset_{formset.prefix}_container">' for form in formset: yield from Form.wrap_with_form(form, *self, standalone=False).render(context) yield "</div>" # empty/template form yield from htmlgenerator.DIV( htmlgenerator.DIV( Form.wrap_with_form(formset.empty_form, *self, standalone=False)), id=f"empty_{ formset.prefix }_form", _class="template-form", style="display:none;", ).render(context) # add-new-form button yield from htmlgenerator.DIV( Button( _("Add"), id=f"add_{formset.prefix}_button", onclick= f"formset_add('{ formset.prefix }', '#formset_{ formset.prefix }_container');", icon=Icon("add"), notext=True, small=True, ), _class="bx--form-item", ).render(context) yield from htmlgenerator.SCRIPT( mark_safe( f"""document.addEventListener("DOMContentLoaded", e => init_formset("{ formset.prefix }"));""" )).render(context)
def _display_email_without_subscription(email): modal_add = modal_add_subscription(email) return hg.BaseElement( hg.DIV(email.email, style="font-weight: bold;"), hg.DIV(_("No subscription yet for "), email.email), layout.button.Button( _("Add subscription"), buttontype="ghost", icon="add", **modal_add.openerattributes, ), modal_add, )
def is_interested_indicator(is_interested, is_active): if is_interested and is_active: color = "#198038" text = _("active") elif is_interested: color = "#e0e0e0" text = _("active") else: color = "#e0e0e0" text = _("inactive") return hg.DIV( hg.DIV("●", style=f"color: {color}; display: inline-block;"), hg.DIV(text, style="display: inline-block; padding-left: 8px"), style="display: inline-block;", )
def __init__( self, fieldname, light=False, widgetattributes={}, **attributes, ): self.fieldname = fieldname attributes["_class"] = (attributes.get("_class", "") + " bx--form-item bx--text-input-wrapper") widgetattributes["_class"] = ( widgetattributes.get("_class", "") + f" bx--text-input {'bx--text-input--light' if light else ''}") super().__init__( htmlgenerator.LABEL(_class="bx--label"), htmlgenerator.DIV( htmlgenerator.INPUT(**widgetattributes), _class="bx--text-input__field-wrapper", ), **attributes, ) # for easier reference in the render method: self.label = self[0] self.input = self[1][0]
def get_layout(self): if hasattr(self, "layout") and self.layout is not None: ret = self.layout else: formfields = filter_fieldlist( self.model, [f for f in self.fields if isinstance(f, str)] if self.fields else None, for_form=True, ) ret = hg.BaseElement() for field in self.fields or formfields: if field in formfields: ret.append(breadlayout.forms.FormField(field)) else: ret.append(field) if self.ajax_urlparameter in self.request.GET: return breadlayout.forms.Form(hg.C("form"), ret) # wrap with form will add a submit button return hg.DIV( header(), breadlayout.tile.Tile( breadlayout.forms.Form( hg.C("form"), ret, breadlayout.forms.helpers.Submit() ), _class="theme-white", ), )
def searchperson(request): query = request.GET.get("q") highlight = CustomHighlighter(query) if not query or len(query) < settings.MIN_CHARACTERS_DYNAMIC_SEARCH: return HttpResponse("") query_set = (SearchQuerySet().models(models.Person).autocomplete( name_auto=query).filter_or(personnumber=query)) def onclick(person): link = reverse_model( person, "edit", kwargs={"pk": person.pk}, ) return f"document.location = '{link}'" ret = _display_results(query_set, highlight, onclick) return HttpResponse( hg.DIV( ret, _class="raised", style= "margin-bottom: 1rem; padding: 16px 0 48px 48px; background-color: #fff", ).render({}))
def sync_help_modal(): return bread.layout.modal.Modal( _("Help"), _("The button below is currently the only way of getting new subcribers from the mailer into our system. Is it also the only way of getting updates for subscribers that we already have in our system. This is what happens when the button is pressed:" ), hg.DIV( hg.UL( hg.LI( _("For all the Subscriptions that are in the relevant segment in the Mailer, we check whether the email address is already in BasxConnect." ), _class="bx--list__item", ), hg.LI( _("If an email address is already in BasxConnect, the downloaded subscription will be attached to the email address and override the current values in case there are any." ), _class="bx--list__item", ), hg.LI( _("If an email address is not yet in BasxConnect, a new person will be created with that email address." ), _class="bx--list__item", ), _class="bx--list--unordered", ), style="padding-left: 1rem;", ), width=8, )
def relationshipstab(request): person = get_object_or_404(Person, pk=request.resolver_match.kwargs["pk"]) modal_from = modal_add_relationship_from(person) modal_to = modal_add_relationship_to(person) return layout.tabs.Tab( _("Relationships"), utils.grid_inside_tab( R( utils.tiling_col( relationships_datatable( request, title=_("Relationships to person"), queryset=hg.F( lambda c: c["object"].relationships_from.all()), primary_button=button_add_relationship_to(modal_to), ), modal_to, hg.DIV(style="margin-top: 4rem;"), relationships_datatable( request, title=_("Relationships from person"), queryset=hg.F( lambda c: c["object"].relationships_to.all()), primary_button=button_add_relationship_from( modal_from), ), modal_from, )), ), )
def error_layout( request, status_code: int, status_title: str, description: Union[str, hg.BaseElement], exception_detail: str = None, ): return hg.BaseElement( hg.H1(f"{status_code}: {status_title}", style="margin-bottom: 1rem;"), hg.P( description, style="margin-bottom: 1rem;", ), hg.If( bool(exception_detail), hg.BaseElement( hg.H4("Detail", style="margin-bottom: 1rem;"), hg.DIV( exception_detail, style=( "border: 1px solid grey;" "padding: 1rem;" "font-family: monospace;" "margin-bottom: 1rem;" ), ), ), ), Button.from_link( Link( label=_("Back to homepage"), href=hg.F(lambda c: c["request"].META["SCRIPT_NAME"] or "/"), ) ), )
def get_layout(self): form_fields = [layout.forms.FormField(field) for field in [*self.fields]] + [ hg.If( hg.F( lambda c: c["object"].person.primary_email_address and c["object"].person.primary_email_address.pk != c["object"].pk ), layout.forms.FormField("is_primary"), "", ), hg.If( hg.F( lambda c: apps.is_installed("basxconnect.mailer_integration") and hasattr(c["object"], "subscription") ), layout.forms.FormField("propagate_change_to_mailer"), "", ), ] return layout.grid.Grid( hg.H3(_("Edit Email")), layout.grid.Row( layout.grid.Col( layout.forms.Form( hg.C("form"), hg.DIV(*form_fields), layout.forms.helpers.Submit(), ), width=4, ) ), gutter=False, )
def __init__( self, fieldname, placeholder="", rows=None, cols=None, light=False, widgetattributes={}, **attributes, ): self.fieldname = fieldname attributes["_class"] = attributes.get("_class", "") + " bx--form-item" widgetattributes["_class"] = ( widgetattributes.get("_class", "") + f" bx--text-area bx--text-area--v2 {'bx--text-area--light' if light else ''}", ) if rows: widgetattributes["rows"] = rows if cols: widgetattributes["cols"] = cols super().__init__( htmlgenerator.LABEL(_class="bx--label"), htmlgenerator.DIV( htmlgenerator.TEXTAREA(placeholder=placeholder, **widgetattributes), _class="bx--text-area__wrapper", ), **attributes, ) # for easier reference in the render method: self.label = self[0] self.input = self[1][0]
def get_layout(self): self.checkboxcounterid = hg.html_id(self, "checkbox-counter") ret = super().get_layout() toolbar = list( ret.filter(lambda e, a: getattr(e, "attributes", {}).get( "_class", "") == "bx--toolbar-content"))[0] nfilters = self._checkbox_count() toolbar.insert( -2, hg.DIV( hg.SPAN(nfilters, id=self.checkboxcounterid), layout.icon.Icon( "close", focusable="false", size=15, role="img", onclick= f"document.location = '{self.request.path}?reset=1'", ), role="button", _class= "bx--list-box__selection bx--list-box__selection--multi bx--tag--filter", style="margin: auto 0.5rem;" + (" display: none;" if nfilters == 0 else ""), tabindex="0", title=("Reset"), ), ) return ret
def person_metadata(model): return tiling_col( # we need this to take exactly as much space as a real header hg.DIV("", style="margin-bottom:3.25rem;"), display_field_label_and_value("personnumber"), display_field_label_and_value("maintype"), display_field_label_and_value("type"), display_label_and_value(_("Status"), active_toggle()), display_label_and_value( _("Changed"), hg.BaseElement( ObjectFieldValue("history.first.history_date.date"), " / ", hg.C("object.history.first.history_user"), ), ), display_label_and_value( _("Created"), hg.BaseElement( ObjectFieldValue("history.last.history_date.date"), " / ", hg.C("object.history.last.history_user"), ), ), open_modal_popup_button( _("Meta data"), model, f"{model._meta.model_name}_ajax_edit_metadata"), style="border-left: none;", )
def _loading_indicator(resultcontainerid): return hg.DIV( Loading(small=True), id=hg.format("{}-indicator", resultcontainerid), _class="htmx-indicator", style="position: absolute; right: 2rem", )
def __init__( self, title, subtitle, kind="info", lowcontrast=False, hideclosebutton=False, hidetimestamp=False, **attributes, ): """ kind: can be one of "error" "info", "info-square", "success", "warning", "warning-alt" """ assert ( kind in KIND_ICON_MAPPING ), f"kind '{kind}' does not exists, must be one of {KIND_ICON_MAPPING.keys()}" self.hidetimestamp = hidetimestamp attributes["data-notification"] = True attributes["_class"] = ( attributes.get("_class", "") + f" bx--toast-notification bx--toast-notification--{kind}" ) if lowcontrast: attributes["_class"] += " bx--toast-notification--low-contrast" attributes["role"] = "alert" timestampelem = ( [ htmlgenerator.P( _("Time stamp "), _class="bx--toast-notification__caption" ) ] if not hidetimestamp else [] ) children = [ Icon( KIND_ICON_MAPPING[kind], size=20, _class="bx--toast-notification__icon", ), htmlgenerator.DIV( htmlgenerator.H3(title, _class="bx--toast-notification__title"), htmlgenerator.P(subtitle, _class="bx--toast-notification__subtitle"), *timestampelem, _class="bx--toast-notification__details", ), ] if not hideclosebutton: children.append( htmlgenerator.BUTTON( Icon("close", size=20, _class="bx--toast-notification__close-icon"), data_notification_btn=True, _class="bx--toast-notification__close-button", aria_label="close", ) ) super().__init__(*children, **attributes)
def __init__( self, size="xl", placeholder=None, widgetattributes=None, backend=None, resultcontainerid=None, show_result_container=True, resultcontainer_onload_js=None, disabled=False, **kwargs, ): """ :param SearchBackendConfig backend: Where and how to get search results """ kwargs["_class"] = kwargs.get("_class", "") + f" bx--search bx--search--{size}" kwargs["data_search"] = True kwargs["role"] = "search" width = kwargs.get("width", None) if width: kwargs["style"] = kwargs.get("style", "") + f"width:{width};" widgetattributes = { "id": "search__" + hg.html_id(self), "_class": "bx--search-input", "type": "text", "placeholder": placeholder or _("Search"), "autocomplete": "off", **(widgetattributes or {}), } if backend: if resultcontainerid is None: resultcontainerid = f"search-result-{hg.html_id((self, backend.url))}" widgetattributes["hx_get"] = backend.url widgetattributes["hx_trigger"] = "changed, click, keyup changed delay:500ms" widgetattributes["hx_target"] = hg.format("#{}", resultcontainerid) widgetattributes["hx_indicator"] = hg.format( "#{}-indicator", resultcontainerid ) widgetattributes["name"] = backend.query_parameter self.close_button = _close_button(resultcontainerid, widgetattributes) super().__init__( hg.DIV( hg.LABEL(_("Search"), _class="bx--label", _for=widgetattributes["id"]), hg.INPUT(**widgetattributes), _search_icon(), self.close_button, hg.If(backend is not None, _loading_indicator(resultcontainerid)), **kwargs, ), hg.If( backend is not None and show_result_container, _result_container(resultcontainerid, resultcontainer_onload_js, width), ), style=hg.If(disabled, hg.BaseElement("display: none")), )
def full(title, datatable, primary_button, helper_text=None): header = [hg.H4(title)] if helper_text: header.append( hg.P(helper_text, _class="bx--data-table-header__description")) return hg.DIV( hg.DIV(*header, _class="bx--data-table-header"), hg.SECTION( hg.DIV( hg.DIV(_class="bx--action-list"), hg.DIV( hg.P( hg.SPAN(0, data_items_selected=True), _(" items selected"), _class="bx--batch-summary__para", ), _class="bx--batch-summary", ), _class="bx--batch-actions", aria_label=_("Table Action Bar"), ), hg.DIV( hg.DIV(Search(), _class="bx--toolbar-search-container-expandable"), primary_button, _class="bx--toolbar-content", ), _class="bx--table-toolbar", ), datatable, _class="bx--data-table-container", data_table=True, )
def __init__( self, links, menuiconname="overflow-menu--vertical", menuname=None, direction="bottom", flip=False, item_attributes={}, **attributes, ): attributes["data-overflow-menu"] = True attributes["_class"] = attributes.get("_class", "") + " bx--overflow-menu" item_attributes["_class"] = (item_attributes.get("_class", "") + " bx--overflow-menu-options__option") menuid = hg.F(lambda c: OverflowMenu.MENUID_TEMPLATE % hg.html_id( c.get("row", self))) triggerid = hg.F(lambda c: (OverflowMenu.MENUID_TEMPLATE % hg.html_id( c.get("row", self))) + "-trigger") super().__init__( hg.BUTTON( Icon(menuiconname, size=16), _class="bx--overflow-menu__trigger" + (" bx--tooltip__trigger bx--tooltip--a11y bx--tooltip--right bx--tooltip--align-start" if menuname is not None else ""), aria_haspopup="true", aria_expanded="false", aria_controls=menuid, type="button", id=triggerid, ), hg.DIV( hg.UL( hg.Iterator( links, "link", hg.LI( hg.F(asoverflowbutton), **item_attributes, ), ), _class="bx--overflow-menu-options__content", ), _class="bx--overflow-menu-options" + (" bx--overflow-menu--flip" if flip else ""), tabindex="-1", role="menu", aria_labelledby=triggerid, data_floating_menu_direction=direction, id=menuid, ), **attributes, ) if menuname is not None: self[0].insert(0, hg.SPAN(menuname, _class="bx--assistive-text"))
def __init__(self, errors): super().__init__( errors, hg.DIV( hg.UL(hg.Iterator(errors or (), "error", hg.LI(hg.C("error")))), _class="bx--form-requirement", ), )
def header(): editbutton = breadlayout.button.Button( _("Edit"), buttontype="ghost", icon="edit", notext=True, ).as_href(ModelHref.from_object(hg.C("object"), "edit")) readbutton = breadlayout.button.Button( _("Read"), buttontype="ghost", icon="view", notext=True, ).as_href(ModelHref.from_object(hg.C("object"), "read")) deletebutton = breadlayout.button.Button( _("Delete"), buttontype="tertiary", icon="trash-can", notext=True, style="border-color: red; background-color: inherit", ).as_href(ModelHref.from_object(hg.C("object"), "delete")) deletebutton[1].attributes["style"] = "fill: red; color: red;" copybutton = breadlayout.button.Button( _("Copy"), buttontype="ghost", icon="copy", notext=True, ).as_href(ModelHref.from_object(hg.C("object"), "copy")) return hg.DIV( hg.H3( hg.If( hg.C("object"), hg.BaseElement( hg.SPAN(hg.C("object")), hg.SPAN( hg.If( hg.C("request").resolver_match.url_name.endswith(".read"), editbutton, readbutton, ), copybutton, breadlayout.button.PrintPageButton(buttontype="ghost"), deletebutton, _class="no-print", style="margin-bottom: 1rem; margin-left: 1rem", width=3, ), ), hg.SPAN(hg.format(_("Add {}"), hg.C("view").model._meta.verbose_name)), ), ), style="padding-top: 1rem", )
def get_layout(self): fields = hg.BaseElement( *[layout.forms.FormField(f) for f in self.object.active_fields()] ) return hg.BaseElement( hg.H1(self.object, style="margin-bottom: 2rem"), hg.DIV( hg.DIV( layout.forms.Form( hg.C("form"), fields, layout.forms.helpers.Submit() ), style="padding: 1rem", ), hg.DIV( self.object.as_svg(), style="width: 40%; border: 1px solid gray" ), style="display: flex", ), )
def __init__( self, item_iterator, iteratorclass=htmlgenerator.Iterator, menuname=None, direction="bottom", flip=False, item_attributes={}, **attributes, ): # making the class inline seems better, I think we can enforce scoping the type to this instance of OverflowMenu class MenuItemValueProvider(htmlgenerator.ValueProvider): attributename = "item" """item_iterator: an iterable which contains bread.menu.Action objects where the onclick value is what will be passed to the onclick attribute of the menu-item (and therefore should be javascript, e.g. "window.location.href='/home'"). All three item_iterator in the tuple can be lazy objects iteratorclass: If the Iterator needs additional values in order to generate item_iterator it can be customized and passed here""" attributes["data-overflow-menu"] = True attributes["_class"] = attributes.get("_class", "") + " bx--overflow-menu" menuid = f"overflow-menu-{hash(id(self))}" super().__init__( htmlgenerator.BUTTON( Icon("overflow-menu--vertical", size=16), _class="bx--overflow-menu__trigger" + (" bx--tooltip__trigger bx--tooltip--a11y bx--tooltip--right bx--tooltip--align-start" if menuname is not None else ""), aria_haspopup="true", aria_expanded="false", aria_controls=menuid, _id=f"{menuid}-trigger", ), htmlgenerator.DIV( htmlgenerator.UL( iteratorclass( item_iterator, MenuItemValueProvider.Binding(OverflowMenuItem)( MenuItemValueProvider, **item_attributes), MenuItemValueProvider, ), _class="bx--overflow-menu-options__content", ), _class="bx--overflow-menu-options" + (" bx--overflow-menu--flip" if flip else ""), tabindex="-1", role="menu", aria_labelledby=f"{menuid}-trigger", data_floating_menu_direction=direction, id=menuid, ), **attributes, ) if menuname is not None: self[0].insert( 0, htmlgenerator.SPAN(menuname, _class="bx--assistive-text"))
def __init__(self, helptext, disabled=False): super().__init__( helptext, hg.DIV( helptext, _class=hg.BaseElement( "bx--form__helper-text", hg.If(disabled, " bx--form__helper-text--disabled"), ), ), )
def maintainance_package_layout(request): PYPI_API = "https://pypi.python.org/pypi/{}/json" PACKAGE_NAMES = ("basx-bread", "basxconnect", "htmlgenerator") package_current = [] package_latest = [] for package_name in PACKAGE_NAMES: current_version = pkg_resources.get_distribution(package_name).version newer_version = _("unable to load") # load the latest package info from the PyPI API pkg_info_req = requests.get(PYPI_API.format(package_name)) if pkg_info_req.status_code == requests.codes.ok: newer_version = pkg_info_req.json()["info"]["version"] package_current.append(current_version) package_latest.append(newer_version) return DataTable( columns=[ DataTableColumn( header=_("Package"), cell=hg.DIV(hg.C("row.package_name")), ), DataTableColumn( header=_("Current"), cell=hg.DIV(hg.C("row.package_current")), ), DataTableColumn( header=_("Latest"), cell=(hg.DIV(hg.C("row.package_latest"))), ), ], row_iterator=[{ "package_name": pkg_name, "package_current": pkg_current, "package_latest": pkg_latest, } for pkg_name, pkg_current, pkg_latest in zip( PACKAGE_NAMES, package_current, package_latest)], )
def __init__(self, platform, company, searchbar, actions=(), *args, **kwargs): super().__init__( hg.If( HasBreadCookieValue("sidenav-hidden", "true"), variable_size_header_part(hg.BaseElement(), company, searchbar, "5rem"), variable_size_header_part( hg.SPAN(platform, _class="bx--header__name--prefix"), company, searchbar, "18rem", ), ), hg.DIV( hg.If( hg.F(lambda c: c["request"].user.is_authenticated), hg.A( hg.SPAN( hg.C("request.user.get_username"), _class="bx--header__name--prefix", ), _class="bx--header__name", href=reverse("userprofile"), title=hg.C("request.user.get_username"), style="padding: 0; margin-right: 1rem", ), ), hg.If( hg.F(lambda c: c["request"].user.is_authenticated), hg.BUTTON( Icon( "logout", size=20, _class="bx--navigation-menu-panel-expand-icon", aria_hidden="true", ), Icon( "logout", size=20, _class="bx--navigation-menu-panel-collapse-icon", aria_hidden="true", ), _class="bx--header__menu-trigger bx--header__action", title=_("Logout"), data_navigation_menu_panel_label_expand=_("Logout"), data_navigation_menu_panel_label_collapse=_("Close"), onclick=f"document.location = '{reverse('logout')}'", ), ), _class="bx--header__global", ), _class="bx--header", data_header=True, )
def asbutton(self): return hg.DIV( hg.SPAN(self.email, style="margin-right: 0.25rem"), hg.SPAN(style="flex-grow: 1"), button.Button( icon="email", onclick=f"window.location = 'mailto:{self.email}';", buttontype="ghost", _class="bx--overflow-menu", ), style="display: flex; flex-wrap: nowrap; align-items: center", )
def display_label_and_value(label, value): return R( C( hg.DIV( label, style="font-weight: bold;", ), width=6, ), C(value), style="padding-bottom: 1.5rem;", )
def active_toggle(): toggle = layout.toggle.Toggle(None, _("Inactive"), _("Active"), style="margin-top:-1rem; margin-bottom:0;") toggle.input.attributes["id"] = "person_active_toggle2" toggle.input.attributes["hx_trigger"] = "change" toggle.input.attributes["hx_post"] = hg.F(lambda c: reverse_lazy( "core.person.togglestatus", args=[c["object"].pk])) toggle.input.attributes["checked"] = hg.F(lambda c: c["object"].active) toggle.label.attributes["_for"] = toggle.input.attributes["id"] return hg.DIV(toggle)
def get_layout(self): form_fields = [layout.forms.FormField(field) for field in [*self.fields]] + [ hg.If( hg.F( lambda c: c["object"].person.primary_postal_address and c["object"].person.primary_postal_address.pk != c["object"].pk ), layout.forms.FormField("is_primary"), "", ) ] return hg.DIV(layout.components.forms.Form(hg.C("form"), *form_fields))
def display_sync_persons(sync_status): return hg.Iterator( hg.F(lambda c: c["row"].persons.filter(sync_status=sync_status)), "person", hg.DIV( hg.format( "{} {} <{}>", hg.C("person.first_name"), hg.C("person.last_name"), hg.C("person.email"), )), )