def _resultcontainer_onload_js(backend, resultcontainerid, tag_id, widget_id): on_click = hg.format( """function(evt) {{ let label = $('{}', this).innerHTML; let value = $('{}', this).innerHTML; $('#{}').value = value; $('#{}').innerHTML = label; $('#{}').style = 'display: inline-block;'; }}""", backend.result_label_selector, backend.result_value_selector, widget_id, tag_id, tag_id, autoescape=False, ) return hg.format( """ document.addEventListener('click', (evt) => this.innerHTML=''); htmx.onLoad(function(target) {{ // remove existing onlick attribute in case it e.g. redirects to the selected person $$('#{} {}')._.setAttribute('onclick', null); $$('#{} {}')._ .addEventListener('click', {}); }});""", resultcontainerid, backend.result_selector, resultcontainerid, backend.result_selector, on_click, )
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 with_ajax_content(cls, heading, url, label="", size="xs", submitlabel=None, id=None, **attributes): """ Same arguments as Modal() except ``url`` replaces ``content`` and ``submitlabel`` replaces ``buttons`` url: string or htmlgenerator.Lazy submitlabel: string or an htmlgenerator element which will be displayed on the submit button. A value of None means no submit button should be displayed. """ buttons = (Button(_("Cancel"), buttontype="ghost", data_modal_close=True), ) if submitlabel: buttons += (Button(submitlabel, type="submit"), ) modal = cls( heading, hg.DIV( Loading(), style= "opacity: 0.5; background-color: #EEE; text-align: center;", ), label="", buttons=buttons, size=size, id=id, with_form=bool(submitlabel), **attributes, ) if submitlabel: buttons[1].attributes["hx_post"] = url # note: we always use multipart forms, avoids some issues # see ./forms/__init__.py:Form.__init__ buttons[1].attributes["hx_encoding"] = "multipart/form-data" buttons[1].attributes["hx_target"] = hg.format( "#{} .bx--modal-content", modal.id) buttons[1].attributes["hx_include"] = hg.format( "#{} .bx--modal-content", modal.id) modal.openerattributes["hx_get"] = url modal.openerattributes["hx_target"] = hg.format( "#{} .bx--modal-content", modal.id) return modal
def __init__( self, label=None, help_text=None, errors=None, inputelement_attrs=None, boundfield=None, backend=None, **attributes, ): """ :param SearchBackendConfig backend: Where and how to get search results """ inputelement_attrs = inputelement_attrs or {} # This works inside a formset. Might need to be changed for other usages. widget_id = inputelement_attrs.get("id") resultcontainerid = hg.format("search-result-{}", widget_id) tag_id = hg.format("{}-tag", widget_id) super().__init__( label, Tag( hg.F(lambda c: hg.resolve_lazy(boundfield, c).field.to_python( hg.resolve_lazy(boundfield, c).value())) if boundfield else "", id=tag_id, style=hg.If( inputelement_attrs.get("value"), hg.BaseElement(""), hg.BaseElement("display: none;"), ), onclick="return false;", ), self.get_input_element(inputelement_attrs, errors, type="hidden"), Search( backend=backend, resultcontainerid=resultcontainerid, resultcontainer_onload_js=_resultcontainer_onload_js( backend, resultcontainerid, tag_id, widget_id), size="lg", disabled=inputelement_attrs.get("disabled", False), widgetattributes={"id": hg.format("search__{}", widget_id)}, ), help_text, errors, **hg.merge_html_attrs(attributes, {"_class": "bx--text-input-wrapper"}), )
def variable_size_header_part(platform, company, searchbar, searchbar_position): return hg.BaseElement( hg.A( logo(), platform, _class="bx--header__name", style="font-weight: 400", # override carbon design href=hg.F(lambda c: c["request"].META["SCRIPT_NAME"] or "/"), ), hg.If( searchbar, hg.SPAN( searchbar, style=f"position: absolute; left: {searchbar_position}", _class="theme-gray-100", ), "", ), hg.A( hg.SPAN( company, style=hg.format( "position: absolute; left: {}", hg.If(searchbar, "50%", searchbar_position), ), ), _class="bx--header__name", style="font-weight: 400", # override carbon design href=hg.F(lambda c: c["request"].META["SCRIPT_NAME"] or "/"), ), )
def _loading_indicator(resultcontainerid): return hg.DIV( Loading(small=True), id=hg.format("{}-indicator", resultcontainerid), _class="htmx-indicator", style="position: absolute; right: 2rem", )
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 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"), )), )
def __init__(self, label=None, help_text=None, errors=None, inputelement_attrs=None, boundfield=None, **attributes): super().__init__(label=label, help_text=help_text, errors=errors, inputelement_attrs=inputelement_attrs, boundfield=boundfield, **attributes) def introspections(context): return json.dumps(DjangoQLSchemaSerializer().serialize( DjangoQLSchema( hg.resolve_lazy(boundfield, context).value().queryset.model))) if boundfield: self.append( hg.SCRIPT( hg.format( """ document.addEventListener("DOMContentLoaded", () => DjangoQL.DOMReady(function () {{ new DjangoQL({{ introspections: {}, selector: 'textarea[name={}]', syntaxHelp: '{}', autoResize: false }}); }})); """, hg.F(introspections), inputelement_attrs.get("name"), reverse("reporthelp"), autoescape=False, ), )) self.append( hg.LINK( rel="stylesheet", type="text/css", href=staticfiles_storage.url( "djangoql/css/completion.css"), )) self.append( hg.SCRIPT( src=staticfiles_storage.url("djangoql/js/completion.js")))
def _close_button(resultcontainerid, widgetattributes): kwargs = { "_class": hg.BaseElement( "bx--search-close", hg.If(widgetattributes.get("value"), None, " bx--search-close--hidden"), ), "title": _("Clear search input"), "aria_label": _("Clear search input"), "type": "button", } if resultcontainerid is not None: kwargs["onclick"] = hg.format( "document.getElementById('{}').innerHTML = '';", resultcontainerid ) return hg.BUTTON(Icon("close", size=20, _class="bx--search-clear"), **kwargs)
def __init__( self, heading, *content, label="", size="sm", buttons=(), id=None, with_form=False, **attributes, ): """ heading: Modal title content: content elements of the modal, can be empty label: small informative label above heading size: one of ["xs", "sm", "md", "lg"] buttons: buttons displayed on bottom of modal, last button has default focus the attribute "data_modal_close" can be set on an button in order to make it a cancel button In order to open the modal just pass self.openerattributes as kwargs to another html element, e.g. a button modal = modal.Modal("My Modal", "Hello world") button.Button("Model", **modal.openerattributes) """ if size not in Modal.SIZES: raise ValueError( f"argument 'size' has value {size} but needs to be one of {Modal.SIZES}" ) if buttons and "data_modal_primary_focus" not in buttons[-1].attributes: buttons[-1].attributes["data_modal_primary_focus"] = True attributes["_class"] = attributes.get("_class", "") + " bx--modal" self.id = hg.html_id(self, prefix="modal-") if id is None else id self.openerattributes = { "data_modal_target": hg.format("#{}", self.id) } self.contentcontainer = hg.DIV( *content, _class="bx--modal-content" + (" bx--modal-content--with-form" if with_form else ""), tabindex=0, ) super().__init__( hg.DIV( hg.DIV( hg.P( label, _class="bx--modal-header__label", ), hg.P( heading, _class="bx--modal-header__heading", ), hg.BUTTON( Icon( "close", size=16, _class="bx--modal-close__icon", aria_hidden="true", ), _class="bx--modal-close", type="button", data_modal_close=True, ), _class="bx--modal-header", ), self.contentcontainer, hg.DIV(_class="bx--modal-content--overflow-indicator"), hg.DIV( *buttons, _class="bx--modal-footer", ) if buttons else "", _class=f"bx--modal-container bx--modal-container--{size}", ), data_modal=True, id=self.id, role="dialog", aria_modal="true", tabindex="-1", **attributes, )
def __init__(self, form, *children, use_csrf=True, standalone=True, **kwargs): """ form: lazy evaluated value which should resolve to the form object children: any child elements, can be formfields or other use_csrf: add a CSRF input, but only for POST submission and standalone forms standalone: if true, will add a CSRF token and will render enclosing FORM-element """ self.standalone = standalone attributes = {"method": "POST", "autocomplete": "off"} attributes.update(kwargs) if (attributes["method"].upper() == "POST" and use_csrf is not False and standalone is True): children = (CsrfToken(), ) + children if self.standalone and "enctype" not in attributes: # note: We will always use "multipart/form-data" instead of the # default "application/x-www-form-urlencoded" inside bread. We do # this because forms with file uploads require multipart/form-data. # Not distinguishing between two encoding types can save us some issues, # especially when handling files. # The only draw back with this is a slightly larger payload because # multipart-encoding takes a little bit more space attributes["enctype"] = "multipart/form-data" super().__init__( hg.WithContext( # generic errors hg.If( form.non_field_errors(), hg.Iterator( form.non_field_errors(), "formerror", InlineNotification(_("Form error"), hg.C("formerror"), kind="error"), ), ), # errors from hidden fields hg.If( form.hidden_fields(), hg.Iterator( form.hidden_fields(), "hiddenfield", hg.Iterator( hg.C("hiddenfield").errors, "hiddenfield_error", InlineNotification( _("Hidden field error: "), hg.format( "{}: {}", hg.C("hiddenfield").name, hg.C("hiddenfield_error"), ), kind="error", ), ), ), ), *children, **{DEFAULT_FORM_CONTEXTNAME: form}, ), **attributes, )
class PersonBrowseView(BrowseView): columns = [ DataTableColumn( layout.ObjectFieldLabel("personnumber", models.Person), hg.DIV( hg.C("row.personnumber"), style=hg.If(hg.C("row.deleted"), "text-decoration:line-through"), ), "personnumber__int", ), DataTableColumn( hg.DIV( layout.tooltip.IconTooltip( hg.UL( hg.LI(hg.SPAN("●", style="color: green"), " ", _("Active")), hg.LI(hg.SPAN("●", style="color: red"), " ", _("Inactive")), ), position="top", )), hg.DIV( "●", style=hg.format("color: {}", hg.If(hg.C("row.active"), "green", "red")), ), "active", ), DataTableColumn(layout.ObjectFieldLabel("_type", models.Person), hg.C("row._type"), "_type"), DataTableColumn( _("Name"), hg.DIV( hg.If( hg.F(lambda context: type(context["row"]) == models. NaturalPerson), hg.C("row.last_name"), hg.C("row.name"), ), style=hg.If(hg.C("row.deleted"), "text-decoration:line-through"), ), "default_sorting_name", ), DataTableColumn( layout.ObjectFieldLabel("first_name", models.NaturalPerson), hg.DIV( hg.If( hg.F(lambda context: type(context["row"]) == models. NaturalPerson), hg.C("row.first_name"), "", ), style=hg.If(hg.C("row.deleted"), "text-decoration:line-through"), ), "naturalperson__first_name", ), "primary_postal_address.address", "primary_postal_address.postcode", "primary_postal_address.city", "primary_postal_address.country", DataTableColumn( _("Email"), hg.C("row.primary_email_address.asbutton", ), "primary_email_address__email", False, ), DataTableColumn( layout.ObjectFieldLabel("tags", models.Person), hg.UL( hg.Iterator(hg.C("row.tags.all"), "tag", layout.tag.Tag(hg.C("tag")))), ), ] bulkactions = ( BulkAction( "add-tag", label=_("Add tag"), iconname="add", action=bulkaddtag, ), BulkAction( "remove-tag", label=_("Remove tag"), iconname="subtract", action=bulkremovetag, ), BulkAction( "delete", label=_("Delete"), iconname="trash-can", action=bulkdelete, ), BulkAction( "restore", label=_("Restore"), iconname="restart", action=bulkrestore, ), BulkAction( "excel", label=_("Excel"), iconname="download", action=export, ), ) search_backend = layout.search.SearchBackendConfig(url=reverse( "basxconnect.core.views.person.search_person_view.searchperson")) rowclickaction = BrowseView.gen_rowclickaction("read") viewstate_sessionkey = "personbrowseview" class FilterForm(forms.Form): naturalperson = forms.BooleanField(required=False, label=_("Natural Person")) legalperson = forms.BooleanField(required=False, label=_("Legal Person")) personassociation = forms.BooleanField(required=False, label=_("Person Association")) naturalperson_subtypes = forms.ModelMultipleChoiceField( queryset=models.Term.objects.filter( vocabulary__slug="naturaltype"), widget=forms.CheckboxSelectMultiple, required=False, label="", ) legalperson_subtypes = forms.ModelMultipleChoiceField( queryset=models.Term.objects.filter(vocabulary__slug="legaltype"), widget=forms.CheckboxSelectMultiple, required=False, label="", ) personassociation_subtypes = forms.ModelMultipleChoiceField( queryset=models.Term.objects.filter( vocabulary__slug="associationtype"), widget=forms.CheckboxSelectMultiple, required=False, label="", ) tags = forms.ModelMultipleChoiceField( queryset=models.Term.objects.filter(vocabulary__slug="tag"), widget=forms.CheckboxSelectMultiple, required=False, ) preferred_language = forms.MultipleChoiceField( choices=settings.PREFERRED_LANGUAGES, widget=forms.CheckboxSelectMultiple, required=False, ) status = forms.MultipleChoiceField( choices=[("active", _("Active")), ("inactive", _("Inactive"))], widget=forms.CheckboxSelectMultiple, required=False, ) trash = forms.BooleanField(required=False, label=_("Trash")) 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 _filterform(self): return self.FilterForm({"status": ["active"], **self.request.GET}) def _checkbox_count(self): counter = 0 form = self._filterform() if form.is_valid(): counter += 1 if form.cleaned_data["naturalperson"] else 0 counter += 1 if form.cleaned_data["legalperson"] else 0 counter += 1 if form.cleaned_data["personassociation"] else 0 counter += form.cleaned_data["naturalperson_subtypes"].count() counter += form.cleaned_data["legalperson_subtypes"].count() counter += form.cleaned_data["personassociation_subtypes"].count() counter += form.cleaned_data["tags"].count() counter += len(form.cleaned_data["preferred_language"]) counter += ( 1 if "inactive" in form.cleaned_data["status"] else 0 ) # don't count the "active" checkbox, it is a permanent default counter += 1 if form.cleaned_data["trash"] else 0 return counter def get_queryset(self): form = self._filterform() qs = super().get_queryset() if form.is_valid(): ret = ((qs.filter( deleted=form.cleaned_data.get("trash", False))).select_related( "primary_email_address", "primary_postal_address", "_type").prefetch_related("tags")) if any([ form.cleaned_data[i] for i in ( "naturalperson", "legalperson", "personassociation", "naturalperson_subtypes", "legalperson_subtypes", "personassociation_subtypes", ) ]): q = Q() for i in ("naturalperson", "legalperson", "personassociation"): # setup some logic descriptors maintype_selected = bool(form.cleaned_data[i]) subtype_selected = bool(form.cleaned_data[f"{i}_subtypes"]) all_subtypes_selected = bool( form.cleaned_data[f"{i}_subtypes"].count() == form.fields[f"{i}_subtypes"].queryset.count()) # the semantics for this filter are not 100% clear # there are also cases where a subtype has the wrong maintype # This code tries to make the selection consistent to what a user # would expect, but these expectations can still vary... if maintype_selected: typeq = Q(_maintype=i) if subtype_selected: if not all_subtypes_selected: typeq &= Q(_type__in=form. cleaned_data[f"{i}_subtypes"]) else: typeq &= ~Q(_type__in=form.fields[f"{i}_subtypes"]. queryset) q |= typeq else: q |= Q(_type__in=form.cleaned_data[f"{i}_subtypes"]) ret = ret.filter(q) if form.cleaned_data.get("tags"): ret = ret.filter(tags__in=form.cleaned_data["tags"]) if form.cleaned_data.get("preferred_language"): ret = ret.filter(preferred_language__in=form. cleaned_data["preferred_language"]) if len(form.cleaned_data.get("status") ) == 1 and not form.cleaned_data.get("trash", False): ret = ret.filter( active=form.cleaned_data.get("status")[0] == "active") return ret return qs def get_settingspanel(self): return hg.DIV( layout.forms.Form( self._filterform(), hg.DIV( hg.DIV( hg.DIV(layout.helpers.Label(_("Person Type"))), hg.DIV( hg.DIV( hg.DIV( layout.forms.FormField( "naturalperson", onclick= "updateCheckboxGroupItems(this.parentElement.parentElement)", ), hg.DIV( layout.forms.FormField( "naturalperson_subtypes", style="padding-left: 1rem", ), style="margin-top: -2rem", ), ), layout.forms.FormField( "personassociation", onclick= "updateCheckboxGroupItems(this.parentElement.parentElement)", ), hg.DIV( layout.forms.FormField( "personassociation_subtypes", style="padding-left: 1rem", ), style="margin-top: -2rem", ), style="margin-right: 16px", ), hg.DIV( layout.forms.FormField( "legalperson", onclick= "updateCheckboxGroupItems(this.parentElement.parentElement)", ), hg.DIV( layout.forms.FormField( "legalperson_subtypes", style="padding-left: 1rem", ), style="margin-top: -2rem", ), style="margin-right: 16px", ), style="display: flex", ), style= "border-right: #ccc solid 1px; margin: 0 16px 0 0", ), hg.DIV( hg.DIV( layout.forms.FormField("tags"), style="margin-right: 16px", ), style= "border-right: #ccc solid 1px; margin: 0 16px 0 0; overflow-y: scroll", ), hg.DIV( hg.DIV( layout.forms.FormField("preferred_language"), style="margin-right: 16px", ), style= "border-right: #ccc solid 1px; margin: 0 16px 0 0", ), hg.DIV( hg.DIV(layout.forms.FormField("status"), style="flex-grow: 0"), hg.DIV(style="flex-grow: 1"), hg.DIV( layout.forms.FormField("trash"), style="max-height: 2rem", ), style="display: flex; flex-direction: column", ), style= "display: flex; max-height: 50vh; padding: 24px 32px 0 32px", ), hg.DIV( layout.button.Button( _("Cancel"), buttontype="ghost", onclick= "this.parentElement.parentElement.parentElement.parentElement.parentElement.style.display = 'none'", ), layout.button.Button.from_link( Link( label=_("Reset"), href=self.request.path + "?reset=1", iconname=None, ), buttontype="secondary", ), layout.button.Button( pgettext_lazy("apply filter", "Filter"), type="submit", ), style= "display: flex; justify-content: flex-end; margin-top: 24px", _class="bx--modal-footer", ), method="GET", ), hg.SCRIPT( mark_safe(""" function updateCheckboxGroupItems(group) { var items = $$('input[type=checkbox]', group); var value = items[0].getAttribute('aria-checked'); value = value == 'true' ? 'true' : 'false'; for(var i = 1; i < items.length; ++i) { new CarbonComponents.Checkbox(items[i]).setState(value); } } function updateCheckboxCounter(group) { var items = $$('input[type=checkbox]', group); var count = 0; for(item of items) { if(!(item.name === 'status' && item.value === 'active')) count += item.getAttribute('aria-checked') == 'true' ? 1 : 0 } $('#%s').innerHTML = count; $('#%s').closest('div[role=button]').style.display = count === 0 ? "none" : "flex"; } """ % (self.checkboxcounterid, self.checkboxcounterid))), style="background-color: #fff", onclick="updateCheckboxCounter(this)", )
def from_queryset( queryset, # column behaviour columns: Iterable[Union[str, "DataTableColumn"]] = (), prevent_automatic_sortingnames=False, # row behaviour rowvariable="row", rowactions: Iterable[Link] = (), rowactions_dropdown=False, rowclickaction=None, # bulkaction behaviour bulkactions: Iterable[Link] = (), checkbox_for_bulkaction_name="_selected", # toolbar configuration title=None, primary_button: Optional[Button] = None, settingspanel: Any = None, pagination_config: Optional[PaginationConfig] = None, search_urlparameter: Optional[str] = None, model=None, # required if queryset is Lazy **kwargs, ): """TODO: Write Docs!!!! Yeah yeah, on it already... :param settingspanel: A panel which will be opened when clicking on the "Settings" button of the datatable, usefull e.g. for showing filter options. Currently only one button and one panel are supported. More buttons and panels could be interesting but may to over- engineered because it is a rare case and it is not difficutl to add another button by modifying the datatable after creation. """ if not isinstance(queryset, hg.Lazy): model = queryset.model if model is None: raise ValueError( "Argument for 'model' must be given if 'queryset' is of type hg.Lazy" ) columns = columns or filter_fieldlist(model, ["__all__"]) title = title or pretty_modelname(model, plural=True) if primary_button is None: primary_button = Button.from_link( Link( href=ModelHref(model, "add"), label=_("Add %s") % pretty_modelname(model), permissions=[ f"{model._meta.app_label}.add_{model._meta.model_name}" ], ), icon=Icon("add", size=20), ) if rowactions_dropdown: objectactions_menu: hg.HTMLElement = OverflowMenu( rowactions, flip=True, item_attributes={"_class": "bx--table-row--menu-option"}, ) else: objectactions_menu = hg.DIV( hg.Iterator( rowactions, "link", hg.F(lambda c: Button.from_link( c["link"], notext=True, small=True, buttontype="ghost", _class="bx--overflow-menu", ) if isinstance(c["link"], Link) else c["link"]), ), style="display: flex; justify-content: flex-end;", ) column_definitions: List[DataTableColumn] = [] for col in columns: if not (isinstance(col, DataTableColumn) or isinstance(col, str)): raise ValueError( f"Argument 'columns' needs to be of a List[str] or a List[DataTableColumn], but found {col}" ) td_attributes: Optional[dict] = None if rowclickaction and getattr(col, "enable_row_click", True): assert isinstance(rowclickaction, Link), "rowclickaction must be of type Link" td_attributes = { **aslink_attributes(rowclickaction.href), **(rowclickaction.attributes or {}), } # convert simple string (modelfield) to column definition if isinstance(col, str): col = DataTableColumn.from_modelfield( col, model, prevent_automatic_sortingnames, rowvariable, td_attributes=td_attributes, ) else: if td_attributes: col = col._replace( td_attributes=td_attributes) # type: ignore column_definitions.append(col) return DataTable( column_definitions + ([ DataTableColumn( "", objectactions_menu, td_attributes=hg.F( lambda c: { "_class": "bx--table-column-menu" if rowactions_dropdown else "" }), th_attributes=hg.F( lambda c: {"_class": "bx--table-column-menu"}), ) ] if rowactions else []), # querysets are cached, the call to all will make sure a new query is used in every request hg.F(lambda c: queryset), **kwargs, ).with_toolbar( title, helper_text=hg.format( "{} {}", hg.F(lambda c: len(hg.resolve_lazy(queryset, c)) if pagination_config is None else pagination_config. paginator.count), model._meta.verbose_name_plural, ), primary_button=primary_button, bulkactions=bulkactions, pagination_config=pagination_config, checkbox_for_bulkaction_name=checkbox_for_bulkaction_name, search_urlparameter=search_urlparameter, settingspanel=settingspanel, )
def __init__( self, label=None, help_text=None, errors=None, inputelement_attrs=None, boundfield=None, **attributes, ): inputelement_attrs = inputelement_attrs or {} uploadbutton = hg.LABEL( hg.SPAN(_("Select file"), role="button"), tabindex=0, _class=hg.BaseElement( "bx--btn bx--btn--tertiary", hg.If(inputelement_attrs.get("disabled"), " bx--btn--disabled"), ), data_file_drop_container=True, disabled=inputelement_attrs.get("disabled"), data_invalid=hg.If(getattr(errors, "condition", False), True), _for=inputelement_attrs.get("id"), ) input = self.get_input_element( inputelement_attrs, errors, onload=""" that = this; document.addEventListener('change', (e) => { that.parentElement.querySelector('[data-file-container]').innerHTML = ''; var widget = new CarbonComponents.FileUploader(that.parentElement); widget._displayFilenames(); widget.setState('edit'); }); """, ) # we can only clear the field if it originates form a django field # otherwise it has no use clearbox = None if boundfield: checkbox_name = hg.F( lambda c: hg.resolve_lazy(boundfield, c).field.widget. clear_checkbox_name(hg.resolve_lazy(boundfield, c).html_name)) checkbox_id = hg.F( lambda c: hg.resolve_lazy(boundfield, c).field.widget. clear_checkbox_id(hg.resolve_lazy(checkbox_name, c))) clearbox = hg.If( self.clearable, hg.INPUT( type="checkbox", name=checkbox_name, id=checkbox_id, style="display: none", ), ) # clearbutton is always used, to allow clearing a just selected field in the browser clearbutton = hg.If( self.clearable, hg.SPAN( hg.BUTTON( Icon("close", size=16), _class="bx--file-close", type="button", aria_label="close", onclick=hg.If( clearbox, hg.format("$('#{}').checked = 'checked';", checkbox_id), ), ), data_for=inputelement_attrs.get("id"), _class="bx--file__state-container", ), ) super().__init__( label, hg.DIV( uploadbutton, input, clearbox, hg.DIV( hg.If( inputelement_attrs.get("value"), hg.SPAN( hg.P( hg.If( hg.F(lambda c: hasattr( hg.resolve_lazy(inputelement_attrs, c). get("value").file, "name", )), hg.A( hg.F(lambda c: os.path.basename( hg.resolve_lazy( inputelement_attrs, c).get( "value").name)), href=hg.F(lambda c: settings.MEDIA_URL + hg.resolve_lazy( inputelement_attrs, c ).get("value").name), ), hg.F(lambda c: os.path.basename( hg.resolve_lazy(inputelement_attrs, c). get("value").name)), ), _class="bx--file-filename", ), clearbutton, _class="bx--file__selected-file", ), ), data_file_container=True, _class="bx--file-container", ), help_text, errors, _class="bx--file", data_file=True, ), **attributes, )
def as_email(value): return hg.A(value, href=hg.format("mailto: {}", value))
def get_attribute_description_modal(obj): from . import datatable, modal columns = [] fields = {f.name: f for f in obj._meta.get_fields()} for i in set(dir(obj) + list(vars(obj))): try: desc = _get_attribute_description(obj, i, fields) if desc is not None and desc[3]: f = desc[3]._meta.get_fields() additional_attrs = list( filter( None, ( _get_attribute_description(desc[3], a, f) for a in set(dir(desc[3]) + list(vars(desc[3]))) ), ) ) desc = ( desc[0], desc[1], desc[2], hg.BaseElement( hg.UL( hg.Iterator( additional_attrs, "attr", hg.LI( hg.format("{}.{}", i, hg.C("attr.0")), style="font-weight: 700", ), ) ), ), ) if desc is not None: columns.append(desc) except Exception as e: columns.append((i, _("Unknown"), e)) return modal.Modal( _("Available columns"), hg.DIV( hg.DIV(_("Bold text marks valid column names")), datatable.DataTable( columns=[ datatable.DataTableColumn( _("Column name"), hg.SPAN(hg.C("row.0"), style="font-weight: 700"), ), datatable.DataTableColumn( _("Description"), hg.F(lambda c: c["row"][2]) ), datatable.DataTableColumn(_("Type"), hg.F(lambda c: c["row"][1])), datatable.DataTableColumn(_("Extended columns"), hg.C("row.3")), ], row_iterator=sorted(columns), ), ), size="lg", )
def display_postal(): postal = hg.C("i") modal = layout.modal.Modal.with_ajax_content( heading=edit_heading(models.Postal), url=ModelHref( models.Postal, "ajax_edit", kwargs={"pk": postal.pk}, query={"asajax": True}, ), id=hg.format("postal-modal-{}", hg.C("i").id), submitlabel=_("Save"), ) is_inactive = hg.F(lambda c: c["i"].valid_until and c["i"].valid_until < timezone.now().date()) return R( C( hg.DIV( postal.type, hg.If( hg.F(lambda c: c["i"].person.primary_postal_address and c[ "i"].person.primary_postal_address.pk == c["i"].pk), hg.BaseElement(" (", _("primary"), ")"), ), style="font-weight: bold; margin-bottom: 1rem;", ), hg.DIV( ObjectFieldValue("address", object_contextname="i"), style="margin-bottom: 0.25rem;", ), hg.DIV(postal.postcode, " ", postal.city, style="margin-bottom: 0.25rem;"), hg.DIV(postal.get_country_display()), hg.If( postal.valid_from, hg.DIV( hg.SPAN(_("Valid from: "), style="font-weight: bold;"), ObjectFieldValue("valid_from", object_contextname="i"), " ", style= "display: inline-block; margin-top: 1rem; margin-right: 1rem;", ), hg.BaseElement(), ), hg.If( postal.valid_until, hg.DIV( hg.SPAN(_("Valid until: "), style="font-weight: bold;"), ObjectFieldValue("valid_until", object_contextname="i"), style="display: inline-block; margin-top: 1rem;", ), hg.BaseElement(), ), hg.DIV(edit_postal_button(modal), ), ), _class=hg.If(is_inactive, "inactive_postal", ""), style=hg.If( is_inactive, "display: none; margin-top: 1.5rem; color: #a8a8a8;", "margin-top: 1.5rem;", ), )