├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── changelog.rst ├── conf.py ├── contributing.rst ├── github.py ├── index.rst ├── installation.rst ├── make.bat └── usage │ ├── admin.png │ ├── api.rst │ ├── getting_started.rst │ ├── index.rst │ └── recurrence_field.rst ├── pdm.lock ├── pyproject.toml ├── pytest.ini ├── recurrence ├── __init__.py ├── base.py ├── choices.py ├── compat.py ├── exceptions.py ├── fields.py ├── forms.py ├── locale │ ├── ar │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── eu │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── he │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── hu │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── nl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── django.po │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ └── sk │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ ├── django.po │ │ ├── djangojs.mo │ │ └── djangojs.po ├── managers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_alter_date_id_alter_param_id_alter_recurrence_id_and_more.py │ └── __init__.py ├── models.py ├── settings.py └── static │ └── recurrence │ ├── css │ └── recurrence.css │ ├── img │ └── recurrence-calendar-icon.png │ └── js │ ├── recurrence-widget.init.js │ ├── recurrence-widget.js │ └── recurrence.js ├── tests ├── __init__.py ├── models.py ├── settings.py ├── test_exclusions.py ├── test_fields.py ├── test_magic_methods.py ├── test_managers_recurrence.py ├── test_managers_rule.py ├── test_nulls.py ├── test_occurrences.py ├── test_recurrences_without_limits.py ├── test_saving.py ├── test_serialization.py ├── test_settings.py ├── test_to_text.py ├── test_to_weekday.py ├── test_tz.py └── test_weekday.py └── tox.ini /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "12:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'jazzband/django-recurrence' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up PDM for Python 3.9 17 | uses: pdm-project/setup-pdm@v4 18 | with: 19 | python-version: 3.9 20 | 21 | - name: Install dependencies 22 | run: | 23 | pdm sync -d -G dev 24 | 25 | - name: Build wheels and sdist 26 | run: | 27 | pdm build 28 | 29 | - name: Upload packages to Jazzband 30 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 31 | uses: pypa/gh-action-pypi-publish@release/v1.10.2 32 | with: 33 | user: jazzband 34 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 35 | repository-url: https://jazzband.co/projects/django-recurrence/upload 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: build (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}) 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | max-parallel: 5 12 | matrix: 13 | python-version: ['3.9', '3.10', '3.11', '3.12'] 14 | django-version: ['4.0', '4.1', '4.2', '5.0', '5.1'] 15 | include: 16 | # Tox configuration for documentation environment 17 | - python-version: '3.9' 18 | django-version: 'docs' 19 | # Tox configuration for QA environment 20 | - python-version: '3.9' 21 | django-version: 'qa' 22 | # Django main 23 | - python-version: '3.10' 24 | django-version: 'main' 25 | experimental: true 26 | - python-version: '3.11' 27 | django-version: 'main' 28 | experimental: true 29 | - python-version: '3.12' 30 | django-version: 'main' 31 | experimental: true 32 | # - python-version: '3.13' 33 | # django-version: 'main' 34 | # experimental: true 35 | exclude: 36 | # Exclude Django 4.0 for Python 3.7 as it is not supported 37 | - python-version: '3.11' 38 | django-version: '4.0' 39 | - python-version: '3.12' 40 | django-version: '4.0' 41 | # - python-version: '3.13' 42 | # django-version: '4.0' 43 | - python-version: '3.12' 44 | django-version: '4.1' 45 | # - python-version: '3.13' 46 | # django-version: '4.1' 47 | - python-version: '3.9' 48 | django-version: '5.0' 49 | # - python-version: '3.13' 50 | # django-version: '5.0' 51 | - python-version: '3.9' 52 | django-version: '5.1' 53 | steps: 54 | - uses: actions/checkout@v3 55 | - name: Set up PDM for Python ${{ matrix.python-version }} 56 | uses: pdm-project/setup-pdm@v4 57 | with: 58 | python-version: ${{ matrix.python-version }} 59 | - name: Install dependencies 60 | run: | 61 | pdm sync -d -G dev 62 | 63 | - name: Run pytest 64 | run: | 65 | pdm run -v pytest --cov-report term-missing --cov-report xml --cov=docopt 66 | env: 67 | DJANGO: ${{ matrix.django-version }} 68 | 69 | - name: Upload coverage 70 | uses: codecov/codecov-action@v2 71 | with: 72 | name: Python ${{ matrix.python-version }} 73 | 74 | - name: Build wheels and sdist 75 | run: | 76 | pdm build 77 | 78 | - name: Save artifacts 79 | uses: actions/upload-artifact@v4 80 | with: 81 | # These are pure-python, so we don't need to worry about platform 82 | name: wheels-python-${{ matrix.python-version }}-django-${{ matrix.django-version }} 83 | # name: Python ${{ matrix.python-version }} 84 | path: | 85 | dist/*.whl 86 | dist/*.tar.gz 87 | 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | tests/test_db.sqlite 4 | .coverage 5 | htmlcov/ 6 | .cache/ 7 | .tox/ 8 | docs/_build 9 | build/ 10 | dist/ 11 | .pytest_cache/ 12 | /.idea 13 | /.vscode 14 | /venv 15 | coverage.xml 16 | .eggs/ 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: [] 2 | -------------------------------------------------------------------------------- /.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 | version: 2 6 | build: 7 | os: ubuntu-20.04 8 | tools: 9 | python: "3.7" 10 | python: 11 | install: 12 | - requirements: requirements.txt 13 | sphinx: 14 | configuration: docs/conf.py 15 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 2 | Changes 3 | ======= 4 | 5 | 6 | 1.12 7 | ---- 8 | * Drop official support for Django versions 2.2, 3.2. 9 | * Drop official support for Python versions 3.7, 3.8; 10 | * Added support for DEFAULT_AUTO_FIELD 11 | * Minor fixes 12 | 13 | 1.11.1 (2021-01-25) 14 | ------------------- 15 | 16 | - Fix reStructuredTest syntax to be PyPI compliant. 17 | 18 | 1.11.0 (2021-01-21) 19 | ------------------- 20 | 21 | - Add Django 4.0 and Python 3.10 support. 22 | Deprecate EOL Django and Python versions. 23 | [aleksihakli] 24 | - Fix weekday deserialization typing bug. 25 | [apirobot] 26 | - Fix default ``recurrence.language_code`` not being set 27 | by using ``en-us`` for locale in frontend as the default. 28 | [jleclanche] 29 | 30 | 1.10.3 31 | ------ 32 | 33 | * Add Hungarian localisation #161. 34 | 35 | 1.10.2 36 | ------ 37 | 38 | * Add Hebrew localisation #159. 39 | 40 | 1.10.1 41 | ------ 42 | 43 | * Update path to jQuery to match the one Django admin provides #148. 44 | 45 | 1.10.0 46 | ------ 47 | 48 | * Fixes and official support for Django 2.1 and 2.2 #143, #142; 49 | * Remove support for Python 2.7 and 3.5, remove support for Django 2.0 #145. 50 | 51 | 1.9.0 52 | ----- 53 | 54 | * Fix for using the recurrence widget in admin inlines #137. 55 | 56 | 1.8.2 57 | ----- 58 | 59 | * Minor fix for Django 2.0 #134; 60 | * Minor packaging fix #135. 61 | 62 | 1.8.1 63 | ----- 64 | 65 | * Bad release, do not use. 66 | 67 | 1.8.0 68 | ----- 69 | 70 | This release contains two backwards incompatible changes - 71 | please read the notes below for details. 72 | 73 | * django-recurrence now returns timezone aware ``datetime`` objects 74 | in most cases #130. If ``USE_TZ`` is ``True`` (it 75 | defaults to off with a stock Django install) then you'll now get 76 | timezone aware ``datetime`` objects back from django-recurrence. If 77 | you have ``USE_TZ`` as ``True``, and you don't want this behaviour, 78 | you can set ``RECURRENCE_USE_TZ`` to ``False``, but please let us 79 | know (via GitHub issues) that you wanted to opt out, so we can 80 | understand what your use case is. 81 | * ``RecurrenceField`` instances without ``required=False`` will now 82 | require at least one rule or date. This change is intended to bring 83 | django-recurrence into line with how custom fields should 84 | behave. If you don't want to require at least one rule or date, 85 | just set ``require=False`` on your field #133. 86 | * Improvements to avoid inline styles #85; 87 | * Handle changes to ``javascript_catalog`` in Django 2 #131. 88 | 89 | 1.7.0 90 | ----- 91 | 92 | * Drop official support for Django versions 1.7, 1.8, 1.9, 1.10; 93 | * Fixes for saving ``None`` into a ``RecurrenceField`` causing a 94 | ``TypeError`` #89`, #122; 95 | * Drop official support for Python 3.3 and Python 3.4; 96 | * Provisional support for Python 3.7 (only for Django 2.0 and up); 97 | * Ensure use of ``render`` on Django widgets always passes the 98 | ``renderer`` argument, to ensure support for Django 2.1 #125; 99 | * Django 2.0 compatibility fix for usage of django-recurrence with 100 | Django REST framework #126. 101 | 102 | 1.6.0 103 | ----- 104 | 105 | * Fixes for Python 3 #105; 106 | * Support for Django 2.0 #109, #110; 107 | * Switch back a couple of instances of ``DeserializationError`` to 108 | ``ValidationError`` #111; 109 | * Switch around how we set dates in the date selector widget to avoid 110 | issues with short months #113. 111 | 112 | 1.5.0 113 | ----- 114 | 115 | * Add Slovakian translations #98; 116 | * Add support for events occurring at a fixed point before the 117 | end of the month - e.g. the second last Tuesday before the end of the month #88; 118 | * Add minor style changes to make django-recurrence compatible with Wagtail #100; 119 | * Allow changing the behaviour of generating recurrences on 120 | ``dtstart`` by default. You can opt in to this by setting 121 | ``include_dtstart=False`` on your ``RecurrenceField`` #93; 122 | * Ensure broken values raise ``DeserializationError`` where expected #103. 123 | 124 | 1.4.1 125 | ----- 126 | 127 | * Make PO-Revision-Date parseable by babel #75; 128 | * Update installation notes to cover Django 1.10 #74; 129 | * Add German translation #77; 130 | * Add Brazilian translation #79; 131 | * Ensure the migrations are included when installing #78; 132 | * Fix order of arguments to ``to_dateutil_rruleset`` #81. 133 | 134 | 1.4.0 135 | ----- 136 | 137 | * Improve our testing setup to also cover Python 3.5; 138 | * Fixes for Django 1.10 #69. 139 | 140 | 1.3.1 141 | ----- 142 | 143 | * Add Basque translations #67. 144 | 145 | 1.3.0 146 | ----- 147 | 148 | * Drop official support for Django 1.4, Django 1.5, Django 1.6 and 149 | Python 2.6 (no changes have been made to deliberately break older 150 | versions, but older versions will not be tested going forward); 151 | * Add official support for Django 1.8 and Django 1.9 #62; 152 | * Fix for a bug in ``Rule`` creation where the weekday parameter is 153 | an instance of ``Weekday`` rather than an integer #57. 154 | 155 | 1.2.0 156 | ----- 157 | 158 | * Added an option for events to occur on the fourth of a given 159 | weekday of the month #29; 160 | * Fixed an off-by-one bug in the ``to_text`` method for events 161 | happening on a regular month each year #30; 162 | * Fixed a bug in the JavaScript widget where the date for monthly 163 | events on a fixed date of the month had the description rendered 164 | incorrectly if the day selected was more than the number of days in 165 | the current calendar month #31; 166 | * Added a French translation #32 - this may be backwards 167 | incompatible if have overriden the widget JavaScript such that 168 | there is no ``language_code`` member of your recurrence object; 169 | * Added a Spanish translation #49; 170 | * Added database migrations - running ``python manage.py migrate 171 | recurrence --fake`` should be sufficient for this version - nothing 172 | has changed about the database schema between 1.1.0 and 1.2.0; 173 | * Fix broken tests for Django 1.4. 174 | 175 | 1.1.0 176 | ----- 177 | 178 | * Added experimental Python 3 support. 179 | * Added extensive test coverage (from 0% to 81%). 180 | * Added documentation (including this changelog). 181 | * Removed ``RecurrenceModelField`` and ``RecurrenceModelDescriptor``, 182 | which don't appear to have worked as expected for some time. 183 | * Fixed a bug introduced in 1.0.3 which prevented the 184 | django-recurrence JavaScript from working #27. 185 | * Don't raise ``ValueError`` if you save ``None`` into a 186 | ``RecurrenceField`` with ``null=False`` #22, for 187 | consistency with other field types. 188 | * Make sure an empty recurrence object is falsey #25. 189 | * Fix a copy-paste error in ``to_recurrence_object`` which prevented 190 | exclusion rules from being populated correctly. 191 | * Fix a typo in ``create_from_recurrence_object`` which prevented it 192 | working with inclusion or exclusion rules. 193 | * Various other very minor bugfixes. 194 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://jazzband.co/static/img/jazzband.svg 2 | :target: https://jazzband.co/ 3 | :alt: Jazzband 4 | 5 | This is a `Jazzband `_ project. By contributing you agree to abide by the `Contributor Code of Conduct `_ and follow the `guidelines `_. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Tamas Kemenczy and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include MANIFEST.in 4 | include pytest.ini 5 | recursive-include docs Makefile make.bat *.rst *.py *.png 6 | recursive-include recurrence/static *.css *.png *.js 7 | recursive-include recurrence/locale *.mo *.po 8 | recursive-include tests *.py 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | coverage: 2 | pytest 3 | 4 | test: 5 | pytest 6 | 7 | testall: 8 | tox 9 | 10 | build: clean 11 | python setup.py sdist bdist_wheel 12 | 13 | clean: 14 | rm -rf dist/* 15 | rm -rf build/* 16 | 17 | push: build 18 | git push 19 | 20 | release: push 21 | twine upload -r pypi dist/* 22 | 23 | .PHONY: coverage test testall build clean push 24 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | django-recurrence 3 | ================= 4 | 5 | .. image:: https://jazzband.co/static/img/badge.svg 6 | :target: https://jazzband.co/ 7 | :alt: Jazzband 8 | 9 | .. image:: https://img.shields.io/github/stars/jazzband/django-recurrence.svg?label=Stars&style=socialcA 10 | :target: https://github.com/jazzband/django-recurrence 11 | :alt: GitHub 12 | 13 | .. image:: https://img.shields.io/pypi/v/django-recurrence.svg 14 | :target: https://pypi.org/project/django-recurrence/ 15 | :alt: PyPI release 16 | 17 | .. image:: https://img.shields.io/pypi/pyversions/django-recurrence.svg 18 | :target: https://pypi.org/project/django-recurrence/ 19 | :alt: Supported Python versions 20 | 21 | .. image:: https://img.shields.io/pypi/djversions/django-recurrence.svg 22 | :target: https://pypi.org/project/django-recurrence/ 23 | :alt: Supported Django versions 24 | 25 | .. image:: https://img.shields.io/readthedocs/django-recurrence.svg 26 | :target: https://django-recurrence.readthedocs.io/ 27 | :alt: Documentation 28 | 29 | .. image:: https://github.com/jazzband/django-recurrence/workflows/Test/badge.svg 30 | :target: https://github.com/jazzband/django-recurrence/actions 31 | :alt: GitHub actions 32 | 33 | .. image:: https://codecov.io/gh/jazzband/django-recurrence/branch/master/graph/badge.svg 34 | :target: https://codecov.io/gh/jazzband/django-recurrence 35 | :alt: Coverage 36 | 37 | 38 | django-recurrence is a utility for working with recurring dates in Django. 39 | 40 | 41 | Functionality 42 | ------------- 43 | 44 | * Recurrence/Rule objects using a subset of rfc2445 45 | (wraps ``dateutil.rrule``) for specifying recurring date/times, 46 | * ``RecurrenceField`` for storing recurring datetimes in the database, and 47 | * JavaScript widget. 48 | 49 | ``RecurrenceField`` provides a Django model field which serializes 50 | recurrence information for storage in the database. 51 | 52 | For example - say you were storing information about a university course 53 | in your app. You could use a model like this: 54 | 55 | .. code:: python 56 | 57 | import recurrence.fields 58 | 59 | class Course(models.Model): 60 | title = models.CharField(max_length=200) 61 | start = models.TimeField() 62 | end = models.TimeField() 63 | recurrences = recurrence.fields.RecurrenceField() 64 | 65 | You’ll notice that I’m storing my own start and end time. 66 | The recurrence field only deals with *recurrences* 67 | not with specific time information. 68 | I have an event that starts at 2pm. 69 | Its recurrences would be “every Friday”. 70 | 71 | 72 | Documentation 73 | ------------- 74 | 75 | For more information on installation and configuration see the documentation at: 76 | 77 | https://django-recurrence.readthedocs.io/ 78 | 79 | 80 | Issues 81 | ------ 82 | 83 | If you have questions or have trouble using the app please file a bug report at: 84 | 85 | https://github.com/jazzband/django-recurrence/issues 86 | 87 | 88 | Contributions 89 | ------------- 90 | 91 | All contributions are welcome! 92 | 93 | It is best to separate proposed changes and PRs into small, distinct patches 94 | by type so that they can be merged faster into upstream and released quicker. 95 | 96 | One way to organize contributions would be to separate PRs for e.g. 97 | 98 | * bugfixes, 99 | * new features, 100 | * code and design improvements, 101 | * documentation improvements, or 102 | * tooling and CI improvements. 103 | 104 | Merging contributions requires passing the checks configured 105 | with the CI. This includes running tests and linters successfully 106 | on the currently officially supported Python and Django versions. 107 | 108 | The test automation is run automatically with GitHub Actions, but you can 109 | run it locally with the ``tox`` command before pushing commits. 110 | 111 | This is a `Jazzband `_ project. By contributing you agree to abide by the `Contributor Code of Conduct `_ and follow the `guidelines `_. 112 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-recurrence.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-recurrence.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-recurrence" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-recurrence" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. changelog: 2 | 3 | .. include:: ../CHANGES.rst 4 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Contributions to django-recurrence are very welcome - whether in the 5 | form of bug reports, feature requests, or patches. Bug reports and 6 | feature requests are tracked on our `GitHub issues page 7 | `_. 8 | 9 | If you want to make changes to django-recurrence, you'll need to fork 10 | our GitHub repository, make any changes you want, and send us a pull 11 | request. Feel free to `file an issue 12 | `_ if 13 | you want help getting set up. 14 | 15 | Running the tests 16 | ----------------- 17 | 18 | The easiest way to run the tests is to run:: 19 | 20 | make testall 21 | 22 | from the root of your local copy of the django-recurrence 23 | repository. This will require that you have tox installed. If you 24 | don't have tox installed, you can install it with ``pip install 25 | tox``. Running all the tests also requires that you have Python 2.6, 26 | Python 2.7, Python 3.3 and Python 3.4 installed locally. 27 | 28 | This will run tests against all supported Python and Django versions, 29 | check the documentation can be built, and will also run ``flake8``, 30 | an automated code-linting tool. 31 | 32 | If that sounds like too much work, feel free to just run tests on 33 | whatever your local version of Python is. You can do this by 34 | running:: 35 | 36 | pip install -r requirements_test.txt ! You only need to run this once 37 | make test 38 | 39 | If you want to see what our code coverage is like, install everything 40 | in ``requirements_test.txt`` (as shown above), then run:: 41 | 42 | make coverage 43 | 44 | Working with the documentation 45 | ------------------------------ 46 | 47 | Our documentation is written with Sphinx, and can be built using:: 48 | 49 | tox -e docs 50 | 51 | Once this command is run, it'll print out the folder the generated 52 | HTML documentation is available in. 53 | -------------------------------------------------------------------------------- /docs/github.py: -------------------------------------------------------------------------------- 1 | from docutils import nodes, utils 2 | from docutils.parsers.rst.roles import set_classes 3 | 4 | 5 | # With thanks to Doug Hellman for writing 6 | # https://doughellmann.com/blog/2010/05/09/defining-custom-roles-in-sphinx/ 7 | # - this code is derived from an example BitBucket configuration. 8 | 9 | 10 | def make_issue_node(rawtext, app, slug, options): 11 | """Create a link to a GitHub issue. 12 | 13 | :param rawtext: Text being replaced with link node. 14 | :param app: Sphinx application context 15 | :param slug: ID of the thing to link to 16 | :param options: Options dictionary passed to role func. 17 | """ 18 | # 19 | try: 20 | base = app.config.github_project_url 21 | if not base: 22 | raise AttributeError 23 | except AttributeError as err: 24 | raise ValueError('github_project_url configuration value is not set (%s)' % str(err)) 25 | 26 | slash = '/' if base[-1] != '/' else '' 27 | ref = base + slash + 'issues/' + slug + '/' 28 | set_classes(options) 29 | node = nodes.reference(rawtext, '#' + utils.unescape(slug), refuri=ref, 30 | **options) 31 | return node 32 | 33 | 34 | def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): 35 | """Link to a GitHub issue. 36 | 37 | Returns 2 part tuple containing list of nodes to insert into the 38 | document and a list of system messages. Both are allowed to be 39 | empty. 40 | 41 | :param name: The role name used in the document. 42 | :param rawtext: The entire markup snippet, with role. 43 | :param text: The text marked with the role. 44 | :param lineno: The line number where rawtext appears in the input. 45 | :param inliner: The inliner instance that called us. 46 | :param options: Directive options for customization. 47 | :param content: The directive content for customization. 48 | """ 49 | try: 50 | issue_num = int(text) 51 | if issue_num <= 0: 52 | raise ValueError 53 | except ValueError: 54 | msg = inliner.reporter.error( 55 | 'GitHub issue number must be a number greater than or equal to 1; ' 56 | '"%s" is invalid.' % text, line=lineno) 57 | prb = inliner.problematic(rawtext, rawtext, msg) 58 | return [prb], [msg] 59 | app = inliner.document.settings.env.app 60 | node = make_issue_node(rawtext, app, str(issue_num), options) 61 | return [node], [] 62 | 63 | 64 | def setup(app): 65 | """Install the plugin. 66 | 67 | :param app: Sphinx application context. 68 | """ 69 | app.add_role('issue', ghissue_role) 70 | app.add_config_value('github_project_url', None, 'env') 71 | return 72 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | django-recurrence 2 | ***************** 3 | 4 | django-recurrence is a utility for working with recurring dates in 5 | Django. 6 | 7 | It provides: 8 | 9 | - Recurrence/Rule objects using a subset of rfc2445 (wraps 10 | ``dateutil.rrule``) for specifying recurring date/times; 11 | - ``RecurrenceField`` for storing recurring datetimes in the 12 | database; 13 | - a JavaScript widget. 14 | 15 | Contents 16 | -------- 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | 21 | installation 22 | usage/index 23 | contributing 24 | changelog 25 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Installation 4 | ============ 5 | 6 | .. contents:: 7 | :local: 8 | 9 | 10 | Download the library 11 | -------------------- 12 | 13 | Firstly, you'll need to install ``django-recurrence`` from PyPI. The 14 | easiest way to do this is with pip:: 15 | 16 | pip install django-recurrence 17 | 18 | Then, make sure ``recurrence`` is in your ``INSTALLED_APPS`` setting: 19 | 20 | .. code-block:: python 21 | 22 | INSTALLED_APPS = ( 23 | ... 24 | 'recurrence', 25 | ) 26 | 27 | Supported Django and Python versions 28 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 29 | 30 | Currently, django-recurrence supports Python 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, and 3.12. 31 | 32 | django-recurrence is currently tested with django 2.2, 3.2, 4.0, 4.1, 4.2, and 5.0 33 | 34 | Set up internationalization 35 | --------------------------- 36 | 37 | .. note:: 38 | 39 | This step is currently mandatory, but may be bypassed with an 40 | extra bit of javascript. See :issue:`47` for details. 41 | 42 | Using a translation of django-recurrence other than 43 | ``en`` requires that django-recurrence's JavaScript can 44 | access the translation strings. This is handled with Django's built 45 | in ``javascript_catalog`` view, which you must install by adding the 46 | following to your project ``urls.py`` file: 47 | 48 | .. code-block:: python 49 | 50 | import django 51 | from django.urls import re_path as url 52 | from django.views.i18n import JavaScriptCatalog 53 | 54 | # Your normal URLs here... 55 | 56 | # If you already have a js_info_dict dictionary, just add 57 | # 'recurrence' to the existing 'packages' tuple. 58 | js_info_dict = { 59 | 'packages': ('recurrence', ), 60 | } 61 | 62 | # jsi18n can be anything you like here 63 | urlpatterns += [ 64 | url(r'^jsi18n/$', JavaScriptCatalog.as_view(), js_info_dict), 65 | ] 66 | 67 | 68 | Configure static files 69 | ---------------------- 70 | 71 | django-recurrence includes some static files (all to do with 72 | rendering the JavaScript widget that makes handling recurring dates 73 | easier). To ensure these are served correctly, you'll probably want 74 | to ensure you also have ``django.contrib.staticfiles`` in your 75 | ``INSTALLED_APPS`` setting, and run:: 76 | 77 | python manage.py collectstatic 78 | 79 | .. note:: 80 | After collecting static files, you can use ``{{ form.media }}`` to 81 | include recurrence's static files within your templates. 82 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-recurrence.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-recurrence.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/usage/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/docs/usage/admin.png -------------------------------------------------------------------------------- /docs/usage/api.rst: -------------------------------------------------------------------------------- 1 | Generating Recurrences Programmatically 2 | --------------------------------------- 3 | 4 | .. contents:: 5 | :local: 6 | 7 | ``Rule`` and ``Recurrence`` 8 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 9 | 10 | To create recurrence objects, the two main classes you'll need are 11 | ``Rule`` and ``Recurrence``. 12 | 13 | ``Rule`` specifies a single rule (e.g. "every third Friday of the 14 | month"), and a ``Recurrence`` is a collection of rules (some of which 15 | may be inclusion rules, others of which may be exclusion rules), 16 | together with date limits, and other configuration parameters. 17 | 18 | For example: 19 | 20 | .. code-block:: python 21 | 22 | from datetime import datetime 23 | import recurrence 24 | 25 | 26 | myrule = recurrence.Rule( 27 | recurrence.DAILY 28 | ) 29 | 30 | pattern = recurrence.Recurrence( 31 | dtstart=datetime(2014, 1, 2, 0, 0, 0), 32 | dtend=datetime(2014, 1, 3, 0, 0, 0), 33 | rrules=[myrule, ] 34 | ) 35 | 36 | You can then generate a set of recurrences for that recurrence 37 | pattern, like this:: 38 | 39 | >>> list(mypattern.occurrences()) 40 | [datetime.datetime(2014, 1, 2, 0, 0), datetime.datetime(2014, 1, 3, 0, 0)] 41 | 42 | Exclusion Rules 43 | ^^^^^^^^^^^^^^^ 44 | 45 | You can specify exclusion rules too, which are exactly the same as 46 | inclusion rules, but they represent rules which match dates which 47 | should *not* be included in the list of occurrences. Inclusion rules 48 | are provided to the ``Recurrence`` object using the kwarg ``rrules``, 49 | and exclusion rules are provided to the ``Recurrence`` object using 50 | the kwargs ``exrules``. 51 | 52 | Adding or Excluding Individual Dates 53 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 54 | 55 | Similarly, you can specify individual dates to include or exclude 56 | using ``rdates`` and ``exdates``, both of which should be a list of 57 | ``datetime.datetime`` objects. 58 | 59 | .. code-block:: python 60 | 61 | from datetime import datetime 62 | import recurrence 63 | 64 | pattern = recurrence.Recurrence( 65 | rdates=[ 66 | datetime(2014, 1, 1, 0, 0, 0), 67 | datetime(2014, 1, 2, 0, 0, 0), 68 | ] 69 | ) 70 | -------------------------------------------------------------------------------- /docs/usage/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | --------------- 3 | 4 | Once you've :ref:`installed django-recurrence `, you'll 5 | generally want to start by using it in one of your models, which can 6 | be done like this: 7 | 8 | .. code-block:: python 9 | :emphasize-lines: 5 10 | 11 | from recurrence.fields import RecurrenceField 12 | 13 | class Course(models.Model): 14 | title = models.CharField(max_length=200) 15 | recurrences = RecurrenceField() 16 | 17 | If you use the ``Course`` model in Django's administrative interface, 18 | or in any forms, it should be rendered with a pretty form field, 19 | which makes selecting relatively complex recurrence patterns easy. 20 | 21 | .. figure:: admin.png 22 | :alt: The form field for recurrence fields 23 | 24 | Using this form it's possible to specify relatively complex 25 | recurrence rules - such as an event that happens every third Thursday 26 | of the month, unless that Thursday happens to be the 21st of the 27 | month, and so on. 28 | 29 | 30 | Form Usage 31 | ---------------------- 32 | 33 | .. code-block:: python 34 | 35 | from django import forms 36 | from .models import Course 37 | 38 | class CourseForm(forms.ModelForm): 39 | class Meta: 40 | model = Course 41 | fields = ('title', 'recurrences',) 42 | 43 | .. note:: 44 | 45 | Be sure to add ``{{ form.media }}`` to your template or 46 | statically link recurrence.css and recurrence.js. 47 | 48 | .. code-block:: html 49 | 50 | 51 |
52 | {% csrf_token %} 53 | {{ form.media }} 54 | {{ form }} 55 | 56 |
57 | -------------------------------------------------------------------------------- /docs/usage/index.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | getting_started 8 | recurrence_field 9 | api 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "django-recurrence" 3 | version = "1.12.1" 4 | description = "Django utility wrapping dateutil.rrule" 5 | authors = [ 6 | {name = "Tamas Kemenczy", email = "tamas.kemenczy@gmail.com"}, 7 | {name = "Lino Helms", email = "lino@lino.io"} 8 | ] 9 | dependencies = [ 10 | "flake8==4.0.1", 11 | "pytest-cov==3.0.0", 12 | "pytest-django==4.5.2", 13 | "pytest-sugar==0.9.5", 14 | "pytest==7.1.2", 15 | "sphinx==4.3.2", 16 | "sphinx-rtd-theme==1.0.0", 17 | "tox==3.25.1", 18 | "django>=4.0", 19 | "python-dateutil", 20 | ] 21 | requires-python = ">=3.9" 22 | readme = "README.rst" 23 | license = {text = "BSD"} 24 | classifiers = [ 25 | "Development Status :: 5 - Production/Stable", 26 | "Environment :: Plugins", 27 | "Environment :: Web Environment", 28 | "Framework :: Django", 29 | "Framework :: Django", 30 | "Framework :: Django :: 4.0", 31 | "Framework :: Django :: 4.1", 32 | "Framework :: Django :: 4.2", 33 | "Framework :: Django :: 5.0", 34 | "Intended Audience :: Developers", 35 | "License :: OSI Approved :: BSD License", 36 | "Operating System :: OS Independent", 37 | "Programming Language :: Python", 38 | "Programming Language :: Python :: 3", 39 | "Programming Language :: Python :: 3.10", 40 | "Programming Language :: Python :: 3.11", 41 | "Programming Language :: Python :: 3.12", 42 | "Programming Language :: Python :: 3.9", 43 | "Programming Language :: Python :: Implementation", 44 | "Programming Language :: Python :: Implementation :: CPython", 45 | "Programming Language :: Python :: Implementation :: PyPy", 46 | ] 47 | 48 | [project.urls] 49 | Homepage = "https://github.com/jazzband/django-recurrence" 50 | [build-system] 51 | requires = ["pdm-backend"] 52 | build-backend = "pdm.backend" 53 | 54 | [tool.pdm] 55 | distribution = true 56 | 57 | [tool.pdm.dev-dependencies] 58 | dev = [ 59 | "setuptools>=69.0.3", 60 | "tox-pdm>=0.6.1", 61 | "tox>=3.25.1", 62 | ] 63 | 64 | [tool.pdm.package-dir] 65 | recurrence = "recurrence" 66 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = tests.settings 3 | addopts = --cov recurrence --cov-append --cov-branch --cov-report term-missing --cov-report=xml 4 | testpaths = tests 5 | pythonpath = . 6 | -------------------------------------------------------------------------------- /recurrence/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from recurrence.base import ( 4 | MO, TU, WE, TH, FR, SA, SU, 5 | MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY, 6 | YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY, 7 | JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, 8 | SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER, 9 | 10 | validate, serialize, deserialize, 11 | to_utc, to_weekday, 12 | from_dateutil_rrule, from_dateutil_rruleset, 13 | Recurrence, Rule, Weekday, 14 | ) 15 | 16 | from recurrence.exceptions import ( 17 | RecurrenceError, SerializationError, DeserializationError, ValidationError 18 | ) 19 | -------------------------------------------------------------------------------- /recurrence/choices.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | 3 | import recurrence 4 | 5 | 6 | FREQUENCY_CHOICES = ( 7 | (recurrence.SECONDLY, _('Secondly')), 8 | (recurrence.MINUTELY, _('Minutely')), 9 | (recurrence.HOURLY, _('Hourly')), 10 | (recurrence.DAILY, _('Daily')), 11 | (recurrence.WEEKLY, _('Weekly')), 12 | (recurrence.MONTHLY, _('Monthly')), 13 | (recurrence.YEARLY, _('Yearly')), 14 | ) 15 | 16 | WEEKDAY_CHOICES = ( 17 | (recurrence.MONDAY, _('Monday')), 18 | (recurrence.TUESDAY, _('Tuesday')), 19 | (recurrence.WEDNESDAY, _('Wednesday')), 20 | (recurrence.THURSDAY, _('Thursday')), 21 | (recurrence.FRIDAY, _('Friday')), 22 | (recurrence.SATURDAY, _('Saturday')), 23 | (recurrence.SUNDAY, _('Sunday')), 24 | ) 25 | 26 | MONTH_CHOICES = ( 27 | (recurrence.JANUARY, _('January')), 28 | (recurrence.FEBRUARY, _('February')), 29 | (recurrence.MARCH, _('March')), 30 | (recurrence.APRIL, _('April')), 31 | (recurrence.MAY, _('May')), 32 | (recurrence.JUNE, _('June')), 33 | (recurrence.JULY, _('July')), 34 | (recurrence.AUGUST, _('August')), 35 | (recurrence.SEPTEMBER, _('September')), 36 | (recurrence.OCTOBER, _('October')), 37 | (recurrence.NOVEMBER, _('November')), 38 | (recurrence.DECEMBER, _('December')), 39 | ) 40 | 41 | EXCLUSION = False 42 | INCLUSION = True 43 | MODE_CHOICES = ( 44 | (INCLUSION, _('Inclusion')), 45 | (EXCLUSION, _('Exclusion')), 46 | ) 47 | -------------------------------------------------------------------------------- /recurrence/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.db.models.fields.subclassing import Creator 3 | except ImportError: 4 | # This class was removed in Django 1.10, so I've pulled it into 5 | # django-recurrence. 6 | 7 | class Creator: 8 | """ 9 | A placeholder class that provides a way to set the attribute 10 | on the model. 11 | """ 12 | def __init__(self, field): 13 | self.field = field 14 | 15 | def __get__(self, obj, type=None): 16 | if obj is None: 17 | return self 18 | return obj.__dict__[self.field.name] 19 | 20 | def __set__(self, obj, value): 21 | obj.__dict__[self.field.name] = self.field.to_python(value) 22 | -------------------------------------------------------------------------------- /recurrence/exceptions.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | 3 | 4 | class RecurrenceError(ValidationError): 5 | pass 6 | 7 | 8 | class SerializationError(RecurrenceError): 9 | pass 10 | 11 | 12 | class DeserializationError(RecurrenceError): 13 | pass 14 | -------------------------------------------------------------------------------- /recurrence/fields.py: -------------------------------------------------------------------------------- 1 | from django.db.models import fields 2 | import recurrence 3 | from recurrence import forms 4 | from recurrence.compat import Creator 5 | 6 | 7 | # Do not use SubfieldBase meta class because is removed in Django 1.10 8 | 9 | class RecurrenceField(fields.Field): 10 | """Field that stores a `recurrence.base.Recurrence` to the database.""" 11 | 12 | def __init__(self, include_dtstart=True, **kwargs): 13 | self.include_dtstart = include_dtstart 14 | super(RecurrenceField, self).__init__(**kwargs) 15 | 16 | def get_internal_type(self): 17 | return 'TextField' 18 | 19 | def to_python(self, value): 20 | if value is None or isinstance(value, recurrence.Recurrence): 21 | return value 22 | value = super(RecurrenceField, self).to_python(value) or u'' 23 | return recurrence.deserialize(value, self.include_dtstart) 24 | 25 | def from_db_value(self, value, *args, **kwargs): 26 | return self.to_python(value) 27 | 28 | def get_prep_value(self, value): 29 | if not isinstance(value, str): 30 | value = recurrence.serialize(value) 31 | return value 32 | 33 | def contribute_to_class(self, cls, *args, **kwargs): 34 | super(RecurrenceField, self).contribute_to_class(cls, *args, **kwargs) 35 | setattr(cls, self.name, Creator(self)) 36 | 37 | def value_to_string(self, obj): 38 | return self.get_prep_value(self.value_from_object(obj)) 39 | 40 | def formfield(self, **kwargs): 41 | defaults = { 42 | 'form_class': forms.RecurrenceField, 43 | 'widget': forms.RecurrenceWidget, 44 | } 45 | defaults.update(kwargs) 46 | return super().formfield(**defaults) 47 | -------------------------------------------------------------------------------- /recurrence/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms, urls 2 | from django.conf import settings 3 | from django.views import i18n 4 | from django.utils.translation import gettext_lazy as _ 5 | from django.contrib.staticfiles.storage import staticfiles_storage 6 | 7 | import recurrence 8 | from recurrence import exceptions 9 | 10 | 11 | class RecurrenceWidget(forms.Textarea): 12 | 13 | def __init__(self, attrs=None, **kwargs): 14 | self.js_widget_options = kwargs 15 | defaults = {'class': 'recurrence-widget'} 16 | if attrs is not None: 17 | defaults.update(attrs) 18 | super().__init__(defaults) 19 | 20 | def get_media(self): 21 | extra = '' if settings.DEBUG else '.min' 22 | js = [ 23 | 'admin/js/vendor/jquery/jquery%s.js' % extra, 24 | 'admin/js/jquery.init.js', 25 | staticfiles_storage.url('recurrence/js/recurrence.js'), 26 | staticfiles_storage.url('recurrence/js/recurrence-widget.js'), 27 | staticfiles_storage.url('recurrence/js/recurrence-widget.init.js'), 28 | ] 29 | i18n_media = find_recurrence_i18n_js_catalog() 30 | if i18n_media: 31 | js.insert(0, i18n_media) 32 | 33 | return forms.Media( 34 | js=js, css={ 35 | 'all': ( 36 | staticfiles_storage.url('recurrence/css/recurrence.css'), 37 | ), 38 | }, 39 | ) 40 | media = property(get_media) 41 | 42 | 43 | class RecurrenceField(forms.CharField): 44 | """ 45 | A Field that accepts the recurrence related parameters of rfc2445. 46 | 47 | Values are deserialized into `recurrence.base.Recurrence` objects. 48 | """ 49 | widget = RecurrenceWidget 50 | default_error_messages = { 51 | 'invalid_frequency': _( 52 | u'Invalid frequency.'), 53 | 'max_rrules_exceeded': _( 54 | u'Max rules exceeded. The limit is %(limit)s'), 55 | 'max_exrules_exceeded': _( 56 | u'Max exclusion rules exceeded. The limit is %(limit)s'), 57 | 'max_rdates_exceeded': _( 58 | u'Max dates exceeded. The limit is %(limit)s'), 59 | 'max_exdates_exceeded': _( 60 | u'Max exclusion dates exceeded. The limit is %(limit)s'), 61 | 'recurrence_required': _( 62 | u'This field is required. Set either a recurrence rule or date.'), 63 | } 64 | 65 | def __init__( 66 | self, 67 | frequencies=None, accept_dtstart=True, accept_dtend=True, 68 | max_rrules=None, max_exrules=None, max_rdates=None, max_exdates=None, 69 | *args, **kwargs 70 | ): 71 | """ 72 | Create a recurrence field. 73 | 74 | A `RecurrenceField` takes the same parameters as a `CharField` 75 | field with some additional paramaters. 76 | 77 | :Parameters: 78 | `frequencies` : sequence 79 | A sequence of the frequency constants specifying which 80 | frequencies are valid for input. By default all 81 | frequencies are valid. 82 | 83 | `accept_dtstart` : bool 84 | Whether to accept a dtstart value passed in the input. 85 | 86 | `accept_dtend` : bool 87 | Whether to accept a dtend value passed in the input. 88 | 89 | `max_rrules` : int 90 | The max number of rrules to accept in the input. A 91 | value of ``0`` means input of rrules is disabled. 92 | 93 | `max_exrules` : int 94 | The max number of exrules to accept in the input. A 95 | value of ``0`` means input of exrules is disabled. 96 | 97 | `max_rdates` : int 98 | The max number of rdates to accept in the input. A 99 | value of ``0`` means input of rdates is disabled. 100 | 101 | `max_exdates` : int 102 | The max number of exdates to accept in the input. A 103 | value of ``0`` means input of exdates is disabled. 104 | """ 105 | self.accept_dtstart = accept_dtstart 106 | self.accept_dtend = accept_dtend 107 | self.max_rrules = max_rrules 108 | self.max_exrules = max_exrules 109 | self.max_rdates = max_rdates 110 | self.max_exdates = max_exdates 111 | if frequencies is not None: 112 | self.frequencies = frequencies 113 | else: 114 | self.frequencies = ( 115 | recurrence.YEARLY, recurrence.MONTHLY, 116 | recurrence.WEEKLY, recurrence.DAILY, 117 | recurrence.HOURLY, recurrence.MINUTELY, 118 | recurrence.SECONDLY, 119 | ) 120 | super().__init__(*args, **kwargs) 121 | 122 | def clean(self, value): 123 | """ 124 | Validates that ``value`` deserialized into a 125 | `recurrence.base.Recurrence` object falls within the 126 | parameters specified to the `RecurrenceField` constructor. 127 | """ 128 | try: 129 | recurrence_obj = recurrence.deserialize(value) 130 | except exceptions.DeserializationError as error: 131 | raise forms.ValidationError(error.args[0]) 132 | except TypeError: 133 | return None 134 | if not self.accept_dtstart: 135 | recurrence_obj.dtstart = None 136 | if not self.accept_dtend: 137 | recurrence_obj.dtend = None 138 | 139 | if self.max_rrules is not None: 140 | if len(recurrence_obj.rrules) > self.max_rrules: 141 | raise forms.ValidationError( 142 | self.error_messages['max_rrules_exceeded'] % { 143 | 'limit': self.max_rrules 144 | } 145 | ) 146 | if self.max_exrules is not None: 147 | if len(recurrence_obj.exrules) > self.max_exrules: 148 | raise forms.ValidationError( 149 | self.error_messages['max_exrules_exceeded'] % { 150 | 'limit': self.max_exrules 151 | } 152 | ) 153 | if self.max_rdates is not None: 154 | if len(recurrence_obj.rdates) > self.max_rdates: 155 | raise forms.ValidationError( 156 | self.error_messages['max_rdates_exceeded'] % { 157 | 'limit': self.max_rdates 158 | } 159 | ) 160 | if self.max_exdates is not None: 161 | if len(recurrence_obj.exdates) > self.max_exdates: 162 | raise forms.ValidationError( 163 | self.error_messages['max_exdates_exceeded'] % { 164 | 'limit': self.max_exdates 165 | } 166 | ) 167 | 168 | for rrule in recurrence_obj.rrules: 169 | if rrule.freq not in self.frequencies: 170 | raise forms.ValidationError( 171 | self.error_messages['invalid_frequency']) 172 | for exrule in recurrence_obj.exrules: 173 | if exrule.freq not in self.frequencies: 174 | raise forms.ValidationError( 175 | self.error_messages['invalid_frequency']) 176 | 177 | if self.required: 178 | if not recurrence_obj.rrules and not recurrence_obj.rdates and not recurrence_obj.exdates and not recurrence_obj.exrules: 179 | raise forms.ValidationError( 180 | self.error_messages['recurrence_required'] 181 | ) 182 | 183 | return recurrence_obj 184 | 185 | 186 | _recurrence_javascript_catalog_url = None 187 | 188 | 189 | def find_recurrence_i18n_js_catalog(): 190 | # used cached version 191 | global _recurrence_javascript_catalog_url 192 | if _recurrence_javascript_catalog_url: 193 | return _recurrence_javascript_catalog_url 194 | 195 | # first try to use the dynamic form of the javascript_catalog view 196 | if hasattr(i18n, 'javascript_catalog'): 197 | try: 198 | return urls.reverse( 199 | i18n.javascript_catalog, kwargs={'packages': 'recurrence'}) 200 | except urls.NoReverseMatch: 201 | pass 202 | 203 | # then scan the entire urlconf for a javascript_catalague pattern 204 | # that manually selects recurrence as one of the packages to include 205 | def check_urlpatterns(urlpatterns): 206 | for pattern in urlpatterns: 207 | if hasattr(pattern, 'url_patterns'): 208 | match = check_urlpatterns(pattern.url_patterns) 209 | if match: 210 | return match 211 | elif (hasattr(i18n, 'javascript_catalog') and pattern.callback == i18n.javascript_catalog and 212 | 'recurrence' in pattern.default_args.get('packages', [])): 213 | if pattern.name: 214 | return urls.reverse(pattern.name) 215 | else: 216 | return urls.reverse(pattern.callback) 217 | 218 | root_urlconf = __import__(settings.ROOT_URLCONF, {}, {}, ['']) 219 | url = check_urlpatterns(root_urlconf.urlpatterns) 220 | # cache it for subsequent use 221 | _recurrence_javascript_catalog_url = url 222 | return url 223 | -------------------------------------------------------------------------------- /recurrence/locale/ar/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/ar/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /recurrence/locale/ar/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/ar/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /recurrence/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /recurrence/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Django Recurrence 1.4.1\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-02-24 23:21+0100\n" 11 | "PO-Revision-Date: 2017-02-24 23:22+0100\n" 12 | "Last-Translator: Denis Anuschewski\n" 13 | "Language-Team: \n" 14 | "Language: German\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: base.py:1087 20 | msgid "annually" 21 | msgstr "jährlich" 22 | 23 | #: base.py:1087 24 | msgid "monthly" 25 | msgstr "monatlich" 26 | 27 | #: base.py:1087 28 | msgid "weekly" 29 | msgstr "wöchentlich" 30 | 31 | #: base.py:1087 32 | msgid "daily" 33 | msgstr "täglich" 34 | 35 | #: base.py:1088 36 | msgid "hourly" 37 | msgstr "stündlich" 38 | 39 | #: base.py:1088 40 | msgid "minutely" 41 | msgstr "minütlich" 42 | 43 | #: base.py:1088 44 | msgid "secondly" 45 | msgstr "sekündlich" 46 | 47 | #: base.py:1091 48 | msgid "years" 49 | msgstr "Jahre" 50 | 51 | #: base.py:1091 52 | msgid "months" 53 | msgstr "Monate" 54 | 55 | #: base.py:1091 56 | msgid "weeks" 57 | msgstr "Wochen" 58 | 59 | #: base.py:1091 60 | msgid "days" 61 | msgstr "Tage" 62 | 63 | #: base.py:1092 64 | msgid "hours" 65 | msgstr "Stunden" 66 | 67 | #: base.py:1092 68 | msgid "minutes" 69 | msgstr "Minuten" 70 | 71 | #: base.py:1092 72 | msgid "seconds" 73 | msgstr "Sekunden" 74 | 75 | #: base.py:1097 76 | #, python-format 77 | msgid "1st %(weekday)s" 78 | msgstr "1. %(weekday)s" 79 | 80 | #: base.py:1098 81 | #, python-format 82 | msgid "2nd %(weekday)s" 83 | msgstr "2. %(weekday)s" 84 | 85 | #: base.py:1099 86 | #, python-format 87 | msgid "3rd %(weekday)s" 88 | msgstr "3. %(weekday)s" 89 | 90 | #: base.py:1100 base.py:1126 91 | #, python-format 92 | msgid "last %(weekday)s" 93 | msgstr "letzten %(weekday)s" 94 | 95 | #: base.py:1101 96 | #, python-format 97 | msgid "2nd last %(weekday)s" 98 | msgstr "2. letzten %(weekday)s" 99 | 100 | #: base.py:1102 101 | #, python-format 102 | msgid "3rd last %(weekday)s" 103 | msgstr "3. letzten %(weekday)s" 104 | 105 | #: base.py:1105 base.py:1131 106 | msgid "last" 107 | msgstr "letzten" 108 | 109 | #: base.py:1106 110 | msgid "2nd last" 111 | msgstr "2. letzten" 112 | 113 | #: base.py:1107 114 | msgid "3rd last" 115 | msgstr "3. letzten" 116 | 117 | #: base.py:1108 118 | msgid "4th last" 119 | msgstr "4. letzten" 120 | 121 | #: base.py:1111 122 | msgid "Mon" 123 | msgstr "Mo" 124 | 125 | #: base.py:1111 126 | msgid "Tue" 127 | msgstr "Di" 128 | 129 | #: base.py:1111 130 | msgid "Wed" 131 | msgstr "Mi" 132 | 133 | #: base.py:1112 134 | msgid "Thu" 135 | msgstr "Do" 136 | 137 | #: base.py:1112 138 | msgid "Fri" 139 | msgstr "Fr" 140 | 141 | #: base.py:1112 142 | msgid "Sat" 143 | msgstr "Sa" 144 | 145 | #: base.py:1112 146 | msgid "Sun" 147 | msgstr "So" 148 | 149 | #: base.py:1115 150 | msgid "Jan" 151 | msgstr "Jan" 152 | 153 | #: base.py:1115 154 | msgid "Feb" 155 | msgstr "Feb" 156 | 157 | #: base.py:1115 158 | msgid "Mar" 159 | msgstr "Mrz" 160 | 161 | #: base.py:1115 162 | msgid "Apr" 163 | msgstr "Apr" 164 | 165 | #: base.py:1116 166 | msgid "Jun" 167 | msgstr "Jun" 168 | 169 | #: base.py:1116 170 | msgid "Jul" 171 | msgstr "Jul" 172 | 173 | #: base.py:1116 174 | msgid "Aug" 175 | msgstr "Aug" 176 | 177 | #: base.py:1117 178 | msgid "Sep" 179 | msgstr "Sep" 180 | 181 | #: base.py:1117 182 | msgid "Oct" 183 | msgstr "Okt" 184 | 185 | #: base.py:1117 186 | msgid "Nov" 187 | msgstr "Nov" 188 | 189 | #: base.py:1117 190 | msgid "Dec" 191 | msgstr "Dez" 192 | 193 | #: base.py:1122 194 | #, python-format 195 | msgid "first %(weekday)s" 196 | msgstr "ersten %(weekday)s" 197 | 198 | #: base.py:1123 199 | #, python-format 200 | msgid "second %(weekday)s" 201 | msgstr "zweiten %(weekday)s" 202 | 203 | #: base.py:1124 204 | #, python-format 205 | msgid "third %(weekday)s" 206 | msgstr "dritten %(weekday)s" 207 | 208 | #: base.py:1125 209 | #, python-format 210 | msgid "fourth %(weekday)s" 211 | msgstr "vierten %(weekday)s" 212 | 213 | #: base.py:1127 214 | #, python-format 215 | msgid "second last %(weekday)s" 216 | msgstr "vorletzten %(weekday)s" 217 | 218 | #: base.py:1128 219 | #, python-format 220 | msgid "third last %(weekday)s" 221 | msgstr "drittletzten %(weekday)s" 222 | 223 | #: base.py:1132 224 | msgid "second last" 225 | msgstr "vorletzten" 226 | 227 | #: base.py:1133 228 | msgid "third last" 229 | msgstr "drittletzten" 230 | 231 | #: base.py:1134 232 | msgid "fourth last" 233 | msgstr "viertletzten" 234 | 235 | #: base.py:1137 choices.py:17 236 | msgid "Monday" 237 | msgstr "Montag" 238 | 239 | #: base.py:1137 choices.py:18 240 | msgid "Tuesday" 241 | msgstr "Dienstag" 242 | 243 | #: base.py:1137 choices.py:19 244 | msgid "Wednesday" 245 | msgstr "Mittwoch" 246 | 247 | #: base.py:1138 choices.py:20 248 | msgid "Thursday" 249 | msgstr "Donnerstag" 250 | 251 | #: base.py:1138 choices.py:21 252 | msgid "Friday" 253 | msgstr "Freitag" 254 | 255 | #: base.py:1138 choices.py:22 256 | msgid "Saturday" 257 | msgstr "Samstag" 258 | 259 | #: base.py:1138 choices.py:23 260 | msgid "Sunday" 261 | msgstr "Sonntag" 262 | 263 | #: base.py:1141 choices.py:27 264 | msgid "January" 265 | msgstr "Januar" 266 | 267 | #: base.py:1141 choices.py:28 268 | msgid "February" 269 | msgstr "Februar" 270 | 271 | #: base.py:1141 choices.py:29 272 | msgid "March" 273 | msgstr "März" 274 | 275 | #: base.py:1141 choices.py:30 276 | msgid "April" 277 | msgstr "April" 278 | 279 | #: base.py:1142 choices.py:32 280 | msgid "June" 281 | msgstr "Juni" 282 | 283 | #: base.py:1142 choices.py:33 284 | msgid "July" 285 | msgstr "Juli" 286 | 287 | #: base.py:1142 choices.py:34 288 | msgid "August" 289 | msgstr "August" 290 | 291 | #: base.py:1143 choices.py:35 292 | msgid "September" 293 | msgstr "September" 294 | 295 | #: base.py:1143 choices.py:36 296 | msgid "October" 297 | msgstr "Oktober" 298 | 299 | #: base.py:1143 choices.py:37 300 | msgid "November" 301 | msgstr "November" 302 | 303 | #: base.py:1143 choices.py:38 304 | msgid "December" 305 | msgstr "Dezember" 306 | 307 | #: base.py:1161 base.py:1178 base.py:1189 base.py:1202 base.py:1220 308 | msgid ", " 309 | msgstr "" 310 | 311 | #: base.py:1167 312 | #, python-format 313 | msgid "every %(number)s %(freq)s" 314 | msgstr "alle %(number)s %(freq)s" 315 | 316 | #: base.py:1181 base.py:1205 317 | #, python-format 318 | msgid "each %(items)s" 319 | msgstr "jeden %(items)s" 320 | 321 | #: base.py:1184 base.py:1193 base.py:1197 322 | #, python-format 323 | msgid "on the %(items)s" 324 | msgstr "am %(items)s" 325 | 326 | #: base.py:1212 327 | msgid "occuring once" 328 | msgstr "einmalig" 329 | 330 | #: base.py:1214 331 | #, python-format 332 | msgid "occuring %(number)s times" 333 | msgstr "bis zum %(number)s. Mal" 334 | 335 | #: base.py:1217 336 | #, python-format 337 | msgid "until %(date)s" 338 | msgstr "bis %(date)s" 339 | 340 | #: choices.py:7 341 | msgid "Secondly" 342 | msgstr "Sekündlich" 343 | 344 | #: choices.py:8 345 | msgid "Minutely" 346 | msgstr "Minütlich" 347 | 348 | #: choices.py:9 349 | msgid "Hourly" 350 | msgstr "Stündlich" 351 | 352 | #: choices.py:10 353 | msgid "Daily" 354 | msgstr "Täglich" 355 | 356 | #: choices.py:11 357 | msgid "Weekly" 358 | msgstr "Wöchentlich" 359 | 360 | #: choices.py:12 361 | msgid "Monthly" 362 | msgstr "Monatlich" 363 | 364 | #: choices.py:13 365 | msgid "Yearly" 366 | msgstr "Jährlich" 367 | 368 | #: choices.py:31 369 | msgid "May" 370 | msgstr "Mai" 371 | 372 | #: choices.py:44 373 | msgid "Inclusion" 374 | msgstr "Einschließlich" 375 | 376 | #: choices.py:45 377 | msgid "Exclusion" 378 | msgstr "Ohne" 379 | 380 | #: forms.py:72 381 | msgid "Invalid frequency." 382 | msgstr "Ungültige Frequenz." 383 | 384 | #: forms.py:74 385 | #, python-format 386 | msgid "Max rules exceeded. The limit is %(limit)s" 387 | msgstr "Maximale Anzahl an %(limit)s Regeln ist überschritten." 388 | 389 | #: forms.py:76 390 | #, python-format 391 | msgid "Max exclusion rules exceeded. The limit is %(limit)s" 392 | msgstr "Maximale Anzahl an %(limit)s Ausschlussregeln ist überschritten." 393 | 394 | #: forms.py:78 395 | #, python-format 396 | msgid "Max dates exceeded. The limit is %(limit)s" 397 | msgstr "Maximale Anzahl an %(limit)s Datumsangaben ist überschritten." 398 | 399 | #: forms.py:80 400 | #, python-format 401 | msgid "Max exclusion dates exceeded. The limit is %(limit)s" 402 | msgstr "" 403 | "Maximale Anzahl an %(limit)s ausschließenden Datumsangaben ist überschritten." 404 | 405 | #~ msgid "year" 406 | #~ msgstr "Jahr" 407 | 408 | #~ msgid "month" 409 | #~ msgstr "Monat" 410 | 411 | #~ msgid "week" 412 | #~ msgstr "Woche" 413 | 414 | #~ msgid "day" 415 | #~ msgstr "Tag" 416 | 417 | #~ msgid "hour" 418 | #~ msgstr "Stunde" 419 | 420 | #~ msgid "minute" 421 | #~ msgstr "Minute" 422 | 423 | #~ msgid "second" 424 | #~ msgstr "Sekunde" 425 | -------------------------------------------------------------------------------- /recurrence/locale/de/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/de/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /recurrence/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /recurrence/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-02-24 23:21+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: base.py:1087 21 | msgid "annually" 22 | msgstr "" 23 | 24 | #: base.py:1087 25 | msgid "monthly" 26 | msgstr "" 27 | 28 | #: base.py:1087 29 | msgid "weekly" 30 | msgstr "" 31 | 32 | #: base.py:1087 33 | msgid "daily" 34 | msgstr "" 35 | 36 | #: base.py:1088 37 | msgid "hourly" 38 | msgstr "" 39 | 40 | #: base.py:1088 41 | msgid "minutely" 42 | msgstr "" 43 | 44 | #: base.py:1088 45 | msgid "secondly" 46 | msgstr "" 47 | 48 | #: base.py:1091 49 | msgid "years" 50 | msgstr "" 51 | 52 | #: base.py:1091 53 | msgid "months" 54 | msgstr "" 55 | 56 | #: base.py:1091 57 | msgid "weeks" 58 | msgstr "" 59 | 60 | #: base.py:1091 61 | msgid "days" 62 | msgstr "" 63 | 64 | #: base.py:1092 65 | msgid "hours" 66 | msgstr "" 67 | 68 | #: base.py:1092 69 | msgid "minutes" 70 | msgstr "" 71 | 72 | #: base.py:1092 73 | msgid "seconds" 74 | msgstr "" 75 | 76 | #: base.py:1097 77 | #, python-format 78 | msgid "1st %(weekday)s" 79 | msgstr "" 80 | 81 | #: base.py:1098 82 | #, python-format 83 | msgid "2nd %(weekday)s" 84 | msgstr "" 85 | 86 | #: base.py:1099 87 | #, python-format 88 | msgid "3rd %(weekday)s" 89 | msgstr "" 90 | 91 | #: base.py:1100 base.py:1126 92 | #, python-format 93 | msgid "last %(weekday)s" 94 | msgstr "" 95 | 96 | #: base.py:1101 97 | #, python-format 98 | msgid "2nd last %(weekday)s" 99 | msgstr "" 100 | 101 | #: base.py:1102 102 | #, python-format 103 | msgid "3rd last %(weekday)s" 104 | msgstr "" 105 | 106 | #: base.py:1105 base.py:1131 107 | msgid "last" 108 | msgstr "" 109 | 110 | #: base.py:1106 111 | msgid "2nd last" 112 | msgstr "" 113 | 114 | #: base.py:1107 115 | msgid "3rd last" 116 | msgstr "" 117 | 118 | #: base.py:1108 119 | msgid "4th last" 120 | msgstr "" 121 | 122 | #: base.py:1111 123 | msgid "Mon" 124 | msgstr "" 125 | 126 | #: base.py:1111 127 | msgid "Tue" 128 | msgstr "" 129 | 130 | #: base.py:1111 131 | msgid "Wed" 132 | msgstr "" 133 | 134 | #: base.py:1112 135 | msgid "Thu" 136 | msgstr "" 137 | 138 | #: base.py:1112 139 | msgid "Fri" 140 | msgstr "" 141 | 142 | #: base.py:1112 143 | msgid "Sat" 144 | msgstr "" 145 | 146 | #: base.py:1112 147 | msgid "Sun" 148 | msgstr "" 149 | 150 | #: base.py:1115 151 | msgid "Jan" 152 | msgstr "" 153 | 154 | #: base.py:1115 155 | msgid "Feb" 156 | msgstr "" 157 | 158 | #: base.py:1115 159 | msgid "Mar" 160 | msgstr "" 161 | 162 | #: base.py:1115 163 | msgid "Apr" 164 | msgstr "" 165 | 166 | #: base.py:1116 167 | msgid "Jun" 168 | msgstr "" 169 | 170 | #: base.py:1116 171 | msgid "Jul" 172 | msgstr "" 173 | 174 | #: base.py:1116 175 | msgid "Aug" 176 | msgstr "" 177 | 178 | #: base.py:1117 179 | msgid "Sep" 180 | msgstr "" 181 | 182 | #: base.py:1117 183 | msgid "Oct" 184 | msgstr "" 185 | 186 | #: base.py:1117 187 | msgid "Nov" 188 | msgstr "" 189 | 190 | #: base.py:1117 191 | msgid "Dec" 192 | msgstr "" 193 | 194 | #: base.py:1122 195 | #, python-format 196 | msgid "first %(weekday)s" 197 | msgstr "" 198 | 199 | #: base.py:1123 200 | #, python-format 201 | msgid "second %(weekday)s" 202 | msgstr "" 203 | 204 | #: base.py:1124 205 | #, python-format 206 | msgid "third %(weekday)s" 207 | msgstr "" 208 | 209 | #: base.py:1125 210 | #, python-format 211 | msgid "fourth %(weekday)s" 212 | msgstr "" 213 | 214 | #: base.py:1127 215 | #, python-format 216 | msgid "second last %(weekday)s" 217 | msgstr "" 218 | 219 | #: base.py:1128 220 | #, python-format 221 | msgid "third last %(weekday)s" 222 | msgstr "" 223 | 224 | #: base.py:1132 225 | msgid "second last" 226 | msgstr "" 227 | 228 | #: base.py:1133 229 | msgid "third last" 230 | msgstr "" 231 | 232 | #: base.py:1134 233 | msgid "fourth last" 234 | msgstr "" 235 | 236 | #: base.py:1137 choices.py:17 237 | msgid "Monday" 238 | msgstr "" 239 | 240 | #: base.py:1137 choices.py:18 241 | msgid "Tuesday" 242 | msgstr "" 243 | 244 | #: base.py:1137 choices.py:19 245 | msgid "Wednesday" 246 | msgstr "" 247 | 248 | #: base.py:1138 choices.py:20 249 | msgid "Thursday" 250 | msgstr "" 251 | 252 | #: base.py:1138 choices.py:21 253 | msgid "Friday" 254 | msgstr "" 255 | 256 | #: base.py:1138 choices.py:22 257 | msgid "Saturday" 258 | msgstr "" 259 | 260 | #: base.py:1138 choices.py:23 261 | msgid "Sunday" 262 | msgstr "" 263 | 264 | #: base.py:1141 choices.py:27 265 | msgid "January" 266 | msgstr "" 267 | 268 | #: base.py:1141 choices.py:28 269 | msgid "February" 270 | msgstr "" 271 | 272 | #: base.py:1141 choices.py:29 273 | msgid "March" 274 | msgstr "" 275 | 276 | #: base.py:1141 choices.py:30 277 | msgid "April" 278 | msgstr "" 279 | 280 | #: base.py:1142 choices.py:32 281 | msgid "June" 282 | msgstr "" 283 | 284 | #: base.py:1142 choices.py:33 285 | msgid "July" 286 | msgstr "" 287 | 288 | #: base.py:1142 choices.py:34 289 | msgid "August" 290 | msgstr "" 291 | 292 | #: base.py:1143 choices.py:35 293 | msgid "September" 294 | msgstr "" 295 | 296 | #: base.py:1143 choices.py:36 297 | msgid "October" 298 | msgstr "" 299 | 300 | #: base.py:1143 choices.py:37 301 | msgid "November" 302 | msgstr "" 303 | 304 | #: base.py:1143 choices.py:38 305 | msgid "December" 306 | msgstr "" 307 | 308 | #: base.py:1161 base.py:1178 base.py:1189 base.py:1202 base.py:1220 309 | msgid ", " 310 | msgstr "" 311 | 312 | #: base.py:1167 313 | #, python-format 314 | msgid "every %(number)s %(freq)s" 315 | msgstr "" 316 | 317 | #: base.py:1181 base.py:1205 318 | #, python-format 319 | msgid "each %(items)s" 320 | msgstr "" 321 | 322 | #: base.py:1184 base.py:1193 base.py:1197 323 | #, python-format 324 | msgid "on the %(items)s" 325 | msgstr "" 326 | 327 | #: base.py:1212 328 | msgid "occuring once" 329 | msgstr "" 330 | 331 | #: base.py:1214 332 | #, python-format 333 | msgid "occuring %(number)s times" 334 | msgstr "" 335 | 336 | #: base.py:1217 337 | #, python-format 338 | msgid "until %(date)s" 339 | msgstr "" 340 | 341 | #: choices.py:7 342 | msgid "Secondly" 343 | msgstr "" 344 | 345 | #: choices.py:8 346 | msgid "Minutely" 347 | msgstr "" 348 | 349 | #: choices.py:9 350 | msgid "Hourly" 351 | msgstr "" 352 | 353 | #: choices.py:10 354 | msgid "Daily" 355 | msgstr "" 356 | 357 | #: choices.py:11 358 | msgid "Weekly" 359 | msgstr "" 360 | 361 | #: choices.py:12 362 | msgid "Monthly" 363 | msgstr "" 364 | 365 | #: choices.py:13 366 | msgid "Yearly" 367 | msgstr "" 368 | 369 | #: choices.py:31 370 | msgid "May" 371 | msgstr "" 372 | 373 | #: choices.py:44 374 | msgid "Inclusion" 375 | msgstr "" 376 | 377 | #: choices.py:45 378 | msgid "Exclusion" 379 | msgstr "" 380 | 381 | #: forms.py:72 382 | msgid "Invalid frequency." 383 | msgstr "" 384 | 385 | #: forms.py:74 386 | #, python-format 387 | msgid "Max rules exceeded. The limit is %(limit)s" 388 | msgstr "" 389 | 390 | #: forms.py:76 391 | #, python-format 392 | msgid "Max exclusion rules exceeded. The limit is %(limit)s" 393 | msgstr "" 394 | 395 | #: forms.py:78 396 | #, python-format 397 | msgid "Max dates exceeded. The limit is %(limit)s" 398 | msgstr "" 399 | 400 | #: forms.py:80 401 | #, python-format 402 | msgid "Max exclusion dates exceeded. The limit is %(limit)s" 403 | msgstr "" 404 | -------------------------------------------------------------------------------- /recurrence/locale/en/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/en/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /recurrence/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /recurrence/locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # FULL NAME , 2015. 6 | # 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-02-24 23:21+0100\n" 12 | "PO-Revision-Date: 2017-02-24 23:22+0100\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "X-Generator: Gtranslator 2.91.7\n" 20 | 21 | #: base.py:1087 22 | msgid "annually" 23 | msgstr "anualmente" 24 | 25 | #: base.py:1087 26 | msgid "monthly" 27 | msgstr "mensualmente" 28 | 29 | #: base.py:1087 30 | msgid "weekly" 31 | msgstr "semanalmente" 32 | 33 | #: base.py:1087 34 | msgid "daily" 35 | msgstr "diario" 36 | 37 | #: base.py:1088 38 | msgid "hourly" 39 | msgstr "cada hora" 40 | 41 | #: base.py:1088 42 | msgid "minutely" 43 | msgstr "cada minuto" 44 | 45 | #: base.py:1088 46 | msgid "secondly" 47 | msgstr "cada segundo" 48 | 49 | #: base.py:1091 50 | msgid "years" 51 | msgstr "años" 52 | 53 | #: base.py:1091 54 | msgid "months" 55 | msgstr "meses" 56 | 57 | #: base.py:1091 58 | msgid "weeks" 59 | msgstr "semanas" 60 | 61 | #: base.py:1091 62 | msgid "days" 63 | msgstr "días" 64 | 65 | #: base.py:1092 66 | msgid "hours" 67 | msgstr "horas" 68 | 69 | #: base.py:1092 70 | msgid "minutes" 71 | msgstr "minutos" 72 | 73 | #: base.py:1092 74 | msgid "seconds" 75 | msgstr "segundos" 76 | 77 | #: base.py:1097 78 | #, python-format 79 | msgid "1st %(weekday)s" 80 | msgstr "1º %(weekday)s" 81 | 82 | #: base.py:1098 83 | #, python-format 84 | msgid "2nd %(weekday)s" 85 | msgstr "2º %(weekday)s" 86 | 87 | #: base.py:1099 88 | #, python-format 89 | msgid "3rd %(weekday)s" 90 | msgstr "3º %(weekday)s" 91 | 92 | #: base.py:1100 base.py:1126 93 | #, python-format 94 | msgid "last %(weekday)s" 95 | msgstr "último %(weekday)s" 96 | 97 | #: base.py:1101 98 | #, python-format 99 | msgid "2nd last %(weekday)s" 100 | msgstr "2º último %(weekday)s" 101 | 102 | #: base.py:1102 103 | #, python-format 104 | msgid "3rd last %(weekday)s" 105 | msgstr "3º último %(weekday)s" 106 | 107 | #: base.py:1105 base.py:1131 108 | msgid "last" 109 | msgstr "último" 110 | 111 | #: base.py:1106 112 | msgid "2nd last" 113 | msgstr "2º último" 114 | 115 | #: base.py:1107 116 | msgid "3rd last" 117 | msgstr "3º último" 118 | 119 | #: base.py:1108 120 | msgid "4th last" 121 | msgstr "4º último" 122 | 123 | #: base.py:1111 124 | msgid "Mon" 125 | msgstr "Lun" 126 | 127 | #: base.py:1111 128 | msgid "Tue" 129 | msgstr "Mar" 130 | 131 | #: base.py:1111 132 | msgid "Wed" 133 | msgstr "Mier" 134 | 135 | #: base.py:1112 136 | msgid "Thu" 137 | msgstr "Jue" 138 | 139 | #: base.py:1112 140 | msgid "Fri" 141 | msgstr "Vie" 142 | 143 | #: base.py:1112 144 | msgid "Sat" 145 | msgstr "Sab" 146 | 147 | #: base.py:1112 148 | msgid "Sun" 149 | msgstr "Dom" 150 | 151 | #: base.py:1115 152 | msgid "Jan" 153 | msgstr "Ene" 154 | 155 | #: base.py:1115 156 | msgid "Feb" 157 | msgstr "Feb" 158 | 159 | #: base.py:1115 160 | msgid "Mar" 161 | msgstr "Mar" 162 | 163 | #: base.py:1115 164 | msgid "Apr" 165 | msgstr "Abr" 166 | 167 | #: base.py:1116 168 | msgid "Jun" 169 | msgstr "Jun" 170 | 171 | #: base.py:1116 172 | msgid "Jul" 173 | msgstr "Jul" 174 | 175 | #: base.py:1116 176 | msgid "Aug" 177 | msgstr "Ago" 178 | 179 | #: base.py:1117 180 | msgid "Sep" 181 | msgstr "Sep" 182 | 183 | #: base.py:1117 184 | msgid "Oct" 185 | msgstr "Oct" 186 | 187 | #: base.py:1117 188 | msgid "Nov" 189 | msgstr "Nov" 190 | 191 | #: base.py:1117 192 | msgid "Dec" 193 | msgstr "Dic" 194 | 195 | #: base.py:1122 196 | #, python-format 197 | msgid "first %(weekday)s" 198 | msgstr "primer %(weekday)s" 199 | 200 | #: base.py:1123 201 | #, python-format 202 | msgid "second %(weekday)s" 203 | msgstr "segundo %(weekday)s" 204 | 205 | #: base.py:1124 206 | #, python-format 207 | msgid "third %(weekday)s" 208 | msgstr "tercero %(weekday)s" 209 | 210 | #: base.py:1125 211 | #, python-format 212 | msgid "fourth %(weekday)s" 213 | msgstr "cuarto %(weekday)s" 214 | 215 | #: base.py:1127 216 | #, python-format 217 | msgid "second last %(weekday)s" 218 | msgstr "segundo último %(weekday)s" 219 | 220 | #: base.py:1128 221 | #, python-format 222 | msgid "third last %(weekday)s" 223 | msgstr "tercer último %(weekday)s" 224 | 225 | #: base.py:1132 226 | msgid "second last" 227 | msgstr "penúltimo" 228 | 229 | #: base.py:1133 230 | msgid "third last" 231 | msgstr "antepenúltimo" 232 | 233 | #: base.py:1134 234 | msgid "fourth last" 235 | msgstr "cuarto último" 236 | 237 | #: base.py:1137 choices.py:17 238 | msgid "Monday" 239 | msgstr "Lunes" 240 | 241 | #: base.py:1137 choices.py:18 242 | msgid "Tuesday" 243 | msgstr "Martes" 244 | 245 | #: base.py:1137 choices.py:19 246 | msgid "Wednesday" 247 | msgstr "Miércoles" 248 | 249 | #: base.py:1138 choices.py:20 250 | msgid "Thursday" 251 | msgstr "Jueves" 252 | 253 | #: base.py:1138 choices.py:21 254 | msgid "Friday" 255 | msgstr "Viernes" 256 | 257 | #: base.py:1138 choices.py:22 258 | msgid "Saturday" 259 | msgstr "Sábado" 260 | 261 | #: base.py:1138 choices.py:23 262 | msgid "Sunday" 263 | msgstr "Domingo" 264 | 265 | #: base.py:1141 choices.py:27 266 | msgid "January" 267 | msgstr "Enero" 268 | 269 | #: base.py:1141 choices.py:28 270 | msgid "February" 271 | msgstr "Febrero" 272 | 273 | #: base.py:1141 choices.py:29 274 | msgid "March" 275 | msgstr "Marzo" 276 | 277 | #: base.py:1141 choices.py:30 278 | msgid "April" 279 | msgstr "Abril" 280 | 281 | #: base.py:1142 choices.py:32 282 | msgid "June" 283 | msgstr "Junio" 284 | 285 | #: base.py:1142 choices.py:33 286 | msgid "July" 287 | msgstr "Julio" 288 | 289 | #: base.py:1142 choices.py:34 290 | msgid "August" 291 | msgstr "Agosto" 292 | 293 | #: base.py:1143 choices.py:35 294 | msgid "September" 295 | msgstr "Septiembre" 296 | 297 | #: base.py:1143 choices.py:36 298 | msgid "October" 299 | msgstr "Octubre" 300 | 301 | #: base.py:1143 choices.py:37 302 | msgid "November" 303 | msgstr "Noviembre " 304 | 305 | #: base.py:1143 choices.py:38 306 | msgid "December" 307 | msgstr "Diciembre" 308 | 309 | #: base.py:1161 base.py:1178 base.py:1189 base.py:1202 base.py:1220 310 | msgid ", " 311 | msgstr ", " 312 | 313 | #: base.py:1167 314 | #, python-format 315 | msgid "every %(number)s %(freq)s" 316 | msgstr "cada %(number)s %(freq)s" 317 | 318 | #: base.py:1181 base.py:1205 319 | #, python-format 320 | msgid "each %(items)s" 321 | msgstr "cada %(items)s" 322 | 323 | #: base.py:1184 base.py:1193 base.py:1197 324 | #, python-format 325 | msgid "on the %(items)s" 326 | msgstr "el %(items)s" 327 | 328 | #: base.py:1212 329 | msgid "occuring once" 330 | msgstr "ocurre una vez" 331 | 332 | #: base.py:1214 333 | #, python-format 334 | msgid "occuring %(number)s times" 335 | msgstr "ocurre %(number)s veces" 336 | 337 | #: base.py:1217 338 | #, python-format 339 | msgid "until %(date)s" 340 | msgstr "hasta %(date)s" 341 | 342 | #: choices.py:7 343 | msgid "Secondly" 344 | msgstr "Cada segundo" 345 | 346 | #: choices.py:8 347 | msgid "Minutely" 348 | msgstr "Cada minuto" 349 | 350 | #: choices.py:9 351 | msgid "Hourly" 352 | msgstr "Cada hora" 353 | 354 | #: choices.py:10 355 | msgid "Daily" 356 | msgstr "A diario" 357 | 358 | #: choices.py:11 359 | msgid "Weekly" 360 | msgstr "Semanalmente" 361 | 362 | #: choices.py:12 363 | msgid "Monthly" 364 | msgstr "Mensualmente" 365 | 366 | #: choices.py:13 367 | msgid "Yearly" 368 | msgstr "Anualmente" 369 | 370 | #: choices.py:31 371 | msgid "May" 372 | msgstr "Mayo" 373 | 374 | #: choices.py:44 375 | msgid "Inclusion" 376 | msgstr "Inclusión" 377 | 378 | #: choices.py:45 379 | msgid "Exclusion" 380 | msgstr "Exclusión" 381 | 382 | #: forms.py:72 383 | msgid "Invalid frequency." 384 | msgstr "Frecuencia incorrecta" 385 | 386 | #: forms.py:74 387 | #, python-format 388 | msgid "Max rules exceeded. The limit is %(limit)s" 389 | msgstr "Número máximo de reglas excedidas. El límite es %(limit)s" 390 | 391 | #: forms.py:76 392 | #, python-format 393 | msgid "Max exclusion rules exceeded. The limit is %(limit)s" 394 | msgstr "Máximo de reglas de exclusión excedidas. El límite es %(limit)s" 395 | 396 | #: forms.py:78 397 | #, python-format 398 | msgid "Max dates exceeded. The limit is %(limit)s" 399 | msgstr "Número máximo de fecha excedidas. El límite es %(limit)s" 400 | 401 | #: forms.py:80 402 | #, python-format 403 | msgid "Max exclusion dates exceeded. The limit is %(limit)s" 404 | msgstr "Máximo de fechas de exclusión excedidas. El límite es %(limit)s" 405 | 406 | #~ msgid "year" 407 | #~ msgstr "año" 408 | 409 | #~ msgid "month" 410 | #~ msgstr "mes" 411 | 412 | #~ msgid "week" 413 | #~ msgstr "semana" 414 | 415 | #~ msgid "day" 416 | #~ msgstr "día" 417 | 418 | #~ msgid "hour" 419 | #~ msgstr "hora" 420 | 421 | #~ msgid "minute" 422 | #~ msgstr "minuto" 423 | 424 | #~ msgid "second" 425 | #~ msgstr "segundo" 426 | -------------------------------------------------------------------------------- /recurrence/locale/es/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/es/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /recurrence/locale/eu/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/eu/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /recurrence/locale/eu/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Basque translations for django-recurrence 2 | # Copyright (C) Tamas Kemenczy and individual contributors 3 | # This file is distributed under the same license as the django-recurrence package. 4 | # Unai Zalakain , 2016. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 1.3.0\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-02-24 23:21+0100\n" 11 | "PO-Revision-Date: 2016-05-14 19:21+0100\n" 12 | "Last-Translator: Unai Zalakain \n" 13 | "Language: eu\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 18 | 19 | #: base.py:1087 20 | msgid "annually" 21 | msgstr "urtero" 22 | 23 | #: base.py:1087 24 | msgid "monthly" 25 | msgstr "hilabetero" 26 | 27 | #: base.py:1087 28 | msgid "weekly" 29 | msgstr "astero" 30 | 31 | #: base.py:1087 32 | msgid "daily" 33 | msgstr "egunero" 34 | 35 | #: base.py:1088 36 | msgid "hourly" 37 | msgstr "orduro" 38 | 39 | #: base.py:1088 40 | msgid "minutely" 41 | msgstr "minuturo" 42 | 43 | #: base.py:1088 44 | msgid "secondly" 45 | msgstr "segunduro" 46 | 47 | #: base.py:1091 48 | msgid "years" 49 | msgstr "urteak" 50 | 51 | #: base.py:1091 52 | msgid "months" 53 | msgstr "hilabeteak" 54 | 55 | #: base.py:1091 56 | msgid "weeks" 57 | msgstr "asteak" 58 | 59 | #: base.py:1091 60 | msgid "days" 61 | msgstr "egunak" 62 | 63 | #: base.py:1092 64 | msgid "hours" 65 | msgstr "orduak" 66 | 67 | #: base.py:1092 68 | msgid "minutes" 69 | msgstr "minutuak" 70 | 71 | #: base.py:1092 72 | msgid "seconds" 73 | msgstr "segunduak" 74 | 75 | #: base.py:1097 76 | #, python-format 77 | msgid "1st %(weekday)s" 78 | msgstr "1. %(weekday)s" 79 | 80 | #: base.py:1098 81 | #, python-format 82 | msgid "2nd %(weekday)s" 83 | msgstr "2. %(weekday)s" 84 | 85 | #: base.py:1099 86 | #, python-format 87 | msgid "3rd %(weekday)s" 88 | msgstr "3. %(weekday)s" 89 | 90 | #: base.py:1100 base.py:1126 91 | #, python-format 92 | msgid "last %(weekday)s" 93 | msgstr "azken %(weekday)s" 94 | 95 | #: base.py:1101 96 | #, python-format 97 | msgid "2nd last %(weekday)s" 98 | msgstr "azkenaurreko %(weekday)s" 99 | 100 | #: base.py:1102 101 | #, python-format 102 | msgid "3rd last %(weekday)s" 103 | msgstr "azken-hirugarren %(weekday)s" 104 | 105 | #: base.py:1105 base.py:1131 106 | #, fuzzy 107 | #| msgid "last %(weekday)s" 108 | msgid "last" 109 | msgstr "azken %(weekday)s" 110 | 111 | #: base.py:1106 112 | #, fuzzy 113 | #| msgid "2nd last %(weekday)s" 114 | msgid "2nd last" 115 | msgstr "azkenaurreko %(weekday)s" 116 | 117 | #: base.py:1107 118 | #, fuzzy 119 | #| msgid "3rd last %(weekday)s" 120 | msgid "3rd last" 121 | msgstr "azken-hirugarren %(weekday)s" 122 | 123 | #: base.py:1108 124 | #, fuzzy 125 | #| msgid "third last %(weekday)s" 126 | msgid "4th last" 127 | msgstr "azken-hirugarren %(weekday)s" 128 | 129 | #: base.py:1111 130 | msgid "Mon" 131 | msgstr "Astl" 132 | 133 | #: base.py:1111 134 | msgid "Tue" 135 | msgstr "Astr" 136 | 137 | #: base.py:1111 138 | msgid "Wed" 139 | msgstr "Astz" 140 | 141 | #: base.py:1112 142 | msgid "Thu" 143 | msgstr "Ostg" 144 | 145 | #: base.py:1112 146 | msgid "Fri" 147 | msgstr "Osti" 148 | 149 | #: base.py:1112 150 | msgid "Sat" 151 | msgstr "Lar" 152 | 153 | #: base.py:1112 154 | msgid "Sun" 155 | msgstr "Iga" 156 | 157 | #: base.py:1115 158 | msgid "Jan" 159 | msgstr "Urt" 160 | 161 | #: base.py:1115 162 | msgid "Feb" 163 | msgstr "Ots" 164 | 165 | #: base.py:1115 166 | msgid "Mar" 167 | msgstr "Mar" 168 | 169 | #: base.py:1115 170 | msgid "Apr" 171 | msgstr "Api" 172 | 173 | #: base.py:1116 174 | msgid "Jun" 175 | msgstr "Eka" 176 | 177 | #: base.py:1116 178 | msgid "Jul" 179 | msgstr "Uzt" 180 | 181 | #: base.py:1116 182 | msgid "Aug" 183 | msgstr "Abu" 184 | 185 | #: base.py:1117 186 | msgid "Sep" 187 | msgstr "Ira" 188 | 189 | #: base.py:1117 190 | msgid "Oct" 191 | msgstr "Urr" 192 | 193 | #: base.py:1117 194 | msgid "Nov" 195 | msgstr "Aza" 196 | 197 | #: base.py:1117 198 | msgid "Dec" 199 | msgstr "Abe" 200 | 201 | #: base.py:1122 202 | #, python-format 203 | msgid "first %(weekday)s" 204 | msgstr "lehen %(weekday)s" 205 | 206 | #: base.py:1123 207 | #, python-format 208 | msgid "second %(weekday)s" 209 | msgstr "bigarren %(weekday)s" 210 | 211 | #: base.py:1124 212 | #, python-format 213 | msgid "third %(weekday)s" 214 | msgstr "hirugarren %(weekday)s" 215 | 216 | #: base.py:1125 217 | #, python-format 218 | msgid "fourth %(weekday)s" 219 | msgstr "laugarren %(weekday)s" 220 | 221 | #: base.py:1127 222 | #, python-format 223 | msgid "second last %(weekday)s" 224 | msgstr "azkenaurreko %(weekday)s" 225 | 226 | #: base.py:1128 227 | #, python-format 228 | msgid "third last %(weekday)s" 229 | msgstr "azken-hirugarren %(weekday)s" 230 | 231 | #: base.py:1132 232 | #, fuzzy 233 | #| msgid "second last %(weekday)s" 234 | msgid "second last" 235 | msgstr "azkenaurreko %(weekday)s" 236 | 237 | #: base.py:1133 238 | #, fuzzy 239 | #| msgid "third last %(weekday)s" 240 | msgid "third last" 241 | msgstr "azken-hirugarren %(weekday)s" 242 | 243 | #: base.py:1134 244 | #, fuzzy 245 | #| msgid "fourth %(weekday)s" 246 | msgid "fourth last" 247 | msgstr "laugarren %(weekday)s" 248 | 249 | #: base.py:1137 choices.py:17 250 | msgid "Monday" 251 | msgstr "Astelehena" 252 | 253 | #: base.py:1137 choices.py:18 254 | msgid "Tuesday" 255 | msgstr "Asteartea" 256 | 257 | #: base.py:1137 choices.py:19 258 | msgid "Wednesday" 259 | msgstr "Asteazkena" 260 | 261 | #: base.py:1138 choices.py:20 262 | msgid "Thursday" 263 | msgstr "Osteguna" 264 | 265 | #: base.py:1138 choices.py:21 266 | msgid "Friday" 267 | msgstr "Ostirala" 268 | 269 | #: base.py:1138 choices.py:22 270 | msgid "Saturday" 271 | msgstr "Larunbata" 272 | 273 | #: base.py:1138 choices.py:23 274 | msgid "Sunday" 275 | msgstr "Igandea" 276 | 277 | #: base.py:1141 choices.py:27 278 | msgid "January" 279 | msgstr "Urtarrila" 280 | 281 | #: base.py:1141 choices.py:28 282 | msgid "February" 283 | msgstr "Otsaila" 284 | 285 | #: base.py:1141 choices.py:29 286 | msgid "March" 287 | msgstr "Martxoa" 288 | 289 | #: base.py:1141 choices.py:30 290 | msgid "April" 291 | msgstr "Apirila" 292 | 293 | #: base.py:1142 choices.py:32 294 | msgid "June" 295 | msgstr "Ekaina" 296 | 297 | #: base.py:1142 choices.py:33 298 | msgid "July" 299 | msgstr "Uztaila" 300 | 301 | #: base.py:1142 choices.py:34 302 | msgid "August" 303 | msgstr "Abuztua" 304 | 305 | #: base.py:1143 choices.py:35 306 | msgid "September" 307 | msgstr "Iraila" 308 | 309 | #: base.py:1143 choices.py:36 310 | msgid "October" 311 | msgstr "Urria" 312 | 313 | #: base.py:1143 choices.py:37 314 | msgid "November" 315 | msgstr "Azaroa" 316 | 317 | #: base.py:1143 choices.py:38 318 | msgid "December" 319 | msgstr "Abendua" 320 | 321 | #: base.py:1161 base.py:1178 base.py:1189 base.py:1202 base.py:1220 322 | msgid ", " 323 | msgstr ", " 324 | 325 | #: base.py:1167 326 | #, python-format 327 | msgid "every %(number)s %(freq)s" 328 | msgstr "%(number)s %(freq)sero" 329 | 330 | #: base.py:1181 base.py:1205 331 | #, python-format 332 | msgid "each %(items)s" 333 | msgstr "%(items)sero" 334 | 335 | #: base.py:1184 base.py:1193 base.py:1197 336 | #, python-format 337 | msgid "on the %(items)s" 338 | msgstr "%(items)setan" 339 | 340 | #: base.py:1212 341 | msgid "occuring once" 342 | msgstr "behin gertatzen" 343 | 344 | #: base.py:1214 345 | #, python-format 346 | msgid "occuring %(number)s times" 347 | msgstr "%(number)stan gertatzen" 348 | 349 | #: base.py:1217 350 | #, python-format 351 | msgid "until %(date)s" 352 | msgstr "%(date)s arte" 353 | 354 | #: choices.py:7 355 | msgid "Secondly" 356 | msgstr "Segunduro" 357 | 358 | #: choices.py:8 359 | msgid "Minutely" 360 | msgstr "Minuturo" 361 | 362 | #: choices.py:9 363 | msgid "Hourly" 364 | msgstr "Orduro" 365 | 366 | #: choices.py:10 367 | msgid "Daily" 368 | msgstr "Egunero" 369 | 370 | #: choices.py:11 371 | msgid "Weekly" 372 | msgstr "Astero" 373 | 374 | #: choices.py:12 375 | msgid "Monthly" 376 | msgstr "Hilabetero" 377 | 378 | #: choices.py:13 379 | msgid "Yearly" 380 | msgstr "Urtero" 381 | 382 | #: choices.py:31 383 | msgid "May" 384 | msgstr "Maiatza" 385 | 386 | #: choices.py:44 387 | msgid "Inclusion" 388 | msgstr "Inklusio" 389 | 390 | #: choices.py:45 391 | msgid "Exclusion" 392 | msgstr "Exklusio" 393 | 394 | #: forms.py:72 395 | msgid "Invalid frequency." 396 | msgstr "Maiztasun baliogabea." 397 | 398 | #: forms.py:74 399 | #, python-format 400 | msgid "Max rules exceeded. The limit is %(limit)s" 401 | msgstr "Erregelen kopuru maximoa gainditua. Muga %(limit)s da" 402 | 403 | #: forms.py:76 404 | #, python-format 405 | msgid "Max exclusion rules exceeded. The limit is %(limit)s" 406 | msgstr "Exklusio erregelen kopuru maximoa gainditua. Muga %(limit)s da" 407 | 408 | #: forms.py:78 409 | #, python-format 410 | msgid "Max dates exceeded. The limit is %(limit)s" 411 | msgstr "Daten kopuru maximoa gainditua. Muga %(limit)s da" 412 | 413 | #: forms.py:80 414 | #, python-format 415 | msgid "Max exclusion dates exceeded. The limit is %(limit)s" 416 | msgstr "Exklusio daten kopuru maximoa gainditua. Muga %(limit)s da" 417 | -------------------------------------------------------------------------------- /recurrence/locale/eu/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/eu/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /recurrence/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /recurrence/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PACKAGE VERSION\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-02-24 23:21+0100\n" 11 | "PO-Revision-Date: 2017-02-24 23:23+0100\n" 12 | "Last-Translator: Olivier Le Brouster \n" 13 | "Language-Team: LANGUAGE \n" 14 | "Language: \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | "X-Generator: Poedit 1.5.4\n" 20 | 21 | #: base.py:1087 22 | msgid "annually" 23 | msgstr "annuel" 24 | 25 | #: base.py:1087 26 | msgid "monthly" 27 | msgstr "mensuellement" 28 | 29 | #: base.py:1087 30 | msgid "weekly" 31 | msgstr "hebdomadaire" 32 | 33 | #: base.py:1087 34 | msgid "daily" 35 | msgstr "quotidien" 36 | 37 | #: base.py:1088 38 | msgid "hourly" 39 | msgstr "toutes les heures" 40 | 41 | #: base.py:1088 42 | msgid "minutely" 43 | msgstr "toutes les minutes" 44 | 45 | #: base.py:1088 46 | msgid "secondly" 47 | msgstr "toutes les secondes" 48 | 49 | #: base.py:1091 50 | msgid "years" 51 | msgstr "années" 52 | 53 | #: base.py:1091 54 | msgid "months" 55 | msgstr "mois" 56 | 57 | #: base.py:1091 58 | msgid "weeks" 59 | msgstr "semaines" 60 | 61 | #: base.py:1091 62 | msgid "days" 63 | msgstr "jours" 64 | 65 | #: base.py:1092 66 | msgid "hours" 67 | msgstr "heures" 68 | 69 | #: base.py:1092 70 | msgid "minutes" 71 | msgstr "minutes" 72 | 73 | #: base.py:1092 74 | msgid "seconds" 75 | msgstr "secondes" 76 | 77 | #: base.py:1097 78 | #, python-format 79 | msgid "1st %(weekday)s" 80 | msgstr "1er %(weekday)s" 81 | 82 | #: base.py:1098 83 | #, python-format 84 | msgid "2nd %(weekday)s" 85 | msgstr "2ème %(weekday)s" 86 | 87 | #: base.py:1099 88 | #, python-format 89 | msgid "3rd %(weekday)s" 90 | msgstr "3ème %(weekday)s" 91 | 92 | #: base.py:1100 base.py:1126 93 | #, python-format 94 | msgid "last %(weekday)s" 95 | msgstr "dernier %(weekday)s" 96 | 97 | #: base.py:1101 98 | #, python-format 99 | msgid "2nd last %(weekday)s" 100 | msgstr "avant dernier %(weekday)s" 101 | 102 | #: base.py:1102 103 | #, python-format 104 | msgid "3rd last %(weekday)s" 105 | msgstr "3ème dernier %(weekday)s" 106 | 107 | #: base.py:1105 base.py:1131 108 | msgid "last" 109 | msgstr "dernier" 110 | 111 | #: base.py:1106 112 | msgid "2nd last" 113 | msgstr "2ème dernier" 114 | 115 | #: base.py:1107 116 | msgid "3rd last" 117 | msgstr "3ème dernier" 118 | 119 | #: base.py:1108 120 | msgid "4th last" 121 | msgstr "4ème dernier" 122 | 123 | #: base.py:1111 124 | msgid "Mon" 125 | msgstr "Lun" 126 | 127 | #: base.py:1111 128 | msgid "Tue" 129 | msgstr "Mar" 130 | 131 | #: base.py:1111 132 | msgid "Wed" 133 | msgstr "Mer" 134 | 135 | #: base.py:1112 136 | msgid "Thu" 137 | msgstr "Jeu" 138 | 139 | #: base.py:1112 140 | msgid "Fri" 141 | msgstr "Ven" 142 | 143 | #: base.py:1112 144 | msgid "Sat" 145 | msgstr "Sam" 146 | 147 | #: base.py:1112 148 | msgid "Sun" 149 | msgstr "Dim" 150 | 151 | #: base.py:1115 152 | msgid "Jan" 153 | msgstr "Jan" 154 | 155 | #: base.py:1115 156 | msgid "Feb" 157 | msgstr "Fév" 158 | 159 | #: base.py:1115 160 | msgid "Mar" 161 | msgstr "Mar" 162 | 163 | #: base.py:1115 164 | msgid "Apr" 165 | msgstr "Avr" 166 | 167 | #: base.py:1116 168 | msgid "Jun" 169 | msgstr "Juin" 170 | 171 | #: base.py:1116 172 | msgid "Jul" 173 | msgstr "Juil" 174 | 175 | #: base.py:1116 176 | msgid "Aug" 177 | msgstr "Aoû" 178 | 179 | #: base.py:1117 180 | msgid "Sep" 181 | msgstr "Sep" 182 | 183 | #: base.py:1117 184 | msgid "Oct" 185 | msgstr "Oct" 186 | 187 | #: base.py:1117 188 | msgid "Nov" 189 | msgstr "Nov" 190 | 191 | #: base.py:1117 192 | msgid "Dec" 193 | msgstr "Déc" 194 | 195 | #: base.py:1122 196 | #, python-format 197 | msgid "first %(weekday)s" 198 | msgstr "premier %(weekday)s" 199 | 200 | #: base.py:1123 201 | #, python-format 202 | msgid "second %(weekday)s" 203 | msgstr "deuxième %(weekday)s" 204 | 205 | #: base.py:1124 206 | #, python-format 207 | msgid "third %(weekday)s" 208 | msgstr "troisième %(weekday)s" 209 | 210 | #: base.py:1125 211 | #, python-format 212 | msgid "fourth %(weekday)s" 213 | msgstr "quatrième %(weekday)s" 214 | 215 | #: base.py:1127 216 | #, python-format 217 | msgid "second last %(weekday)s" 218 | msgstr "avant dernier %(weekday)s" 219 | 220 | #: base.py:1128 221 | #, python-format 222 | msgid "third last %(weekday)s" 223 | msgstr "troisième dernier %(weekday)s" 224 | 225 | #: base.py:1132 226 | msgid "second last" 227 | msgstr "pénultième" 228 | 229 | #: base.py:1133 230 | msgid "third last" 231 | msgstr "antépénultième" 232 | 233 | #: base.py:1134 234 | msgid "fourth last" 235 | msgstr "quatrième dernier" 236 | 237 | #: base.py:1137 choices.py:17 238 | msgid "Monday" 239 | msgstr "lundi" 240 | 241 | #: base.py:1137 choices.py:18 242 | msgid "Tuesday" 243 | msgstr "mardi" 244 | 245 | #: base.py:1137 choices.py:19 246 | msgid "Wednesday" 247 | msgstr "mercredi" 248 | 249 | #: base.py:1138 choices.py:20 250 | msgid "Thursday" 251 | msgstr "jeudi" 252 | 253 | #: base.py:1138 choices.py:21 254 | msgid "Friday" 255 | msgstr "vendredi" 256 | 257 | #: base.py:1138 choices.py:22 258 | msgid "Saturday" 259 | msgstr "samedi" 260 | 261 | #: base.py:1138 choices.py:23 262 | msgid "Sunday" 263 | msgstr "dimanche" 264 | 265 | #: base.py:1141 choices.py:27 266 | msgid "January" 267 | msgstr "janvier" 268 | 269 | #: base.py:1141 choices.py:28 270 | msgid "February" 271 | msgstr "février" 272 | 273 | #: base.py:1141 choices.py:29 274 | msgid "March" 275 | msgstr "mars" 276 | 277 | #: base.py:1141 choices.py:30 278 | msgid "April" 279 | msgstr "avril" 280 | 281 | #: base.py:1142 choices.py:32 282 | msgid "June" 283 | msgstr "juin" 284 | 285 | #: base.py:1142 choices.py:33 286 | msgid "July" 287 | msgstr "juillet" 288 | 289 | #: base.py:1142 choices.py:34 290 | msgid "August" 291 | msgstr "août" 292 | 293 | #: base.py:1143 choices.py:35 294 | msgid "September" 295 | msgstr "septembre" 296 | 297 | #: base.py:1143 choices.py:36 298 | msgid "October" 299 | msgstr "octobre" 300 | 301 | #: base.py:1143 choices.py:37 302 | msgid "November" 303 | msgstr "novembre" 304 | 305 | #: base.py:1143 choices.py:38 306 | msgid "December" 307 | msgstr "décembre" 308 | 309 | #: base.py:1161 base.py:1178 base.py:1189 base.py:1202 base.py:1220 310 | msgid ", " 311 | msgstr ", " 312 | 313 | #: base.py:1167 314 | #, python-format 315 | msgid "every %(number)s %(freq)s" 316 | msgstr "tous les %(number)s %(freq)s" 317 | 318 | #: base.py:1181 base.py:1205 319 | #, python-format 320 | msgid "each %(items)s" 321 | msgstr "chaque %(items)s" 322 | 323 | #: base.py:1184 base.py:1193 base.py:1197 324 | #, python-format 325 | msgid "on the %(items)s" 326 | msgstr "pour le %(items)s" 327 | 328 | #: base.py:1212 329 | msgid "occuring once" 330 | msgstr "se répète une fois" 331 | 332 | #: base.py:1214 333 | #, python-format 334 | msgid "occuring %(number)s times" 335 | msgstr "se répète %(number)s fois" 336 | 337 | #: base.py:1217 338 | #, python-format 339 | msgid "until %(date)s" 340 | msgstr "jusqu'à %(date)s" 341 | 342 | #: choices.py:7 343 | msgid "Secondly" 344 | msgstr "Toutes les secondes" 345 | 346 | #: choices.py:8 347 | msgid "Minutely" 348 | msgstr "Toutes les minutes" 349 | 350 | #: choices.py:9 351 | msgid "Hourly" 352 | msgstr "Toutes les heures" 353 | 354 | #: choices.py:10 355 | msgid "Daily" 356 | msgstr "Quotidien" 357 | 358 | #: choices.py:11 359 | msgid "Weekly" 360 | msgstr "Hebdomadaire" 361 | 362 | #: choices.py:12 363 | msgid "Monthly" 364 | msgstr "Mensuel" 365 | 366 | #: choices.py:13 367 | msgid "Yearly" 368 | msgstr "Annuel" 369 | 370 | #: choices.py:31 371 | msgid "May" 372 | msgstr "mai" 373 | 374 | #: choices.py:44 375 | msgid "Inclusion" 376 | msgstr "Inclusion" 377 | 378 | #: choices.py:45 379 | msgid "Exclusion" 380 | msgstr "Exclusion" 381 | 382 | #: forms.py:72 383 | msgid "Invalid frequency." 384 | msgstr "Fréquence invalide" 385 | 386 | #: forms.py:74 387 | #, python-format 388 | msgid "Max rules exceeded. The limit is %(limit)s" 389 | msgstr "Nombre maximum de règles dépassé. La limite est %(limit)s" 390 | 391 | #: forms.py:76 392 | #, python-format 393 | msgid "Max exclusion rules exceeded. The limit is %(limit)s" 394 | msgstr "Nombre maximum de règles d'exclusion déppassé. La limite est %(limit)s" 395 | 396 | #: forms.py:78 397 | #, python-format 398 | msgid "Max dates exceeded. The limit is %(limit)s" 399 | msgstr "Nombre maximum de dates dépassé. La limite est %(limit)s" 400 | 401 | #: forms.py:80 402 | #, python-format 403 | msgid "Max exclusion dates exceeded. The limit is %(limit)s" 404 | msgstr "Nombre maximum de dates éxclues dépassé. La limite est %(limit)s" 405 | 406 | #~ msgid "year" 407 | #~ msgstr "année" 408 | 409 | #~ msgid "month" 410 | #~ msgstr "mois" 411 | 412 | #~ msgid "week" 413 | #~ msgstr "semaine" 414 | 415 | #~ msgid "day" 416 | #~ msgstr "jour" 417 | 418 | #~ msgid "hour" 419 | #~ msgstr "heure" 420 | 421 | #~ msgid "minute" 422 | #~ msgstr "minute" 423 | 424 | #~ msgid "second" 425 | #~ msgstr "seconde" 426 | -------------------------------------------------------------------------------- /recurrence/locale/fr/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/fr/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /recurrence/locale/he/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/he/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /recurrence/locale/he/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/he/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /recurrence/locale/hu/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/hu/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /recurrence/locale/hu/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Hungarian translation for django-recurrence. 2 | # Copyright (C) 2020 Free Software Foundation, Inc. 3 | # This file is distributed under the same license as the django-recurrence package. 4 | # 5 | # Balázs Úr , 2020. 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-recurrence master\n" 9 | "Report-Msgid-Bugs-To: https://github.com/jazzband/django-recurrence/issues\n" 10 | "POT-Creation-Date: 2017-02-24 23:21+0100\n" 11 | "PO-Revision-Date: 2020-02-26 00:09+0100\n" 12 | "Last-Translator: Balázs Úr \n" 13 | "Language-Team: Hungarian\n" 14 | "Language: hu\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Lokalize 19.04.3\n" 20 | 21 | #: base.py:1087 22 | msgid "annually" 23 | msgstr "évente" 24 | 25 | #: base.py:1087 26 | msgid "monthly" 27 | msgstr "havonta" 28 | 29 | #: base.py:1087 30 | msgid "weekly" 31 | msgstr "hetente" 32 | 33 | #: base.py:1087 34 | msgid "daily" 35 | msgstr "naponta" 36 | 37 | #: base.py:1088 38 | msgid "hourly" 39 | msgstr "óránként" 40 | 41 | #: base.py:1088 42 | msgid "minutely" 43 | msgstr "percenként" 44 | 45 | #: base.py:1088 46 | msgid "secondly" 47 | msgstr "másodpercenként" 48 | 49 | #: base.py:1091 50 | msgid "years" 51 | msgstr "év" 52 | 53 | #: base.py:1091 54 | msgid "months" 55 | msgstr "hónap" 56 | 57 | #: base.py:1091 58 | msgid "weeks" 59 | msgstr "hét" 60 | 61 | #: base.py:1091 62 | msgid "days" 63 | msgstr "nap" 64 | 65 | #: base.py:1092 66 | msgid "hours" 67 | msgstr "óra" 68 | 69 | #: base.py:1092 70 | msgid "minutes" 71 | msgstr "perc" 72 | 73 | #: base.py:1092 74 | msgid "seconds" 75 | msgstr "másodperc" 76 | 77 | #: base.py:1097 78 | #, python-format 79 | msgid "1st %(weekday)s" 80 | msgstr "1. %(weekday)s" 81 | 82 | #: base.py:1098 83 | #, python-format 84 | msgid "2nd %(weekday)s" 85 | msgstr "2. %(weekday)s" 86 | 87 | #: base.py:1099 88 | #, python-format 89 | msgid "3rd %(weekday)s" 90 | msgstr "3. %(weekday)s" 91 | 92 | #: base.py:1100 base.py:1126 93 | #, python-format 94 | msgid "last %(weekday)s" 95 | msgstr "utolsó %(weekday)s" 96 | 97 | #: base.py:1101 98 | #, python-format 99 | msgid "2nd last %(weekday)s" 100 | msgstr "2. utolsó %(weekday)s" 101 | 102 | #: base.py:1102 103 | #, python-format 104 | msgid "3rd last %(weekday)s" 105 | msgstr "3. utolsó %(weekday)s" 106 | 107 | #: base.py:1105 base.py:1131 108 | msgid "last" 109 | msgstr "utolsó" 110 | 111 | #: base.py:1106 112 | msgid "2nd last" 113 | msgstr "2. utolsó" 114 | 115 | #: base.py:1107 116 | msgid "3rd last" 117 | msgstr "3. utolsó" 118 | 119 | #: base.py:1108 120 | msgid "4th last" 121 | msgstr "4. utolsó" 122 | 123 | #: base.py:1111 124 | msgid "Mon" 125 | msgstr "Hét" 126 | 127 | #: base.py:1111 128 | msgid "Tue" 129 | msgstr "Ked" 130 | 131 | #: base.py:1111 132 | msgid "Wed" 133 | msgstr "Sze" 134 | 135 | #: base.py:1112 136 | msgid "Thu" 137 | msgstr "Csü" 138 | 139 | #: base.py:1112 140 | msgid "Fri" 141 | msgstr "Pén" 142 | 143 | #: base.py:1112 144 | msgid "Sat" 145 | msgstr "Szo" 146 | 147 | #: base.py:1112 148 | msgid "Sun" 149 | msgstr "Vas" 150 | 151 | #: base.py:1115 152 | msgid "Jan" 153 | msgstr "Jan" 154 | 155 | #: base.py:1115 156 | msgid "Feb" 157 | msgstr "Feb" 158 | 159 | #: base.py:1115 160 | msgid "Mar" 161 | msgstr "Már" 162 | 163 | #: base.py:1115 164 | msgid "Apr" 165 | msgstr "Ápr" 166 | 167 | #: base.py:1116 168 | msgid "Jun" 169 | msgstr "Jún" 170 | 171 | #: base.py:1116 172 | msgid "Jul" 173 | msgstr "Júl" 174 | 175 | #: base.py:1116 176 | msgid "Aug" 177 | msgstr "Aug" 178 | 179 | #: base.py:1117 180 | msgid "Sep" 181 | msgstr "Sze" 182 | 183 | #: base.py:1117 184 | msgid "Oct" 185 | msgstr "Okt" 186 | 187 | #: base.py:1117 188 | msgid "Nov" 189 | msgstr "Nov" 190 | 191 | #: base.py:1117 192 | msgid "Dec" 193 | msgstr "Dec" 194 | 195 | #: base.py:1122 196 | #, python-format 197 | msgid "first %(weekday)s" 198 | msgstr "első %(weekday)s" 199 | 200 | #: base.py:1123 201 | #, python-format 202 | msgid "second %(weekday)s" 203 | msgstr "második %(weekday)s" 204 | 205 | #: base.py:1124 206 | #, python-format 207 | msgid "third %(weekday)s" 208 | msgstr "harmadik %(weekday)s" 209 | 210 | #: base.py:1125 211 | #, python-format 212 | msgid "fourth %(weekday)s" 213 | msgstr "negyedik %(weekday)s" 214 | 215 | #: base.py:1127 216 | #, python-format 217 | msgid "second last %(weekday)s" 218 | msgstr "második utolsó %(weekday)s" 219 | 220 | #: base.py:1128 221 | #, python-format 222 | msgid "third last %(weekday)s" 223 | msgstr "harmadik utolsó %(weekday)s" 224 | 225 | #: base.py:1132 226 | msgid "second last" 227 | msgstr "második utolsó" 228 | 229 | #: base.py:1133 230 | msgid "third last" 231 | msgstr "harmadik utolsó" 232 | 233 | #: base.py:1134 234 | msgid "fourth last" 235 | msgstr "negyedik utolsó" 236 | 237 | #: base.py:1137 choices.py:17 238 | msgid "Monday" 239 | msgstr "Hétfő" 240 | 241 | #: base.py:1137 choices.py:18 242 | msgid "Tuesday" 243 | msgstr "Kedd" 244 | 245 | #: base.py:1137 choices.py:19 246 | msgid "Wednesday" 247 | msgstr "Szerda" 248 | 249 | #: base.py:1138 choices.py:20 250 | msgid "Thursday" 251 | msgstr "Csütörtök" 252 | 253 | #: base.py:1138 choices.py:21 254 | msgid "Friday" 255 | msgstr "Péntek" 256 | 257 | #: base.py:1138 choices.py:22 258 | msgid "Saturday" 259 | msgstr "Szombat" 260 | 261 | #: base.py:1138 choices.py:23 262 | msgid "Sunday" 263 | msgstr "Vasárnap" 264 | 265 | #: base.py:1141 choices.py:27 266 | msgid "January" 267 | msgstr "Január" 268 | 269 | #: base.py:1141 choices.py:28 270 | msgid "February" 271 | msgstr "Február" 272 | 273 | #: base.py:1141 choices.py:29 274 | msgid "March" 275 | msgstr "Március" 276 | 277 | #: base.py:1141 choices.py:30 278 | msgid "April" 279 | msgstr "Április" 280 | 281 | #: base.py:1142 choices.py:32 282 | msgid "June" 283 | msgstr "Június" 284 | 285 | #: base.py:1142 choices.py:33 286 | msgid "July" 287 | msgstr "Július" 288 | 289 | #: base.py:1142 choices.py:34 290 | msgid "August" 291 | msgstr "Augusztus" 292 | 293 | #: base.py:1143 choices.py:35 294 | msgid "September" 295 | msgstr "Szeptember" 296 | 297 | #: base.py:1143 choices.py:36 298 | msgid "October" 299 | msgstr "Október" 300 | 301 | #: base.py:1143 choices.py:37 302 | msgid "November" 303 | msgstr "November" 304 | 305 | #: base.py:1143 choices.py:38 306 | msgid "December" 307 | msgstr "December" 308 | 309 | #: base.py:1161 base.py:1178 base.py:1189 base.py:1202 base.py:1220 310 | msgid ", " 311 | msgstr ", " 312 | 313 | #: base.py:1167 314 | #, python-format 315 | msgid "every %(number)s %(freq)s" 316 | msgstr "minden %(number)s. %(freq)s" 317 | 318 | #: base.py:1181 base.py:1205 319 | #, python-format 320 | msgid "each %(items)s" 321 | msgstr "minden %(items)s" 322 | 323 | #: base.py:1184 base.py:1193 base.py:1197 324 | #, python-format 325 | msgid "on the %(items)s" 326 | msgstr "ekkor: %(items)s" 327 | 328 | #: base.py:1212 329 | msgid "occuring once" 330 | msgstr "egyszer fordul elő" 331 | 332 | #: base.py:1214 333 | #, python-format 334 | msgid "occuring %(number)s times" 335 | msgstr "%(number)s alkalommal fordul elő" 336 | 337 | #: base.py:1217 338 | #, python-format 339 | msgid "until %(date)s" 340 | msgstr "eddig: %(date)s" 341 | 342 | #: choices.py:7 343 | msgid "Secondly" 344 | msgstr "Másodpercenként" 345 | 346 | #: choices.py:8 347 | msgid "Minutely" 348 | msgstr "Percenként" 349 | 350 | #: choices.py:9 351 | msgid "Hourly" 352 | msgstr "Óránként" 353 | 354 | #: choices.py:10 355 | msgid "Daily" 356 | msgstr "Naponta" 357 | 358 | #: choices.py:11 359 | msgid "Weekly" 360 | msgstr "Hetente" 361 | 362 | #: choices.py:12 363 | msgid "Monthly" 364 | msgstr "Havonta" 365 | 366 | #: choices.py:13 367 | msgid "Yearly" 368 | msgstr "Évente" 369 | 370 | #: choices.py:31 371 | msgid "May" 372 | msgstr "Május" 373 | 374 | #: choices.py:44 375 | msgid "Inclusion" 376 | msgstr "Tartalmazás" 377 | 378 | #: choices.py:45 379 | msgid "Exclusion" 380 | msgstr "Kizárás" 381 | 382 | #: forms.py:72 383 | msgid "Invalid frequency." 384 | msgstr "Érvénytelen gyakoriság." 385 | 386 | #: forms.py:74 387 | #, python-format 388 | msgid "Max rules exceeded. The limit is %(limit)s" 389 | msgstr "A szabályok legnagyobb száma elérve. A korlát %(limit)s" 390 | 391 | #: forms.py:76 392 | #, python-format 393 | msgid "Max exclusion rules exceeded. The limit is %(limit)s" 394 | msgstr "A kizárási szabályok legnagyobb száma elérve. A korlát %(limit)s" 395 | 396 | #: forms.py:78 397 | #, python-format 398 | msgid "Max dates exceeded. The limit is %(limit)s" 399 | msgstr "A dátumok legnagyobb száma elérve. A korlát %(limit)s" 400 | 401 | #: forms.py:80 402 | #, python-format 403 | msgid "Max exclusion dates exceeded. The limit is %(limit)s" 404 | msgstr "A kizárási dátumok legnagyobb száma elérve. A korlát %(limit)s" 405 | -------------------------------------------------------------------------------- /recurrence/locale/hu/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/hu/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /recurrence/locale/nl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/nl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /recurrence/locale/nl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Django Recurrence 1.0.1\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-02-24 23:21+0100\n" 11 | "PO-Revision-Date: 2017-02-24 23:28+0100\n" 12 | "Last-Translator: Bart Heesink \n" 13 | "Language-Team: \n" 14 | "Language: Dutch\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 1.5.7\n" 19 | "X-Poedit-SourceCharset: UTF-8\n" 20 | 21 | #: base.py:1087 22 | msgid "annually" 23 | msgstr "jaarlijks" 24 | 25 | #: base.py:1087 26 | msgid "monthly" 27 | msgstr "maandelijks" 28 | 29 | #: base.py:1087 30 | msgid "weekly" 31 | msgstr "wekelijks" 32 | 33 | #: base.py:1087 34 | msgid "daily" 35 | msgstr "dagelijks" 36 | 37 | #: base.py:1088 38 | msgid "hourly" 39 | msgstr "elk uur" 40 | 41 | #: base.py:1088 42 | msgid "minutely" 43 | msgstr "elke minuut" 44 | 45 | #: base.py:1088 46 | msgid "secondly" 47 | msgstr "elke seconde" 48 | 49 | #: base.py:1091 50 | msgid "years" 51 | msgstr "jaren" 52 | 53 | #: base.py:1091 54 | msgid "months" 55 | msgstr "maanden" 56 | 57 | #: base.py:1091 58 | msgid "weeks" 59 | msgstr "weken" 60 | 61 | #: base.py:1091 62 | msgid "days" 63 | msgstr "dagen" 64 | 65 | #: base.py:1092 66 | msgid "hours" 67 | msgstr "uren" 68 | 69 | #: base.py:1092 70 | msgid "minutes" 71 | msgstr "minuten" 72 | 73 | #: base.py:1092 74 | msgid "seconds" 75 | msgstr "seconden" 76 | 77 | #: base.py:1097 78 | #, python-format 79 | msgid "1st %(weekday)s" 80 | msgstr "1e %(weekday)s" 81 | 82 | #: base.py:1098 83 | #, python-format 84 | msgid "2nd %(weekday)s" 85 | msgstr "2e %(weekday)s" 86 | 87 | #: base.py:1099 88 | #, python-format 89 | msgid "3rd %(weekday)s" 90 | msgstr "3e %(weekday)s" 91 | 92 | #: base.py:1100 base.py:1126 93 | #, python-format 94 | msgid "last %(weekday)s" 95 | msgstr "laatste %(weekday)s" 96 | 97 | #: base.py:1101 98 | #, python-format 99 | msgid "2nd last %(weekday)s" 100 | msgstr "2 na laatste %(weekday)s" 101 | 102 | #: base.py:1102 103 | #, python-format 104 | msgid "3rd last %(weekday)s" 105 | msgstr "3 na laatste %(weekday)s" 106 | 107 | #: base.py:1105 base.py:1131 108 | msgid "last" 109 | msgstr "laatste" 110 | 111 | #: base.py:1106 112 | msgid "2nd last" 113 | msgstr "2 na laatste" 114 | 115 | #: base.py:1107 116 | msgid "3rd last" 117 | msgstr "3 na laatste" 118 | 119 | #: base.py:1108 120 | msgid "4th last" 121 | msgstr "4 na laatste" 122 | 123 | #: base.py:1111 124 | msgid "Mon" 125 | msgstr "ma" 126 | 127 | #: base.py:1111 128 | msgid "Tue" 129 | msgstr "di" 130 | 131 | #: base.py:1111 132 | msgid "Wed" 133 | msgstr "wo" 134 | 135 | #: base.py:1112 136 | msgid "Thu" 137 | msgstr "do" 138 | 139 | #: base.py:1112 140 | msgid "Fri" 141 | msgstr "vr" 142 | 143 | #: base.py:1112 144 | msgid "Sat" 145 | msgstr "za" 146 | 147 | #: base.py:1112 148 | msgid "Sun" 149 | msgstr "zo" 150 | 151 | #: base.py:1115 152 | msgid "Jan" 153 | msgstr "jan" 154 | 155 | #: base.py:1115 156 | msgid "Feb" 157 | msgstr "feb" 158 | 159 | #: base.py:1115 160 | msgid "Mar" 161 | msgstr "mrt" 162 | 163 | #: base.py:1115 164 | msgid "Apr" 165 | msgstr "apr" 166 | 167 | #: base.py:1116 168 | msgid "Jun" 169 | msgstr "jun" 170 | 171 | #: base.py:1116 172 | msgid "Jul" 173 | msgstr "jul" 174 | 175 | #: base.py:1116 176 | msgid "Aug" 177 | msgstr "aug" 178 | 179 | #: base.py:1117 180 | msgid "Sep" 181 | msgstr "sep" 182 | 183 | #: base.py:1117 184 | msgid "Oct" 185 | msgstr "okt" 186 | 187 | #: base.py:1117 188 | msgid "Nov" 189 | msgstr "nov" 190 | 191 | #: base.py:1117 192 | msgid "Dec" 193 | msgstr "dec" 194 | 195 | #: base.py:1122 196 | #, python-format 197 | msgid "first %(weekday)s" 198 | msgstr "eerste %(weekday)s" 199 | 200 | #: base.py:1123 201 | #, python-format 202 | msgid "second %(weekday)s" 203 | msgstr "tweede %(weekday)s" 204 | 205 | #: base.py:1124 206 | #, python-format 207 | msgid "third %(weekday)s" 208 | msgstr "derde %(weekday)s" 209 | 210 | #: base.py:1125 211 | #, python-format 212 | msgid "fourth %(weekday)s" 213 | msgstr "vierde %(weekday)s" 214 | 215 | #: base.py:1127 216 | #, python-format 217 | msgid "second last %(weekday)s" 218 | msgstr "twee na laatste %(weekday)s" 219 | 220 | #: base.py:1128 221 | #, python-format 222 | msgid "third last %(weekday)s" 223 | msgstr "drie na laatste %(weekday)s" 224 | 225 | #: base.py:1132 226 | msgid "second last" 227 | msgstr "voorlaatste" 228 | 229 | #: base.py:1133 230 | msgid "third last" 231 | msgstr "derde laatste" 232 | 233 | #: base.py:1134 234 | msgid "fourth last" 235 | msgstr "vierde laatste" 236 | 237 | #: base.py:1137 choices.py:17 238 | msgid "Monday" 239 | msgstr "maandag" 240 | 241 | #: base.py:1137 choices.py:18 242 | msgid "Tuesday" 243 | msgstr "dinsdag" 244 | 245 | #: base.py:1137 choices.py:19 246 | msgid "Wednesday" 247 | msgstr "woensdag" 248 | 249 | #: base.py:1138 choices.py:20 250 | msgid "Thursday" 251 | msgstr "donderdag" 252 | 253 | #: base.py:1138 choices.py:21 254 | msgid "Friday" 255 | msgstr "vrijdag" 256 | 257 | #: base.py:1138 choices.py:22 258 | msgid "Saturday" 259 | msgstr "zaterdag" 260 | 261 | #: base.py:1138 choices.py:23 262 | msgid "Sunday" 263 | msgstr "zondag" 264 | 265 | #: base.py:1141 choices.py:27 266 | msgid "January" 267 | msgstr "januari" 268 | 269 | #: base.py:1141 choices.py:28 270 | msgid "February" 271 | msgstr "februari" 272 | 273 | #: base.py:1141 choices.py:29 274 | msgid "March" 275 | msgstr "maart" 276 | 277 | #: base.py:1141 choices.py:30 278 | msgid "April" 279 | msgstr "april" 280 | 281 | #: base.py:1142 choices.py:32 282 | msgid "June" 283 | msgstr "juni" 284 | 285 | #: base.py:1142 choices.py:33 286 | msgid "July" 287 | msgstr "juli" 288 | 289 | #: base.py:1142 choices.py:34 290 | msgid "August" 291 | msgstr "augustus" 292 | 293 | #: base.py:1143 choices.py:35 294 | msgid "September" 295 | msgstr "september" 296 | 297 | #: base.py:1143 choices.py:36 298 | msgid "October" 299 | msgstr "oktober" 300 | 301 | #: base.py:1143 choices.py:37 302 | msgid "November" 303 | msgstr "november" 304 | 305 | #: base.py:1143 choices.py:38 306 | msgid "December" 307 | msgstr "december" 308 | 309 | #: base.py:1161 base.py:1178 base.py:1189 base.py:1202 base.py:1220 310 | msgid ", " 311 | msgstr "" 312 | 313 | #: base.py:1167 314 | #, python-format 315 | msgid "every %(number)s %(freq)s" 316 | msgstr "elke %(number)s %(freq)s" 317 | 318 | #: base.py:1181 base.py:1205 319 | #, python-format 320 | msgid "each %(items)s" 321 | msgstr "iedere %(items)s" 322 | 323 | #: base.py:1184 base.py:1193 base.py:1197 324 | #, python-format 325 | msgid "on the %(items)s" 326 | msgstr "op de %(items)s" 327 | 328 | #: base.py:1212 329 | msgid "occuring once" 330 | msgstr "éénmalig" 331 | 332 | #: base.py:1214 333 | #, python-format 334 | msgid "occuring %(number)s times" 335 | msgstr "%(number)s keer herhalen" 336 | 337 | #: base.py:1217 338 | #, python-format 339 | msgid "until %(date)s" 340 | msgstr "tot %(date)s" 341 | 342 | #: choices.py:7 343 | msgid "Secondly" 344 | msgstr "Elke seconde" 345 | 346 | #: choices.py:8 347 | msgid "Minutely" 348 | msgstr "Elke minuut" 349 | 350 | #: choices.py:9 351 | msgid "Hourly" 352 | msgstr "Elk uur" 353 | 354 | #: choices.py:10 355 | msgid "Daily" 356 | msgstr "Dagelijks" 357 | 358 | #: choices.py:11 359 | msgid "Weekly" 360 | msgstr "Wekelijks" 361 | 362 | #: choices.py:12 363 | msgid "Monthly" 364 | msgstr "Maandelijks" 365 | 366 | #: choices.py:13 367 | msgid "Yearly" 368 | msgstr "Jaarlijks" 369 | 370 | #: choices.py:31 371 | msgid "May" 372 | msgstr "mei" 373 | 374 | #: choices.py:44 375 | msgid "Inclusion" 376 | msgstr "Inclusief" 377 | 378 | #: choices.py:45 379 | msgid "Exclusion" 380 | msgstr "Uitgezonderd" 381 | 382 | #: forms.py:72 383 | msgid "Invalid frequency." 384 | msgstr "Ongeldige herhaling." 385 | 386 | #: forms.py:74 387 | #, python-format 388 | msgid "Max rules exceeded. The limit is %(limit)s" 389 | msgstr "Maximaal aantal van %(limit)s regels is overschreden." 390 | 391 | #: forms.py:76 392 | #, python-format 393 | msgid "Max exclusion rules exceeded. The limit is %(limit)s" 394 | msgstr "Maximaal aantal van %(limit)s uitgezonderde regels is overschreden." 395 | 396 | #: forms.py:78 397 | #, python-format 398 | msgid "Max dates exceeded. The limit is %(limit)s" 399 | msgstr "Maximaal aantal van %(limit)s data overschreden." 400 | 401 | #: forms.py:80 402 | #, python-format 403 | msgid "Max exclusion dates exceeded. The limit is %(limit)s" 404 | msgstr "Maximaal aantal van %(limit)s uitgezonderde data is overschreden." 405 | 406 | #~ msgid "year" 407 | #~ msgstr "jaar" 408 | 409 | #~ msgid "month" 410 | #~ msgstr "maand" 411 | 412 | #~ msgid "week" 413 | #~ msgstr "week" 414 | 415 | #~ msgid "day" 416 | #~ msgstr "dag" 417 | 418 | #~ msgid "hour" 419 | #~ msgstr "uur" 420 | 421 | #~ msgid "minute" 422 | #~ msgstr "minuut" 423 | 424 | #~ msgid "second" 425 | #~ msgstr "seconde" 426 | -------------------------------------------------------------------------------- /recurrence/locale/nl/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/nl/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /recurrence/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /recurrence/locale/pt_BR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # Diego Ponciano , 2016. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-02-24 23:21+0100\n" 11 | "PO-Revision-Date: 2017-02-24 23:32+0100\n" 12 | "Last-Translator: Diego Ponciano \n" 13 | "Language-Team: \n" 14 | "Language: pt_BR\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Gtranslator 2.91.7\n" 19 | 20 | #: base.py:1087 21 | msgid "annually" 22 | msgstr "anualmente" 23 | 24 | #: base.py:1087 25 | msgid "monthly" 26 | msgstr "mensalmente" 27 | 28 | #: base.py:1087 29 | msgid "weekly" 30 | msgstr "semanalmente" 31 | 32 | #: base.py:1087 33 | msgid "daily" 34 | msgstr "diariamente" 35 | 36 | #: base.py:1088 37 | msgid "hourly" 38 | msgstr "a cada hora" 39 | 40 | #: base.py:1088 41 | msgid "minutely" 42 | msgstr "a cada minuto" 43 | 44 | #: base.py:1088 45 | msgid "secondly" 46 | msgstr "a cada segundo" 47 | 48 | #: base.py:1091 49 | msgid "years" 50 | msgstr "anos" 51 | 52 | #: base.py:1091 53 | msgid "months" 54 | msgstr "meses" 55 | 56 | #: base.py:1091 57 | msgid "weeks" 58 | msgstr "semanas" 59 | 60 | #: base.py:1091 61 | msgid "days" 62 | msgstr "dias" 63 | 64 | #: base.py:1092 65 | msgid "hours" 66 | msgstr "horas" 67 | 68 | #: base.py:1092 69 | msgid "minutes" 70 | msgstr "minutos" 71 | 72 | #: base.py:1092 73 | msgid "seconds" 74 | msgstr "segundos" 75 | 76 | #: base.py:1097 77 | #, python-format 78 | msgid "1st %(weekday)s" 79 | msgstr "1º %(weekday)s" 80 | 81 | #: base.py:1098 82 | #, python-format 83 | msgid "2nd %(weekday)s" 84 | msgstr "2º %(weekday)s" 85 | 86 | #: base.py:1099 87 | #, python-format 88 | msgid "3rd %(weekday)s" 89 | msgstr "3º %(weekday)s" 90 | 91 | #: base.py:1100 base.py:1126 92 | #, python-format 93 | msgid "last %(weekday)s" 94 | msgstr "último %(weekday)s" 95 | 96 | #: base.py:1101 97 | #, python-format 98 | msgid "2nd last %(weekday)s" 99 | msgstr "2º último %(weekday)s" 100 | 101 | #: base.py:1102 102 | #, python-format 103 | msgid "3rd last %(weekday)s" 104 | msgstr "3º último %(weekday)s" 105 | 106 | #: base.py:1105 base.py:1131 107 | msgid "last" 108 | msgstr "último" 109 | 110 | #: base.py:1106 111 | msgid "2nd last" 112 | msgstr "2º último" 113 | 114 | #: base.py:1107 115 | msgid "3rd last" 116 | msgstr "3º último" 117 | 118 | #: base.py:1108 119 | msgid "4th last" 120 | msgstr "4º último" 121 | 122 | #: base.py:1111 123 | msgid "Mon" 124 | msgstr "Seg" 125 | 126 | #: base.py:1111 127 | msgid "Tue" 128 | msgstr "Ter" 129 | 130 | #: base.py:1111 131 | msgid "Wed" 132 | msgstr "Qua" 133 | 134 | #: base.py:1112 135 | msgid "Thu" 136 | msgstr "Qui" 137 | 138 | #: base.py:1112 139 | msgid "Fri" 140 | msgstr "Sex" 141 | 142 | #: base.py:1112 143 | msgid "Sat" 144 | msgstr "Sáb" 145 | 146 | #: base.py:1112 147 | msgid "Sun" 148 | msgstr "Dom" 149 | 150 | #: base.py:1115 151 | msgid "Jan" 152 | msgstr "Jan" 153 | 154 | #: base.py:1115 155 | msgid "Feb" 156 | msgstr "Fev" 157 | 158 | #: base.py:1115 159 | msgid "Mar" 160 | msgstr "Mar" 161 | 162 | #: base.py:1115 163 | msgid "Apr" 164 | msgstr "Abr" 165 | 166 | #: base.py:1116 167 | msgid "Jun" 168 | msgstr "Jun" 169 | 170 | #: base.py:1116 171 | msgid "Jul" 172 | msgstr "Jul" 173 | 174 | #: base.py:1116 175 | msgid "Aug" 176 | msgstr "Ago" 177 | 178 | #: base.py:1117 179 | msgid "Sep" 180 | msgstr "Set" 181 | 182 | #: base.py:1117 183 | msgid "Oct" 184 | msgstr "Out" 185 | 186 | #: base.py:1117 187 | msgid "Nov" 188 | msgstr "Nov" 189 | 190 | #: base.py:1117 191 | msgid "Dec" 192 | msgstr "Dez" 193 | 194 | #: base.py:1122 195 | #, python-format 196 | msgid "first %(weekday)s" 197 | msgstr "primeiro %(weekday)s" 198 | 199 | #: base.py:1123 200 | #, python-format 201 | msgid "second %(weekday)s" 202 | msgstr "segundo %(weekday)s" 203 | 204 | #: base.py:1124 205 | #, python-format 206 | msgid "third %(weekday)s" 207 | msgstr "terceiro %(weekday)s" 208 | 209 | #: base.py:1125 210 | #, python-format 211 | msgid "fourth %(weekday)s" 212 | msgstr "quarto %(weekday)s" 213 | 214 | #: base.py:1127 215 | #, python-format 216 | msgid "second last %(weekday)s" 217 | msgstr "penúltimo %(weekday)s" 218 | 219 | #: base.py:1128 220 | #, python-format 221 | msgid "third last %(weekday)s" 222 | msgstr "antepenúltimo %(weekday)s" 223 | 224 | #: base.py:1132 225 | msgid "second last" 226 | msgstr "penúltimo" 227 | 228 | #: base.py:1133 229 | msgid "third last" 230 | msgstr "antepenúltimo" 231 | 232 | #: base.py:1134 233 | msgid "fourth last" 234 | msgstr "quarto último" 235 | 236 | #: base.py:1137 choices.py:17 237 | msgid "Monday" 238 | msgstr "Segunda" 239 | 240 | #: base.py:1137 choices.py:18 241 | msgid "Tuesday" 242 | msgstr "Terça" 243 | 244 | #: base.py:1137 choices.py:19 245 | msgid "Wednesday" 246 | msgstr "Quarta" 247 | 248 | #: base.py:1138 choices.py:20 249 | msgid "Thursday" 250 | msgstr "Quinta" 251 | 252 | #: base.py:1138 choices.py:21 253 | msgid "Friday" 254 | msgstr "Sexta" 255 | 256 | #: base.py:1138 choices.py:22 257 | msgid "Saturday" 258 | msgstr "Sábado" 259 | 260 | #: base.py:1138 choices.py:23 261 | msgid "Sunday" 262 | msgstr "Domingo" 263 | 264 | #: base.py:1141 choices.py:27 265 | msgid "January" 266 | msgstr "Janeiro" 267 | 268 | #: base.py:1141 choices.py:28 269 | msgid "February" 270 | msgstr "Fevereiro" 271 | 272 | #: base.py:1141 choices.py:29 273 | msgid "March" 274 | msgstr "Março" 275 | 276 | #: base.py:1141 choices.py:30 277 | msgid "April" 278 | msgstr "Abril" 279 | 280 | #: base.py:1142 choices.py:32 281 | msgid "June" 282 | msgstr "Junho" 283 | 284 | #: base.py:1142 choices.py:33 285 | msgid "July" 286 | msgstr "Julho" 287 | 288 | #: base.py:1142 choices.py:34 289 | msgid "August" 290 | msgstr "Agosto" 291 | 292 | #: base.py:1143 choices.py:35 293 | msgid "September" 294 | msgstr "Setembro" 295 | 296 | #: base.py:1143 choices.py:36 297 | msgid "October" 298 | msgstr "Outubro" 299 | 300 | #: base.py:1143 choices.py:37 301 | msgid "November" 302 | msgstr "Novembro" 303 | 304 | #: base.py:1143 choices.py:38 305 | msgid "December" 306 | msgstr "Dezembro" 307 | 308 | #: base.py:1161 base.py:1178 base.py:1189 base.py:1202 base.py:1220 309 | msgid ", " 310 | msgstr "," 311 | 312 | #: base.py:1167 313 | #, python-format 314 | msgid "every %(number)s %(freq)s" 315 | msgstr "a cada %(number)s %(freq)s" 316 | 317 | #: base.py:1181 base.py:1205 318 | #, python-format 319 | msgid "each %(items)s" 320 | msgstr "cada %(items)s" 321 | 322 | #: base.py:1184 base.py:1193 base.py:1197 323 | #, python-format 324 | msgid "on the %(items)s" 325 | msgstr "nos %(items)s" 326 | 327 | #: base.py:1212 328 | msgid "occuring once" 329 | msgstr "que ocorre uma vez" 330 | 331 | #: base.py:1214 332 | #, python-format 333 | msgid "occuring %(number)s times" 334 | msgstr "que ocorre %(number)s vezes" 335 | 336 | #: base.py:1217 337 | #, python-format 338 | msgid "until %(date)s" 339 | msgstr "até %(date)s" 340 | 341 | #: choices.py:7 342 | msgid "Secondly" 343 | msgstr "A cada segundo" 344 | 345 | #: choices.py:8 346 | msgid "Minutely" 347 | msgstr "A cada minuto" 348 | 349 | #: choices.py:9 350 | msgid "Hourly" 351 | msgstr "A cada hora" 352 | 353 | #: choices.py:10 354 | msgid "Daily" 355 | msgstr "Diariamente" 356 | 357 | #: choices.py:11 358 | msgid "Weekly" 359 | msgstr "Semanalmente" 360 | 361 | #: choices.py:12 362 | msgid "Monthly" 363 | msgstr "Mensalmente" 364 | 365 | #: choices.py:13 366 | msgid "Yearly" 367 | msgstr "Anualmente" 368 | 369 | #: choices.py:31 370 | msgid "May" 371 | msgstr "Mai" 372 | 373 | #: choices.py:44 374 | msgid "Inclusion" 375 | msgstr "Inclusão" 376 | 377 | #: choices.py:45 378 | msgid "Exclusion" 379 | msgstr "Exclusão" 380 | 381 | #: forms.py:72 382 | msgid "Invalid frequency." 383 | msgstr "Freqência inválida." 384 | 385 | #: forms.py:74 386 | #, python-format 387 | msgid "Max rules exceeded. The limit is %(limit)s" 388 | msgstr "Máximo de regras excedido. O limite é %(limit)s" 389 | 390 | #: forms.py:76 391 | #, python-format 392 | msgid "Max exclusion rules exceeded. The limit is %(limit)s" 393 | msgstr "Máximo de regras de exclusão excedido. O limite é %(limit)s" 394 | 395 | #: forms.py:78 396 | #, python-format 397 | msgid "Max dates exceeded. The limit is %(limit)s" 398 | msgstr "Máximo de datas excedido. O limite é %(limit)s" 399 | 400 | #: forms.py:80 401 | #, python-format 402 | msgid "Max exclusion dates exceeded. The limit is %(limit)s" 403 | msgstr "Máximo de datas excluídas excedido. O limite é %(limit)s" 404 | 405 | #~ msgid "year" 406 | #~ msgstr "ano" 407 | 408 | #~ msgid "month" 409 | #~ msgstr "mês" 410 | 411 | #~ msgid "week" 412 | #~ msgstr "semana" 413 | 414 | #~ msgid "day" 415 | #~ msgstr "dia" 416 | 417 | #~ msgid "hour" 418 | #~ msgstr "hora" 419 | 420 | #~ msgid "minute" 421 | #~ msgstr "minuto" 422 | 423 | #~ msgid "second" 424 | #~ msgstr "segundo" 425 | -------------------------------------------------------------------------------- /recurrence/locale/pt_BR/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/pt_BR/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /recurrence/locale/sk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/sk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /recurrence/locale/sk/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-02-24 23:21+0100\n" 11 | "PO-Revision-Date: 2017-07-28 13:58+0200\n" 12 | "Language: sk\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Last-Translator: \n" 17 | "Language-Team: \n" 18 | "X-Generator: Poedit 2.0.1\n" 19 | 20 | #: base.py:1087 21 | msgid "annually" 22 | msgstr "ročne" 23 | 24 | #: base.py:1087 25 | msgid "monthly" 26 | msgstr "mesačne" 27 | 28 | #: base.py:1087 29 | msgid "weekly" 30 | msgstr "týždenne" 31 | 32 | #: base.py:1087 33 | msgid "daily" 34 | msgstr "denne" 35 | 36 | #: base.py:1088 37 | msgid "hourly" 38 | msgstr "každú zvolenú hodinu" 39 | 40 | #: base.py:1088 41 | msgid "minutely" 42 | msgstr "každú zvolenú minútu" 43 | 44 | #: base.py:1088 45 | msgid "secondly" 46 | msgstr "každú zvolenú sekundu" 47 | 48 | #: base.py:1091 49 | msgid "years" 50 | msgstr "roky" 51 | 52 | #: base.py:1091 53 | msgid "months" 54 | msgstr "mesiace" 55 | 56 | #: base.py:1091 57 | msgid "weeks" 58 | msgstr "týždne" 59 | 60 | #: base.py:1091 61 | msgid "days" 62 | msgstr "dni" 63 | 64 | #: base.py:1092 65 | msgid "hours" 66 | msgstr "hodiny" 67 | 68 | #: base.py:1092 69 | msgid "minutes" 70 | msgstr "minúty" 71 | 72 | #: base.py:1092 73 | msgid "seconds" 74 | msgstr "sekundu" 75 | 76 | #: base.py:1097 77 | #, python-format 78 | msgid "1st %(weekday)s" 79 | msgstr "prvý %(weekday)s" 80 | 81 | #: base.py:1098 82 | #, python-format 83 | msgid "2nd %(weekday)s" 84 | msgstr "druhý %(weekday)s" 85 | 86 | #: base.py:1099 87 | #, python-format 88 | msgid "3rd %(weekday)s" 89 | msgstr "tretí %(weekday)s" 90 | 91 | #: base.py:1100 base.py:1126 92 | #, python-format 93 | msgid "last %(weekday)s" 94 | msgstr "posledný %(weekday)s" 95 | 96 | #: base.py:1101 97 | #, python-format 98 | msgid "2nd last %(weekday)s" 99 | msgstr "predposledný %(weekday)s" 100 | 101 | #: base.py:1102 102 | #, python-format 103 | msgid "3rd last %(weekday)s" 104 | msgstr "predpredposledný %(weekday)s" 105 | 106 | #: base.py:1105 base.py:1131 107 | msgid "last" 108 | msgstr "posledný" 109 | 110 | #: base.py:1106 111 | msgid "2nd last" 112 | msgstr "predposledný" 113 | 114 | #: base.py:1107 115 | msgid "3rd last" 116 | msgstr "tretí od konca" 117 | 118 | #: base.py:1108 119 | msgid "4th last" 120 | msgstr "štvrý od konca" 121 | 122 | #: base.py:1111 123 | msgid "Mon" 124 | msgstr "Po" 125 | 126 | #: base.py:1111 127 | msgid "Tue" 128 | msgstr "Ut" 129 | 130 | #: base.py:1111 131 | msgid "Wed" 132 | msgstr "St" 133 | 134 | #: base.py:1112 135 | msgid "Thu" 136 | msgstr "Št" 137 | 138 | #: base.py:1112 139 | msgid "Fri" 140 | msgstr "Pi" 141 | 142 | #: base.py:1112 143 | msgid "Sat" 144 | msgstr "So" 145 | 146 | #: base.py:1112 147 | msgid "Sun" 148 | msgstr "Ne" 149 | 150 | #: base.py:1115 151 | msgid "Jan" 152 | msgstr "Jan" 153 | 154 | #: base.py:1115 155 | msgid "Feb" 156 | msgstr "Feb" 157 | 158 | #: base.py:1115 159 | msgid "Mar" 160 | msgstr "Mar" 161 | 162 | #: base.py:1115 163 | msgid "Apr" 164 | msgstr "Apr" 165 | 166 | #: base.py:1116 167 | msgid "Jun" 168 | msgstr "Jún" 169 | 170 | #: base.py:1116 171 | msgid "Jul" 172 | msgstr "Júl" 173 | 174 | #: base.py:1116 175 | msgid "Aug" 176 | msgstr "Aug" 177 | 178 | #: base.py:1117 179 | msgid "Sep" 180 | msgstr "Sep" 181 | 182 | #: base.py:1117 183 | msgid "Oct" 184 | msgstr "Okt" 185 | 186 | #: base.py:1117 187 | msgid "Nov" 188 | msgstr "Nov" 189 | 190 | #: base.py:1117 191 | msgid "Dec" 192 | msgstr "Dec" 193 | 194 | #: base.py:1122 195 | #, python-format 196 | msgid "first %(weekday)s" 197 | msgstr "prvý %(weekday)s" 198 | 199 | #: base.py:1123 200 | #, python-format 201 | msgid "second %(weekday)s" 202 | msgstr "druhý %(weekday)s" 203 | 204 | #: base.py:1124 205 | #, python-format 206 | msgid "third %(weekday)s" 207 | msgstr "tretí %(weekday)s" 208 | 209 | #: base.py:1125 210 | #, python-format 211 | msgid "fourth %(weekday)s" 212 | msgstr "štvrtý %(weekday)s" 213 | 214 | #: base.py:1127 215 | #, python-format 216 | msgid "second last %(weekday)s" 217 | msgstr "predposledný %(weekday)s" 218 | 219 | #: base.py:1128 220 | #, python-format 221 | msgid "third last %(weekday)s" 222 | msgstr "tretí %(weekday)s od konca" 223 | 224 | #: base.py:1132 225 | msgid "second last" 226 | msgstr "predpokledný" 227 | 228 | #: base.py:1133 229 | msgid "third last" 230 | msgstr "tretí od konca" 231 | 232 | #: base.py:1134 233 | msgid "fourth last" 234 | msgstr "štvrtý od konca" 235 | 236 | #: base.py:1137 choices.py:17 237 | msgid "Monday" 238 | msgstr "Pondelok" 239 | 240 | #: base.py:1137 choices.py:18 241 | msgid "Tuesday" 242 | msgstr "Utorok" 243 | 244 | #: base.py:1137 choices.py:19 245 | msgid "Wednesday" 246 | msgstr "Streda" 247 | 248 | #: base.py:1138 choices.py:20 249 | msgid "Thursday" 250 | msgstr "Štvrtok" 251 | 252 | #: base.py:1138 choices.py:21 253 | msgid "Friday" 254 | msgstr "Piatok" 255 | 256 | #: base.py:1138 choices.py:22 257 | msgid "Saturday" 258 | msgstr "Sobota" 259 | 260 | #: base.py:1138 choices.py:23 261 | msgid "Sunday" 262 | msgstr "Nedeľa" 263 | 264 | #: base.py:1141 choices.py:27 265 | msgid "January" 266 | msgstr "Január" 267 | 268 | #: base.py:1141 choices.py:28 269 | msgid "February" 270 | msgstr "Február" 271 | 272 | #: base.py:1141 choices.py:29 273 | msgid "March" 274 | msgstr "Marec" 275 | 276 | #: base.py:1141 choices.py:30 277 | msgid "April" 278 | msgstr "Apríl" 279 | 280 | #: base.py:1142 choices.py:32 281 | msgid "June" 282 | msgstr "Jún" 283 | 284 | #: base.py:1142 choices.py:33 285 | msgid "July" 286 | msgstr "Júl" 287 | 288 | #: base.py:1142 choices.py:34 289 | msgid "August" 290 | msgstr "August" 291 | 292 | #: base.py:1143 choices.py:35 293 | msgid "September" 294 | msgstr "September" 295 | 296 | #: base.py:1143 choices.py:36 297 | msgid "October" 298 | msgstr "Október" 299 | 300 | #: base.py:1143 choices.py:37 301 | msgid "November" 302 | msgstr "November" 303 | 304 | #: base.py:1143 choices.py:38 305 | msgid "December" 306 | msgstr "December" 307 | 308 | #: base.py:1161 base.py:1178 base.py:1189 base.py:1202 base.py:1220 309 | msgid ", " 310 | msgstr ", " 311 | 312 | #: base.py:1167 313 | #, python-format 314 | msgid "every %(number)s %(freq)s" 315 | msgstr "každý %(number)s %(freq)s" 316 | 317 | #: base.py:1181 base.py:1205 318 | #, python-format 319 | msgid "each %(items)s" 320 | msgstr "každý %(items)s" 321 | 322 | #: base.py:1184 base.py:1193 base.py:1197 323 | #, python-format 324 | msgid "on the %(items)s" 325 | msgstr "o %(items)s" 326 | 327 | #: base.py:1212 328 | msgid "occuring once" 329 | msgstr "nastáva raz" 330 | 331 | #: base.py:1214 332 | #, python-format 333 | msgid "occuring %(number)s times" 334 | msgstr "nastáva %(number)s krát" 335 | 336 | #: base.py:1217 337 | #, python-format 338 | msgid "until %(date)s" 339 | msgstr "do %(date)s" 340 | 341 | #: choices.py:7 342 | msgid "Secondly" 343 | msgstr "Každú zvolenú sekundu" 344 | 345 | #: choices.py:8 346 | msgid "Minutely" 347 | msgstr "Každú zvolenú minútu" 348 | 349 | #: choices.py:9 350 | msgid "Hourly" 351 | msgstr "Každú zvolenú hodinu" 352 | 353 | #: choices.py:10 354 | msgid "Daily" 355 | msgstr "Denne" 356 | 357 | #: choices.py:11 358 | msgid "Weekly" 359 | msgstr "Týždenne" 360 | 361 | #: choices.py:12 362 | msgid "Monthly" 363 | msgstr "Mesačne" 364 | 365 | #: choices.py:13 366 | msgid "Yearly" 367 | msgstr "Ročne" 368 | 369 | #: choices.py:31 370 | msgid "May" 371 | msgstr "Máj" 372 | 373 | #: choices.py:44 374 | msgid "Inclusion" 375 | msgstr "Zahrnuté" 376 | 377 | #: choices.py:45 378 | msgid "Exclusion" 379 | msgstr "Vylúčené" 380 | 381 | #: forms.py:72 382 | msgid "Invalid frequency." 383 | msgstr "Nesprávna frekvencia." 384 | 385 | #: forms.py:74 386 | #, python-format 387 | msgid "Max rules exceeded. The limit is %(limit)s" 388 | msgstr "Maximum pravidiel prekročených. Limit je %(limit)s" 389 | 390 | #: forms.py:76 391 | #, python-format 392 | msgid "Max exclusion rules exceeded. The limit is %(limit)s" 393 | msgstr "Maximum vylučovacích pravidiel prekročených. Limit je %(limit)s" 394 | 395 | #: forms.py:78 396 | #, python-format 397 | msgid "Max dates exceeded. The limit is %(limit)s" 398 | msgstr "Maximum dátumov prekročené. Limit je %(limit)s" 399 | 400 | #: forms.py:80 401 | #, python-format 402 | msgid "Max exclusion dates exceeded. The limit is %(limit)s" 403 | msgstr "Maximum vylučovacích dátumov prekročené. Limit je %(limit)s" 404 | -------------------------------------------------------------------------------- /recurrence/locale/sk/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/locale/sk/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /recurrence/managers.py: -------------------------------------------------------------------------------- 1 | from django.db.models import manager 2 | 3 | import recurrence 4 | from recurrence import choices, to_utc 5 | 6 | 7 | class RuleManager(manager.Manager): 8 | def to_rule_object(self, rule_model): 9 | rule_args = (rule_model.freq,) 10 | rule_kwargs = { 11 | 'interval': rule_model.interval, 12 | 'wkst': rule_model.wkst, 13 | 'count': rule_model.count, 14 | 'until': to_utc(rule_model.until), 15 | } 16 | 17 | for param in recurrence.Rule.byparams: 18 | if param == 'byday': 19 | # see recurrence.base docstrings about byday handling 20 | rule_kwargs[param] = (map( 21 | lambda v: recurrence.Weekday(*v), 22 | rule_model.params.filter(param=param).values_list( 23 | 'value', 'index')) or None) 24 | else: 25 | rule_kwargs[param] = (map( 26 | lambda v: v[0], 27 | rule_model.params.filter(param=param).values_list( 28 | 'value' 29 | ) 30 | ) or None) 31 | 32 | return recurrence.Rule(*rule_args, **rule_kwargs) 33 | 34 | def create_from_rule_object(self, mode, rule_obj, recurrence_model): 35 | until = to_utc(rule_obj.until) 36 | 37 | rule_model = self.create( 38 | recurrence=recurrence_model, mode=mode, 39 | freq=rule_obj.freq, interval=rule_obj.interval, wkst=rule_obj.wkst, 40 | count=rule_obj.count, until=until) 41 | 42 | for param in recurrence.Rule.byparams: 43 | value_list = getattr(rule_obj, param, None) 44 | if not value_list: 45 | continue 46 | if not hasattr(value_list, '__iter__'): 47 | value_list = [value_list] 48 | for value in value_list: 49 | if param == 'byday': 50 | # see recurrence.base docstrings about byday handling 51 | weekday = recurrence.to_weekday(value) 52 | rule_model.params.create( 53 | param=param, value=weekday.number, index=weekday.index) 54 | else: 55 | rule_model.params.create(param=param, value=value) 56 | 57 | return rule_model 58 | 59 | 60 | class RecurrenceManager(manager.Manager): 61 | def to_recurrence_object(self, recurrence_model): 62 | rrules, exrules, rdates, exdates = [], [], [], [] 63 | 64 | for rule_model in recurrence_model.rules.filter(mode=choices.INCLUSION): 65 | rrules.append(rule_model.to_rule_object()) 66 | for exrule_model in recurrence_model.rules.filter(mode=choices.EXCLUSION): 67 | exrules.append(exrule_model.to_rule_object()) 68 | 69 | for rdate_model in recurrence_model.dates.filter(mode=choices.INCLUSION): 70 | rdates.append(to_utc(rdate_model.dt)) 71 | for exdate_model in recurrence_model.dates.filter(mode=choices.EXCLUSION): 72 | exdates.append(to_utc(exdate_model.dt)) 73 | 74 | dtstart = to_utc(recurrence_model.dtstart) 75 | dtend = to_utc(recurrence_model.dtend) 76 | 77 | return recurrence.Recurrence( 78 | dtstart, dtend, rrules, exrules, rdates, exdates) 79 | 80 | def create_from_recurrence_object(self, recurrence_obj): 81 | from recurrence import models 82 | 83 | recurrence_model = self.create( 84 | dtstart=to_utc(recurrence_obj.dtstart), 85 | dtend=to_utc(recurrence_obj.dtend)) 86 | 87 | for rrule in recurrence_obj.rrules: 88 | models.Rule.objects.create_from_rule_object( 89 | choices.INCLUSION, rrule, recurrence_model) 90 | for exrule in recurrence_obj.exrules: 91 | models.Rule.objects.create_from_rule_object( 92 | choices.EXCLUSION, exrule, recurrence_model) 93 | 94 | for dt in recurrence_obj.rdates: 95 | recurrence_model.dates.create(mode=choices.INCLUSION, dt=to_utc(dt)) 96 | for dt in recurrence_obj.exdates: 97 | recurrence_model.dates.create(mode=choices.EXCLUSION, dt=to_utc(dt)) 98 | 99 | return recurrence_model 100 | -------------------------------------------------------------------------------- /recurrence/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import models, migrations 2 | import django.db.models.deletion 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name='Date', 13 | fields=[ 14 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 15 | ('mode', models.BooleanField(default=True, choices=[(True, 'Inclusion'), (False, 'Exclusion')])), 16 | ('dt', models.DateTimeField()), 17 | ], 18 | ), 19 | migrations.CreateModel( 20 | name='Param', 21 | fields=[ 22 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 23 | ('param', models.CharField(max_length=16)), 24 | ('value', models.IntegerField()), 25 | ('index', models.IntegerField(default=0)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='Recurrence', 30 | fields=[ 31 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 32 | ('dtstart', models.DateTimeField(null=True, blank=True)), 33 | ('dtend', models.DateTimeField(null=True, blank=True)), 34 | ], 35 | ), 36 | migrations.CreateModel( 37 | name='Rule', 38 | fields=[ 39 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 40 | ('mode', models.BooleanField(default=True, choices=[(True, 'Inclusion'), (False, 'Exclusion')])), 41 | ('freq', models.PositiveIntegerField(choices=[(6, 'Secondly'), (5, 'Minutely'), (4, 'Hourly'), (3, 'Daily'), (2, 'Weekly'), (1, 'Monthly'), (0, 'Yearly')])), 42 | ('interval', models.PositiveIntegerField(default=1)), 43 | ('wkst', models.PositiveIntegerField(default=0, null=True, blank=True)), 44 | ('count', models.PositiveIntegerField(null=True, blank=True)), 45 | ('until', models.DateTimeField(null=True, blank=True)), 46 | ('recurrence', models.ForeignKey(related_name='rules', on_delete=django.db.models.deletion.CASCADE, to='recurrence.Recurrence')), 47 | ], 48 | ), 49 | migrations.AddField( 50 | model_name='param', 51 | name='rule', 52 | field=models.ForeignKey(related_name='params', on_delete=django.db.models.deletion.CASCADE, to='recurrence.Rule'), 53 | ), 54 | migrations.AddField( 55 | model_name='date', 56 | name='recurrence', 57 | field=models.ForeignKey(related_name='dates', on_delete=django.db.models.deletion.CASCADE, to='recurrence.Recurrence'), 58 | ), 59 | ] 60 | -------------------------------------------------------------------------------- /recurrence/migrations/0002_alter_date_id_alter_param_id_alter_recurrence_id_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-04-28 10:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('recurrence', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='date', 15 | name='id', 16 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 17 | ), 18 | migrations.AlterField( 19 | model_name='param', 20 | name='id', 21 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 22 | ), 23 | migrations.AlterField( 24 | model_name='recurrence', 25 | name='id', 26 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 27 | ), 28 | migrations.AlterField( 29 | model_name='rule', 30 | name='id', 31 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /recurrence/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/migrations/__init__.py -------------------------------------------------------------------------------- /recurrence/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | import recurrence as recur 4 | from recurrence import managers, choices 5 | 6 | 7 | class Recurrence(models.Model): 8 | dtstart = models.DateTimeField(null=True, blank=True) 9 | dtend = models.DateTimeField(null=True, blank=True) 10 | 11 | objects = managers.RecurrenceManager() 12 | 13 | def to_recurrence_object(self): 14 | return Recurrence.objects.to_recurrence_object(self) 15 | 16 | 17 | class Rule(models.Model): 18 | recurrence = models.ForeignKey(Recurrence, related_name='rules', on_delete=models.CASCADE) 19 | mode = models.BooleanField(default=True, choices=choices.MODE_CHOICES) 20 | freq = models.PositiveIntegerField(choices=choices.FREQUENCY_CHOICES) 21 | interval = models.PositiveIntegerField(default=1) 22 | wkst = models.PositiveIntegerField( 23 | default=recur.Rule.firstweekday, null=True, blank=True) 24 | count = models.PositiveIntegerField(null=True, blank=True) 25 | until = models.DateTimeField(null=True, blank=True) 26 | 27 | objects = managers.RuleManager() 28 | 29 | def to_rule_object(self): 30 | return self.__class__.objects.to_rule_object(self) 31 | 32 | 33 | class Date(models.Model): 34 | recurrence = models.ForeignKey(Recurrence, related_name='dates', on_delete=models.CASCADE) 35 | mode = models.BooleanField(default=True, choices=choices.MODE_CHOICES) 36 | dt = models.DateTimeField() 37 | 38 | 39 | class Param(models.Model): 40 | rule = models.ForeignKey(Rule, related_name='params', on_delete=models.CASCADE) 41 | param = models.CharField(max_length=16) 42 | value = models.IntegerField() 43 | index = models.IntegerField(default=0) 44 | -------------------------------------------------------------------------------- /recurrence/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings for django recurrence. 3 | 4 | DESERIALIZE_TZ determines if a datetime serialized as rfc2445 text 5 | is returned as a naive datetime or a datetime with timezone. 6 | 7 | If the django setting RECURRENCE_USE_TZ is true 8 | then a timezone aware datetime is returned 9 | 10 | If the django setting RECURRENCE_USE_TZ is false 11 | then a naive datetime is returned 12 | 13 | If this setting is not present the setting USE_TZ is used 14 | as a default. 15 | """ 16 | 17 | from django.conf import settings 18 | 19 | 20 | def deserialize_tz(): 21 | try: 22 | return settings.RECURRENCE_USE_TZ 23 | except AttributeError: 24 | return settings.USE_TZ 25 | -------------------------------------------------------------------------------- /recurrence/static/recurrence/css/recurrence.css: -------------------------------------------------------------------------------- 1 | div.recurrence-widget { 2 | float: left; 3 | border: 1px solid #ccc; 4 | } 5 | 6 | div.recurrence-widget div.panel { 7 | border-bottom: 1px solid #ddd; 8 | } 9 | 10 | div.recurrence-widget select, 11 | div.recurrence-widget input { 12 | margin: 0 .5em; 13 | width: auto; 14 | } 15 | 16 | div.recurrence-widget a.recurrence-label { 17 | display: block; 18 | cursor: pointer; 19 | padding: .5em .75em; 20 | font-weight: bold; 21 | white-space: nowrap; 22 | padding-right: 3em; 23 | color: black; 24 | } 25 | 26 | @media (prefers-color-scheme: dark) { 27 | div.recurrence-widget a.recurrence-label { 28 | color: white; 29 | } 30 | } 31 | 32 | div.recurrence-widget .remove, 33 | div.recurrence-widget .remove:link, 34 | div.recurrence-widget .remove:visited { 35 | float: left; 36 | padding: .25em .5em; 37 | font-size: 1.1em; 38 | font-weight: bold; 39 | cursor: pointer; 40 | text-decoration: none; 41 | color: #555; 42 | line-height: 1.4em; 43 | } 44 | 45 | div.recurrence-widget .remove:hover { 46 | color: #a00; 47 | } 48 | 49 | div.recurrence-widget .control { 50 | padding: .5em 1em .5em .5em; 51 | } 52 | 53 | div.recurrence-widget .add-button, 54 | div.recurrence-widget .add-button:link, 55 | div.recurrence-widget .add-button:visited { 56 | color: green; 57 | } 58 | 59 | div.recurrence-widget .plus { 60 | font-size: 1.2em; 61 | font-weight: bold; 62 | padding-right: .5em; 63 | } 64 | 65 | div.recurrence-widget .add-button:hover { 66 | color: green; 67 | } 68 | 69 | div.recurrence-widget .control .add-date { 70 | padding-left: 1em; 71 | margin-left: 1em; 72 | border-left: 1px solid #ddd; 73 | } 74 | 75 | div.recurrence-widget .freq { 76 | display: inline 77 | } 78 | 79 | div.recurrence-widget .interval { 80 | display: inline; 81 | padding-left: 1em; 82 | } 83 | 84 | div.recurrence-widget form { 85 | overflow: hidden; 86 | padding: .5em 2.2em; 87 | border-top: 1px solid #ddd; 88 | } 89 | 90 | div.recurrence-widget .form { 91 | padding: .5em 0; 92 | } 93 | 94 | div.recurrence-widget .radio { 95 | margin-left: 0; 96 | } 97 | 98 | div.recurrence-widget .radio:before { 99 | padding: 0px; 100 | } 101 | 102 | div.recurrence-widget .form ul { 103 | margin: 0; 104 | padding: 0; 105 | } 106 | 107 | div.recurrence-widget .form ul li { 108 | list-style-type: none; 109 | } 110 | 111 | div.recurrence-widget .section { 112 | margin: .5em 1em; 113 | } 114 | 115 | div.recurrence-widget .until-count { 116 | margin: 0; 117 | padding: 0; 118 | } 119 | 120 | div.recurrence-widget .until-count li { 121 | margin: 0; 122 | padding: 0; 123 | display: inline; 124 | list-style-type: none; 125 | } 126 | 127 | div.recurrence-widget .until-count .count { 128 | padding-left: 1em; 129 | } 130 | 131 | div.recurrence-widget .monthday .grid { 132 | margin-left: .5em; 133 | } 134 | 135 | div.recurrence-widget .grid { 136 | border: 0; 137 | border-top: 1px solid #ccc; 138 | border-left: 1px solid #ccc; 139 | } 140 | 141 | div.recurrence-widget .grid td { 142 | text-align: center; 143 | border-bottom: 1px solid #ccc; 144 | border-right: 1px solid #ccc; 145 | padding: .8em; 146 | cursor: pointer; 147 | } 148 | 149 | div.recurrence-widget .grid td.active { 150 | background-color: #ddd; 151 | } 152 | 153 | div.recurrence-widget .grid td.empty { 154 | border: 0; 155 | } 156 | 157 | div.recurrence-widget .grid-disabled td.active { 158 | background-color: #ddd; 159 | } 160 | 161 | div.recurrence-widget .disabled { 162 | opacity: .75; 163 | filter: alpha(opacity=75); 164 | } 165 | 166 | div.recurrence-widget .limit ul { 167 | margin-top: .5em; 168 | } 169 | 170 | div.recurrence-widget .weekly .section { 171 | margin: 1em 0; 172 | } 173 | 174 | div.recurrence-widget .weekly td { 175 | width: 2.5em; 176 | } 177 | 178 | div.recurrence-widget .yearly table { 179 | margin: .5em 0 1em 0; 180 | } 181 | 182 | div.recurrence-widget .yearly td { 183 | padding: 1em 1.5em; 184 | } 185 | 186 | div.recurrence-widget .checkbox { 187 | margin-left: 0; 188 | } 189 | 190 | div.recurrence-widget .checkbox:before { 191 | width: auto; 192 | height: auto; 193 | } 194 | 195 | div.recurrence-widget .until-count { 196 | margin-left: 1.5em; 197 | } 198 | 199 | div.recurrence-widget .mode { 200 | padding: .25em 0 1em 0; 201 | } 202 | 203 | div.recurrence-widget .date-selector .calendar-button { 204 | background: url('../img/recurrence-calendar-icon.png') no-repeat center; 205 | padding: .25em; 206 | } 207 | 208 | .recurrence-widget.hidden, div.recurrence-widget .hidden { 209 | display: none; 210 | } 211 | 212 | table.recurrence-calendar { 213 | position: absolute; 214 | background-color: white; 215 | border-top: 1px solid #ccc; 216 | border-left: 1px solid #ccc; 217 | /*outline: 3px solid white;*/ 218 | margin: 0; 219 | } 220 | 221 | table.recurrence-calendar td { 222 | padding: 0; 223 | } 224 | 225 | table.recurrence-calendar a, 226 | table.recurrence-calendar a:link, 227 | table.recurrence-calendar a:visited { 228 | color: #555; 229 | font-weight: bold; 230 | } 231 | 232 | table.recurrence-calendar .year, 233 | table.recurrence-calendar .navigation { 234 | border-right: 1px solid #ccc; 235 | } 236 | 237 | table.recurrence-calendar .body td { 238 | border: 0; 239 | border-bottom: 1px solid #ccc; 240 | border-right: 1px solid #ccc; 241 | } 242 | 243 | table.recurrence-calendar .body td.header { 244 | border-top: 1px solid #ccc; 245 | font-weight: bold; 246 | color: #555; 247 | } 248 | 249 | table.recurrence-calendar .body td.header, 250 | table.recurrence-calendar .body td.day { 251 | padding: .25em; 252 | text-align: center; 253 | vertical-align: center; 254 | } 255 | 256 | table.recurrence-calendar .body td.day { 257 | cursor: pointer; 258 | } 259 | 260 | table.recurrence-calendar .body td.active { 261 | background-color: #ddd; 262 | } 263 | 264 | table.recurrence-calendar .body td.empty { 265 | background-color: #eee; 266 | } 267 | 268 | table.recurrence-calendar .year { 269 | padding-top: .25em; 270 | padding-right: 2em; 271 | text-align: center; 272 | } 273 | 274 | table.recurrence-calendar .navigation { 275 | width: 100%; 276 | } 277 | 278 | table.recurrence-calendar .navigation { 279 | text-align: center; 280 | } 281 | 282 | table.recurrence-calendar .navigation td.prev-year, 283 | table.recurrence-calendar .navigation td.next-year, 284 | table.recurrence-calendar .navigation td.prev-month, 285 | table.recurrence-calendar .navigation td.next-month { 286 | /*width: 1%;*/ 287 | font-size: .8em; 288 | } 289 | 290 | table.recurrence-calendar .remove, 291 | table.recurrence-calendar .remove:link, 292 | table.recurrence-calendar .remove:visited { 293 | float: left; 294 | padding: .1em .25em; 295 | padding-bottom: 0; 296 | font-size: 1.1em; 297 | font-weight: bold; 298 | cursor: pointer; 299 | text-decoration: none; 300 | color: #555; 301 | line-height: 1.2em; 302 | } 303 | 304 | table.recurrence-calendar .remove:hover { 305 | color: #a00; 306 | } 307 | 308 | table.recurrence-calendar .footer { 309 | height: .5em; 310 | overflow: hidden; 311 | border-bottom: 1px solid #ccc; 312 | border-right: 1px solid #ccc; 313 | } 314 | -------------------------------------------------------------------------------- /recurrence/static/recurrence/img/recurrence-calendar-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/recurrence/static/recurrence/img/recurrence-calendar-icon.png -------------------------------------------------------------------------------- /recurrence/static/recurrence/js/recurrence-widget.init.js: -------------------------------------------------------------------------------- 1 | django.jQuery(document).ready(function () { 2 | // Init on all existing recurrence fields 3 | initRecurrenceWidget(); 4 | // Begin DOM observation for new inline additions 5 | const targetNode = document.getElementById('container'); 6 | if (targetNode !== null) { 7 | const config = { attributes: false, childList: true, subtree: true }; 8 | observer.observe(targetNode, config); 9 | } else { 10 | console.warn('Recurrence widget: No node with id=\'container\' found. Inline additions will not be observed.'); 11 | } 12 | }); 13 | 14 | /* 15 | Method iterates over all textareas with .recurrence-widget class name, excluding the last one with '__prefix__', 16 | if no parameters provided. 17 | If specific field item is prvided it will only init this particular field to prevent, unnecessary iterations. 18 | */ 19 | function initRecurrenceWidget($field) { 20 | if (!$field) { 21 | const recurrenceFields = django.jQuery(document).find('textarea.recurrence-widget:not([id*="__prefix__"])'); 22 | django.jQuery.each(recurrenceFields, function (index, field) { 23 | const $field = django.jQuery(field); 24 | new recurrence.widget.Widget($field.attr('id'), {}); 25 | }); 26 | } else { 27 | new recurrence.widget.Widget($field.attr('id'), {}); 28 | } 29 | } 30 | 31 | /* 32 | MutationObserver 33 | */ 34 | const callback = function (mutationsList, observer) { 35 | for (const mutation of mutationsList) { 36 | if (mutation.type === 'childList') { 37 | // Check if nodes were added 38 | const addedNodes = mutation.addedNodes; 39 | if (addedNodes.length > 0) { 40 | const $addedRecurrenceField = django.jQuery(addedNodes[0]).find('.recurrence-widget'); 41 | // Length has to be 1, to prevent cases when draging inlines returns length 0 or more than 1. 42 | if ($addedRecurrenceField.length === 1) { 43 | initRecurrenceWidget($addedRecurrenceField); 44 | } 45 | } 46 | } 47 | } 48 | }; 49 | const observer = new MutationObserver(callback); 50 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-recurrence/6aaff5fb0f3319e5d2170c05896bde61877200cc/tests/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from recurrence.fields import RecurrenceField 3 | 4 | 5 | class EventWithNoNulls(models.Model): 6 | recurs = RecurrenceField(null=False) 7 | 8 | 9 | class EventWithNulls(models.Model): 10 | recurs = RecurrenceField(null=True) 11 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'ENGINE': 'django.db.backends.sqlite3', 4 | 'NAME': 'test_db.sqlite' 5 | } 6 | } 7 | 8 | MIDDLEWARE_CLASSES = ( 9 | 'django.contrib.sessions.middleware.SessionMiddleware', 10 | 'django.middleware.common.CommonMiddleware', 11 | 'django.middleware.csrf.CsrfViewMiddleware', 12 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 13 | 'django.contrib.messages.middleware.MessageMiddleware', 14 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 15 | ) 16 | 17 | TEMPLATE_CONTEXT_PROCESSORS = ( 18 | 'django.contrib.auth.context_processors.auth', 19 | 'django.core.context_processors.debug', 20 | 'django.core.context_processors.i18n', 21 | 'django.core.context_processors.media', 22 | 'django.core.context_processors.request', 23 | 'django.core.context_processors.static', 24 | 'django.core.context_processors.tz', 25 | ) 26 | 27 | INSTALLED_APPS = ( 28 | 'django.contrib.admin', 29 | 'django.contrib.auth', 30 | 'django.contrib.contenttypes', 31 | 'django.contrib.sessions', 32 | 'django.contrib.messages', 33 | 'django.contrib.staticfiles', 34 | 'recurrence', 35 | 'tests', 36 | ) 37 | 38 | # ROOT_URLCONF = 'tests.urls' 39 | 40 | PASSWORD_HASHERS = ( 41 | 'django.contrib.auth.hashers.MD5PasswordHasher', 42 | 'django.contrib.auth.hashers.SHA1PasswordHasher', 43 | ) 44 | 45 | STATIC_URL = '/fakestatictrees/' 46 | MEDIA_ROOT = 'tests/testmedia/' 47 | 48 | TIME_ZONE = 'Europe/London' 49 | LANGUAGE_CODE = 'en-us' 50 | SITE_ID = 1 51 | USE_I18N = True 52 | USE_L10N = True 53 | USE_TZ = True 54 | SECRET_KEY = 'thisbagismadefromrecycledmaterial' 55 | TEMPLATE_DEBUG = True 56 | RECURRENCE_USE_TZ = False 57 | -------------------------------------------------------------------------------- /tests/test_exclusions.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from recurrence import Recurrence, Rule 3 | import recurrence 4 | 5 | 6 | def test_exclusion_date(): 7 | rule = Rule( 8 | recurrence.DAILY 9 | ) 10 | 11 | pattern = Recurrence( 12 | dtstart=datetime(2014, 1, 2, 0, 0, 0), 13 | dtend=datetime(2014, 1, 4, 0, 0, 0), 14 | rrules=[rule], 15 | exdates=[ 16 | datetime(2014, 1, 3, 0, 0, 0) 17 | ] 18 | ) 19 | 20 | occurrences = [ 21 | instance for instance in 22 | pattern.occurrences() 23 | ] 24 | 25 | assert occurrences == [ 26 | datetime(2014, 1, 2, 0, 0, 0), 27 | datetime(2014, 1, 4, 0, 0, 0), 28 | ] 29 | 30 | assert 2 == pattern.count() 31 | 32 | 33 | def test_exclusion_date_no_limits(): 34 | pattern = Recurrence( 35 | rdates=[ 36 | datetime(2014, 1, 1, 0, 0, 0), 37 | datetime(2014, 1, 2, 0, 0, 0), 38 | ], 39 | exdates=[ 40 | datetime(2014, 1, 2, 0, 0, 0) 41 | ] 42 | ) 43 | 44 | occurrences = [ 45 | instance for instance in 46 | pattern.occurrences() 47 | ] 48 | 49 | assert occurrences == [ 50 | datetime(2014, 1, 1, 0, 0, 0), 51 | ] 52 | 53 | assert 1 == pattern.count() 54 | 55 | 56 | def test_exclusion_rule(): 57 | inclusion_rule = Rule( 58 | recurrence.DAILY 59 | ) 60 | 61 | exclusion_rule = Rule( 62 | recurrence.WEEKLY, 63 | byday=recurrence.THURSDAY 64 | ) 65 | 66 | pattern = Recurrence( 67 | dtstart=datetime(2014, 1, 2, 0, 0, 0), 68 | dtend=datetime(2014, 1, 4, 0, 0, 0), 69 | rrules=[inclusion_rule], 70 | exrules=[exclusion_rule] 71 | ) 72 | 73 | occurrences = [ 74 | instance for instance in 75 | pattern.occurrences() 76 | ] 77 | 78 | assert occurrences == [ 79 | datetime(2014, 1, 3, 0, 0, 0), 80 | datetime(2014, 1, 4, 0, 0, 0), 81 | ] 82 | 83 | assert 2 == pattern.count() 84 | -------------------------------------------------------------------------------- /tests/test_fields.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | from django import forms 5 | 6 | import recurrence 7 | from recurrence import Recurrence, Rule 8 | from recurrence.forms import RecurrenceField 9 | 10 | 11 | def test_clean_normal_value(): 12 | field = RecurrenceField() 13 | value = "RRULE:FREQ=WEEKLY;BYDAY=TU" 14 | 15 | obj = field.clean(value) 16 | 17 | assert len(obj.rrules) == 1 18 | assert obj.rrules[0].to_text() == "weekly, each Tuesday" 19 | 20 | 21 | def test_clean_invalid_value(): 22 | field = RecurrenceField() 23 | value = "RRULE:FREQS=WEEKLY" 24 | 25 | with pytest.raises(forms.ValidationError) as e: 26 | field.clean(value) 27 | assert e.value.messages[0] == "bad parameter: FREQS" 28 | 29 | 30 | def test_strip_dtstart_and_dtend_if_required(): 31 | rule = Rule( 32 | recurrence.WEEKLY 33 | ) 34 | 35 | limits = Recurrence( 36 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 37 | dtend=datetime(2014, 2, 3, 0, 0, 0), 38 | rrules=[rule] 39 | ) 40 | 41 | value = recurrence.serialize(limits) 42 | 43 | field = RecurrenceField() 44 | cleaned_value = field.clean(value) 45 | assert cleaned_value == limits 46 | assert cleaned_value.dtstart == datetime(2014, 1, 1, 0, 0, 0) 47 | assert cleaned_value.dtend == datetime(2014, 2, 3, 0, 0, 0) 48 | 49 | field = RecurrenceField(accept_dtstart=False, accept_dtend=False) 50 | cleaned_value = field.clean(value) 51 | assert cleaned_value != limits 52 | assert cleaned_value.dtstart is None 53 | assert cleaned_value.dtend is None 54 | 55 | 56 | def test_check_max_rrules(): 57 | rule = Rule( 58 | recurrence.WEEKLY 59 | ) 60 | 61 | limits = Recurrence( 62 | rrules=[rule] 63 | ) 64 | 65 | value = recurrence.serialize(limits) 66 | 67 | field = RecurrenceField(max_rrules=0) 68 | with pytest.raises(forms.ValidationError) as e: 69 | field.clean(value) 70 | assert e.value.messages[0] == "Max rules exceeded. The limit is 0" 71 | 72 | 73 | def test_check_max_exrules(): 74 | rule = Rule( 75 | recurrence.WEEKLY 76 | ) 77 | 78 | limits = Recurrence( 79 | exrules=[rule] 80 | ) 81 | 82 | value = recurrence.serialize(limits) 83 | 84 | field = RecurrenceField(max_exrules=0) 85 | with pytest.raises(forms.ValidationError) as e: 86 | field.clean(value) 87 | assert e.value.messages[0] == ("Max exclusion rules exceeded. " 88 | "The limit is 0") 89 | 90 | 91 | def test_check_max_rdates(): 92 | limits = Recurrence( 93 | rdates=[ 94 | datetime(2014, 1, 1, 0, 0, 0), 95 | datetime(2014, 1, 2, 0, 0, 0), 96 | ] 97 | ) 98 | 99 | value = recurrence.serialize(limits) 100 | 101 | field = RecurrenceField(max_rdates=2) 102 | field.clean(value) 103 | 104 | field = RecurrenceField(max_rdates=1) 105 | with pytest.raises(forms.ValidationError) as e: 106 | field.clean(value) 107 | assert e.value.messages[0] == "Max dates exceeded. The limit is 1" 108 | 109 | 110 | def test_check_max_exdates(): 111 | limits = Recurrence( 112 | exdates=[ 113 | datetime(2014, 1, 1, 0, 0, 0), 114 | datetime(2014, 1, 2, 0, 0, 0), 115 | ] 116 | ) 117 | 118 | value = recurrence.serialize(limits) 119 | 120 | field = RecurrenceField(max_exdates=2) 121 | field.clean(value) 122 | 123 | field = RecurrenceField(max_exdates=1) 124 | with pytest.raises(forms.ValidationError) as e: 125 | field.clean(value) 126 | assert e.value.messages[0] == ("Max exclusion dates exceeded. " 127 | "The limit is 1") 128 | 129 | 130 | def test_check_allowable_frequencies(): 131 | rule = Rule( 132 | recurrence.WEEKLY 133 | ) 134 | 135 | limits = Recurrence( 136 | rrules=[rule] 137 | ) 138 | 139 | value = recurrence.serialize(limits) 140 | 141 | field = RecurrenceField(frequencies=[ 142 | recurrence.WEEKLY 143 | ]) 144 | field.clean(value) 145 | 146 | field = RecurrenceField(frequencies=[ 147 | recurrence.YEARLY 148 | ]) 149 | with pytest.raises(forms.ValidationError) as e: 150 | field.clean(value) 151 | assert e.value.messages[0] == "Invalid frequency." 152 | 153 | limits = Recurrence( 154 | exrules=[rule] 155 | ) 156 | 157 | value = recurrence.serialize(limits) 158 | 159 | with pytest.raises(forms.ValidationError) as e: 160 | field.clean(value) 161 | assert e.value.messages[0] == "Invalid frequency." 162 | 163 | 164 | def test_include_dtstart_from_field(): 165 | rule = Rule( 166 | recurrence.WEEKLY, 167 | byday=recurrence.MONDAY 168 | ) 169 | 170 | limits = Recurrence( 171 | rrules=[rule] 172 | ) 173 | 174 | value = recurrence.serialize(limits) 175 | 176 | model_field = recurrence.fields.RecurrenceField() # Test with include_dtstart=True (default) 177 | rec_obj = model_field.to_python(value) 178 | assert rec_obj == limits 179 | # 2nd of August (dtstart) is expected but only for inc=True 180 | assert rec_obj.between(datetime(2015, 8, 2), datetime(2015, 8, 11), inc=True, dtstart=datetime(2015, 8, 2)) == [ 181 | datetime(2015, 8, 2, 0, 0), datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0)] 182 | assert rec_obj.between(datetime(2015, 8, 2), datetime(2015, 8, 11), inc=False, dtstart=datetime(2015, 8, 2)) == [ 183 | datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0)] 184 | 185 | model_field = recurrence.fields.RecurrenceField(include_dtstart=False) # Test with include_dtstart=False 186 | rec_obj = model_field.to_python(value) 187 | assert rec_obj == limits 188 | # 2nd of August (dtstart) is not expected regardless of inc 189 | assert rec_obj.between(datetime(2015, 8, 2), datetime(2015, 8, 11), inc=True, dtstart=datetime(2015, 8, 2)) == [ 190 | datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0)] 191 | assert rec_obj.between(datetime(2015, 8, 2), datetime(2015, 8, 11), inc=False, dtstart=datetime(2015, 8, 2)) == [ 192 | datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0)] 193 | 194 | 195 | def test_include_dtstart_from_object(): 196 | rule = Rule( 197 | recurrence.WEEKLY, 198 | byday=recurrence.MONDAY 199 | ) 200 | 201 | limits = Recurrence( # include_dtstart=True (default) 202 | rrules=[rule] 203 | ) 204 | 205 | assert limits.between(datetime(2015, 8, 2), datetime(2015, 8, 11), inc=True, dtstart=datetime(2015, 8, 2)) == [ 206 | datetime(2015, 8, 2, 0, 0), datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0)] 207 | assert limits.between(datetime(2015, 8, 2), datetime(2015, 8, 11), inc=False, dtstart=datetime(2015, 8, 2)) == [ 208 | datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0)] 209 | 210 | limits = Recurrence( # include_dtstart=False (dtstart is expected to not be included) 211 | include_dtstart=False, 212 | rrules=[rule] 213 | ) 214 | 215 | assert limits.between(datetime(2015, 8, 2), datetime(2015, 8, 11), inc=True, dtstart=datetime(2015, 8, 2)) == [ 216 | datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0)] 217 | assert limits.between(datetime(2015, 8, 2), datetime(2015, 8, 11), inc=False, dtstart=datetime(2015, 8, 2)) == [ 218 | datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0)] 219 | 220 | 221 | def test_none_fieldvalue(): 222 | field = RecurrenceField() 223 | value = None 224 | return_obj = field.clean(value) 225 | 226 | assert isinstance(return_obj, Recurrence) or return_obj is None 227 | -------------------------------------------------------------------------------- /tests/test_magic_methods.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from recurrence import Recurrence, Rule 3 | import recurrence 4 | 5 | 6 | def test_truthiness_with_single_rrule(): 7 | rule = Rule( 8 | recurrence.DAILY 9 | ) 10 | 11 | object = Recurrence( 12 | rrules=[rule] 13 | ) 14 | 15 | assert bool(object) 16 | 17 | 18 | def test_truthiness_with_single_exrule(): 19 | rule = Rule( 20 | recurrence.DAILY 21 | ) 22 | 23 | object = Recurrence( 24 | exrules=[rule] 25 | ) 26 | 27 | assert bool(object) 28 | 29 | 30 | def test_truthiness_with_single_rdate(): 31 | object = Recurrence( 32 | rdates=[datetime(2014, 12, 31, 0, 0, 0)] 33 | ) 34 | 35 | assert bool(object) 36 | 37 | 38 | def test_truthiness_with_single_exdate(): 39 | object = Recurrence( 40 | exdates=[datetime(2014, 12, 31, 0, 0, 0)] 41 | ) 42 | 43 | assert bool(object) 44 | 45 | 46 | def test_truthiness_with_dtstart(): 47 | object = Recurrence( 48 | dtstart=datetime(2014, 12, 31, 0, 0, 0) 49 | ) 50 | 51 | assert bool(object) 52 | 53 | 54 | def test_truthiness_with_dtend(): 55 | object = Recurrence( 56 | dtend=datetime(2014, 12, 31, 0, 0, 0) 57 | ) 58 | 59 | assert bool(object) 60 | 61 | 62 | def test_falsiness_with_empty_recurrence_object(): 63 | assert not bool(Recurrence()) 64 | -------------------------------------------------------------------------------- /tests/test_managers_recurrence.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.utils.timezone import make_aware 4 | from recurrence import choices, deserialize 5 | from recurrence.models import Date, Recurrence, Rule 6 | import pytest 7 | 8 | import recurrence 9 | 10 | 11 | @pytest.mark.django_db 12 | def test_recurrence_to_recurrence_object(): 13 | limits = Recurrence.objects.create() 14 | Rule.objects.create( 15 | recurrence=limits, 16 | mode=choices.INCLUSION, 17 | freq=recurrence.WEEKLY 18 | ) 19 | object = limits.to_recurrence_object() 20 | assert [r.to_text() for r in object.rrules] == ['weekly'] 21 | assert object.exrules == [] 22 | assert object.rdates == [] 23 | assert object.exdates == [] 24 | 25 | 26 | @pytest.mark.django_db 27 | def test_recurrence_to_recurrence_object_complex(): 28 | limits = Recurrence.objects.create( 29 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 30 | dtend=datetime(2014, 12, 31, 0, 0, 0), 31 | ) 32 | 33 | Rule.objects.create( 34 | recurrence=limits, 35 | mode=choices.INCLUSION, 36 | freq=recurrence.WEEKLY, 37 | until=make_aware(datetime(2014, 12, 31, 0, 0, 0)) 38 | ) 39 | 40 | Rule.objects.create( 41 | recurrence=limits, 42 | mode=choices.EXCLUSION, 43 | freq=recurrence.MONTHLY, 44 | until=make_aware(datetime(2013, 12, 31, 0, 0, 0)) 45 | ) 46 | 47 | Date.objects.create( 48 | recurrence=limits, 49 | mode=choices.INCLUSION, 50 | dt=make_aware(datetime(2012, 12, 31, 0, 0, 0)) 51 | ) 52 | 53 | Date.objects.create( 54 | recurrence=limits, 55 | mode=choices.EXCLUSION, 56 | dt=make_aware(datetime(2011, 12, 31, 0, 0, 0)) 57 | ) 58 | 59 | object = limits.to_recurrence_object() 60 | 61 | assert object.dtstart == make_aware(datetime(2014, 1, 1, 0, 0, 0)) 62 | assert object.dtend == make_aware(datetime(2014, 12, 31, 0, 0, 0)) 63 | 64 | assert len(object.rrules) == 1 65 | output_rule = object.rrules[0] 66 | assert output_rule.freq == recurrence.WEEKLY 67 | assert output_rule.until == make_aware(datetime(2014, 12, 31, 0, 0, 0)) 68 | 69 | assert len(object.exrules) == 1 70 | output_rule = object.exrules[0] 71 | assert output_rule.freq == recurrence.MONTHLY 72 | assert output_rule.until == make_aware(datetime(2013, 12, 31, 0, 0, 0)) 73 | 74 | 75 | @pytest.mark.django_db 76 | def test_recurrence_to_recurrence_object_non_naive_sd_ed(): 77 | limits = Recurrence.objects.create( 78 | dtstart=make_aware(datetime(2014, 1, 1, 0, 0, 0)), 79 | dtend=make_aware(datetime(2014, 12, 31, 0, 0, 0)), 80 | ) 81 | 82 | object = limits.to_recurrence_object() 83 | 84 | assert object.dtstart == make_aware(datetime(2014, 1, 1, 0, 0, 0)) 85 | assert object.dtend == make_aware(datetime(2014, 12, 31, 0, 0, 0)) 86 | 87 | 88 | @pytest.mark.django_db 89 | def test_create_from_recurrence_object(): 90 | inrule = recurrence.Rule( 91 | recurrence.WEEKLY 92 | ) 93 | exrule = recurrence.Rule( 94 | recurrence.MONTHLY 95 | ) 96 | 97 | limits = recurrence.Recurrence( 98 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 99 | dtend=datetime(2014, 2, 3, 0, 0, 0), 100 | rrules=[inrule], 101 | exrules=[exrule], 102 | rdates=[datetime(2014, 2, 15, 0, 0, 0)], 103 | exdates=[make_aware(datetime(2014, 11, 29, 0, 0, 0))] 104 | ) 105 | 106 | object = Recurrence.objects.create_from_recurrence_object(limits) 107 | 108 | assert object.dtstart == make_aware(datetime(2014, 1, 1, 0, 0, 0)) 109 | assert object.dtend == make_aware(datetime(2014, 2, 3, 0, 0, 0)) 110 | 111 | rules = object.rules.all() 112 | assert len(rules) == 2 113 | 114 | in_rules = [r for r in rules if r.mode == choices.INCLUSION] 115 | out_rules = [r for r in rules if r.mode == choices.EXCLUSION] 116 | 117 | assert len(in_rules) == 1 118 | assert len(out_rules) == 1 119 | 120 | assert in_rules[0].freq == recurrence.WEEKLY 121 | assert out_rules[0].freq == recurrence.MONTHLY 122 | 123 | dates = object.dates.all() 124 | assert len(dates) == 2 125 | 126 | in_dates = [d for d in dates if d.mode == choices.INCLUSION] 127 | out_dates = [d for d in dates if d.mode == choices.EXCLUSION] 128 | 129 | assert len(in_dates) == 1 130 | assert len(out_dates) == 1 131 | 132 | assert in_dates[0].dt == make_aware(datetime(2014, 2, 15, 0, 0, 0)) 133 | assert out_dates[0].dt == make_aware(datetime(2014, 11, 29, 0, 0, 0)) 134 | 135 | 136 | @pytest.mark.django_db 137 | def test_deserialize_to_recurrence_regression(): 138 | """Regression test for checking weekday serialization works correctly: 139 | https://github.com/jazzband/django-recurrence/pull/176 140 | """ 141 | 142 | obj = deserialize('DTSTART:20201015T000000\nRRULE:FREQ=WEEKLY;COUNT=30;INTERVAL=1;WKST=MO') 143 | Recurrence.objects.create_from_recurrence_object(obj) 144 | -------------------------------------------------------------------------------- /tests/test_managers_rule.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from dateutil import tz 4 | from django.utils.timezone import make_aware 5 | from recurrence import choices 6 | from recurrence.models import Param, Recurrence, Rule 7 | import pytest 8 | 9 | import recurrence 10 | 11 | 12 | @pytest.mark.django_db 13 | def test_create_from_rule_object(): 14 | limits = Recurrence.objects.create() 15 | rule = Rule.objects.create( 16 | recurrence=limits, 17 | mode=choices.INCLUSION, 18 | freq=recurrence.WEEKLY, 19 | until=make_aware(datetime(2014, 12, 31, 0, 0, 0), tz.UTC) 20 | ) 21 | object = Rule.objects.create_from_rule_object( 22 | choices.EXCLUSION, 23 | rule, 24 | limits 25 | ) 26 | 27 | assert rule.to_rule_object() == object.to_rule_object() 28 | assert rule.mode is True 29 | assert object.mode is False 30 | 31 | 32 | @pytest.mark.django_db 33 | def test_create_from_rule_object_byday(): 34 | limits = Recurrence.objects.create() 35 | rule = Rule.objects.create( 36 | recurrence=limits, 37 | mode=choices.INCLUSION, 38 | freq=recurrence.WEEKLY, 39 | until=make_aware(datetime(2014, 12, 31, 0, 0, 0), tz.UTC) 40 | ) 41 | Param.objects.create( 42 | rule=rule, 43 | param='byday', 44 | value=6, 45 | index=0 46 | ) 47 | 48 | expected = 'RRULE:FREQ=WEEKLY;UNTIL=20141231T000000Z;BYDAY=SU' 49 | 50 | original = Rule.objects.to_rule_object(rule) 51 | serialized = recurrence.serialize(original) 52 | assert serialized == expected 53 | 54 | object = Rule.objects.create_from_rule_object( 55 | choices.INCLUSION, 56 | original, 57 | limits 58 | ) 59 | 60 | serialized = recurrence.serialize(object.to_rule_object()) 61 | assert serialized == expected 62 | 63 | 64 | @pytest.mark.django_db 65 | def test_create_from_rule_object_bymonth(): 66 | limits = Recurrence.objects.create() 67 | rule = Rule.objects.create( 68 | recurrence=limits, 69 | mode=choices.INCLUSION, 70 | freq=recurrence.WEEKLY, 71 | until=make_aware(datetime(2014, 12, 31, 0, 0, 0), tz.UTC) 72 | ) 73 | Param.objects.create( 74 | rule=rule, 75 | param='bymonth', 76 | value=6, 77 | index=0 78 | ) 79 | Param.objects.create( 80 | rule=rule, 81 | param='bymonth', 82 | value=8, 83 | index=1 84 | ) 85 | 86 | expected = 'RRULE:FREQ=WEEKLY;UNTIL=20141231T000000Z;BYMONTH=6,8' 87 | 88 | original = Rule.objects.to_rule_object(rule) 89 | serialized = recurrence.serialize(original) 90 | assert serialized == expected 91 | 92 | object = Rule.objects.create_from_rule_object( 93 | choices.INCLUSION, 94 | original, 95 | limits 96 | ) 97 | 98 | serialized = recurrence.serialize(object.to_rule_object()) 99 | assert serialized == expected 100 | -------------------------------------------------------------------------------- /tests/test_nulls.py: -------------------------------------------------------------------------------- 1 | from django.db import IntegrityError 2 | from recurrence import Recurrence 3 | from tests.models import EventWithNulls, EventWithNoNulls 4 | import pytest 5 | 6 | 7 | @pytest.mark.django_db 8 | def test_recurs_can_be_explicitly_none_if_none_is_allowed(): 9 | # Check we can save None correctly 10 | event = EventWithNulls.objects.create(recurs=None) 11 | assert event.recurs is None 12 | 13 | # Check we can deserialize None correctly 14 | reloaded = EventWithNulls.objects.get(pk=event.pk) 15 | assert reloaded.recurs is None 16 | 17 | 18 | @pytest.mark.django_db 19 | def test_recurs_cannot_be_explicitly_none_if_none_is_disallowed(): 20 | with pytest.raises(IntegrityError): 21 | EventWithNoNulls.objects.create(recurs=None) 22 | 23 | 24 | @pytest.mark.django_db 25 | def test_recurs_can_be_empty_even_if_none_is_disallowed(): 26 | event = EventWithNoNulls.objects.create(recurs=Recurrence()) 27 | assert event.recurs == Recurrence() 28 | -------------------------------------------------------------------------------- /tests/test_occurrences.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from recurrence import Recurrence, Rule 3 | import recurrence 4 | 5 | 6 | RULE = Rule( 7 | recurrence.DAILY 8 | ) 9 | 10 | PATTERN = Recurrence( 11 | dtstart=datetime(2014, 1, 2, 0, 0, 0), 12 | dtend=datetime(2014, 1, 3, 0, 0, 0), 13 | rrules=[RULE] 14 | ) 15 | 16 | 17 | def test_occurrences_with_implicit_start_and_end(): 18 | occurrences = [ 19 | instance for instance in 20 | PATTERN.occurrences() 21 | ] 22 | 23 | assert occurrences == [ 24 | datetime(2014, 1, 2, 0, 0, 0), 25 | datetime(2014, 1, 3, 0, 0, 0), 26 | ] 27 | 28 | assert 2 == PATTERN.count() 29 | 30 | 31 | def test_occurrences_with_explicit_start(): 32 | occurrences = [ 33 | instance for instance in 34 | PATTERN.occurrences( 35 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 36 | ) 37 | ] 38 | 39 | # If you specify dtstart, you get occurrences based on the rules 40 | # from the Recurrence object, which may be outside of the 41 | # Recurrence object's range. 42 | assert occurrences == [ 43 | datetime(2014, 1, 1, 0, 0, 0), 44 | datetime(2014, 1, 2, 0, 0, 0), 45 | datetime(2014, 1, 3, 0, 0, 0), 46 | ] 47 | 48 | assert 3 == PATTERN.count( 49 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 50 | ) 51 | 52 | 53 | def test_occurrences_with_explicit_end(): 54 | occurrences = [ 55 | instance for instance in 56 | PATTERN.occurrences( 57 | dtend=datetime(2014, 1, 4, 0, 0, 0), 58 | ) 59 | ] 60 | 61 | # If you specify dtend, you get occurrences based on the rules 62 | # from the Recurrence object, which may be outside of the 63 | # Recurrence object's range. 64 | assert occurrences == [ 65 | datetime(2014, 1, 2, 0, 0, 0), 66 | datetime(2014, 1, 3, 0, 0, 0), 67 | datetime(2014, 1, 4, 0, 0, 0), 68 | ] 69 | 70 | assert 3 == PATTERN.count( 71 | dtend=datetime(2014, 1, 4, 0, 0, 0), 72 | ) 73 | 74 | 75 | def test_occurrences_with_explicit_start_and_end(): 76 | occurrences = [ 77 | instance for instance in 78 | PATTERN.occurrences( 79 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 80 | dtend=datetime(2014, 1, 4, 0, 0, 0), 81 | ) 82 | ] 83 | 84 | # If you specify dtstart or dtend, you get occurrences based on 85 | # the rules from the Recurrence object, which may be outside of 86 | # the Recurrence object's range. 87 | assert occurrences == [ 88 | datetime(2014, 1, 1, 0, 0, 0), 89 | datetime(2014, 1, 2, 0, 0, 0), 90 | datetime(2014, 1, 3, 0, 0, 0), 91 | datetime(2014, 1, 4, 0, 0, 0), 92 | ] 93 | 94 | assert 4 == PATTERN.count( 95 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 96 | dtend=datetime(2014, 1, 4, 0, 0, 0), 97 | ) 98 | 99 | 100 | def test_occurrences_with_specific_include_dates(): 101 | pattern = Recurrence( 102 | rdates=[ 103 | datetime(2014, 1, 1, 0, 0, 0), 104 | datetime(2014, 1, 2, 0, 0, 0), 105 | ] 106 | ) 107 | 108 | occurrences = [ 109 | instance for instance in 110 | pattern.occurrences( 111 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 112 | dtend=datetime(2014, 1, 4, 0, 0, 0), 113 | ) 114 | ] 115 | 116 | assert occurrences == [ 117 | datetime(2014, 1, 1, 0, 0, 0), 118 | datetime(2014, 1, 2, 0, 0, 0), 119 | datetime(2014, 1, 4, 0, 0, 0), 120 | ] 121 | 122 | assert 3 == pattern.count( 123 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 124 | dtend=datetime(2014, 1, 4, 0, 0, 0), 125 | ) 126 | 127 | all_occurrences = [ 128 | instance for instance in 129 | pattern.occurrences() 130 | ] 131 | 132 | assert all_occurrences == [ 133 | datetime(2014, 1, 1, 0, 0, 0), 134 | datetime(2014, 1, 2, 0, 0, 0), 135 | ] 136 | 137 | assert 2 == pattern.count() 138 | 139 | 140 | def test_occurrences_until(): 141 | rule = Rule( 142 | recurrence.DAILY, 143 | until=datetime(2014, 1, 3, 0, 0, 0) 144 | ) 145 | 146 | pattern = Recurrence( 147 | rrules=[ 148 | rule 149 | ] 150 | ) 151 | 152 | occurrences = [ 153 | instance for instance in 154 | pattern.occurrences( 155 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 156 | dtend=datetime(2014, 1, 5, 0, 0, 0), 157 | ) 158 | ] 159 | 160 | assert occurrences == [ 161 | datetime(2014, 1, 1, 0, 0, 0), 162 | datetime(2014, 1, 2, 0, 0, 0), 163 | datetime(2014, 1, 3, 0, 0, 0), 164 | # We always get dtend, for reasons that aren't entirely clear 165 | datetime(2014, 1, 5, 0, 0, 0), 166 | ] 167 | 168 | assert 4 == pattern.count( 169 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 170 | dtend=datetime(2014, 1, 5, 0, 0, 0), 171 | ) 172 | 173 | occurrences = [ 174 | instance for instance in 175 | pattern.occurrences( 176 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 177 | dtend=datetime(2014, 1, 2, 0, 0, 0), 178 | ) 179 | ] 180 | 181 | assert occurrences == [ 182 | datetime(2014, 1, 1, 0, 0, 0), 183 | datetime(2014, 1, 2, 0, 0, 0), 184 | ] 185 | 186 | assert 2 == pattern.count( 187 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 188 | dtend=datetime(2014, 1, 2, 0, 0, 0), 189 | ) 190 | 191 | 192 | def test_before(): 193 | assert PATTERN.before( 194 | datetime(2014, 1, 3, 0, 0, 0) 195 | ) == datetime(2014, 1, 2, 0, 0, 0) 196 | 197 | assert PATTERN.before( 198 | datetime(2014, 1, 3, 0, 0, 0), 199 | inc=True 200 | ) == datetime(2014, 1, 3, 0, 0, 0) 201 | 202 | 203 | def test_after(): 204 | assert PATTERN.after( 205 | datetime(2014, 1, 2, 0, 0, 0) 206 | ) == datetime(2014, 1, 3, 0, 0, 0) 207 | 208 | assert PATTERN.after( 209 | datetime(2014, 1, 2, 0, 0, 0), 210 | inc=True 211 | ) == datetime(2014, 1, 2, 0, 0, 0) 212 | -------------------------------------------------------------------------------- /tests/test_recurrences_without_limits.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from recurrence import Recurrence, Rule 3 | import recurrence 4 | 5 | 6 | RULE = Rule( 7 | recurrence.DAILY 8 | ) 9 | 10 | PATTERN = Recurrence( 11 | rrules=[RULE] 12 | ) 13 | 14 | 15 | def test_between_without_dtend_and_dtstart(): 16 | occurrences = [ 17 | instance for instance in 18 | PATTERN.between( 19 | datetime(2014, 1, 1, 0, 0, 0), 20 | datetime(2014, 1, 4, 0, 0, 0) 21 | ) 22 | ] 23 | 24 | # We get back nothing, since dtstart and dtend will have defaulted 25 | # to the current time, and January 2014 is in the past. 26 | assert occurrences == [] 27 | 28 | 29 | def test_between_with_dtend_and_dtstart_dtend_lower_than_end(): 30 | occurrences = [ 31 | instance for instance in 32 | PATTERN.between( 33 | datetime(2014, 1, 1, 0, 0, 0), 34 | datetime(2014, 1, 6, 0, 0, 0), 35 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 36 | dtend=datetime(2014, 1, 4, 0, 0, 0), 37 | ) 38 | ] 39 | 40 | assert occurrences == [ 41 | datetime(2014, 1, 2, 0, 0, 0), 42 | datetime(2014, 1, 3, 0, 0, 0), 43 | datetime(2014, 1, 4, 0, 0, 0), 44 | ] 45 | 46 | 47 | def test_between_with_dtend_and_dtstart_dtend_higher_than_end(): 48 | occurrences = [ 49 | instance for instance in 50 | PATTERN.between( 51 | datetime(2014, 1, 1, 0, 0, 0), 52 | datetime(2014, 1, 6, 0, 0, 0), 53 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 54 | dtend=datetime(2014, 1, 8, 0, 0, 0), 55 | ) 56 | ] 57 | 58 | assert occurrences == [ 59 | datetime(2014, 1, 2, 0, 0, 0), 60 | datetime(2014, 1, 3, 0, 0, 0), 61 | datetime(2014, 1, 4, 0, 0, 0), 62 | datetime(2014, 1, 5, 0, 0, 0), 63 | ] 64 | 65 | 66 | def test_between_with_dtend_and_dtstart_limits_equal_exclusive(): 67 | occurrences = [ 68 | instance for instance in 69 | PATTERN.between( 70 | datetime(2014, 1, 1, 0, 0, 0), 71 | datetime(2014, 1, 6, 0, 0, 0), 72 | dtstart=datetime(2014, 1, 2, 0, 0, 0), 73 | dtend=datetime(2014, 1, 6, 0, 0, 0), 74 | ) 75 | ] 76 | 77 | assert occurrences == [ 78 | datetime(2014, 1, 2, 0, 0, 0), 79 | datetime(2014, 1, 3, 0, 0, 0), 80 | datetime(2014, 1, 4, 0, 0, 0), 81 | datetime(2014, 1, 5, 0, 0, 0), 82 | ] 83 | 84 | 85 | def test_between_with_dtend_and_dtstart_limits_equal_inclusive(): 86 | occurrences = [ 87 | instance for instance in 88 | PATTERN.between( 89 | datetime(2014, 1, 1, 0, 0, 0), 90 | datetime(2014, 1, 6, 0, 0, 0), 91 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 92 | dtend=datetime(2014, 1, 6, 0, 0, 0), 93 | inc=True 94 | ) 95 | ] 96 | 97 | assert occurrences == [ 98 | datetime(2014, 1, 1, 0, 0, 0), 99 | datetime(2014, 1, 2, 0, 0, 0), 100 | datetime(2014, 1, 3, 0, 0, 0), 101 | datetime(2014, 1, 4, 0, 0, 0), 102 | datetime(2014, 1, 5, 0, 0, 0), 103 | datetime(2014, 1, 6, 0, 0, 0), 104 | ] 105 | 106 | 107 | def test_between_with_dtend_and_dtstart_dtstart_lower_than_start(): 108 | occurrences = [ 109 | instance for instance in 110 | PATTERN.between( 111 | datetime(2014, 1, 2, 0, 0, 0), 112 | datetime(2014, 1, 6, 0, 0, 0), 113 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 114 | dtend=datetime(2014, 1, 6, 0, 0, 0), 115 | ) 116 | ] 117 | 118 | assert occurrences == [ 119 | datetime(2014, 1, 3, 0, 0, 0), 120 | datetime(2014, 1, 4, 0, 0, 0), 121 | datetime(2014, 1, 5, 0, 0, 0), 122 | ] 123 | 124 | 125 | def test_between_with_dtend_and_dtstart_dtstart_higher_than_start(): 126 | occurrences = [ 127 | instance for instance in 128 | PATTERN.between( 129 | datetime(2014, 1, 1, 0, 0, 0), 130 | datetime(2014, 1, 6, 0, 0, 0), 131 | dtstart=datetime(2014, 1, 2, 0, 0, 0), 132 | dtend=datetime(2014, 1, 6, 0, 0, 0), 133 | ) 134 | ] 135 | 136 | assert occurrences == [ 137 | datetime(2014, 1, 2, 0, 0, 0), 138 | datetime(2014, 1, 3, 0, 0, 0), 139 | datetime(2014, 1, 4, 0, 0, 0), 140 | datetime(2014, 1, 5, 0, 0, 0), 141 | ] 142 | -------------------------------------------------------------------------------- /tests/test_saving.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from django.core.exceptions import ValidationError 3 | from recurrence import Recurrence, Rule 4 | from tests.models import EventWithNoNulls 5 | import pytest 6 | import recurrence 7 | 8 | 9 | @pytest.mark.django_db 10 | def test_recurrence_text_pattern_is_saved(): 11 | event = EventWithNoNulls.objects.create( 12 | recurs="RRULE:FREQ=WEEKLY;BYDAY=TU" 13 | ) 14 | 15 | assert len(event.recurs.rrules) == 1 16 | assert event.recurs.rrules[0].to_text() == "weekly, each Tuesday" 17 | recurrence_info = event.recurs 18 | 19 | event = EventWithNoNulls.objects.get(pk=event.pk) 20 | assert recurrence_info == event.recurs 21 | 22 | 23 | @pytest.mark.django_db 24 | def test_recurrence_object_is_saved(): 25 | rule = Rule( 26 | recurrence.WEEKLY 27 | ) 28 | 29 | limits = Recurrence( 30 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 31 | dtend=datetime(2014, 2, 3, 0, 0, 0), 32 | rrules=[rule] 33 | ) 34 | 35 | event = EventWithNoNulls.objects.create( 36 | recurs=limits 37 | ) 38 | 39 | instances = event.recurs.between( 40 | datetime(2010, 1, 1, 0, 0, 0), 41 | datetime(2020, 12, 31, 0, 0, 0) 42 | ) 43 | 44 | assert instances == [ 45 | datetime(2014, 1, 1, 0, 0), 46 | datetime(2014, 1, 8, 0, 0), 47 | datetime(2014, 1, 15, 0, 0), 48 | datetime(2014, 1, 22, 0, 0), 49 | datetime(2014, 1, 29, 0, 0), 50 | datetime(2014, 2, 3, 0, 0) # We always get dtend 51 | ] 52 | 53 | event = EventWithNoNulls.objects.get(pk=event.pk) 54 | 55 | assert event.recurs == limits 56 | 57 | assert event.recurs.between( 58 | datetime(2010, 1, 1, 0, 0, 0), 59 | datetime(2020, 12, 31, 0, 0, 0) 60 | ) == instances 61 | 62 | 63 | @pytest.mark.django_db 64 | @pytest.mark.parametrize('value', [ 65 | ' ', 'invalid', 'RRULE:', 'RRULE:FREQ=', 'RRULE:FREQ=invalid' 66 | ]) 67 | def test_recurrence_text_pattern_invalid(value): 68 | with pytest.raises(ValidationError): 69 | EventWithNoNulls.objects.create( 70 | recurs=value 71 | ) 72 | 73 | 74 | @pytest.mark.django_db 75 | def test_invalid_frequency_recurrence_object_raises(): 76 | with pytest.raises(ValidationError): 77 | EventWithNoNulls.objects.create( 78 | recurs=Recurrence( 79 | rrules=[Rule('fish')] 80 | ) 81 | ) 82 | 83 | with pytest.raises(ValidationError): 84 | EventWithNoNulls.objects.create( 85 | recurs=Recurrence( 86 | rrules=[Rule(42)] 87 | ) 88 | ) 89 | 90 | 91 | @pytest.mark.django_db 92 | def test_invalid_interval_recurrence_object_raises(): 93 | with pytest.raises(ValidationError): 94 | EventWithNoNulls.objects.create( 95 | recurs=Recurrence( 96 | rrules=[Rule(recurrence.DAILY, interval=0)] 97 | ) 98 | ) 99 | 100 | with pytest.raises(ValidationError): 101 | EventWithNoNulls.objects.create( 102 | recurs=Recurrence( 103 | rrules=[Rule(recurrence.DAILY, interval='cat')] 104 | ) 105 | ) 106 | 107 | 108 | @pytest.mark.django_db 109 | def test_invalid_wkst_recurrence_object_raises(): 110 | with pytest.raises(ValidationError): 111 | EventWithNoNulls.objects.create( 112 | recurs=Recurrence( 113 | rrules=[Rule(recurrence.DAILY, wkst=17)] 114 | ) 115 | ) 116 | 117 | 118 | @pytest.mark.django_db 119 | def test_invalid_until_recurrence_object_raises(): 120 | with pytest.raises(ValidationError): 121 | EventWithNoNulls.objects.create( 122 | recurs=Recurrence( 123 | rrules=[Rule(recurrence.DAILY, until=17)] 124 | ) 125 | ) 126 | 127 | 128 | @pytest.mark.django_db 129 | def test_invalid_count_recurrence_object_raises(): 130 | with pytest.raises(ValidationError): 131 | EventWithNoNulls.objects.create( 132 | recurs=Recurrence( 133 | rrules=[Rule(recurrence.DAILY, count='fish')] 134 | ) 135 | ) 136 | 137 | 138 | @pytest.mark.django_db 139 | def test_invalid_byday_recurrence_object_raises(): 140 | with pytest.raises(ValidationError): 141 | EventWithNoNulls.objects.create( 142 | recurs=Recurrence( 143 | rrules=[Rule(recurrence.DAILY, byday='house')] 144 | ) 145 | ) 146 | 147 | 148 | @pytest.mark.django_db 149 | def test_invalid_bymonth_too_high_recurrence_object_raises(): 150 | with pytest.raises(ValidationError): 151 | EventWithNoNulls.objects.create( 152 | recurs=Recurrence( 153 | rrules=[Rule(recurrence.DAILY, bymonth=[1, 32])] 154 | ) 155 | ) 156 | 157 | 158 | @pytest.mark.django_db 159 | def test_invalid_bymonth_toolow_recurrence_object_raises(): 160 | with pytest.raises(ValidationError): 161 | EventWithNoNulls.objects.create( 162 | recurs=Recurrence( 163 | rrules=[Rule(recurrence.DAILY, bymonth=[0, ])] 164 | ) 165 | ) 166 | 167 | 168 | @pytest.mark.django_db 169 | def test_invalid_exclusion_interval_recurrence_object_raises(): 170 | with pytest.raises(ValidationError): 171 | EventWithNoNulls.objects.create( 172 | recurs=Recurrence( 173 | exrules=[Rule(recurrence.DAILY, interval=0)] 174 | ) 175 | ) 176 | 177 | 178 | @pytest.mark.django_db 179 | def test_invalid_date_recurrence_object_raises(): 180 | with pytest.raises(ValidationError): 181 | EventWithNoNulls.objects.create( 182 | recurs=Recurrence( 183 | rdates=["fish"] 184 | ) 185 | ) 186 | 187 | 188 | @pytest.mark.django_db 189 | def test_invalid_exclusion_date_recurrence_object_raises(): 190 | with pytest.raises(ValidationError): 191 | EventWithNoNulls.objects.create( 192 | recurs=Recurrence( 193 | exdates=["fish"] 194 | ) 195 | ) 196 | -------------------------------------------------------------------------------- /tests/test_serialization.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from recurrence import Recurrence, Rule 3 | from recurrence.exceptions import DeserializationError 4 | import pytest 5 | import recurrence 6 | 7 | 8 | def test_rule_serialization(): 9 | rule = Rule( 10 | recurrence.WEEKLY 11 | ) 12 | 13 | serialized = recurrence.serialize(rule) 14 | assert 'RRULE:FREQ=WEEKLY' == serialized 15 | assert recurrence.deserialize(serialized) == Recurrence(rrules=[rule]) 16 | 17 | 18 | def test_no_equal_sign(): 19 | with pytest.raises(DeserializationError): 20 | recurrence.deserialize('RRULE:A') 21 | 22 | 23 | def test_no_value(): 24 | with pytest.raises(DeserializationError): 25 | recurrence.deserialize('RRULE:A=') 26 | 27 | 28 | def test_unknown_key(): 29 | with pytest.raises(DeserializationError): 30 | recurrence.deserialize('RRULE:A=X') 31 | 32 | 33 | def test_bad_freq(): 34 | with pytest.raises(DeserializationError): 35 | recurrence.deserialize('RRULE:FREQ=X') 36 | 37 | 38 | def test_bad_interval(): 39 | with pytest.raises(DeserializationError): 40 | recurrence.deserialize('RRULE:INTERVAL=X') 41 | 42 | 43 | def test_bad_wkst(): 44 | with pytest.raises(DeserializationError): 45 | recurrence.deserialize('RRULE:WKST=X') 46 | 47 | 48 | def test_bad_count(): 49 | with pytest.raises(DeserializationError): 50 | recurrence.deserialize('RRULE:COUNT=X') 51 | 52 | 53 | def test_bad_byday(): 54 | with pytest.raises(DeserializationError): 55 | recurrence.deserialize('RRULE:BYDAY=X') 56 | 57 | 58 | def test_bad_BYMONTH(): 59 | with pytest.raises(DeserializationError): 60 | recurrence.deserialize('RRULE:BYMONTH=X') 61 | 62 | 63 | def test_complex_rule_serialization(): 64 | rule = Rule( 65 | recurrence.WEEKLY, 66 | interval=17, 67 | wkst=1, 68 | count=7, 69 | byday=[ 70 | recurrence.to_weekday('-1MO'), 71 | recurrence.to_weekday('TU') 72 | ], 73 | bymonth=[1, 3] 74 | ) 75 | 76 | serialized = recurrence.serialize(rule) 77 | assert ('RRULE:FREQ=WEEKLY;INTERVAL=17;WKST=TU;' 78 | 'COUNT=7;BYDAY=-1MO,TU;BYMONTH=1,3') == serialized 79 | assert recurrence.deserialize(serialized) == Recurrence(rrules=[rule]) 80 | 81 | 82 | def test_complex_rule_serialization_with_weekday_instance(): 83 | rule = Rule( 84 | recurrence.WEEKLY, 85 | interval=17, 86 | wkst=recurrence.to_weekday(1), 87 | count=7, 88 | byday=[ 89 | recurrence.to_weekday('-1MO'), 90 | recurrence.to_weekday('TU') 91 | ], 92 | bymonth=[1, 3] 93 | ) 94 | 95 | serialized = recurrence.serialize(rule) 96 | assert ('RRULE:FREQ=WEEKLY;INTERVAL=17;WKST=TU;' 97 | 'COUNT=7;BYDAY=-1MO,TU;BYMONTH=1,3') == serialized 98 | assert recurrence.deserialize(serialized) == Recurrence(rrules=[rule]) 99 | 100 | 101 | def test_bug_in_count_and_until_rule_serialization(): 102 | # This tests a bug in the way we serialize rules with instance 103 | # counts and an until date. We should really raise a 104 | # ValidationError in validate if you specify both, but we 105 | # currently don't. Once we start doing this, this test can be 106 | # modified to check an exception is raised. 107 | rule = Rule( 108 | recurrence.WEEKLY, 109 | count=7, 110 | until=datetime(2014, 10, 31, 0, 0, 0) 111 | ) 112 | 113 | serialized = recurrence.serialize(rule) 114 | 115 | # Note that we've got no UNTIL value here 116 | assert 'RRULE:FREQ=WEEKLY;COUNT=7' == serialized 117 | 118 | 119 | def test_comma_separated_exdates(): 120 | exdates = [datetime(2022, 3, 14, 21, 0, 0), datetime(2022, 3, 13, 21, 0, 0)] 121 | recurrence_ = Recurrence(exdates=exdates) 122 | text = 'EXDATE:20220314T210000Z,20220313T210000Z' 123 | 124 | deserialized = recurrence.deserialize(text) 125 | assert recurrence_ == deserialized 126 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test settings for django recurrence. 3 | 4 | DESERIALIZE_TZ determines if a datetime serialized as rfc2445 text 5 | is returned as a naive datetime or a datetime with timezone. 6 | 7 | If the he django setting RECURRENCE_USE_TZ is true 8 | then a timezone aware datetime is returned 9 | 10 | If the he django setting RECURRENCE_USE_TZ is false 11 | then a naive datetime is returned 12 | 13 | If this setting is not present the setting USE_TZ is used 14 | as a default. 15 | """ 16 | from django.conf import settings 17 | from django.test import TestCase 18 | from django.test import override_settings 19 | from recurrence import settings as r_settings 20 | 21 | 22 | class TestTimezoneSettings(TestCase): 23 | 24 | @override_settings(RECURRENCE_USE_TZ=True, USE_TZ=False) 25 | def test_recurrence_tz_true(self): 26 | assert r_settings.deserialize_tz() 27 | 28 | @override_settings(RECURRENCE_USE_TZ=False, USE_TZ=True) 29 | def test_recurrence_tz_false(self): 30 | assert not r_settings.deserialize_tz() 31 | 32 | @override_settings(USE_TZ=True) 33 | def test_fallback_to_use_tz_true(self): 34 | del settings.RECURRENCE_USE_TZ 35 | assert r_settings.deserialize_tz() 36 | 37 | @override_settings(USE_TZ=False) 38 | def test_fallback_to_use_tz_false(self): 39 | del settings.RECURRENCE_USE_TZ 40 | assert not r_settings.deserialize_tz() 41 | -------------------------------------------------------------------------------- /tests/test_to_text.py: -------------------------------------------------------------------------------- 1 | from recurrence import Rule 2 | import recurrence 3 | 4 | 5 | def test_rule_to_text_simple(): 6 | assert Rule( 7 | recurrence.WEEKLY 8 | ).to_text() == 'weekly' 9 | 10 | 11 | def test_rule_to_text_interval(): 12 | assert Rule( 13 | recurrence.WEEKLY, 14 | interval=3 15 | ).to_text() == 'every 3 weeks' 16 | 17 | 18 | def test_rule_to_text_oneoff(): 19 | assert Rule( 20 | recurrence.WEEKLY, 21 | count=1 22 | ).to_text() == 'weekly, occuring once' 23 | 24 | 25 | def test_rule_to_text_multiple(): 26 | assert Rule( 27 | recurrence.WEEKLY, 28 | count=5 29 | ).to_text() == 'weekly, occuring 5 times' 30 | 31 | 32 | def test_rule_to_text_yearly_bymonth(): 33 | assert Rule( 34 | recurrence.YEARLY, 35 | bymonth=[1, 3], 36 | ).to_text() == 'annually, each January, March' 37 | 38 | assert Rule( 39 | recurrence.YEARLY, 40 | bymonth=[1, 3], 41 | ).to_text(True) == 'annually, each Jan, Mar' 42 | 43 | 44 | def test_rule_to_text_yearly_byday(): 45 | assert Rule( 46 | recurrence.YEARLY, 47 | byday=[1, 3], 48 | ).to_text() == 'annually, on the Tuesday, Thursday' 49 | 50 | assert Rule( 51 | recurrence.YEARLY, 52 | byday=[1, 3], 53 | ).to_text(True) == 'annually, on the Tue, Thu' 54 | -------------------------------------------------------------------------------- /tests/test_to_weekday.py: -------------------------------------------------------------------------------- 1 | from dateutil.rrule import weekday 2 | import pytest 3 | import recurrence 4 | 5 | 6 | def test_to_weekday_from_weekday(): 7 | day = recurrence.Weekday(4) 8 | 9 | assert recurrence.to_weekday(day) == day 10 | 11 | 12 | def test_to_weekday_from_dateutil_weekday(): 13 | day = weekday(1) 14 | 15 | assert recurrence.to_weekday(day) == recurrence.Weekday(1) 16 | 17 | 18 | def test_to_weekday_from_int(): 19 | assert recurrence.to_weekday(1) == recurrence.Weekday(1) 20 | 21 | with pytest.raises(ValueError): 22 | recurrence.to_weekday(7) 23 | 24 | 25 | def test_to_weekday_from_nonelike(): 26 | with pytest.raises(ValueError): 27 | recurrence.to_weekday(None) 28 | 29 | with pytest.raises(ValueError): 30 | recurrence.to_weekday("") 31 | 32 | 33 | def test_to_weekday_from_string(): 34 | assert recurrence.to_weekday("3") == recurrence.Weekday(3) 35 | 36 | with pytest.raises(ValueError): 37 | recurrence.to_weekday("7") 38 | 39 | assert recurrence.to_weekday("MO") == recurrence.Weekday(0) 40 | assert recurrence.to_weekday("mo") == recurrence.Weekday(0) 41 | assert recurrence.to_weekday("TU") == recurrence.Weekday(1) 42 | assert recurrence.to_weekday("Tu") == recurrence.Weekday(1) 43 | 44 | with pytest.raises(ValueError): 45 | recurrence.to_weekday("FOO") 46 | 47 | assert recurrence.to_weekday("-2TU") == recurrence.Weekday(1, -2) 48 | 49 | # We don't do any validation of the index 50 | assert recurrence.to_weekday("-7SU") == recurrence.Weekday(6, -7) 51 | -------------------------------------------------------------------------------- /tests/test_tz.py: -------------------------------------------------------------------------------- 1 | """Test timezone aware recurrence date times.""" 2 | 3 | from datetime import datetime 4 | from dateutil import tz 5 | from django.test import TestCase 6 | from django.test import override_settings 7 | 8 | import recurrence 9 | from recurrence import Recurrence, Rule 10 | from recurrence.forms import RecurrenceField 11 | from tests.models import EventWithNoNulls 12 | 13 | 14 | @override_settings(RECURRENCE_USE_TZ=True, USE_TZ=True) 15 | class FieldTest(TestCase): 16 | 17 | def test_strip_dtstart_and_dtend_if_required(self): 18 | """Test that naive datetimes will get converted to UTC and returned as UTC.""" 19 | rule = Rule( 20 | recurrence.WEEKLY 21 | ) 22 | 23 | limits = Recurrence( 24 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 25 | dtend=datetime(2014, 2, 3, 0, 0, 0), 26 | rrules=[rule] 27 | ) 28 | 29 | value = recurrence.serialize(limits) 30 | 31 | field = RecurrenceField() 32 | cleaned_value = field.clean(value) 33 | assert cleaned_value.rrules == [rule] 34 | assert cleaned_value.dtstart == datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz.UTC) 35 | assert cleaned_value.dtend == datetime(2014, 2, 3, 0, 0, 0, tzinfo=tz.UTC) 36 | 37 | field = RecurrenceField(accept_dtstart=False, accept_dtend=False) 38 | cleaned_value = field.clean(value) 39 | assert cleaned_value != limits 40 | assert cleaned_value.dtstart is None 41 | assert cleaned_value.dtend is None 42 | 43 | def test_dt_start_and_dtend_converts_to_utc(self): 44 | """Convert the values for dtstart and dtend to UTC.""" 45 | tz_adak = tz.gettz('America/Adak') 46 | 47 | limits = Recurrence( 48 | dtstart=datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz_adak), 49 | dtend=datetime(2014, 2, 3, 0, 0, 0, tzinfo=tz_adak), 50 | ) 51 | 52 | value = recurrence.serialize(limits) 53 | 54 | field = RecurrenceField(required=False) 55 | cleaned_value = field.clean(value) 56 | assert cleaned_value.dtstart == datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz_adak).astimezone(tz.UTC) 57 | assert cleaned_value.dtend == datetime(2014, 2, 3, 0, 0, 0, tzinfo=tz_adak).astimezone(tz.UTC) 58 | assert cleaned_value.dtstart.tzname() == 'UTC' 59 | assert cleaned_value.dtend.tzname() == 'UTC' 60 | 61 | def test_naive_rdates_converted_to_utc(self): 62 | limits = Recurrence( 63 | rdates=[ 64 | datetime(2014, 1, 1, 0, 0, 0), 65 | datetime(2014, 1, 2, 0, 0, 0), 66 | ], 67 | ) 68 | 69 | value = recurrence.serialize(limits) 70 | 71 | field = RecurrenceField() 72 | cleaned_value = field.clean(value) 73 | assert cleaned_value.rdates == [ 74 | datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz.UTC), 75 | datetime(2014, 1, 2, 0, 0, 0, tzinfo=tz.UTC), 76 | ] 77 | for rdate in cleaned_value.rdates: 78 | assert rdate.tzname() == 'UTC' 79 | 80 | def test_aware_rdates_converted_to_utc(self): 81 | tz_adak = tz.gettz('America/Adak') 82 | limits = Recurrence( 83 | rdates=[ 84 | datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz_adak), 85 | datetime(2014, 1, 2, 0, 0, 0, tzinfo=tz_adak), 86 | ], 87 | ) 88 | 89 | value = recurrence.serialize(limits) 90 | 91 | field = RecurrenceField() 92 | cleaned_value = field.clean(value) 93 | assert cleaned_value.rdates == [ 94 | datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz_adak).astimezone(tz.UTC), 95 | datetime(2014, 1, 2, 0, 0, 0, tzinfo=tz_adak).astimezone(tz.UTC), 96 | ] 97 | for rdate in cleaned_value.rdates: 98 | assert rdate.tzname() == 'UTC' 99 | 100 | def test_naive_exdates_converted_to_utc(self): 101 | limits = Recurrence( 102 | exdates=[ 103 | datetime(2014, 1, 1, 0, 0, 0), 104 | datetime(2014, 1, 2, 0, 0, 0), 105 | ], 106 | ) 107 | 108 | value = recurrence.serialize(limits) 109 | 110 | field = RecurrenceField() 111 | cleaned_value = field.clean(value) 112 | assert cleaned_value.exdates == [ 113 | datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz.UTC), 114 | datetime(2014, 1, 2, 0, 0, 0, tzinfo=tz.UTC), 115 | ] 116 | for rdate in cleaned_value.exdates: 117 | assert rdate.tzname() == 'UTC' 118 | 119 | def test_aware_exdates_converted_to_utc(self): 120 | tz_adak = tz.gettz('America/Adak') 121 | limits = Recurrence( 122 | exdates=[ 123 | datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz_adak), 124 | datetime(2014, 1, 2, 0, 0, 0, tzinfo=tz_adak), 125 | ], 126 | ) 127 | 128 | value = recurrence.serialize(limits) 129 | 130 | field = RecurrenceField() 131 | cleaned_value = field.clean(value) 132 | assert cleaned_value.exdates == [ 133 | datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz_adak).astimezone(tz.UTC), 134 | datetime(2014, 1, 2, 0, 0, 0, tzinfo=tz_adak).astimezone(tz.UTC), 135 | ] 136 | for rdate in cleaned_value.exdates: 137 | assert rdate.tzname() == 'UTC' 138 | 139 | def test_naive_until_gets_converted_to_utc(self): 140 | recurs = Recurrence( 141 | rrules=[Rule( 142 | recurrence.DAILY, 143 | until=datetime(2014, 1, 1, 0, 0, 0)) 144 | ], 145 | ) 146 | value = recurrence.serialize(recurs) 147 | field = RecurrenceField() 148 | cleaned_value = field.clean(value) 149 | assert cleaned_value.rrules[0].until == datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz.UTC) 150 | 151 | def test_aware_until_gets_converted_to_utc(self): 152 | tz_adak = tz.gettz('America/Adak') 153 | until = datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz_adak) 154 | recurs = Recurrence( 155 | rrules=[Rule( 156 | recurrence.DAILY, 157 | until=until) 158 | ], 159 | ) 160 | value = recurrence.serialize(recurs) 161 | field = RecurrenceField() 162 | cleaned_value = field.clean(value) 163 | assert cleaned_value.rrules[0].until == until.astimezone(tz.UTC) 164 | 165 | 166 | @override_settings(RECURRENCE_USE_TZ=True, USE_TZ=True) 167 | class SaveTest(TestCase): 168 | 169 | def test_recurrence_object_is_saved(self): 170 | """Test that naive datetimes will get converted to UTC and returned as UTC""" 171 | rule = Rule( 172 | recurrence.WEEKLY 173 | ) 174 | 175 | limits = Recurrence( 176 | dtstart=datetime(2014, 1, 1, 0, 0, 0), 177 | dtend=datetime(2014, 2, 3, 0, 0, 0), 178 | rrules=[rule] 179 | ) 180 | 181 | event = EventWithNoNulls.objects.create( 182 | recurs=limits 183 | ) 184 | 185 | event.refresh_from_db() 186 | 187 | instances = event.recurs.between( 188 | datetime(2010, 1, 1, 0, 0, 0, tzinfo=tz.UTC), 189 | datetime(2020, 12, 31, 0, 0, 0, tzinfo=tz.UTC) 190 | ) 191 | 192 | assert instances == [ 193 | datetime(2014, 1, 1, 0, 0, tzinfo=tz.UTC), 194 | datetime(2014, 1, 8, 0, 0, tzinfo=tz.UTC), 195 | datetime(2014, 1, 15, 0, 0, tzinfo=tz.UTC), 196 | datetime(2014, 1, 22, 0, 0, tzinfo=tz.UTC), 197 | datetime(2014, 1, 29, 0, 0, tzinfo=tz.UTC), 198 | datetime(2014, 2, 3, 0, 0, tzinfo=tz.UTC) # We always get dtend 199 | ] 200 | 201 | event = EventWithNoNulls.objects.get(pk=event.pk) 202 | 203 | expected_limits = Recurrence( 204 | dtstart=datetime(2014, 1, 1, 0, 0, 0, tzinfo=tz.UTC), 205 | dtend=datetime(2014, 2, 3, 0, 0, 0, tzinfo=tz.UTC), 206 | rrules=[rule] 207 | ) 208 | 209 | assert event.recurs == expected_limits 210 | 211 | assert event.recurs.between( 212 | datetime(2010, 1, 1, 0, 0, 0, tzinfo=tz.UTC), 213 | datetime(2020, 12, 31, 0, 0, 0, tzinfo=tz.UTC) 214 | ) == instances 215 | -------------------------------------------------------------------------------- /tests/test_weekday.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from recurrence import Weekday 3 | 4 | 5 | def test_init(): 6 | assert repr(Weekday(3)) == 'TH' 7 | assert repr(Weekday(3, -2)) == '-2TH' 8 | assert repr(Weekday(3, 3)) == '3TH' 9 | 10 | with pytest.raises(ValueError): 11 | Weekday(8) 12 | 13 | with pytest.raises(ValueError): 14 | Weekday('fish') 15 | 16 | 17 | def test_call(): 18 | # I'm not sure why this functionality is useful, but this is what 19 | # calling a weekday currently does. 20 | 21 | day = Weekday(4, -3) 22 | assert day(2) == Weekday(4, 2) 23 | assert day(-3) is day 24 | assert day(None) == Weekday(4) 25 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py312-djdocs 4 | py312-djqa 5 | py{38,39,310,py38}-dj40 6 | py{38,39,310,311,py38}-dj41 7 | py{38,39,310,311,312,py38}-dj42 8 | py{310,311,312}-dj50 9 | py{310,311,312,py38}-djmain 10 | 11 | [gh-actions] 12 | python = 13 | 3.8: py38 14 | 3.9: py39 15 | 3.10: py310 16 | 3.11: py311 17 | 3.12: py312 18 | pypy-3.8: pypy38 19 | 20 | [gh-actions:env] 21 | DJANGO = 22 | docs: djdocs 23 | qa: djqa 24 | 4.0: dj40 25 | 4.1: dj41 26 | 4.2: dj42 27 | 5.0: dj50 28 | main: djmain 29 | 30 | [testenv] 31 | usedevelop = true 32 | setenv = 33 | PYTHONDONTWRITEBYTECODE=1 34 | deps = 35 | -r requirements.txt 36 | dj40: django>=4.0,<4.1 37 | dj41: django>=4.1,<4.2 38 | dj42: django>=4.2,<4.3 39 | dj50: django>=5.0,<5.1 40 | djmain: https://github.com/django/django/archive/main.tar.gz 41 | commands = 42 | pytest 43 | 44 | [testenv:py310-djqa] 45 | basepython = python3.10 46 | ignore_errors = true 47 | deps = 48 | -r requirements.txt 49 | commands = 50 | flake8 recurrence 51 | flake8 setup.py 52 | flake8 tests 53 | 54 | [testenv:py310-djdocs] 55 | basepython = python3.10 56 | changedir = docs 57 | deps = 58 | -r requirements.txt 59 | commands = 60 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 61 | 62 | [flake8] 63 | ignore = E122,E125,E129,E501,W503,W504 64 | --------------------------------------------------------------------------------