This commit is contained in:
Herculino Trotta
2024-10-09 00:31:21 -03:00
parent e78e4cc5e1
commit 3dde44b1cd
139 changed files with 4965 additions and 1004 deletions

View File

View File

@@ -0,0 +1,14 @@
from functools import wraps
from django.core.exceptions import PermissionDenied
def only_htmx(view):
@wraps(view)
def _view(request, *args, **kwargs):
if not request.META.get("HTTP_HX_REQUEST"):
raise PermissionDenied
return view(request, *args, **kwargs)
return _view

View File

View File

View File

@@ -0,0 +1,205 @@
from django import forms
from django.core.exceptions import ValidationError
from django.db import transaction
from apps.common.widgets.tom_select import TomSelect, TomSelectMultiple
# class DynamicModelChoiceField(forms.ModelChoiceField):
# def __init__(self, model, *args, **kwargs):
# self.model = model
# self.queryset = kwargs.pop("queryset", model.objects.all())
# super().__init__(queryset=self.queryset, *args, **kwargs)
# self._created_instance = None
#
# self.widget = TomSelect(clear_button=True, create=True)
#
# def to_python(self, value):
# if value in self.empty_values:
# return None
# try:
# key = self.to_field_name or "pk"
# return self.model.objects.get(**{key: value})
# except (ValueError, TypeError, self.model.DoesNotExist):
# return value # Return the raw value; we'll handle creation in clean()
#
# def clean(self, value):
# if isinstance(value, self.model):
# return value
# if isinstance(value, str):
# try:
# if value.isdigit():
# return self.model.objects.get(id=value)
# else:
# raise self.model.DoesNotExist
# except self.model.DoesNotExist:
# try:
# with transaction.atomic():
# instance = self.model.objects.create(name=value)
# self._created_instance = instance
# return instance
# except Exception as e:
# raise ValidationError(
# self.error_messages["invalid_choice"], code="invalid_choice"
# )
# return super().clean(value)
#
# def bound_data(self, data, initial):
# if self._created_instance and isinstance(data, str):
# if data == self._created_instance.name:
# return self._created_instance.pk
# return super().bound_data(data, initial)
class DynamicModelChoiceField(forms.ModelChoiceField):
def __init__(self, model, *args, **kwargs):
self.model = model
self.queryset = kwargs.pop("queryset", model.objects.all())
super().__init__(queryset=self.queryset, *args, **kwargs)
self._created_instance = None
self.widget = TomSelect(clear_button=True, create=True)
def to_python(self, value):
if value in self.empty_values:
return None
try:
key = self.to_field_name or "pk"
return self.model.objects.get(**{key: value})
except (ValueError, TypeError, self.model.DoesNotExist):
return value # Return the raw value; we'll handle creation in clean()
def clean(self, value):
if value in self.empty_values:
if self.required:
raise ValidationError(self.error_messages["required"], code="required")
return None
if isinstance(value, self.model):
return value
if isinstance(value, str):
value = value.strip()
if not value:
if self.required:
raise ValidationError(
self.error_messages["required"], code="required"
)
return None
try:
if value.isdigit():
return self.model.objects.get(id=value)
else:
raise self.model.DoesNotExist
except self.model.DoesNotExist:
try:
with transaction.atomic():
instance = self.model.objects.create(name=value)
self._created_instance = instance
return instance
except Exception as e:
raise ValidationError(
self.error_messages["invalid_choice"], code="invalid_choice"
)
return super().clean(value)
def bound_data(self, data, initial):
if self._created_instance and isinstance(data, str):
if data == self._created_instance.name:
return self._created_instance.pk
return super().bound_data(data, initial)
class DynamicModelMultipleChoiceField(forms.ModelMultipleChoiceField):
"""
A custom ModelMultipleChoiceField that creates new entries if they don't exist.
This field allows users to select multiple existing options or add new ones.
If a selected option doesn't exist, it will be created in the database.
Attributes:
create_field (str): The name of the field to use when creating new instances.
"""
def __init__(self, model, **kwargs):
"""
Initialize the CreateIfNotExistsModelMultipleChoiceField.
Args:
create_field (str): The name of the field to use when creating new instances.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
"""
self.create_field = kwargs.pop("create_field", None)
if not self.create_field:
raise ValueError("The 'create_field' parameter is required.")
self.model = model
self.queryset = kwargs.pop("queryset", model.objects.all())
super().__init__(queryset=self.queryset, **kwargs)
self.widget = TomSelectMultiple(
remove_button=True, clear_button=True, create=True, checkboxes=True
)
def _create_new_instance(self, value):
"""
Create a new instance of the model with the given value.
Args:
value: The value to use for creating the new instance.
Returns:
Model: The newly created model instance.
Raises:
ValidationError: If there's an error creating the new instance.
"""
try:
with transaction.atomic():
new_instance = self.queryset.model(**{self.create_field: value})
new_instance.full_clean()
new_instance.save()
return new_instance
except Exception as e:
raise ValidationError(f"Error creating new instance: {str(e)}")
def clean(self, value):
"""
Clean and validate the field value.
This method checks if each selected choice exists in the database.
If a choice doesn't exist, it creates a new instance of the model.
Args:
value (list): List of selected values.
Returns:
list: A list containing all selected and newly created model instances.
Raises:
ValidationError: If there's an error during the cleaning process.
"""
if not value:
return []
print(value)
string_values = set(str(v) for v in value)
existing_objects = list(
self.queryset.filter(**{f"{self.create_field}__in": string_values})
)
existing_values = set(
str(getattr(obj, self.create_field)) for obj in existing_objects
)
new_values = string_values - existing_values
new_objects = []
for new_value in new_values:
try:
new_objects.append(self._create_new_instance(new_value))
except ValidationError as e:
raise ValidationError(f"Error creating '{new_value}': {str(e)}")
return existing_objects + new_objects

View File

View File

@@ -0,0 +1,16 @@
import zoneinfo
from django.utils import timezone
from django.shortcuts import render
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
tz = request.COOKIES.get("mytz")
if tz:
timezone.activate(zoneinfo.ZoneInfo(tz))
else:
timezone.activate(zoneinfo.ZoneInfo("UTC"))
return self.get_response(request)

View File

@@ -0,0 +1,41 @@
from django import template
register = template.Library()
# https://github.com/valerymelou/django-active-link/
@register.simple_tag(takes_context=True)
def active_link(
context, views="", namespaces="", css_class="active", inactive_class=""
):
"""
Renders the given CSS class if the request path matches the path of the view.
:param context: The context where the tag was called. Used to access the request object.
:param views: The name of the view or views separated by || (include namespaces if any).
:param namespaces: The name of the namespace or namespaces separated by ||.
:param css_class: The CSS class to render.
:param inactive_class: The CSS class to render if the views is not active.
:return:
"""
request = context.get("request")
if request is None:
# Can't work without the request object.
return ""
if views:
views = views.split("||")
current_view = request.resolver_match.view_name
if current_view in views:
return css_class
elif namespaces:
namespaces = namespaces.split("||")
current_namespace = request.resolver_match.namespaces
if any(item in current_namespace for item in namespaces):
return css_class
else:
return ""
return inactive_class

View File

@@ -0,0 +1,60 @@
from django import template
from django.utils import timezone
from datetime import date
from django.utils.translation import gettext_lazy as _, ngettext_lazy
from django.utils.formats import date_format
register = template.Library()
@register.filter(name="customnaturaldate")
def naturaldate(value):
if not isinstance(value, date):
return value
today = timezone.localdate(timezone.now())
delta = value - today
if delta.days == 0:
return _("today")
elif delta.days == 1:
return _("tomorrow")
elif delta.days == -1:
return _("yesterday")
elif -7 <= delta.days < 0:
return _("last 7 days")
elif 0 < delta.days <= 7:
return _("in the next 7 days")
elif delta.days < -365:
years = abs(delta.days) // 365
return ngettext_lazy("%(years)s year ago", "%(years)s years ago", years) % {
"years": years
}
elif delta.days < -30:
months = abs(delta.days) // 30
return ngettext_lazy(
"%(months)s month ago", "%(months)s months ago", months
) % {"months": months}
elif delta.days < -7:
weeks = abs(delta.days) // 7
return ngettext_lazy("%(weeks)s week ago", "%(weeks)s weeks ago", weeks) % {
"weeks": weeks
}
elif delta.days > 365:
years = delta.days // 365
return ngettext_lazy("in %(years)s year", "in %(years)s years", years) % {
"years": years
}
elif delta.days > 30:
months = delta.days // 30
return ngettext_lazy("in %(months)s month", "in %(months)s months", months) % {
"months": months
}
elif delta.days > 7:
weeks = delta.days // 7
return ngettext_lazy("in %(weeks)s week", "in %(weeks)s weeks", weeks) % {
"weeks": weeks
}
else:
return date_format(value, format="DATE_FORMAT")

View File

@@ -0,0 +1,13 @@
from django import template
from django.conf import settings
register = template.Library()
@register.filter
def site_title(value):
value = value.strip()
if value:
return f"{value} {settings.TITLE_SEPARATOR or '::'} {settings.SITE_TITLE or "SITE_TITLE NOT SET"}"
else:
return settings.SITE_TITLE or "SITE_TITLE NOT SET"

View File

@@ -0,0 +1,65 @@
from crispy_forms.layout import BaseInput
class NoClassSubmit(BaseInput):
"""
Used to create a Submit button descriptor for the {% crispy %} template tag.
Attributes
----------
template: str
The default template which this Layout Object will be rendered
with.
field_classes: str
CSS classes to be applied to the ``<input>``.
input_type: str
The ``type`` attribute of the ``<input>``.
Parameters
----------
name : str
The name attribute of the button.
value : str
The value attribute of the button.
css_id : str, optional
A custom DOM id for the layout object. If not provided the name
argument is slugified and turned into the id for the submit button.
By default None.
css_class : str, optional
Additional CSS classes to be applied to the ``<input>``. By default
None.
template : str, optional
Overrides the default template, if provided. By default None.
**kwargs : dict, optional
Additional attributes are passed to `flatatt` and converted into
key="value", pairs. These attributes are added to the ``<input>``.
Examples
--------
Note: ``form`` arg to ``render()`` is not required for ``BaseInput``
inherited objects.
>>> submit = Submit('Search the Site', 'search this site')
>>> submit.render("", "", Context())
'<input type="submit" name="search-the-site" value="search this site" '
'class="btn btn-primary" id="submit-id-search-the-site"/>'
>>> submit = Submit('Search the Site', 'search this site', css_id="custom-id",
css_class="custom class", my_attr=True, data="my-data")
>>> submit.render("", "", Context())
'<input type="submit" name="search-the-site" value="search this site" '
'class="btn btn-primary custom class" id="custom-id" data="my-data" my-attr/>'
Usually you will not call the render method on the object directly. Instead
add it to your ``Layout`` manually or use the `add_input` method::
class ExampleForm(forms.Form):
[...]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.add_input(Submit('submit', 'Submit'))
"""
input_type = "submit"
field_classes = ""

View File

@@ -0,0 +1,54 @@
from django.forms import widgets, SelectMultiple
from django.utils.translation import gettext_lazy as _
class TomSelect(widgets.Select):
def __init__(
self,
attrs=None,
remove_button=False,
remove_button_text=_("Remove"),
create=False,
create_text=_("Add"),
clear_button=True,
clear_text=_("Clear"),
no_results_text=_("No results..."),
checkboxes=False,
*args,
**kwargs
):
super().__init__(attrs, *args, **kwargs)
self.remove_button = remove_button
self.remove_button_text = remove_button_text
self.clear_button = clear_button
self.create = create
self.create_text = create_text
self.clear_text = clear_text
self.no_results_text = no_results_text
self.checkboxes = checkboxes
def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super().build_attrs(base_attrs, extra_attrs)
attrs["data-txt-no-results"] = self.no_results_text
if self.remove_button:
attrs["data-remove-button"] = "true"
attrs["data-txt-remove"] = self.remove_button_text
if self.create:
attrs["data-create"] = "true"
attrs["data-txt-create"] = self.create_text
if self.clear_button:
attrs["data-clear-button"] = "true"
attrs["data-txt-clear"] = self.clear_text
if self.checkboxes:
attrs["data-checkboxes"] = "true"
return attrs
class TomSelectMultiple(SelectMultiple, TomSelect):
pass