Skip to content

Commit

Permalink
Merge pull request #146 from druids/Django4Support
Browse files Browse the repository at this point in the history
Added django 4 support
  • Loading branch information
matllubos committed Jul 2, 2023
2 parents a64f2b4 + ff51dec commit cbe15ae
Show file tree
Hide file tree
Showing 20 changed files with 116 additions and 94 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/django.yml
Expand Up @@ -34,6 +34,12 @@ jobs:
- python-version: "3.11"
django-version: Django==3.2

- python-version: "3.10"
django-version: Django==4.2

- python-version: "3.11"
django-version: Django==4.2

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand Down
8 changes: 8 additions & 0 deletions chamber/config.py
Expand Up @@ -8,6 +8,14 @@
'PRIVATE_S3_STORAGE_URL_EXPIRATION': 3600,
'AWS_S3_ON': getattr(django_settings, 'AWS_S3_ON', False),
'AWS_REGION': getattr(django_settings, 'AWS_REGION', None),
'SMART_MODEL_ATTRIBUTES': {
'is_cleaned_pre_save': False,
'is_cleaned_post_save': False,
'is_cleaned_pre_delete': False,
'is_cleaned_post_delete': False,
'is_save_atomic': False,
'is_delete_atomic': False,
}
}


Expand Down
3 changes: 1 addition & 2 deletions chamber/formatters/__init__.py
@@ -1,5 +1,4 @@
from django.utils import numberformat
from django.utils.encoding import force_text
from django.utils.safestring import mark_safe


Expand All @@ -16,6 +15,6 @@ def natural_number_with_currency(number, currency, show_decimal_place=True, use_
thousand_sep=' ',
force_grouping=True
),
force_text(currency)
str(currency)
)
return mark_safe(humanized.replace(' ', '\u00a0')) if use_nbsp else humanized
4 changes: 2 additions & 2 deletions chamber/forms/fields.py
@@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import ugettext
from django.utils.translation import gettext

from chamber.config import settings

Expand Down Expand Up @@ -38,7 +38,7 @@ class PriceField(DecimalField):
widget = PriceNumberInput

def __init__(self, *args, **kwargs):
currency = kwargs.pop('currency', ugettext('CZK'))
currency = kwargs.pop('currency', gettext('CZK'))
kwargs.setdefault('max_digits', 10)
kwargs.setdefault('decimal_places', 2)
if 'widget' not in kwargs:
Expand Down
8 changes: 4 additions & 4 deletions chamber/forms/validators.py
Expand Up @@ -3,7 +3,7 @@
import magic # pylint: disable=E0401

from django.core.exceptions import ValidationError
from django.utils.translation import ugettext
from django.utils.translation import gettext
from django.template.defaultfilters import filesizeformat


Expand All @@ -15,7 +15,7 @@ def __init__(self, max_upload_size):
def __call__(self, data):
if data.size > self.max_upload_size:
raise ValidationError(
ugettext('Please keep filesize under {max}. Current filesize {current}').format(
gettext('Please keep filesize under {max}. Current filesize {current}').format(
max=filesizeformat(self.max_upload_size),
current=filesizeformat(data.size)
)
Expand All @@ -33,7 +33,7 @@ def __call__(self, data):
extension_mime_type = mimetypes.guess_type(data.name)[0]

if extension_mime_type not in self.content_types:
raise ValidationError(ugettext('Extension of file name is not allowed'))
raise ValidationError(gettext('Extension of file name is not allowed'))

return data

Expand All @@ -49,6 +49,6 @@ def __call__(self, data):
mime_type = m.id_buffer(data.read(2048))
data.seek(0)
if mime_type not in self.content_types:
raise ValidationError(ugettext('File content was evaluated as not supported file type'))
raise ValidationError(gettext('File content was evaluated as not supported file type'))

return data
9 changes: 8 additions & 1 deletion chamber/models/__init__.py
@@ -1,5 +1,12 @@
from .base import AuditModel, SmartQuerySet, SmartManager, SmartModel, SmartModelBase # noqa: F401
from .base import SmartAuditModel, SmartQuerySet, SmartManager, SmartModel, SmartModelBase # noqa: F401
from .fields import ( # noqa: F401
DecimalField, FileField, ImageField, PrevValuePositiveIntegerField, SubchoicesPositiveIntegerField,
EnumSequencePositiveIntegerField, EnumSequenceCharField, PriceField, PositivePriceField
)


__all__ = (
'SmartAuditModel', 'SmartQuerySet', 'SmartManager', 'SmartModel', 'SmartModelBase',
'DecimalField', 'FileField', 'ImageField', 'PrevValuePositiveIntegerField', 'SubchoicesPositiveIntegerField',
'EnumSequencePositiveIntegerField', 'EnumSequenceCharField', 'PriceField', 'PositivePriceField'
)
30 changes: 14 additions & 16 deletions chamber/models/base.py
Expand Up @@ -2,12 +2,13 @@
from django.db.models.manager import BaseManager
from django.db.models.base import ModelBase
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property

from chamber.exceptions import PersistenceException
from chamber.patch import Options
from chamber.shortcuts import change_and_save, change, bulk_change_and_save
from chamber.config import settings

from .changed_fields import DynamicChangedFields
from .signals import dispatcher_post_save, dispatcher_pre_save
Expand Down Expand Up @@ -67,14 +68,13 @@ class SmartModelBase(ModelBase):
"""

def __new__(cls, name, bases, attrs):

new_cls = super().__new__(cls, name, bases, attrs)
for dispatcher in new_cls.dispatchers:
dispatcher.connect(new_cls)
return new_cls


class AuditModel(models.Model):
class AuditModelMixin:

created_at = models.DateTimeField(
verbose_name=_('created at'),
Expand All @@ -91,11 +91,8 @@ class AuditModel(models.Model):
db_index=True
)

class Meta:
abstract = True


class SmartModel(AuditModel, metaclass=SmartModelBase):
class SmartModel(models.Model, metaclass=SmartModelBase):

objects = SmartManager()

Expand Down Expand Up @@ -234,7 +231,9 @@ def _save(self, update_only_changed_fields=False, is_cleaned_pre_save=None, is_c
post_save_changed_fields = self.changed_fields.get_static_changes()

if not update_fields and update_only_changed_fields:
update_fields = list(post_save_changed_fields.keys()) + ['changed_at']
update_fields = list(post_save_changed_fields.keys()) + [
field.name for field in self._meta.fields if getattr(field, 'auto_now', False)
]
# remove primary key from updating fields
if self._meta.pk.name in update_fields:
update_fields.remove(self._meta.pk.name)
Expand Down Expand Up @@ -360,11 +359,10 @@ class SmartOptions(Options):
meta_class_name = 'SmartMeta'
meta_name = '_smart_meta'
model_class = SmartModel
attributes = {
'is_cleaned_pre_save': True,
'is_cleaned_post_save': False,
'is_cleaned_pre_delete': False,
'is_cleaned_post_delete': False,
'is_save_atomic': False,
'is_delete_atomic': False,
}
attributes = settings.SMART_MODEL_ATTRIBUTES


class SmartAuditModel(AuditModelMixin, SmartModel):

class Meta:
abstract = True
13 changes: 6 additions & 7 deletions chamber/models/fields.py
Expand Up @@ -7,8 +7,7 @@
from django.db import models
from django.db.models import FileField as OriginFileField
from django.db.models.fields import DecimalField as OriginDecimalField
from django.utils.encoding import force_text
from django.utils.translation import ugettext
from django.utils.translation import gettext

from chamber.config import settings
from chamber.forms import fields as chamber_fields
Expand Down Expand Up @@ -71,7 +70,7 @@ def generate_filename(self, instance, filename):
"""
from unidecode import unidecode

return super().generate_filename(instance, unidecode(force_text(filename)))
return super().generate_filename(instance, unidecode(str(filename)))


class FileField(RestrictedFileFieldMixin, OriginFileField):
Expand Down Expand Up @@ -133,12 +132,12 @@ def clean(self, value, model_instance):

def _raise_error_if_value_should_be_empty(self, value, subvalue):
if self.enum and subvalue not in self.enum.categories and value is not None:
raise ValidationError(ugettext('Value must be empty'))
raise ValidationError(gettext('Value must be empty'))

def _raise_error_if_value_not_allowed(self, value, subvalue, model_instance):
allowed_values = self.enum.get_allowed_states(getattr(model_instance, self.supchoices_field_name))
if subvalue in self.enum.categories and value not in allowed_values:
raise ValidationError(ugettext('Allowed choices are {}.').format(
raise ValidationError(gettext('Allowed choices are {}.').format(
', '.join(('{} ({})'.format(*(self.enum.get_label(val), val)) for val in allowed_values))
))

Expand Down Expand Up @@ -169,7 +168,7 @@ def validate(self, value, model_instance):
if ((self.name in model_instance.changed_fields or model_instance.is_adding) and
value not in allowed_next_values):
raise ValidationError(
ugettext('Allowed choices are {}.').format(
gettext('Allowed choices are {}.').format(
', '.join(('{} ({})'.format(*(self.enum.get_label(val), val)) for val in allowed_next_values))))


Expand All @@ -184,7 +183,7 @@ class EnumSequenceCharField(EnumSequenceFieldMixin, models.CharField):
class PriceField(DecimalField):

def __init__(self, *args, **kwargs):
self.currency = kwargs.pop('currency', ugettext('CZK'))
self.currency = kwargs.pop('currency', gettext('CZK'))
super().__init__(*args, **{
'decimal_places': 2,
'max_digits': 10,
Expand Down
6 changes: 3 additions & 3 deletions chamber/models/humanized_helpers/__init__.py
@@ -1,4 +1,4 @@
from django.utils.translation import ugettext
from django.utils.translation import gettext

from chamber.formatters import natural_number_with_currency

Expand All @@ -7,5 +7,5 @@ def price_humanized(value, inst, currency=None):
"""
Return a humanized price
"""
return (natural_number_with_currency(value, ugettext('CZK') if currency is None else currency) if value is not None
else ugettext('(None)'))
return (natural_number_with_currency(value, gettext('CZK') if currency is None else currency) if value is not None
else gettext('(None)'))
3 changes: 1 addition & 2 deletions chamber/utils/http.py
@@ -1,14 +1,13 @@
from collections import OrderedDict

from django.http.request import QueryDict
from django.utils.encoding import force_text


def query_string_from_dict(qs_dict):
qs_prepared_dict = OrderedDict()
for key, val in qs_dict.items():
if isinstance(val, list):
val = '[%s]' % ','.join([force_text(v) for v in val])
val = '[%s]' % ','.join([str(v) for v in val])
qs_prepared_dict[key] = val

qdict = QueryDict('').copy()
Expand Down
38 changes: 29 additions & 9 deletions docs/models.rst
Expand Up @@ -76,14 +76,6 @@ SmartModel

.. class:: chamber.models.SmartModel

.. attribute:: created_at

Because our experience has shown us that datetime of creation is very useful this field ``django.models.DateTimeField`` with ``auto_add_no`` set to ``True`` is added to every model that inherits from ``SmartModel``

.. attribute:: changed_at

This model field is same case as ``created_at`` with the difference that there is used ``auto_now=True`` therefore every date and time of change is stored here.

.. attribute:: dispatchers

List of defined pre or post save dispatchers. More obout it will find _dispatchers
Expand Down Expand Up @@ -165,6 +157,21 @@ SmartModel
The method returns the new instance of the self object which is locked in the database with ``select_for_update``. Method must be used in the django atomic block.


SmartAuditModel
----------

SmartModel with two extra fields `created_at` and `changed_at` to log when the model was created or updated.

.. class:: chamber.models.SmartAuditModel

.. attribute:: created_at

Because our experience has shown us that datetime of creation is very useful this field ``django.models.DateTimeField`` with ``auto_add_no`` set to ``True`` is added to every model that inherits from ``SmartModel``

.. attribute:: changed_at

This model field is same case as ``created_at`` with the difference that there is used ``auto_now=True`` therefore every date and time of change is stored here.

SmartMeta
---------

Expand All @@ -174,7 +181,7 @@ SmartMeta similar like django meta is defined inside ``SmartModel`` and is acces

.. attribute:: is_cleaned_pre_save

Defines if ``SmartModel`` will be automatically validated before saving. Default value is ``True``
Defines if ``SmartModel`` will be automatically validated before saving. Default value is ``False``

.. attribute:: is_cleaned_post_save

Expand Down Expand Up @@ -204,6 +211,19 @@ SmartMeta similar like django meta is defined inside ``SmartModel`` and is acces
is_cleaned_pre_save = True
is_cleaned_pre_delete = True
The default configuration for all smart models can be defined in your settings with setting `CHAMBER_SMART_MODEL_ATTRIBUTES`:

.. code:: python
CHAMBER_SMART_MODEL_ATTRIBUTES = {
'is_cleaned_pre_save': True,
'is_cleaned_post_save': False,
'is_cleaned_pre_delete': False,
'is_cleaned_post_delete': False,
'is_save_atomic': False,
'is_delete_atomic': False,
}
Unknown
-------
Expand Down
2 changes: 1 addition & 1 deletion example/dj/apps/test_chamber/models.py
@@ -1,6 +1,6 @@
from django.contrib.auth.models import AbstractBaseUser
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _

from chamber import models as chamber_models
from chamber.models import fields as chamber_fields
Expand Down
13 changes: 6 additions & 7 deletions example/dj/apps/test_chamber/tests/models/__init__.py
Expand Up @@ -74,7 +74,7 @@ def test_smart_model_initial_values_should_be_unknown_for_not_saved_instance(sel
assert_true(obj.has_changed)
assert_true(obj.changed_fields)
assert_equal(
set(obj.changed_fields.keys()), {'created_at', 'changed_at', 'id', 'datetime', 'name', 'number', 'data'}
set(obj.changed_fields.keys()), {'id', 'datetime', 'name', 'number', 'data'}
)
assert_true(obj.is_adding)
assert_false(obj.is_changing)
Expand Down Expand Up @@ -123,7 +123,7 @@ def test_smart_model_initial_values_should_be_deferred_for_partly_loaded_instanc
def test_smart_model_changed_fields(self):
obj = TestProxySmartModel.objects.create(name='a')
changed_fields = DynamicChangedFields(obj)
assert_equal(len(changed_fields), 4)
assert_equal(len(changed_fields), 2)
changed_fields.from_db()
assert_equal(len(changed_fields), 0)
obj.name = 'b'
Expand All @@ -133,23 +133,22 @@ def test_smart_model_changed_fields(self):
assert_equal(changed_fields.changed_values, {'name': 'b'})
assert_equal(str(changed_fields), "{'name': ValueChange(initial='a', current='b')}")
assert_true(changed_fields.has_key('name'))
assert_false(changed_fields.has_key('changed_at'))
assert_equal(list(changed_fields.values()), [changed_fields['name']])
assert_equal(changed_fields.keys(), {'name'})

static_changed_fields = changed_fields.get_static_changes()
obj.save()

# Initial values is not changed
assert_equal(len(changed_fields), 2)
assert_equal(len(changed_fields), 1)
assert_equal(len(static_changed_fields), 1)
assert_equal(set(changed_fields.keys()), {'name', 'changed_at'})
assert_equal(set(changed_fields.keys()), {'name'})
assert_equal(set(static_changed_fields.keys()), {'name'})
assert_equal(changed_fields['name'].initial, 'a')
assert_equal(changed_fields['name'].current, 'b')

assert_true(changed_fields.has_any_key('name', 'crated_at'))
assert_false(changed_fields.has_any_key('invalid', 'crated_at'))
assert_true(changed_fields.has_any_key('name'))
assert_false(changed_fields.has_any_key('invalid'))

assert_raises(AttributeError, changed_fields.__delitem__, 'name')
assert_raises(AttributeError, changed_fields.clear)
Expand Down

0 comments on commit cbe15ae

Please sign in to comment.