├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── .readthedocs.yaml ├── .travis.yml ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── README.rst ├── birthday ├── __init__.py ├── fields.py ├── managers.py ├── models.py └── tests │ ├── __init__.py │ ├── conftest.py │ ├── models.py │ ├── settings.py │ └── test_models.py ├── docs ├── Makefile ├── conf.py ├── index.rst ├── limitations.rst ├── make.bat └── usage.rst ├── setup.cfg ├── setup.py └── tox.ini /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | env: 15 | TWINE_USERNAME: "__token__" 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.x' 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel twine tox 26 | - name: Build package 27 | run: python setup.py sdist bdist_wheel 28 | - name: Publish package 29 | env: 30 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 31 | run: twine upload dist/* 32 | -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | # You can also specify other tool versions: 14 | # nodejs: "16" 15 | # rust: "1.55" 16 | # golang: "1.17" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # If using Sphinx, optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | dist: xenial 3 | language: python 4 | cache: pip 5 | sudo: false 6 | 7 | python: 8 | - "3.6" 9 | - "3.7" 10 | - "3.8" 11 | - "3.9" 12 | 13 | install: 14 | - pip install tox-travis 15 | 16 | script: 17 | - tox 18 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Jonas Obrist 2 | Basil Shubin 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2017, Jonas Obrist 2 | Copyright (c) 2017-2019, Basil Shubin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | * Neither the name of the author nor the names of other 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include README.rst 3 | include LICENSE 4 | recursive-exclude birthday/fixtures * 5 | recursive-exclude birthday/tests * 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-birthday 2 | =============== 3 | 4 | .. image:: https://img.shields.io/pypi/v/django-birthday.svg 5 | :target: https://pypi.python.org/pypi/django-birthday/ 6 | 7 | .. image:: https://img.shields.io/pypi/dm/django-birthday.svg 8 | :target: https://pypi.python.org/pypi/django-birthday/ 9 | 10 | .. image:: https://img.shields.io/github/license/bashu/django-birthday.svg 11 | :target: https://pypi.python.org/pypi/django-birthday/ 12 | 13 | .. image:: https://img.shields.io/travis/bashu/django-birthday.svg 14 | :target: https://travis-ci.com/github/bashu/django-birthday/ 15 | 16 | django-birthday is a helper library to work with birthdays in models. 17 | 18 | Maintained by `Basil Shubin `_, and some great 19 | `contributors `_. 20 | 21 | Installation 22 | ------------ 23 | 24 | First install the module, preferably in a virtual environment. It can be installed from PyPI: 25 | 26 | .. code-block:: bash 27 | 28 | pip install django-birthday 29 | 30 | Usage 31 | ----- 32 | 33 | django-birthday provides a ``birthday.fields.BirthdayField`` model 34 | field type which is a subclass of ``django.db.models.DateField`` and 35 | thus has the same characteristics as that. It also internally adds a 36 | second field to your model holding the day of the year for that 37 | birthday, this is used for the extra functionality exposed by 38 | ``birthday.managers.BirthdayManager`` which you should use as the 39 | manager on your model. 40 | 41 | A model could look like this: 42 | 43 | .. code-block:: python 44 | 45 | from django.db import models 46 | from django.conf import settings 47 | 48 | from birthday import BirthdayField, BirthdayManager 49 | 50 | 51 | class UserProfile(models.Model): 52 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 53 | birthday = BirthdayField() 54 | 55 | objects = BirthdayManager() 56 | 57 | Get all user profiles within the next 30 days: 58 | 59 | .. code-block:: python 60 | 61 | UserProfile.objects.get_upcoming_birthdays() 62 | 63 | Get all user profiles which have their birthday today: 64 | 65 | .. code-block:: python 66 | 67 | UserProfile.objects.get_birthdays() 68 | 69 | Or order the user profiles according to their birthday: 70 | 71 | .. code-block:: python 72 | 73 | UserProfile.objects.order_by_birthday() 74 | 75 | For more details, see the documentation_ at Read The Docs. 76 | 77 | Contributing 78 | ------------ 79 | 80 | If you like this module, forked it, or would like to improve it, please let us know! 81 | Pull requests are welcome too. :-) 82 | 83 | Credits 84 | ------- 85 | 86 | `django-birthday `_ was originally started by `Jonas Obrist `_ who has now unfortunately abandoned the project. 87 | 88 | License 89 | ------- 90 | 91 | ``django-birthday`` is released under the BSD license. 92 | 93 | .. _documentation: https://django-birthday.readthedocs.io/ 94 | -------------------------------------------------------------------------------- /birthday/__init__.py: -------------------------------------------------------------------------------- 1 | from .fields import BirthdayField # noqa 2 | from .managers import BirthdayManager # noqa 3 | -------------------------------------------------------------------------------- /birthday/fields.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import FieldError 2 | from django.db.models.fields import DateField, PositiveSmallIntegerField 3 | from django.db.models.signals import pre_save 4 | 5 | 6 | def pre_save_listener(instance, **kwargs): 7 | field_obj = instance._meta.birthday_field 8 | 9 | birthday = getattr(instance, field_obj.name) 10 | if not birthday: 11 | return 12 | setattr(instance, field_obj.doy_name, birthday.timetuple().tm_yday) 13 | 14 | 15 | class BirthdayField(DateField): 16 | 17 | def contribute_to_class(self, cls, name): 18 | if hasattr(cls._meta, "birthday_field"): 19 | raise FieldError("django-birthday does not support multiple BirthdayFields on a single model") 20 | cls._meta.birthday_field = self 21 | 22 | self.doy_name = "%s_dayofyear_internal" % name 23 | if not hasattr(cls, self.doy_name): 24 | dayofyear_field = PositiveSmallIntegerField(editable=False, default=None, null=True) 25 | dayofyear_field.creation_counter = self.creation_counter 26 | 27 | cls.add_to_class(self.doy_name, dayofyear_field) 28 | 29 | super().contribute_to_class(cls, name) 30 | 31 | pre_save.connect(pre_save_listener, sender=cls) 32 | -------------------------------------------------------------------------------- /birthday/managers.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.db import models 4 | from django.db.models.query_utils import Q 5 | 6 | CASE = "CASE WHEN %(bdoy)s<%(cdoy)s THEN %(bdoy)s+365 ELSE %(bdoy)s END" 7 | 8 | 9 | def _order(manager, reverse=False, case=False): 10 | cdoy = date.today().timetuple().tm_yday 11 | bdoy = manager._birthday_doy_field 12 | doys = {"cdoy": cdoy, "bdoy": bdoy} 13 | if case: 14 | qs = manager.extra(select={"internal_bday_order": CASE % doys}) 15 | order_field = "internal_bday_order" 16 | else: 17 | qs = manager.all() 18 | order_field = bdoy 19 | if reverse: 20 | return qs.order_by("-%s" % order_field) 21 | return qs.order_by("%s" % order_field) 22 | 23 | 24 | class BirthdayManager(models.Manager): 25 | @property 26 | def _birthday_doy_field(self): 27 | return self.model._meta.birthday_field.doy_name 28 | 29 | def _doy(self, day): 30 | if not day: 31 | day = date.today() 32 | return day.timetuple().tm_yday 33 | 34 | def get_upcoming_birthdays(self, days=30, after=None, include_day=True, order=True, reverse=False): 35 | today = self._doy(after) 36 | limit = today + days 37 | q = Q(**{"{}__gt{}".format(self._birthday_doy_field, "e" if include_day else ""): today}) 38 | q &= Q(**{"%s__lt" % self._birthday_doy_field: limit}) 39 | 40 | if limit > 365: 41 | limit = limit - 365 42 | today = 1 43 | q2 = Q(**{"%s__gte" % self._birthday_doy_field: today}) 44 | q2 &= Q(**{"%s__lt" % self._birthday_doy_field: limit}) 45 | q = q | q2 46 | 47 | if order: 48 | qs = _order(self, reverse, True) 49 | return qs.filter(q) 50 | 51 | return self.filter(q) 52 | 53 | def get_birthdays(self, day=None): 54 | return self.filter(**{self._birthday_doy_field: self._doy(day)}) 55 | 56 | def order_by_birthday(self, reverse=False): 57 | return _order(self, reverse) 58 | -------------------------------------------------------------------------------- /birthday/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-birthday/c80737210266e7bab005c4ac91f0e1304a85dd3e/birthday/models.py -------------------------------------------------------------------------------- /birthday/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-birthday/c80737210266e7bab005c4ac91f0e1304a85dd3e/birthday/tests/__init__.py -------------------------------------------------------------------------------- /birthday/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dummy conftest.py for birthday. 3 | If you don't know what this is for, just leave it empty. 4 | Read more about conftest.py under: 5 | https://pytest.org/latest/plugins.html 6 | """ 7 | 8 | import pytest 9 | -------------------------------------------------------------------------------- /birthday/tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from birthday import BirthdayField, BirthdayManager 4 | 5 | 6 | class TestModel(models.Model): 7 | __test__ = False 8 | 9 | birthday = BirthdayField() 10 | objects = BirthdayManager() 11 | 12 | class Meta: 13 | ordering = ("pk",) 14 | -------------------------------------------------------------------------------- /birthday/tests/settings.py: -------------------------------------------------------------------------------- 1 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 2 | import os 3 | import re 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | 7 | SECRET_KEY = "DUMMY_SECRET_KEY" 8 | 9 | INTERNAL_IPS = [] 10 | 11 | # Application definition 12 | 13 | PROJECT_APPS = ["birthday.tests", "birthday"] 14 | 15 | INSTALLED_APPS = [ 16 | "django.contrib.auth", 17 | "django.contrib.contenttypes", 18 | ] + PROJECT_APPS 19 | 20 | TEMPLATES = [ 21 | { 22 | "BACKEND": "django.template.backends.django.DjangoTemplates", 23 | "DIRS": [], 24 | "APP_DIRS": True, 25 | "OPTIONS": { 26 | "context_processors": [ 27 | "django.contrib.auth.context_processors.auth", 28 | "django.template.context_processors.debug", 29 | "django.template.context_processors.i18n", 30 | "django.template.context_processors.media", 31 | "django.template.context_processors.request", 32 | "django.template.context_processors.static", 33 | "django.template.context_processors.tz", 34 | "django.contrib.messages.context_processors.messages", 35 | ], 36 | }, 37 | }, 38 | ] 39 | 40 | # Database 41 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 42 | 43 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 44 | -------------------------------------------------------------------------------- /birthday/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | 3 | from django.core.exceptions import FieldError 4 | from django.db import models 5 | from django.test import TestCase 6 | 7 | from birthday.fields import BirthdayField 8 | from birthday.managers import BirthdayManager 9 | 10 | from .models import TestModel 11 | 12 | 13 | class BirthdayTest(TestCase): 14 | def setUp(self): 15 | for birthday in ["2001-01-01", "2000-01-02", "2002-12-31"]: 16 | TestModel.objects.create(birthday=datetime.strptime(birthday, "%Y-%m-%d").date()) 17 | 18 | def test_default(self): 19 | self.assertEqual(len(TestModel._meta.fields), 3) 20 | self.assertTrue(hasattr(TestModel._meta, "birthday_field")) 21 | self.assertEqual(TestModel.objects.all().count(), 3) 22 | 23 | def test_ordering(self): 24 | pks1 = [obj.pk for obj in TestModel.objects.order_by("birthday")] 25 | pks2 = [obj.pk for obj in TestModel.objects.order_by_birthday()] 26 | self.assertNotEqual(pks1, pks2) 27 | 28 | doys = [getattr(obj, "birthday_dayofyear_internal") for obj in TestModel.objects.order_by_birthday()] 29 | self.assertEqual(doys, [1, 2, 365]) 30 | doys = [getattr(obj, "birthday_dayofyear_internal") for obj in TestModel.objects.order_by_birthday(True)] 31 | self.assertEqual(doys, [365, 2, 1]) 32 | 33 | years = [obj.birthday.year for obj in TestModel.objects.order_by("birthday")] 34 | self.assertEqual(years, [2000, 2001, 2002]) 35 | 36 | def test_manager(self): 37 | jan1 = date(year=2010, month=1, day=1) 38 | self.assertEqual(TestModel.objects.get_birthdays(jan1).count(), 1) 39 | self.assertEqual(TestModel.objects.get_upcoming_birthdays(30, jan1).count(), 2) 40 | self.assertEqual(TestModel.objects.get_upcoming_birthdays(30, jan1, False).count(), 1) 41 | 42 | dec31 = date(year=2010, month=12, day=31) 43 | self.assertEqual(TestModel.objects.get_birthdays(dec31).count(), 1) 44 | self.assertEqual(TestModel.objects.get_upcoming_birthdays(30, dec31).count(), 3) 45 | 46 | doys = [ 47 | getattr(obj, "birthday_dayofyear_internal") for obj in TestModel.objects.get_upcoming_birthdays(30, dec31) 48 | ] 49 | self.assertEqual(doys, [365, 1, 2]) 50 | doys = [ 51 | getattr(obj, "birthday_dayofyear_internal") 52 | for obj in TestModel.objects.get_upcoming_birthdays(30, dec31, reverse=True) 53 | ] 54 | self.assertEqual(doys, [2, 1, 365]) 55 | doys = [ 56 | getattr(obj, "birthday_dayofyear_internal") 57 | for obj in TestModel.objects.get_upcoming_birthdays(30, dec31, order=False) 58 | ] 59 | self.assertEqual(doys, [1, 2, 365]) 60 | 61 | self.assertEqual(TestModel.objects.get_upcoming_birthdays(30, dec31, False).count(), 2) 62 | self.assertTrue(TestModel.objects.get_birthdays().count() in [0, 1]) 63 | 64 | def test_exception(self): 65 | class BrokenModel(models.Model): 66 | birthday = BirthdayField() 67 | 68 | self.assertRaises(FieldError, BirthdayField().contribute_to_class, BrokenModel, "another_birthday") 69 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 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/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'django-birthday' 21 | copyright = '2021, Basil Shubin' 22 | author = 'Jonas Obrist' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.1.4' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx_rtd_theme', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = [] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = 'sphinx_rtd_theme' 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | html_static_path = ['_static'] 57 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-birthday documentation master file, created by 2 | sphinx-quickstart on Mon Sep 27 20:23:11 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-birthday's documentation! 7 | =========================================== 8 | 9 | django-birthday is a helper library for Django to work with birthdays in models. 10 | 11 | **Warning**: This library is pretty hacky. Use it if you're lazy, not if you're 12 | concerned about your code quality! 13 | 14 | .. toctree:: 15 | :maxdepth: 1 16 | :caption: Contents: 17 | 18 | usage 19 | limitations 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /docs/limitations.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Limitations 3 | =========== 4 | 5 | There are a couple of limitations for django-birthday: 6 | 7 | * You can only have **one** :class:`birthday.fields.BirthdayField` field on a 8 | single model. 9 | * You cannot chain the custom methods provided by the manager. 10 | * Ordering by a :class:`birthday.fields.BirthdayField` while not using 11 | :meth:`birthday.managers.BirthdayManager.order_by_birthday` will order by 12 | **age**, not when their birthday is in a year. -------------------------------------------------------------------------------- /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=. 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | django-birthday provides a :class:`birthday.fields.BirthdayField` model field 6 | type which is a subclass of :class:`django.db.models.DateField` and thus has the 7 | same characteristics as that. It also internally adds a second field to your 8 | model holding the day of the year for that birthday, this is used for the extra 9 | functionality exposed by :class:`birthday.managers.BirthdayManager` which you 10 | should use as the manager on your model. 11 | 12 | 13 | A model could look like this: 14 | 15 | .. code-block:: python 16 | 17 | from django.db import models 18 | from django.conf import settings 19 | 20 | from birthday import BirthdayField, BirthdayManager 21 | 22 | 23 | class UserProfile(models.Model): 24 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 25 | birthday = BirthdayField() 26 | 27 | objects = BirthdayManager() 28 | 29 | 30 | Get all user profiles within the next 30 days: 31 | 32 | .. code-block:: python 33 | 34 | UserProfile.objects.get_upcoming_birthdays() 35 | 36 | Get all user profiles which have their birthday today: 37 | 38 | .. code-block:: python 39 | 40 | UserProfile.objects.get_birthdays() 41 | 42 | Or order the user profiles according to their birthday: 43 | 44 | .. code-block:: python 45 | 46 | UserProfile.objects.order_by_birthday() 47 | 48 | 49 | Method References 50 | ----------------- 51 | 52 | .. method:: birthday.managers.BirthdayManager.get_upcoming_birthdays 53 | 54 | Returns a queryset containing objects that have an upcoming birthday. 55 | 56 | :param days: *Optional*. Amount of days that still count as 'upcoming', 57 | defaults to 30. 58 | :param after: *Optional*. Start day to use, defaults to 'today'. 59 | :param include_day: *Optional*. Include the 'after' day for lookups. 60 | :param order: *Optional*. Whether the queryset should be ordered by birthday, 61 | defaults to True. 62 | :param reverse: *Optional*. Only applies when `order` is True. Apply 63 | reverse ordering. 64 | :rtype: Instance of :class:`django.db.models.query.QuerySet`. 65 | 66 | 67 | .. method:: birthday.managers.BirthdayManager.get_birthdays 68 | 69 | Returns a queryset containing objects which have the birthday on a specific 70 | day. 71 | 72 | :param day: *Optional*. What day to get the birthdays of. Defaults to 73 | 'today'. 74 | :rtype: Instance of :class:`django.db.models.query.QuerySet`. 75 | 76 | 77 | .. method:: birthday.managers.BirthdayManager.order_by_birthday 78 | 79 | Returns a queryset ordered by birthday (not age!). 80 | 81 | :param reverse: *Optional*. Defaults to `False`. Whether or not to reverse 82 | the results. 83 | :rtype: Instance of :class:`django.db.models.query.QuerySet`. 84 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-birthday 3 | version = 0.1.4 4 | description = Helper field and manager for working with birthdays 5 | long_description = file: README.rst 6 | long_description_content_type = text/x-rst 7 | author = Jonas Obrist 8 | author_email = jonas.obrist@divio.ch 9 | maintainer = Basil Shubin 10 | maintainer_email = basil.shubin@gmail.com 11 | url = https://github.com/bashu/django-birthday 12 | download_url = https://github.com/bashu/django-birthday/zipball/master 13 | license = BSD License 14 | classifiers = 15 | Development Status :: 5 - Production/Stable 16 | Environment :: Web Environment 17 | Intended Audience :: Developers 18 | License :: OSI Approved :: BSD License 19 | Operating System :: OS Independent 20 | Programming Language :: Python 21 | Programming Language :: Python :: 3 :: Only 22 | Programming Language :: Python :: 3.6 23 | Programming Language :: Python :: 3.7 24 | Programming Language :: Python :: 3.8 25 | Programming Language :: Python :: 3.9 26 | Framework :: Django 27 | Framework :: Django :: 2.2 28 | Framework :: Django :: 3.0 29 | Framework :: Django :: 3.1 30 | Framework :: Django :: 3.2 31 | Topic :: Internet :: WWW/HTTP 32 | Topic :: Internet :: WWW/HTTP :: Dynamic Content 33 | Topic :: Software Development :: Libraries :: Python Modules 34 | 35 | [options] 36 | zip_safe = False 37 | include_package_data = True 38 | packages = find: 39 | 40 | [options.packages.find] 41 | exclude = example* 42 | 43 | [options.extras_require] 44 | develop = 45 | tox 46 | django 47 | pytest-django 48 | pytest 49 | test = 50 | pytest-django 51 | pytest-cov 52 | pytest 53 | 54 | [bdist_wheel] 55 | # No longer universal (Python 3 only) but leaving this section in here will 56 | # trigger zest to build a wheel. 57 | universal = 0 58 | 59 | [flake8] 60 | # Some sane defaults for the code style checker flake8 61 | # black compatibility 62 | max-line-length = 88 63 | # E203 and W503 have edge cases handled by black 64 | extend-ignore = E203, W503 65 | exclude = 66 | .tox 67 | build 68 | dist 69 | .eggs 70 | 71 | [tool:pytest] 72 | DJANGO_SETTINGS_MODULE = birthday.tests.settings 73 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | distribute = False 3 | envlist = 4 | py{36,37,38,39}-dj{22,30,31,32} 5 | skip_missing_interpreters = True 6 | 7 | [travis] 8 | python = 9 | 3.6: py36 10 | 3.7: py37 11 | 3.8: py38 12 | 3.9: py39 13 | 14 | [testenv] 15 | usedevelop = True 16 | extras = test 17 | setenv = 18 | DJANGO_SETTINGS_MODULE = birthday.tests.settings 19 | deps = 20 | dj22: Django>=2.2,<3.0 21 | dj30: Django>=3.0,<3.1 22 | dj31: Django>=3.1,<3.2 23 | dj32: Django>=3.2,<3.3 24 | commands = pytest --cov --cov-append --cov-report= 25 | --------------------------------------------------------------------------------