Skip to content

Commit

Permalink
Merge pull request #94 from basxsoftwareassociation/feature/views_imp…
Browse files Browse the repository at this point in the history
…rovements

Feature/views improvements
  • Loading branch information
saemideluxe authored Nov 8, 2021
2 parents 20ef8aa + bfdb413 commit cca0bb2
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 54 deletions.
55 changes: 25 additions & 30 deletions bread/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,36 @@
from django.db import models
from django.utils.functional import Promise
from django.utils.html import format_html, format_html_join, linebreaks, mark_safe
from django_countries.fields import CountryField

import bread.settings as app_settings

from . import layout


def format_value(value, fieldtype=None):
def format_value(value):
"""Renders a python value in a nice way in HTML. If a field-definition has an attribute "renderer" set, that function will be used to render the value"""
if hasattr(fieldtype, "renderer"):
return fieldtype.renderer(value)

if isinstance(value, bool) or value is None:
return CONSTANTS[value]

# make referencing fields iterable (normaly RelatedManagers)
if isinstance(value, models.Manager):
value = value.all()

# If there is a hint passed via fieldtype, use the accoring conversion function first (identity otherwise)
# This is mostly helpfull for string-based fields like URLS, emails etc.
value = MODELFIELD_FORMATING_HELPERS.get(fieldtype.__class__, lambda a: a)(value)

if isinstance(value, bool) or value is None:
return CONSTANTS[value]
if isinstance(value, datetime.timedelta):
return as_duration(value)
if isinstance(value, datetime.datetime):
return as_datetime(value)
if isinstance(value, numbers.Number):
return f"{float(value):f}".rstrip("0").rstrip(".")
if isinstance(value, models.fields.files.FieldFile):
return as_download(value)
if isinstance(value, Iterable) and not isinstance(value, (str, bytes, Promise)):
return as_list(value)
if isinstance(value, str):
if "\n" in value:
return as_text(value)
if is_email_simple(value):
return as_email(value)

return value


Expand Down Expand Up @@ -74,13 +69,6 @@ def as_duration(value):
return str(value - datetime.timedelta(microseconds=value.microseconds))


def as_datetime(value):
if value.tzinfo:
value = value.astimezone(tz.gettz(settings.TIME_ZONE))

return value.isoformat(sep=" ", timespec="seconds").rsplit("+", 1)[0]


def as_countries(value):
return as_list((c.name for c in value))

Expand Down Expand Up @@ -149,19 +137,26 @@ def as_video(value):
)


MODELFIELD_FORMATING_HELPERS = {
None: lambda a: a,
models.EmailField: as_email,
models.FileField: as_download,
models.URLField: as_url,
models.TextField: as_text,
models.TimeField: as_time,
models.DateTimeField: as_datetime,
CountryField: as_countries,
}

CONSTANTS = {
None: getattr(settings, "HTML_NONE", app_settings.HTML_NONE),
True: getattr(settings, "HTML_TRUE", app_settings.HTML_TRUE),
False: getattr(settings, "HTML_FALSE", app_settings.HTML_FALSE),
}


# copied from https://github.com/django/django/blob/1f9874d4ca3e7376036646aedf6ac3060f22fd69/django/utils/html.py
# because older versions only have this as a local function available
def is_email_simple(value):
"""Return True if value looks like an email address."""
# An @ must be in the middle of the value.
if "@" not in value or value.startswith("@") or value.endswith("@"):
return False
try:
p1, p2 = value.split("@")
except ValueError:
# value contains more than one @.
return False
# Dot must be in p2 (e.g. example.com)
if "." not in p2 or p2.startswith("."):
return False
return True
7 changes: 4 additions & 3 deletions bread/layout/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,12 @@ def resolve(self, context):
)
if value is None:
value = hg.resolve_lookup(object, self.fieldname)
if self.formatter:
return self.formatter(value)
if isinstance(value, datetime.datetime):
value = localtime(value)
return localize(value, use_l10n=settings.USE_L10N)
value = localize(value, use_l10n=settings.USE_L10N)
if self.formatter:
return self.formatter(value)
return value


FC = FormattedContextValue
Expand Down
41 changes: 20 additions & 21 deletions bread/views/browse.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ def has_permission(self, request, obj=None):
)


def default_bulkactions(model, columns=["__all__"]):
return (
BulkAction(
"excel",
label=_("Excel"),
iconname="download",
action=lambda request, qs: export(qs, columns),
permissions=[f"{model._meta.app_label}.view_{model._meta.model_name}"],
),
BulkAction(
"delete",
label=_("Delete"),
iconname="trash-can",
action=delete,
permissions=[f"{model._meta.app_label}.add_{model._meta.model_name}"],
),
)


class BrowseView(BreadView, LoginRequiredMixin, PermissionListMixin, ListView):
"""TODO: documentation"""

Expand Down Expand Up @@ -97,30 +116,10 @@ def __init__(self, *args, **kwargs):
kwargs.get("viewstate_sessionkey") or self.viewstate_sessionkey
)
super().__init__(*args, **kwargs)
# set some default bulkactions
self.bulkactions = (
kwargs.get("bulkactions")
or self.bulkactions
or (
BulkAction(
"excel",
label=_("Excel"),
iconname="download",
action=lambda request, qs: export(qs, self.columns),
permissions=[
f"{self.model._meta.app_label}.view_{self.model._meta.model_name}"
],
),
BulkAction(
"delete",
label=_("Delete"),
iconname="trash-can",
action=delete,
permissions=[
f"{self.model._meta.app_label}.add_{self.model._meta.model_name}"
],
),
)
or default_bulkactions(self.model, self.columns)
)

def get_layout(self):
Expand Down
47 changes: 47 additions & 0 deletions bread/views/read.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import htmlgenerator as hg
from django import forms
from django.http import HttpResponseNotAllowed
from django.views.generic import DetailView as DjangoReadView
from guardian.mixins import PermissionRequiredMixin

from .. import layout as _layout # prevent name clashing
from ..formatters import format_value
from ..forms.fields import FormsetField
from ..forms.forms import breadmodelform_factory
from ..utils import expand_ALL_constant
from .edit import EditView
from .util import BreadView


# Read view is the same as the edit view but form elements are disabled
Expand Down Expand Up @@ -45,6 +50,48 @@ def get_required_permissions(self, request):
return [f"{self.model._meta.app_label}.view_{self.model.__name__.lower()}"]


class TableReadView(
BreadView,
PermissionRequiredMixin,
DjangoReadView,
):

accept_global_perms = True
fields = ["__all__"]
urlparams = (("pk", int),)

def __init__(self, *args, **kwargs):
self.fields = expand_ALL_constant(
kwargs["model"], kwargs.get("fields") or self.fields
)
super().__init__(*args, **kwargs)

def get_layout(self):
return hg.BaseElement(
hg.H3(self.object),
_layout.datatable.DataTable(
columns=[
_layout.datatable.DataTableColumn(header="", cell=hg.C("row.0")),
_layout.datatable.DataTableColumn(header="", cell=hg.C("row.1")),
],
row_iterator=(
(
field
if isinstance(field, tuple)
else (
_layout.ObjectFieldLabel(field),
_layout.ObjectFieldValue(field, formatter=format_value),
)
)
for field in self.fields
),
),
)

def get_required_permissions(self, request):
return [f"{self.model._meta.app_label}.view_{self.model.__name__.lower()}"]


def layoutasreadonly(layout):
layout.wrap(
lambda element, ancestors: isinstance(element, _layout.form.Form)
Expand Down

0 comments on commit cca0bb2

Please sign in to comment.