├── .circleci └── config.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── django_stuff ├── __init__.py ├── backends │ ├── __init__.py │ └── auth.py ├── fields.py ├── forms.py ├── logging_tools.py ├── models │ ├── __init__.py │ ├── exceptions.py │ ├── generic.py │ ├── history.py │ ├── managers.py │ └── signals.py ├── utils │ ├── __init__.py │ ├── generators.py │ ├── generic.py │ └── string.py ├── validators.py └── version.py ├── docs ├── Makefile ├── make.bat └── source │ ├── backends │ ├── auth.rst │ └── index.rst │ ├── changelog.rst │ ├── conf.py │ ├── development │ ├── installation.rst │ └── release.rst │ ├── fields.rst │ ├── forms.rst │ ├── index.rst │ ├── models │ ├── generic.rst │ ├── history.rst │ ├── index.rst │ └── signals.rst │ ├── quickstart │ └── installation.rst │ └── utils.rst ├── pytest.ini ├── requirements-ci.txt ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── test-django-project ├── django_stuff ├── manage.py ├── pytest.ini ├── test_django_project ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── testapp ├── __init__.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── view.html ├── urls.py └── views.py └── tests ├── __init__.py ├── conftest.py ├── test_fields.py ├── test_forms.py ├── test_models.py ├── test_utils.py └── vcr.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/python:3.6.2 6 | 7 | working_directory: ~/repo 8 | 9 | steps: 10 | - checkout 11 | 12 | # Download and cache dependencies 13 | - restore_cache: 14 | keys: 15 | - v1-dependencies-{{ .Branch }}-{{ checksum "requirements-ci.txt" }} 16 | 17 | - run: 18 | name: install dependencies 19 | command: | 20 | python3 -m venv venv 21 | . venv/bin/activate 22 | pip install -r requirements-ci.txt 23 | 24 | - save_cache: 25 | paths: 26 | - ./venv 27 | key: v1-dependencies-{{ .Branch }}-{{ checksum "requirements-ci.txt" }} 28 | 29 | - run: 30 | name: check pep8 31 | command: 32 | . venv/bin/activate 33 | flake8 -v --ignore=E501 django_stuff test-django-project 34 | 35 | - run: 36 | name: run tests 37 | command: | 38 | . venv/bin/activate 39 | cd ~/repo/test-django-project && py.test -vvv -s --cov django_stuff --cov-report=term-missing --cov-report=html 40 | codecov 41 | 42 | - store_artifacts: 43 | path: test-django-project/htmlcov 44 | destination: test-reports 45 | 46 | branches: 47 | only: 48 | - master 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | *.swp 92 | .idea 93 | db.sqlite3 94 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: git@github.com:pre-commit/pre-commit-hooks 3 | rev: v2.1.0 4 | hooks: 5 | - id: debug-statements 6 | - id: trailing-whitespace 7 | - id: check-merge-conflict 8 | - id: check-executables-have-shebangs 9 | - id: check-ast 10 | - id: check-byte-order-marker 11 | - id: check-json 12 | - id: check-symlinks 13 | - id: check-vcs-permalinks 14 | - id: check-xml 15 | - id: check-yaml 16 | - id: detect-private-key 17 | - id: forbid-new-submodules 18 | - id: flake8 19 | args: ['--exclude=docs/*,*migrations*', '--ignore=E501'] 20 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 0.7.2 4 | ~~~~~ 5 | 6 | - Update delete with force hard_delete 7 | 8 | 0.7.1 9 | ~~~~~ 10 | 11 | - Rename SoftDeleteManager to SoftDeleteSignalsManager 12 | 13 | 0.7.0 14 | ~~~~~ 15 | 16 | - Add restore method to Model and Manager to restore a record 17 | 18 | 0.6.2 19 | ~~~~~ 20 | 21 | - Add Missing Argument to delete on softdelete manager 22 | 23 | 0.6.1 24 | ~~~~~ 25 | 26 | - Fix tests with Signal using create method 27 | 28 | 0.6.0 29 | ~~~~~ 30 | 31 | - Add SoftDeleteSignalModel and Manager to apply Soft delete 32 | 33 | 0.5.1 34 | ~~~~~ 35 | 36 | - Change ugettext_lazy to gettext_lazy cause django 3.0 support 37 | 38 | 0.5.0 39 | ~~~~~ 40 | 41 | - Add sequential values on CPF to set as invalid on CPF validation 42 | 43 | 0.4.3 44 | ~~~~~ 45 | 46 | - Add CPF and CNPJ validation when the values wore equals 47 | 48 | 0.4.2 49 | ~~~~~ 50 | 51 | - Remove unnecessary code 52 | 53 | 0.4.1 54 | ~~~~~ 55 | 56 | - Fix tests and remove Manager. 57 | 58 | 0.4.0 59 | ~~~~~ 60 | 61 | - Add trigger options on save 62 | 63 | 0.3.0 64 | ~~~~~ 65 | 66 | - Refactor Utils 67 | - Add CPF and CNPJ generators 68 | 69 | 0.2.1 70 | ~~~~~ 71 | 72 | * Remove Swagger render. 73 | 74 | 0.2.0 75 | ~~~~~ 76 | 77 | * Add swagger render 78 | 79 | 80 | 0.1.12 81 | ~~~~~~ 82 | 83 | * Add remove special characters function 84 | 85 | 0.1.12 86 | ~~~~~~ 87 | 88 | * Add random datetime generator 89 | 90 | 0.1.11 91 | ~~~~~~ 92 | 93 | * BugFix: Change how to get a new version from changes 94 | 95 | 96 | 0.1.10 97 | ~~~~~~ 98 | 99 | * BugFix: Get version from changes 100 | 101 | 0.1.9 102 | ~~~~~ 103 | 104 | * Finish all base documentation 105 | 106 | 0.1.8 107 | ~~~~~ 108 | 109 | * Update Readme 110 | * Increase test coverage 111 | 112 | 0.1.7 113 | ~~~~~ 114 | 115 | * Updates functions names on utlils to be more clear 116 | * Documentation: Add forms, fields and utils 117 | 118 | 0.1.6 119 | ~~~~~ 120 | 121 | * BugFix: Models SignalsModel and HistoryModel 122 | * Fix tests 123 | * Increase tests coverage 124 | 125 | 0.1.5 126 | ~~~~~ 127 | 128 | * BugFix: Models SignalsModel and HistoryModel 129 | * Fix tests 130 | * Increase tests coverage 131 | 132 | 0.1.5 133 | ~~~~~ 134 | 135 | * Add a centered version command 136 | * Add Sphinx docs base 137 | * Update dev requirements with Sphinx 138 | 139 | 0.1.4 140 | ~~~~~ 141 | 142 | * Update documentation 143 | 144 | 0.1.3 145 | ~~~~~ 146 | 147 | * Add noqa imports to models 148 | 149 | 0.1.2 150 | ~~~~~ 151 | 152 | * Add license on setup.py 153 | 154 | 0.1.1 155 | ~~~~~ 156 | 157 | * Refactor Structure 158 | * Fix CI 159 | * Add new requirements 160 | 161 | 0.1.0 162 | ~~~~~ 163 | 164 | * Initial release 165 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2017 @rhenter (django-stuff) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include *.txt 3 | recursive-include docs *.rst *.py *.png *.jpg *.dot Makefile 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: clean-eggs clean-build clean-htmlcov 2 | @find . -iname '*.pyc' -delete 3 | @find . -iname '*.pyo' -delete 4 | @find . -iname '*~' -delete 5 | @find . -iname '*.swp' -delete 6 | @find . -iname '__pycache__' -delete 7 | 8 | clean-htmlcov: 9 | @rm -fr htmlcov 10 | 11 | clean-eggs: 12 | @find . -name '*.egg' -print0|xargs -0 rm -rf -- 13 | @rm -rf .eggs/ 14 | 15 | clean-build: 16 | @rm -fr build/ 17 | @rm -fr dist/ 18 | @rm -fr *.egg-info 19 | 20 | lint: 21 | pre-commit run -av 22 | 23 | pip-dev: 24 | pip install -r requirements-dev.txt 25 | 26 | test: deps 27 | cd test-django-project && py.test -vvv 28 | 29 | build: clean 30 | python setup.py sdist bdist_wheel 31 | 32 | release: build 33 | git tag `python setup.py -q version` 34 | git push origin `python setup.py -q version` 35 | twine upload dist/* 36 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Django Stuff 3 | ============ 4 | 5 | This Lib will be deprecated. Use https://github.com/rhenter/django-models instead 6 | -------------------------------------------------------------------------------- /django_stuff/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | from .version import __version__ # noqa 5 | from . import utils # noqa 6 | 7 | 8 | __all__ = ['__version__', 'utils'] 9 | -------------------------------------------------------------------------------- /django_stuff/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhenter/django-stuff/7e2901ac1efc3db47977b98e45754e40bfef6891/django_stuff/backends/__init__.py -------------------------------------------------------------------------------- /django_stuff/backends/auth.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | 3 | User = get_user_model() 4 | 5 | 6 | class EmailOrUsernameModelBackend(object): 7 | 8 | def authenticate(self, username=None, password=None): 9 | if '@' in username: 10 | kwargs = {'email': username} 11 | else: 12 | kwargs = {'username': username} 13 | try: 14 | user = User.objects.get(**kwargs) 15 | if user.check_password(password): 16 | return user 17 | except User.DoesNotExist: 18 | return None 19 | 20 | def get_user(self, user_id): 21 | try: 22 | return User.objects.get(pk=user_id) 23 | except User.DoesNotExist: 24 | return None 25 | -------------------------------------------------------------------------------- /django_stuff/fields.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.core.validators import RegexValidator 4 | from django.db import models 5 | 6 | 7 | class UUIDPrimaryKeyField(models.UUIDField): 8 | 9 | def __init__(self, *args, **kwargs): 10 | kwargs['primary_key'] = True 11 | kwargs['unique'] = True 12 | kwargs['editable'] = False 13 | super().__init__(*args, **kwargs) 14 | 15 | def pre_save(self, model_instance, add): 16 | value = super().pre_save(model_instance, add) 17 | 18 | if value is None: 19 | value = uuid.uuid4() 20 | setattr(model_instance, self.attname, value) 21 | 22 | return value 23 | 24 | 25 | class CharFieldDigitsOnly(models.CharField): 26 | default_validators = [RegexValidator(r'^([\s\d]+)$', 'Only digits characters')] 27 | -------------------------------------------------------------------------------- /django_stuff/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ValidationError 2 | from django.forms.fields import CharField 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from .validators import validate_cpf, validate_cnpj 6 | from .utils import is_equal 7 | 8 | 9 | class InvalidValuesField: 10 | invalid_values = () 11 | 12 | def clean(self, value): 13 | value = super().clean(value) 14 | if value in self.invalid_values: 15 | raise ValidationError(_("Invalid number.")) 16 | return value 17 | 18 | 19 | class CPFField(InvalidValuesField, CharField): 20 | invalid_values = ('00000000191',) 21 | 22 | def __init__(self, max_length=14, min_length=11, *args, **kwargs): 23 | super().__init__(max_length=max_length, min_length=min_length, *args, **kwargs) 24 | 25 | def clean(self, value): 26 | value = super().clean(value) 27 | code = validate_cpf(value) 28 | if not code or is_equal(code): 29 | raise ValidationError(_("Invalid CPF number.")) 30 | return code 31 | 32 | 33 | class CNPJField(InvalidValuesField, CharField): 34 | invalid_values = ( 35 | '00000000000000', '22222222000191', '33333333000191', '44444444000191', 36 | '55555555000191', '66666666000191', '77777777000191', '88888888000191', 37 | '99999999000191' 38 | ) 39 | 40 | def __init__(self, min_length=14, max_length=18, *args, **kwargs): 41 | super().__init__(max_length=max_length, min_length=min_length, *args, **kwargs) 42 | 43 | def clean(self, value): 44 | value = super().clean(value) 45 | code = validate_cnpj(value) 46 | if not code or is_equal(code): 47 | raise ValidationError(_("Invalid CNPJ number.")) 48 | return code 49 | 50 | -------------------------------------------------------------------------------- /django_stuff/logging_tools.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import warnings 3 | 4 | 5 | warnings.simplefilter('default') 6 | 7 | 8 | def get_loggers(level, loggers): 9 | logging.addLevelName('DISABLED', logging.CRITICAL + 10) 10 | 11 | log_config = { 12 | 'handlers': ['console'], 13 | 'level': level, 14 | } 15 | 16 | if level == 'DISABLED': 17 | loggers = {'': {'handlers': ['null'], 'level': 'DEBUG', 'propagate': False}} 18 | else: 19 | loggers = {logger.strip(): log_config for logger in loggers} 20 | 21 | loggers.update({ 22 | 'parso': { 23 | 'propagate': False, 24 | } 25 | }) 26 | 27 | return loggers 28 | 29 | 30 | def get_logging_config(loggers=None): 31 | return { 32 | 'version': 1, 33 | 'disable_existing_loggers': False, 34 | 'formatters': { 35 | 'default': {'format': '%(asctime)s %(levelname)s %(name)s:%(lineno)s %(message)s'}, 36 | }, 37 | 'handlers': { 38 | 'console': { 39 | 'level': 'DEBUG', 40 | 'class': 'logging.StreamHandler', 41 | 'formatter': 'default', 42 | }, 43 | 'null': { 44 | 'class': 'logging.NullHandler', 45 | }, 46 | }, 47 | 'loggers': loggers or {}, 48 | } 49 | -------------------------------------------------------------------------------- /django_stuff/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .generic import SlugModel, TimestampedModel, UUIDModel, SerializerModel # noqa 2 | from .signals import SignalsModel # noqa 3 | from .history import HistoryModel # noqa 4 | 5 | __all__ = [ 6 | 'SlugModel', 'TimestampedModel', 'UUIDModel', 'SerializerModel', 7 | 'SignalsModel', 'HistoryModel' 8 | ] 9 | -------------------------------------------------------------------------------- /django_stuff/models/exceptions.py: -------------------------------------------------------------------------------- 1 | class HistoryModelNotSetError(BaseException): 2 | pass 3 | -------------------------------------------------------------------------------- /django_stuff/models/generic.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from uuid import UUID 3 | 4 | from django.db import models 5 | from django.utils.functional import cached_property 6 | from django.utils.translation import gettext_lazy as _ 7 | from rest_framework.serializers import ModelSerializer 8 | 9 | from django_stuff.fields import UUIDPrimaryKeyField 10 | from django_stuff.utils.generators import generate_random_code 11 | 12 | generate_code = partial(generate_random_code, length=8) 13 | 14 | 15 | class BaseModel(models.Model): 16 | class Meta: 17 | abstract = True 18 | 19 | 20 | class SlugModel(BaseModel): 21 | slug = models.SlugField(max_length=16) 22 | 23 | class Meta: 24 | abstract = True 25 | 26 | 27 | class TimestampedModel(BaseModel): 28 | created_at = models.DateTimeField( 29 | auto_now_add=True, verbose_name=_('Created at') 30 | ) 31 | updated_at = models.DateTimeField( 32 | auto_now=True, verbose_name=_('Updated at') 33 | ) 34 | 35 | class Meta: 36 | abstract = True 37 | 38 | 39 | class UUIDModel(BaseModel): 40 | id = UUIDPrimaryKeyField() 41 | 42 | class Meta: 43 | abstract = True 44 | 45 | 46 | class SerializerModel(BaseModel): 47 | 48 | @cached_property 49 | def serializer(self): 50 | class SelfSerializer(ModelSerializer): 51 | class Meta: 52 | pass 53 | 54 | SelfSerializer.Meta.model = self 55 | SelfSerializer.Meta.fields = '__all__' 56 | return SelfSerializer 57 | 58 | def serialize(self): 59 | data = self.serializer(self).data 60 | 61 | for key, value in data.items(): 62 | if isinstance(value, UUID): 63 | data[key] = str(value) 64 | return data 65 | 66 | class Meta: 67 | abstract = True 68 | 69 | 70 | class CodeModel(models.Model): 71 | code = models.CharField( 72 | max_length=32, 73 | default=generate_code, 74 | verbose_name=_('Model code'), 75 | unique=True, 76 | ) 77 | 78 | class Meta: 79 | abstract = True 80 | -------------------------------------------------------------------------------- /django_stuff/models/history.py: -------------------------------------------------------------------------------- 1 | from .generic import TimestampedModel 2 | from .signals import SignalsModel 3 | from .exceptions import HistoryModelNotSetError 4 | 5 | 6 | class HistoryModel(SignalsModel, TimestampedModel): 7 | history_model = None 8 | history_parent_field_name = 'parent' 9 | 10 | def __init__(self, *args, **kwargs): 11 | if not self.history_model: 12 | raise HistoryModelNotSetError( 13 | "You should set the history_model attribute of {}".format(type(self).__name__) 14 | ) 15 | 16 | super().__init__(*args, **kwargs) 17 | 18 | def save_history(self): 19 | data = {self.history_parent_field_name: self} 20 | 21 | history_model_fields = tuple(field.name for field in self.history_model._meta.fields) 22 | 23 | for field in self._meta.fields: 24 | if field.name in ('id', 'created_at', 'updated_at'): 25 | continue 26 | 27 | if field.name in history_model_fields: 28 | value = getattr(self, field.name) 29 | data[field.name] = value 30 | 31 | self.history_model.objects.create(**data) 32 | 33 | def post_save_history(self, context): 34 | self.save_history() 35 | -------------------------------------------------------------------------------- /django_stuff/models/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models, transaction 2 | from django.db.models import QuerySet 3 | from django.db.models.deletion import Collector 4 | from django.utils import timezone 5 | 6 | REPR_OUTPUT_SIZE = 20 7 | 8 | 9 | class SignalsManager(models.Manager): 10 | 11 | def create(self, **kwargs): 12 | model_instance = self.initialize_model_instance(**kwargs) 13 | with transaction.atomic(): 14 | model_instance.save() 15 | return model_instance 16 | 17 | def initialize_model_instance(self, **kwargs): 18 | return self.model(**kwargs) 19 | 20 | 21 | class SoftDeleteQuerySet(QuerySet): 22 | def delete(self): 23 | return super().update(deleted_at=timezone.now(), is_deleted=True) 24 | 25 | def hard_delete(self): 26 | """Delete the records in the current QuerySet.""" 27 | self._not_support_combined_queries('delete') 28 | assert not self.query.is_sliced, \ 29 | "Cannot use 'limit' or 'offset' with delete." 30 | 31 | if self._fields is not None: 32 | raise TypeError("Cannot call delete() after .values() or .values_list()") 33 | 34 | del_query = self._chain() 35 | 36 | # The delete is actually 2 queries - one to find related objects, 37 | # and one to delete. Make sure that the discovery of related 38 | # objects is performed on the same database as the deletion. 39 | del_query._for_write = True 40 | 41 | # Disable non-supported fields. 42 | del_query.query.select_for_update = False 43 | del_query.query.select_related = False 44 | del_query.query.clear_ordering(force_empty=True) 45 | 46 | collector = Collector(using=del_query.db) 47 | collector.collect(del_query) 48 | deleted, _rows_count = collector.delete() 49 | 50 | # Clear the result cache, in case this QuerySet gets reused. 51 | self._result_cache = None 52 | return deleted, _rows_count 53 | 54 | def restore(self): 55 | return super().update(deleted_at=None, is_deleted=False) 56 | 57 | def __repr__(self): 58 | data = list(self[:REPR_OUTPUT_SIZE + 1]) 59 | if len(data) > REPR_OUTPUT_SIZE: 60 | data[-1] = "...(remaining elements truncated)..." 61 | return '' % data 62 | 63 | 64 | class SoftDeleteSignalsManager(SignalsManager): 65 | def __init__(self, *args, **kwargs): 66 | self.show_deleted = kwargs.pop('show_deleted', False) 67 | super().__init__(*args, **kwargs) 68 | 69 | def get_queryset(self): 70 | if self.show_deleted: 71 | return SoftDeleteQuerySet(self.model, using=self._db) 72 | return SoftDeleteQuerySet(self.model, using=self._db).exclude(is_deleted=True) 73 | 74 | def delete(self): 75 | return self.get_queryset().delete() 76 | 77 | def hard_delete(self): 78 | return self.get_queryset().hard_delete() 79 | 80 | def restore(self): 81 | return self.get_queryset().restore() 82 | 83 | def filter(self, *args, **kwargs): 84 | if 'is_deleted' in kwargs: 85 | qs = SoftDeleteQuerySet(self.model, using=self._db) 86 | return qs.filter(*args, **kwargs) 87 | return super().filter(*args, **kwargs) 88 | 89 | def trash(self): 90 | return self.filter(is_deleted=True) 91 | -------------------------------------------------------------------------------- /django_stuff/models/signals.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from django.db import models, transaction 4 | from django.utils import timezone 5 | 6 | from .generic import SerializerModel 7 | from .managers import SignalsManager, SoftDeleteSignalsManager 8 | 9 | 10 | class SignalsModel(SerializerModel): 11 | SOFT_DELETE = False 12 | 13 | class Meta: 14 | abstract = True 15 | 16 | objects = SignalsManager() 17 | 18 | def get_context(self, **kwargs): 19 | force_insert = kwargs.get('force_insert', False) 20 | creation_conditions = ( 21 | self.id is None, 22 | force_insert is True 23 | ) 24 | context = {'is_creation': any(creation_conditions)} 25 | context.update(kwargs) 26 | return context 27 | 28 | def trigger_event(self, event_name, context): 29 | for attribute in dir(self): 30 | if attribute.startswith(event_name): 31 | method = getattr(self, attribute) 32 | if inspect.ismethod(method): 33 | method(context) 34 | 35 | def save(self, *args, **kwargs): 36 | force_insert = kwargs.get('force_insert', False) 37 | context = self.get_context(force_insert=force_insert) 38 | 39 | with transaction.atomic(): 40 | self.trigger_event('pre_save', context) 41 | super().save(*args, **kwargs) 42 | self.trigger_event('post_save', context) 43 | 44 | def delete(self, *args, **kwargs): 45 | context = self.get_context() 46 | 47 | if self.SOFT_DELETE and not kwargs.pop('hard_delete', False): 48 | self.deleted_at = timezone.now() 49 | self.is_deleted = True 50 | self.save() 51 | return 52 | 53 | with transaction.atomic(): 54 | self.trigger_event('pre_delete', context) 55 | super().delete(*args, **kwargs) 56 | self.trigger_event('post_delete', context) 57 | 58 | 59 | class SoftDeleteSignalModel(SignalsModel): 60 | SOFT_DELETE = True 61 | deleted_at = models.DateTimeField(blank=True, null=True) 62 | is_deleted = models.BooleanField(default=False) 63 | 64 | objects = SoftDeleteSignalsManager() 65 | all_objects = SoftDeleteSignalsManager(show_deleted=True) 66 | 67 | class Meta: 68 | abstract = True 69 | 70 | def hard_delete(self): 71 | super().delete(hard_delete=True) 72 | 73 | def restore(self): 74 | self.deleted_at = None 75 | self.is_deleted = False 76 | self.save() 77 | -------------------------------------------------------------------------------- /django_stuff/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | from . import generators # noqa 5 | from .generic import find_path, get_version_from_changes # noqa 6 | from .string import remove_special_characters, remove_accents, is_equal, digits_only # noqa 7 | 8 | __all__ = ['remove_special_characters', 'remove_accents', 'is_equal', 'digits_only', 9 | 'find_path', 'get_version_from_changes'] 10 | -------------------------------------------------------------------------------- /django_stuff/utils/generators.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import random 3 | import string 4 | import time 5 | from datetime import datetime, timedelta 6 | 7 | from django.utils.crypto import get_random_string 8 | 9 | 10 | def generate_random_code(length=10): 11 | allowed = string.ascii_uppercase + string.digits 12 | code = get_random_string(length=length, allowed_chars=allowed) 13 | return code 14 | 15 | 16 | def generate_md5_hashcode(key_word): 17 | keyword = '{}-{}'.format(key_word, time.time()) 18 | hashcode = hashlib.md5(keyword.encode('utf-8')).hexdigest() 19 | return hashcode 20 | 21 | 22 | def generate_datetime(min_year=1900, max_year=datetime.now().year): 23 | """Generate a datetime.""" 24 | start = datetime(min_year, 1, 1, 00, 00, 00) 25 | years = max_year - min_year + 1 26 | end = start + timedelta(days=365 * years) 27 | return start + (end - start) * random.random() 28 | 29 | 30 | def generate_cpf(): 31 | cpf = [random.randint(0, 9) for x in range(9)] 32 | 33 | for _ in range(2): 34 | val = sum([(len(cpf) + 1 - i) * v for i, v in enumerate(cpf)]) % 11 35 | 36 | cpf.append(11 - val if val > 1 else 0) 37 | 38 | return '%s%s%s.%s%s%s.%s%s%s-%s%s' % tuple(cpf) 39 | 40 | 41 | def generate_cnpj(): 42 | def calculate_special_digit(l): 43 | digit = 0 44 | 45 | for i, v in enumerate(l): 46 | digit += v * (i % 8 + 2) 47 | 48 | digit = 11 - digit % 11 49 | 50 | return digit if digit < 10 else 0 51 | 52 | cnpj = [1, 0, 0, 0] + [random.randint(0, 9) for x in range(8)] 53 | 54 | for _ in range(2): 55 | cnpj = [calculate_special_digit(cnpj)] + cnpj 56 | 57 | return '%s%s.%s%s%s.%s%s%s/%s%s%s%s-%s%s' % tuple(cnpj[::-1]) 58 | -------------------------------------------------------------------------------- /django_stuff/utils/generic.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import fnmatch 3 | import os 4 | import re 5 | from unipath import Path 6 | 7 | 8 | def find_path(pattern, path, last_folder_only=False, ignored_dirs=''): 9 | ignored_dirs = ignored_dirs.split(',') 10 | result = [] 11 | 12 | for root, dirs, files in os.walk(path): 13 | dirs[:] = [d for d in dirs if d not in ignored_dirs] 14 | 15 | for name in files: 16 | if fnmatch.fnmatch(name, pattern): 17 | result.append(os.path.join(root, name)) 18 | 19 | if not result: 20 | return '' 21 | 22 | final_path = Path(result[0]).ancestor(1) 23 | relative_path = final_path.replace(str(path), '') 24 | 25 | if last_folder_only: 26 | return relative_path.split('/')[-1] 27 | return relative_path[1:] if relative_path.startswith('/') else relative_path 28 | 29 | 30 | def get_version_from_changes(project_root=''): 31 | default_version = '0.0.0' 32 | current_version = '' 33 | changes = os.path.join(project_root, "CHANGES.rst") 34 | pattern = r'^(?P[0-9]+.[0-9]+(.[0-9]+)?)' 35 | if not os.path.exists(changes): 36 | return default_version 37 | 38 | with codecs.open(changes, encoding='utf-8') as changes: 39 | for line in changes: 40 | match = re.match(pattern, line) 41 | if match: 42 | current_version = match.group("version") 43 | break 44 | return current_version or default_version 45 | -------------------------------------------------------------------------------- /django_stuff/utils/string.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unicodedata 3 | 4 | 5 | def digits_only(raw_number): 6 | return re.sub(r"[^0-9]", "", raw_number) 7 | 8 | 9 | def is_equal(words): 10 | return all(c == words[i - 1] for i, c in enumerate(words) if i > 0) 11 | 12 | 13 | def remove_special_characters(text): 14 | cleaned_text = re.sub(r'([^\s\w]|_)+', '', text.strip()) 15 | return remove_accents(cleaned_text) 16 | 17 | 18 | def remove_accents(text): 19 | return ''.join(c for c in unicodedata.normalize('NFD', text) 20 | if unicodedata.category(c) != 'Mn') 21 | -------------------------------------------------------------------------------- /django_stuff/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.validators import EMPTY_VALUES 2 | 3 | from .utils import is_equal 4 | from .utils.string import digits_only 5 | 6 | SEQ_VALUES = [ 7 | '00000000000', 8 | '11111111111', 9 | '22222222222', 10 | '33333333333', 11 | '44444444444', 12 | '55555555555', 13 | '66666666666', 14 | '77777777777', 15 | '88888888888', 16 | '99999999999' 17 | ] 18 | 19 | 20 | def dv_maker(v): 21 | if v >= 2: 22 | return 11 - v 23 | return 0 24 | 25 | 26 | def validate_cpf(value): 27 | """ 28 | Value can be either a string in the format XXX.XXX.XXX-XX or 29 | an 11-digit number. 30 | """ 31 | if value in EMPTY_VALUES or value in SEQ_VALUES: 32 | return False 33 | orig_value = value[:] 34 | if not value.isdigit(): 35 | value = digits_only(value) 36 | if not value: 37 | return False 38 | 39 | if is_equal(value): 40 | return False 41 | 42 | if len(value) != 11: 43 | return False 44 | orig_dv = value[-2:] 45 | 46 | r1 = range(10, 1, -1) 47 | new_1dv = sum([i * int(value[idx]) for idx, i in enumerate(r1)]) 48 | new_1dv = dv_maker(new_1dv % 11) 49 | value = value[:-2] + str(new_1dv) + value[-1] 50 | r2 = range(11, 1, -1) 51 | new_2dv = sum([i * int(value[idx]) for idx, i in enumerate(r2)]) 52 | new_2dv = dv_maker(new_2dv % 11) 53 | value = value[:-1] + str(new_2dv) 54 | if value[-2:] != orig_dv: 55 | return False 56 | if value.count(value[0]) == 11: 57 | return False 58 | return orig_value 59 | 60 | 61 | def validate_cnpj(value): 62 | """ 63 | Value can be either a string in the format XX.XXX.XXX/XXXX-XX or 64 | a group of 14 characters. 65 | """ 66 | if value in EMPTY_VALUES: 67 | return '' 68 | orig_value = value[:] 69 | if not value.isdigit(): 70 | value = digits_only(value) 71 | if not value: 72 | return False 73 | 74 | if is_equal(value): 75 | return False 76 | 77 | if len(value) != 14: 78 | return False 79 | orig_dv = value[-2:] 80 | 81 | list1 = list(range(5, 1, -1)) 82 | list2 = list(range(9, 1, -1)) 83 | new_1dv = sum([i * int(value[idx]) for idx, i in enumerate(list1 + list2)]) 84 | new_1dv = dv_maker(new_1dv % 11) 85 | value = value[:-2] + str(new_1dv) + value[-1] 86 | list3 = list(range(6, 1, -1)) 87 | new_2dv = sum([i * int(value[idx]) for idx, i in enumerate(list3 + list2)]) 88 | new_2dv = dv_maker(new_2dv % 11) 89 | value = value[:-1] + str(new_2dv) 90 | if value[-2:] != orig_dv: 91 | return False 92 | return orig_value 93 | -------------------------------------------------------------------------------- /django_stuff/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.7.2" 2 | 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = DjangoStuff 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=DjangoStuff 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/backends/auth.rst: -------------------------------------------------------------------------------- 1 | EmailOrUsernameModelBackend 2 | --------------------------- 3 | 4 | Backend to login using email or username. 5 | 6 | Usage: 7 | 8 | Add before the default ModelBackend 9 | 10 | .. code-block:: python 11 | 12 | AUTHENTICATION_BACKENDS = ( 13 | 'django_stuff.backends.auth.EmailOrUsernameModelBackend', 14 | 'django.contrib.auth.backends.ModelBackend' 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /docs/source/backends/index.rst: -------------------------------------------------------------------------------- 1 | Backends 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | auth.rst 8 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Django Stuff documentation build configuration file, created by 5 | # sphinx-quickstart on Wed Nov 29 06:50:23 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')) 23 | sys.path.insert(0, PROJECT_ROOT) 24 | 25 | from django_stuff import __version__ 26 | 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | # 32 | # needs_sphinx = '1.0' 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = ['sphinx.ext.autodoc', 38 | 'sphinx.ext.doctest', 39 | 'sphinx.ext.todo', 40 | 'sphinx.ext.coverage', 41 | 'sphinx.ext.viewcode', 42 | 'sphinx.ext.githubpages'] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = '.rst' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = 'Django Stuff' 58 | copyright = '2019, Rafael Henter' 59 | author = 'Rafael Henter' 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = __version__ 67 | # The full version, including alpha/beta/rc tags. 68 | release = __version__ 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = None 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | # This patterns also effect to html_static_path and html_extra_path 80 | exclude_patterns = [] 81 | 82 | # The name of the Pygments (syntax highlighting) style to use. 83 | pygments_style = 'sphinx' 84 | 85 | # If true, `todo` and `todoList` produce output, else they produce nothing. 86 | todo_include_todos = True 87 | 88 | 89 | # -- Options for HTML output ---------------------------------------------- 90 | 91 | # The theme to use for HTML and HTML Help pages. See the documentation for 92 | # a list of builtin themes. 93 | # 94 | # html_theme = 'alabaster' 95 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 96 | if not on_rtd: 97 | import sphinx_rtd_theme 98 | html_theme = 'sphinx_rtd_theme' 99 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 100 | 101 | # Theme options are theme-specific and customize the look and feel of a theme 102 | # further. For a list of options available for each theme, see the 103 | # documentation. 104 | # 105 | # html_theme_options = {} 106 | 107 | # Add any paths that contain custom static files (such as style sheets) here, 108 | # relative to this directory. They are copied after the builtin static files, 109 | # so a file named "default.css" will overwrite the builtin "default.css". 110 | html_static_path = ['_static'] 111 | 112 | # Custom sidebar templates, must be a dictionary that maps document names 113 | # to template names. 114 | # 115 | # This is required for the alabaster theme 116 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 117 | html_sidebars = { 118 | '**': [ 119 | 'relations.html', # needs 'show_related': True theme option to display 120 | 'searchbox.html', 121 | ] 122 | } 123 | 124 | 125 | # -- Options for HTMLHelp output ------------------------------------------ 126 | 127 | # Output file base name for HTML help builder. 128 | htmlhelp_basename = 'DjangoStuffdoc' 129 | 130 | 131 | # -- Options for LaTeX output --------------------------------------------- 132 | 133 | latex_elements = { 134 | # The paper size ('letterpaper' or 'a4paper'). 135 | # 136 | # 'papersize': 'letterpaper', 137 | 138 | # The font size ('10pt', '11pt' or '12pt'). 139 | # 140 | # 'pointsize': '10pt', 141 | 142 | # Additional stuff for the LaTeX preamble. 143 | # 144 | # 'preamble': '', 145 | 146 | # Latex figure (float) alignment 147 | # 148 | # 'figure_align': 'htbp', 149 | } 150 | 151 | # Grouping the document tree into LaTeX files. List of tuples 152 | # (source start file, target name, title, 153 | # author, documentclass [howto, manual, or own class]). 154 | latex_documents = [ 155 | (master_doc, 'DjangoStuff.tex', 'Django Stuff Documentation', 156 | 'Rafael Henter', 'manual'), 157 | ] 158 | 159 | 160 | # -- Options for manual page output --------------------------------------- 161 | 162 | # One entry per manual page. List of tuples 163 | # (source start file, name, description, authors, manual section). 164 | man_pages = [ 165 | (master_doc, 'DjangoStuff', 'Django Stuff Documentation', 166 | [author], 1) 167 | ] 168 | 169 | 170 | # -- Options for Texinfo output ------------------------------------------- 171 | 172 | # Grouping the document tree into Texinfo files. List of tuples 173 | # (source start file, target name, title, author, 174 | # dir menu entry, description, category) 175 | texinfo_documents = [ 176 | (master_doc, 'DjangoStuff', 'Django Stuff Documentation', 177 | author, 'DjangoStuff', 'One line description of project.', 178 | 'Miscellaneous'), 179 | ] 180 | 181 | 182 | 183 | # -- Options for Epub output ---------------------------------------------- 184 | 185 | # Bibliographic Dublin Core info. 186 | epub_title = project 187 | epub_author = author 188 | epub_publisher = author 189 | epub_copyright = copyright 190 | 191 | # The unique identifier of the text. This can be a ISBN number 192 | # or the project homepage. 193 | # 194 | # epub_identifier = '' 195 | 196 | # A unique identification for the text. 197 | # 198 | # epub_uid = '' 199 | 200 | # A list of files that should not be packed into the epub file. 201 | epub_exclude_files = ['search.html'] 202 | 203 | 204 | -------------------------------------------------------------------------------- /docs/source/development/installation.rst: -------------------------------------------------------------------------------- 1 | Development Installation 2 | ======================== 3 | 4 | Requirements 5 | ------------ 6 | 7 | - Python 3.x 8 | - Django 1.11 or later 9 | 10 | 11 | Development install 12 | ------------------- 13 | 14 | After forking or checking out: 15 | 16 | .. code-block:: bash 17 | 18 | $ cd django-stuff/ 19 | $ pip install -r requirements-dev.txt 20 | $ pre-commit install 21 | 22 | The requirements-dev are only used for development, so we can easily 23 | install/track dependencies required to run the tests using continuous 24 | integration platforms. 25 | 26 | The official entrypoint for distritubution is the ``requirements.txt`` which 27 | contains the minimum requirements to execute the tests. 28 | 29 | 30 | Running tests 31 | ------------- 32 | 33 | .. code-block:: bash 34 | 35 | $ make test 36 | 37 | or: 38 | 39 | .. code-block:: bash 40 | 41 | $ cd test-django-project/ 42 | $ py.test -vv -s 43 | 44 | Generating documentation 45 | ------------------------ 46 | 47 | .. code-block:: bash 48 | 49 | $ cd docs/ 50 | $ make html 51 | -------------------------------------------------------------------------------- /docs/source/development/release.rst: -------------------------------------------------------------------------------- 1 | Release 2 | ------- 3 | 4 | To release a new version, a few steps are required: 5 | 6 | * Add entry to ``CHANGES.rst`` and documentation 7 | 8 | * Review changes in test requirements ``requirements.txt`` 9 | 10 | * Test build with ``make build`` 11 | 12 | * Commit and push changes 13 | 14 | * Release with ``make release`` 15 | -------------------------------------------------------------------------------- /docs/source/fields.rst: -------------------------------------------------------------------------------- 1 | Fields 2 | ====== 3 | 4 | 5 | UUIDPrimaryKeyField 6 | ------------------- 7 | 8 | Field to use UUID as Primary Key of your model instead of the sequential 9 | 10 | usage: 11 | 12 | .. code-block:: python 13 | 14 | from django_stuff.fields import UUIDPrimaryKeyField 15 | 16 | ... 17 | class TestModel(models.Model): 18 | id = UUIDPrimaryKeyField() 19 | ... 20 | 21 | 22 | CharFieldDigitsOnly 23 | ------------------- 24 | 25 | Field to use only digits but with zero on left. 26 | 27 | usage: 28 | 29 | .. code-block:: python 30 | 31 | from django_stuff.fields import CharFieldDigitsOnly 32 | 33 | ... 34 | class TestModel(models.Model): 35 | code = CharFieldDigitsOnly(max_length=10) 36 | ... 37 | -------------------------------------------------------------------------------- /docs/source/forms.rst: -------------------------------------------------------------------------------- 1 | Forms 2 | ===== 3 | 4 | Like django-localflavors the CPFField and CNPJField are validators to Brazilian CPF and CNPJ with a big difference, It removes repeated sequences and undue values. 5 | 6 | Usage: 7 | 8 | Your Just need to add your form class. Example using ModelForm: 9 | 10 | 11 | CPFField 12 | -------- 13 | 14 | .. code-block:: python 15 | 16 | from django_stuff.forms import CPFField 17 | 18 | ... 19 | 20 | class TestForm(forms.ModelForm): 21 | class Meta: 22 | model = YourModel 23 | 24 | fields = [..., 'cpf'] 25 | widgets = { 26 | 'cpf': CNPJField(), 27 | } 28 | 29 | 30 | 31 | 32 | CNPJField 33 | --------- 34 | 35 | .. code-block:: python 36 | 37 | from django_stuff.forms import CNPJField 38 | 39 | ... 40 | 41 | class TestForm(forms.ModelForm): 42 | class Meta: 43 | model = YourModel 44 | 45 | fields = [..., 'cnpj'] 46 | widgets = { 47 | 'cnpj': CNPJField(), 48 | } 49 | 50 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Django Stuff's documentation! 2 | ============================================ 3 | 4 | Django Stuff is a collection of tools and utilities to make your development with Django simpler. 5 | 6 | Features 7 | -------- 8 | 9 | - TimeStamp and History models to giving you information like when your record wore created/updated and History Changes 10 | - UUID Model as primary key or not instead of sequence ID. 11 | - Serializer model to return a dict with all data of your django instance. 12 | - Signals model to add any task before or after you save, update or delete your model 13 | - And many other stuff. For more information, see our documentation following the links below. 14 | - Backend to Login using email or username 15 | 16 | Quickstart 17 | ---------- 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | quickstart/installation.rst 23 | 24 | 25 | User Guide 26 | ---------- 27 | 28 | .. toctree:: 29 | :maxdepth: 2 30 | 31 | backends/index.rst 32 | fields.rst 33 | forms.rst 34 | models/index.rst 35 | utils.rst 36 | 37 | 38 | Development 39 | ----------- 40 | 41 | .. toctree:: 42 | :maxdepth: 1 43 | 44 | development/installation.rst 45 | development/release.rst 46 | 47 | 48 | Downloads 49 | --------- 50 | 51 | - `PDF `_ 52 | 53 | - `Epub `_ 54 | 55 | 56 | Other 57 | ----- 58 | 59 | .. toctree:: 60 | 61 | changelog.rst 62 | -------------------------------------------------------------------------------- /docs/source/models/generic.rst: -------------------------------------------------------------------------------- 1 | Generic Models 2 | ============== 3 | 4 | You just need to add to your template to get the behaviors below. Use as many models as you want. 5 | 6 | 7 | SlugModel 8 | --------- 9 | 10 | Model with a slugField already implemented 11 | 12 | Usage: 13 | 14 | .. code-block:: python 15 | 16 | class YourModel(SlugModel) 17 | ... 18 | 19 | 20 | SerializerModel 21 | --------------- 22 | 23 | Model with serialize method making possible serializer your instance data returning a dict. 24 | 25 | Usage: 26 | 27 | .. code-block:: python 28 | 29 | from django_stuff.models import SerializerModel 30 | ... 31 | 32 | class YourModel(SerializerModel) 33 | ... 34 | 35 | Example of a instance from a Model using the SerializerModel 36 | 37 | .. code-block:: python 38 | 39 | instance.serialize() 40 | { 41 | 'id': 1, 42 | 'name': 'Test' 43 | } 44 | 45 | 46 | TimestampedModel 47 | ---------------- 48 | 49 | Model with created_at and updated_at fields to let you know when your instance wore created and updated 50 | 51 | Usage: 52 | 53 | .. code-block:: python 54 | 55 | from django_stuff.models import TimestampedModel 56 | ... 57 | 58 | class YourModel(TimestampedModel) 59 | ... 60 | 61 | UUIDModel 62 | --------- 63 | 64 | Model with UUIDPrimaryKeyField already implemented 65 | 66 | 67 | Usage: 68 | 69 | .. code-block:: python 70 | 71 | from django_stuff.models import UUIDModel 72 | ... 73 | 74 | class YourModel(UUIDModel) 75 | ... 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /docs/source/models/history.rst: -------------------------------------------------------------------------------- 1 | History Model 2 | ============= 3 | 4 | Model to save changes to specific fields any time the template is saved 5 | 6 | Required fields 7 | --------------- 8 | 9 | - history_model 10 | 11 | Field with the model where will save the changes 12 | 13 | - history_parent_field_name 14 | 15 | Field used in the foreign key in the template where the changes will be stored. 16 | The default value is **parent** 17 | 18 | 19 | Example 20 | ------- 21 | 22 | .. code-block:: python 23 | 24 | from django_stuff.models import HistoryModel 25 | 26 | class HistoryTestModel(models.Model): 27 | parent = models.ForeignKey('TestHistoryModel', related_name='history', on_delete=models.CASCADE) 28 | name = models.CharField(max_length=128) 29 | email = models.CharField(max_length=32, default='example@example.com') 30 | 31 | class TestHistoryModel(HistoryModel): 32 | history_model = HistoryTestModel 33 | name = models.CharField(max_length=128) 34 | email = models.CharField(max_length=32, default='example@example.com') 35 | description = models.CharField(max_length=32,blank=True) 36 | 37 | 38 | Note: In this example, only the **name** and **email** will only be saved in the change history. 39 | -------------------------------------------------------------------------------- /docs/source/models/index.rst: -------------------------------------------------------------------------------- 1 | Models 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | generic.rst 8 | history.rst 9 | signals.rst 10 | -------------------------------------------------------------------------------- /docs/source/models/signals.rst: -------------------------------------------------------------------------------- 1 | Signals Model 2 | ============= 3 | 4 | Model that makes it possible to add any task before or after you save, update or delete your model, just adding a method in your model 5 | 6 | Usage: 7 | 8 | Pre-save 9 | ~~~~~~~~ 10 | 11 | Note: This will be made before you save your model 12 | 13 | .. code-block:: python 14 | 15 | from django_stuff.models import SignalsModel 16 | ... 17 | 18 | class YourModel(SignalsModel) 19 | ... 20 | def pre_save(self): 21 | do_something() 22 | 23 | 24 | 25 | Pos-save 26 | ~~~~~~~~ 27 | 28 | Note: This will be made after you save your model 29 | 30 | .. code-block:: python 31 | 32 | from django_stuff.models import SignalsModel 33 | ... 34 | 35 | class YourModel(SignalsModel) 36 | ... 37 | def pos_save(self): 38 | do_something() 39 | 40 | 41 | Pre-delete 42 | ~~~~~~~~~~ 43 | 44 | Note: This will be made before you delete your model 45 | 46 | .. code-block:: python 47 | 48 | from django_stuff.models import SignalsModel 49 | ... 50 | 51 | class YourModel(SignalsModel) 52 | ... 53 | def pre_delete(self): 54 | do_something() 55 | 56 | 57 | 58 | 59 | Pos-delete 60 | ~~~~~~~~~~ 61 | 62 | Note: This will be made after you delete your model 63 | 64 | .. code-block:: python 65 | 66 | from django_stuff.models import SignalsModel 67 | ... 68 | 69 | class YourModel(SignalsModel) 70 | ... 71 | def pos_delete(self): 72 | do_something() 73 | 74 | 75 | -------------------------------------------------------------------------------- /docs/source/quickstart/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Requirements 5 | ------------ 6 | 7 | - Python 3.x 8 | - Django 1.11 or later 9 | 10 | 11 | Install 12 | ------- 13 | 14 | You can get Django Stuff by using pip: 15 | 16 | .. code:: shell 17 | 18 | $ pip install django-stuff 19 | 20 | 21 | If you want to install it from source, grab the git repository from Gitlab and run setup.py: 22 | 23 | .. code:: shell 24 | 25 | $ git clone git@github.com:rhenter/django_stuff.git 26 | $ cd django_stuff 27 | $ python setup.py install 28 | 29 | 30 | Enable 31 | ------ 32 | 33 | To enable `django_stuff` in your project you need to add it to `INSTALLED_APPS` in your projects 34 | `settings.py` file: 35 | 36 | .. code-block:: python 37 | 38 | INSTALLED_APPS = ( 39 | ... 40 | 'django_stuff', 41 | ... 42 | ) 43 | -------------------------------------------------------------------------------- /docs/source/utils.rst: -------------------------------------------------------------------------------- 1 | Utils 2 | ===== 3 | 4 | generate_md5_hashcode 5 | --------------------- 6 | 7 | Generates an MD5 hash code from a key word 8 | 9 | Usage: 10 | 11 | .. code-block:: python 12 | 13 | In[0]: from django_stuff.utils import generate_md5_hashcode 14 | In[1]: generate_md5_hashcode('test') 15 | Out[1]: '39df5136522809aafdf726f1a8a7d00d' 16 | 17 | 18 | generate_random_code 19 | -------------------- 20 | 21 | Generates an random code with a specific length 22 | 23 | Usage: 24 | 25 | .. code-block:: python 26 | 27 | In[0]: from django_stuff.utils import generate_random_code 28 | In[1]: generate_random_code(16) # with 16 length 29 | Out[1]: 'U7Q2M1FW79WIGSW0' 30 | In[2]: generate_random_code(10) # with 10 length 31 | Out[2]: 'D2U2PHUSBD' 32 | 33 | generate_datetime 34 | -------------------- 35 | 36 | Generates an random datetime 37 | 38 | Usage: 39 | 40 | .. code-block:: python 41 | 42 | In[0]: from django_stuff.utils import generate_datetime 43 | In[1]: generate_datetime() 44 | Out[1]: datetime.datetime(1995, 4, 6, 21, 42, 32, 955163) 45 | In[2]: generate_datetime(min_year=2019) # with min_year 46 | Out[2]: datetime.datetime(2019, 9, 26, 1, 17, 38, 303408) 47 | 48 | is_equal 49 | -------- 50 | 51 | Checks if the word are formed with the same character 52 | 53 | Usage: 54 | 55 | .. code-block:: python 56 | 57 | In[0]: from django_stuff.utils import is_equal 58 | In[1]: is_equal('asdfgh') 59 | Out[1]: False 60 | In[2]: is_equal('aaaaaaaaaa') 61 | Out[2]: True 62 | 63 | sanitize_digits 64 | --------------- 65 | 66 | Removes all non digits characters from a string 67 | 68 | Usage: 69 | 70 | .. code-block:: python 71 | 72 | In[0]: from django_stuff.utils import sanitize_digits 73 | In[1]: sanitize_digits('123.123.123-23') 74 | Out[1]: '12312312323' 75 | In[2]: sanitize_digits('123ASDF') 76 | Out[2]: '123' 77 | 78 | validate_cpf 79 | ------------ 80 | 81 | Validates CPF code and return the original number 82 | 83 | Ps: doesn't matter if you use a masked number or not 84 | 85 | Usage: 86 | 87 | .. code-block:: python 88 | 89 | In[0]: from django_stuff.utils import validate_cpf 90 | In[1]: validate_cnpj('01212312312') # Invalid 91 | Out[1]: False 92 | In[2]: validate_cpf('062.265.326-10') # Valid 93 | Out[2]: '062.265.326-10' 94 | 95 | validate_cnpj 96 | ------------- 97 | 98 | Validates CNPJ code and return the original number 99 | 100 | Ps: doesn't matter if you use a masked number or not 101 | 102 | Usage: 103 | 104 | .. code-block:: python 105 | 106 | In[0]: from django_stuff.utils import validate_cnpj 107 | In[1]: validate_cnpj('12345123/000000') # Invalid 108 | Out[1]: False 109 | In[2]: validate_cnpj('61.553.678/0001-96') # Valid 110 | Out[2]: '61.553.678/0001-96' 111 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -vvv --cov=django_stuff --cov-report=term-missing 3 | -------------------------------------------------------------------------------- /requirements-ci.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | codecov 3 | flake8 4 | pytest-cov 5 | pytest-django 6 | pytest>=3 7 | python-status 8 | vcrpy 9 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements-ci.txt 2 | check-manifest 3 | codecov 4 | coveralls 5 | ipdb 6 | ipython 7 | pre-commit 8 | Sphinx 9 | sphinx-autobuild 10 | sphinx-rtd-theme 11 | twine 12 | wheel 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cached_property>=1.3.0 2 | django>=1.11 3 | djangorestframework>=3.7 4 | unipath 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import re 4 | 5 | from setuptools import setup, find_packages, Command 6 | from django_stuff.utils import get_version_from_changes 7 | 8 | here = os.path.abspath(os.path.dirname(__file__)) 9 | version = get_version_from_changes(here) 10 | 11 | 12 | # Save last Version 13 | def save_version(): 14 | version_path = os.path.join(here, "django_stuff/version.py") 15 | 16 | with open(version_path) as version_file_read: 17 | content_file = version_file_read.read() 18 | 19 | VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" 20 | mo = re.search(VSRE, content_file, re.M) 21 | current_version = mo.group(1) 22 | 23 | content_file = content_file.replace(current_version, "{}".format(version)) 24 | 25 | with open(version_path, 'w') as version_file_write: 26 | version_file_write.write(content_file) 27 | 28 | 29 | save_version() 30 | 31 | 32 | # Get the long description 33 | with codecs.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 34 | long_description = f.read() 35 | 36 | # Get changelog 37 | with codecs.open(os.path.join(here, 'CHANGES.rst'), encoding='utf-8') as f: 38 | changelog = f.read() 39 | 40 | with codecs.open(os.path.join(here, 'requirements.txt')) as f: 41 | install_requires = [line for line in f.readlines() if not line.startswith('#')] 42 | 43 | 44 | class VersionCommand(Command): 45 | description = 'print library version' 46 | user_options = [] 47 | 48 | def initialize_options(self): 49 | pass 50 | 51 | def finalize_options(self): 52 | pass 53 | 54 | def run(self): 55 | print(version) 56 | 57 | 58 | setup( 59 | author='Rafael Henter', 60 | author_email='rafael@henter.com.br', 61 | classifiers=[ 62 | 'Development Status :: 4 - Beta', 63 | 'Framework :: Django', 64 | 'Intended Audience :: Developers', 65 | 'License :: OSI Approved :: MIT License', 66 | 'Programming Language :: Python :: 3', 67 | 'Programming Language :: Python :: 3.4', 68 | 'Programming Language :: Python :: 3.5', 69 | 'Programming Language :: Python :: 3.6', 70 | 'Programming Language :: Python :: 3.7', 71 | 'Topic :: Software Development :: Libraries', 72 | ], 73 | cmdclass={'version': VersionCommand}, 74 | description='Library with common code for Django', 75 | install_requires=install_requires, 76 | keywords='utils tools django', 77 | license='MIT', 78 | long_description=long_description, 79 | name='django-stuff', 80 | packages=find_packages(exclude=['docs', 'tests*']), 81 | url='https://github.com/rhenter/django-stuff', 82 | version=version, 83 | ) 84 | -------------------------------------------------------------------------------- /test-django-project/django_stuff: -------------------------------------------------------------------------------- 1 | ../django_stuff -------------------------------------------------------------------------------- /test-django-project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_django_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /test-django-project/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = test_django_project.settings 3 | addopts = -vvvv --reuse-db --tb=native --cov=django_stuff 4 | 5 | filterwarnings = 6 | ignore::UserWarning 7 | -------------------------------------------------------------------------------- /test-django-project/test_django_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhenter/django-stuff/7e2901ac1efc3db47977b98e45754e40bfef6891/test-django-project/test_django_project/__init__.py -------------------------------------------------------------------------------- /test-django-project/test_django_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for test_django_project project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '0p68&@^^ixsya%i48d32*-iw(2!5j&rw*8mf!le7n7e+yp=nu1' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'rest_framework', 42 | 'django_stuff', 43 | 44 | 'testapp', 45 | ] 46 | 47 | MIDDLEWARE_CLASSES = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'test_django_project.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'test_django_project.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | AUTHENTICATION_BACKENDS = ( 109 | 'testapp.backends.MockAuthBackend', 110 | ) 111 | 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'en-us' 117 | 118 | TIME_ZONE = 'UTC' 119 | 120 | USE_I18N = True 121 | 122 | USE_L10N = True 123 | 124 | USE_TZ = True 125 | 126 | 127 | # Static files (CSS, JavaScript, Images) 128 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 129 | 130 | STATIC_URL = '/static/' 131 | 132 | 133 | CACHES = { 134 | 'default': { 135 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 136 | } 137 | } 138 | 139 | 140 | # Django REST Framework 141 | REST_FRAMEWORK = { 142 | 'UNAUTHENTICATED_USER': None, 143 | 'DEFAULT_PERMISSION_CLASSES': ( 144 | 'rest_framework.permissions.IsAuthenticated', 145 | ), 146 | 'DEFAULT_PAGINATION_CLASS': 147 | 'rest_framework.pagination.PageNumberPagination', 148 | 'PAGE_SIZE': 150, 149 | 'DEFAULT_FILTER_BACKENDS': ( 150 | 'rest_framework.filters.SearchFilter', 151 | 'rest_framework.filters.OrderingFilter' 152 | ) 153 | } 154 | -------------------------------------------------------------------------------- /test-django-project/test_django_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | 3 | 4 | urlpatterns = [ 5 | url(r'', include('testapp.urls', namespace='testapp')), 6 | ] 7 | -------------------------------------------------------------------------------- /test-django-project/test_django_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_django_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_django_project.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /test-django-project/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhenter/django-stuff/7e2901ac1efc3db47977b98e45754e40bfef6891/test-django-project/testapp/__init__.py -------------------------------------------------------------------------------- /test-django-project/testapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-09 21:34 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import django_stuff.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('django_stuff', '__first__'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='HistoryTestModel', 19 | fields=[ 20 | ('id', django_stuff.fields.UUIDPrimaryKeyField(editable=False, primary_key=True, serialize=False, unique=True)), 21 | ('name', models.CharField(max_length=128)), 22 | ('email', models.CharField(default='example@example.com', max_length=32)), 23 | ], 24 | options={ 25 | 'abstract': False, 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='TestDigitsOnlyField', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('digits_only_field', django_stuff.fields.CharFieldDigitsOnly(max_length=5)), 33 | ], 34 | ), 35 | migrations.CreateModel( 36 | name='TestHistoryFail', 37 | fields=[ 38 | ('historymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='django_stuff.HistoryModel')), 39 | ('name', models.CharField(max_length=128)), 40 | ], 41 | options={ 42 | 'abstract': False, 43 | }, 44 | bases=('django_stuff.historymodel',), 45 | ), 46 | migrations.CreateModel( 47 | name='TestHistoryModel', 48 | fields=[ 49 | ('historymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='django_stuff.HistoryModel')), 50 | ('name', models.CharField(max_length=128)), 51 | ('email', models.CharField(default='example@example.com', max_length=32)), 52 | ('description', models.CharField(blank=True, default='Lorem Ipsum', max_length=32)), 53 | ], 54 | options={ 55 | 'abstract': False, 56 | }, 57 | bases=('django_stuff.historymodel', models.Model), 58 | ), 59 | migrations.CreateModel( 60 | name='TestModel', 61 | fields=[ 62 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), 63 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), 64 | ('id', django_stuff.fields.UUIDPrimaryKeyField(editable=False, primary_key=True, serialize=False, unique=True)), 65 | ('name', models.CharField(max_length=128)), 66 | ], 67 | options={ 68 | 'abstract': False, 69 | }, 70 | ), 71 | migrations.CreateModel( 72 | name='TestSignalsModel', 73 | fields=[ 74 | ('id', django_stuff.fields.UUIDPrimaryKeyField(editable=False, primary_key=True, serialize=False, unique=True)), 75 | ('name', models.CharField(max_length=128)), 76 | ], 77 | options={ 78 | 'abstract': False, 79 | }, 80 | ), 81 | migrations.AddField( 82 | model_name='historytestmodel', 83 | name='parent', 84 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='testapp.TestHistoryModel'), 85 | ), 86 | ] 87 | -------------------------------------------------------------------------------- /test-django-project/testapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhenter/django-stuff/7e2901ac1efc3db47977b98e45754e40bfef6891/test-django-project/testapp/migrations/__init__.py -------------------------------------------------------------------------------- /test-django-project/testapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from django_stuff.models import UUIDModel, HistoryModel, SignalsModel, TimestampedModel, SerializerModel 4 | from django_stuff.fields import CharFieldDigitsOnly 5 | 6 | 7 | class TestModel(UUIDModel, TimestampedModel, SerializerModel): 8 | name = models.CharField(max_length=128) 9 | 10 | 11 | class TestSignalsModel(SignalsModel, UUIDModel): 12 | name = models.CharField(max_length=128) 13 | 14 | def __init__(self, *args, **kwargs): 15 | super().__init__(*args, **kwargs) 16 | 17 | self.debug_info = { 18 | 'pre_save_handler_called': 0, 19 | 'post_save_handler_called': 0, 20 | 'pre_update_handler_called': 0, 21 | 'post_update_handler_called': 0, 22 | 'pre_delete_handler_called': 0, 23 | 'post_delete_handler_called': 0 24 | } 25 | 26 | def pre_save(self, context): 27 | self.debug_info['pre_save_handler_called'] += 1 28 | 29 | def post_save(self, context): 30 | self.debug_info['post_save_handler_called'] += 1 31 | 32 | def pre_update(self, context): 33 | self.debug_info['pre_update_handler_called'] += 1 34 | 35 | def post_update(self, context): 36 | self.debug_info['post_update_handler_called'] += 1 37 | 38 | def pre_delete(self, context): 39 | self.debug_info['pre_delete_handler_called'] += 1 40 | 41 | def post_delete(self, context): 42 | self.debug_info['post_delete_handler_called'] += 1 43 | 44 | 45 | class HistoryTestModel(UUIDModel): 46 | parent = models.ForeignKey('TestHistoryModel', related_name='history', on_delete=models.CASCADE) 47 | name = models.CharField(max_length=128) 48 | email = models.CharField(max_length=32, default='example@example.com') 49 | 50 | 51 | class TestHistoryModel(HistoryModel, SignalsModel): 52 | history_model = HistoryTestModel 53 | name = models.CharField(max_length=128) 54 | email = models.CharField(max_length=32, default='example@example.com') 55 | description = models.CharField(max_length=32, default='Lorem Ipsum', blank=True) 56 | 57 | 58 | class TestHistoryFail(HistoryModel): 59 | name = models.CharField(max_length=128) 60 | 61 | 62 | class TestDigitsOnlyField(models.Model): 63 | digits_only_field = CharFieldDigitsOnly(max_length=5) 64 | -------------------------------------------------------------------------------- /test-django-project/testapp/templates/view.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhenter/django-stuff/7e2901ac1efc3db47977b98e45754e40bfef6891/test-django-project/testapp/templates/view.html -------------------------------------------------------------------------------- /test-django-project/testapp/urls.py: -------------------------------------------------------------------------------- 1 | 2 | app_name = 'testapp' 3 | urlpatterns = [ 4 | ] 5 | -------------------------------------------------------------------------------- /test-django-project/testapp/views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhenter/django-stuff/7e2901ac1efc3db47977b98e45754e40bfef6891/test-django-project/testapp/views.py -------------------------------------------------------------------------------- /test-django-project/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhenter/django-stuff/7e2901ac1efc3db47977b98e45754e40bfef6891/test-django-project/tests/__init__.py -------------------------------------------------------------------------------- /test-django-project/tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhenter/django-stuff/7e2901ac1efc3db47977b98e45754e40bfef6891/test-django-project/tests/conftest.py -------------------------------------------------------------------------------- /test-django-project/tests/test_fields.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import uuid 3 | 4 | from django.forms import ValidationError 5 | 6 | from testapp.models import TestModel, TestDigitsOnlyField 7 | 8 | pytestmark = pytest.mark.django_db 9 | 10 | 11 | def test_uuid_primary_key_field(): 12 | obj = TestModel(name='Name') 13 | assert obj.pk is None 14 | obj.save() 15 | assert obj.pk is not None 16 | assert isinstance(obj.pk, uuid.UUID) 17 | 18 | 19 | @pytest.mark.parametrize('value', ('1', '123', '00123', '000')) 20 | def test_char_field_only_digits_valid(value): 21 | assert not TestDigitsOnlyField(digits_only_field=value).full_clean() 22 | 23 | 24 | @pytest.mark.parametrize('value', ('a', '12db', '1.23', '123-4')) 25 | def test_char_field_only_digits_invalid(value): 26 | with pytest.raises(ValidationError) as exc: 27 | TestDigitsOnlyField(digits_only_field=value).full_clean() 28 | assert 'digits_only_field' in exc.value.error_dict 29 | -------------------------------------------------------------------------------- /test-django-project/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django.forms import ValidationError 4 | from django_stuff.forms import CNPJField, CPFField 5 | 6 | 7 | @pytest.mark.parametrize('value', 8 | ['21552411273', 9 | '215.524,112-73'] 10 | ) 11 | def test_cpf_field_valid(value): 12 | assert CPFField().clean(value) == value 13 | 14 | 15 | @pytest.mark.parametrize('value', 16 | CPFField.invalid_values + ( 17 | '12345678901', '12345678901!@', '1234567890112312312' 18 | )) 19 | def test_cpf_field_invalid(value): 20 | with pytest.raises(ValidationError): 21 | CPFField().clean(value) 22 | 23 | 24 | @pytest.mark.parametrize('value', 25 | ['09654773000166', 26 | '09.654.773/0001-66'] 27 | ) 28 | def test_cnpj_field_valid(value): 29 | assert CNPJField().clean(value) == value 30 | 31 | 32 | @pytest.mark.parametrize('value', CNPJField.invalid_values + ('12345678901234', '1234567890112312312', ' ')) 33 | def test_cnpj_field_invalid(value): 34 | with pytest.raises(ValidationError): 35 | CNPJField().clean(value) 36 | -------------------------------------------------------------------------------- /test-django-project/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from uuid import UUID 3 | 4 | from django_stuff.models.exceptions import HistoryModelNotSetError 5 | from testapp.models import TestSignalsModel, TestHistoryModel, TestHistoryFail, TestModel 6 | 7 | pytestmark = pytest.mark.django_db 8 | 9 | 10 | def test_history_model_creation(): 11 | obj = TestHistoryModel.objects.create(name='test 01') 12 | history = obj.history.all()[0] 13 | 14 | assert obj.history.count() == 1 15 | assert obj.name == history.name 16 | assert obj.email == history.email 17 | 18 | 19 | def test_history_model_update(): 20 | # Create: 21 | obj = TestHistoryModel.objects.create(name='test 01') 22 | 23 | # Update: 24 | obj.status = 'step 2' 25 | obj.name = 'Lorem Mister Ipsum' 26 | obj.email = 'exmachine@example.com' 27 | obj.save() 28 | 29 | history = obj.history.all()[1] 30 | 31 | assert obj.history.count() == 2 32 | assert obj.name == history.name 33 | assert obj.email == history.email 34 | 35 | 36 | def test_history_model_unrelated_fields(): 37 | # Create: 38 | obj = TestHistoryModel.objects.create(name='test 01') 39 | obj.description = 'Be like water my friend.' 40 | obj.save() 41 | 42 | history = obj.history.all()[1] 43 | 44 | assert obj.history.count() == 2 45 | 46 | with pytest.raises(AttributeError): 47 | history.description 48 | 49 | 50 | def test_history_model_invalid(): 51 | with pytest.raises(HistoryModelNotSetError): 52 | TestHistoryFail.objects.create(name='test 01') 53 | 54 | 55 | def test_signals_model_save_with_create(): 56 | obj = TestSignalsModel.objects.create(name='test 01') 57 | assert obj.debug_info['pre_save_handler_called'] == 1 58 | assert obj.debug_info['post_save_handler_called'] == 1 59 | 60 | assert obj.debug_info['pre_update_handler_called'] == 0 61 | assert obj.debug_info['post_update_handler_called'] == 0 62 | 63 | assert obj.debug_info['pre_delete_handler_called'] == 0 64 | assert obj.debug_info['post_delete_handler_called'] == 0 65 | 66 | assert obj.pk is not None 67 | 68 | 69 | def test_signals_model_serialize_method(): 70 | obj = TestModel.objects.create(name='test 01') 71 | data = obj.serialize() 72 | 73 | assert isinstance(obj.id, UUID) 74 | assert not isinstance(data['id'], UUID) 75 | 76 | 77 | def test_signals_model_is_creation_context_value(): 78 | obj = TestSignalsModel(name='test 01') 79 | assert obj.get_context()['is_creation'] is True 80 | 81 | obj2 = TestSignalsModel(name='test 02') 82 | obj2.id = 1 83 | assert obj.get_context()['is_creation'] is True 84 | 85 | 86 | def test_signals_model_delete(): 87 | TestSignalsModel.objects.create(name='test 01') 88 | obj = TestSignalsModel.objects.get(name='test 01') 89 | obj.delete() 90 | 91 | assert obj.debug_info['pre_save_handler_called'] == 0 92 | assert obj.debug_info['post_save_handler_called'] == 0 93 | 94 | assert obj.debug_info['pre_update_handler_called'] == 0 95 | assert obj.debug_info['post_update_handler_called'] == 0 96 | 97 | assert obj.debug_info['pre_delete_handler_called'] == 1 98 | assert obj.debug_info['post_delete_handler_called'] == 1 99 | -------------------------------------------------------------------------------- /test-django-project/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django_stuff.utils.string import remove_special_characters, remove_accents 4 | 5 | 6 | @pytest.mark.parametrize('value,expected', [ 7 | ('ASDF/!@#/', 'ASDF'), 8 | ('!$%*ASDF', 'ASDF'), 9 | ('[erasdf]', 'erasdf'), 10 | ('{}áéó', 'aeo'), 11 | 12 | ]) 13 | def test_remove_special_characters(value, expected): 14 | assert remove_special_characters(value) == expected 15 | 16 | 17 | @pytest.mark.parametrize('value,expected', [ 18 | ('caça', 'caca'), 19 | ('AÉMON', 'AEMON'), 20 | ('{}áéó', '{}aeo'), 21 | ('opá olha o chão coração, não é îsso vó e vô?', 'opa olha o chao coracao, nao e isso vo e vo?'), 22 | 23 | ]) 24 | def test_remove_accents(value, expected): 25 | assert remove_accents(value) == expected 26 | -------------------------------------------------------------------------------- /test-django-project/tests/vcr.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import vcr 4 | from django.conf import settings 5 | 6 | CASSETTES_DIR = os.path.join(str(settings.BASE_DIR), 7 | 'fixtures/cassettes') 8 | my_vcr = vcr.VCR( 9 | cassette_library_dir=CASSETTES_DIR, 10 | path_transformer=vcr.VCR.ensure_suffix('.yaml'), 11 | filter_headers=['authorization'], 12 | record_mode='once', 13 | match_on=['method', 'path', 'query'], 14 | ) 15 | --------------------------------------------------------------------------------