668 |
669 | This program is free software: you can redistribute it and/or modify
670 | it under the terms of the GNU Affero General Public License as published by
671 | the Free Software Foundation, either version 3 of the License, or
672 | (at your option) any later version.
673 |
674 | This program is distributed in the hope that it will be useful,
675 | but WITHOUT ANY WARRANTY; without even the implied warranty of
676 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
677 | GNU Affero General Public License for more details.
678 |
679 | You should have received a copy of the GNU Affero General Public License
680 | along with this program. If not, see .
681 |
682 | Also add information on how to contact you by electronic and paper mail.
683 |
684 | If your software can interact with users remotely through a computer
685 | network, you should also make sure that it provides a way for users to
686 | get its source. For example, if your program is a web application, its
687 | interface could display a "Source" link that leads users to an archive
688 | of the code. There are many ways you could offer source, and different
689 | solutions will be better for different programs; see section 13 for the
690 | specific requirements.
691 |
692 | You should also get your employer (if you work as a programmer) or school,
693 | if any, to sign a "copyright disclaimer" for the program, if necessary.
694 | For more information on this, and how to apply and follow the GNU AGPL, see
695 | .
696 |
697 |
--------------------------------------------------------------------------------
/README.txt:
--------------------------------------------------------------------------------
1 | django-date-extensions
2 | by Matthew Somerville
3 |
4 | This code adds a few small extensions to Django's DateField, to handle both
5 | approximate dates (e.g. "March 1963") and default year dates (e.g. assume
6 | "24th June" is the most recent such).
7 |
8 | example contains a hopefully self-contained Django project that simply shows
9 | off a form with these methods of entry.
10 |
11 | Approximate dates
12 | =================
13 |
14 | A new object, ApproximateDate, is used to represent dates that might not have a
15 | month or a day. ApproximateDateField is the model field used to represent these
16 | objects in a Model, and ApproximateDateFormField is the field used in a Django
17 | form. Everything should work seamlessly simply by specifying a model field as
18 | ApproximateDateField rather than DateField.
19 |
20 | Default year dates
21 | ==================
22 |
23 | PrettyDateField is a form field to be used on DateField model fields. It takes
24 | one argument, future, which is a nullable boolean. If True, a date input that
25 | is missing a year will be taken to be the next possible occurrence of that date
26 | - e.g. on 24th November 2009, entering 24th December will be taken to be
27 | 2009-12-24, whilst entering 3rd March will be taken to be 2010-03-03. If future
28 | is False, the reverse occurs, with year-less dates being assumed to be the
29 | closest occurrence of that date in the past.
30 |
31 | If future is not set, then PrettyDateField acts the same as a DateField, only
32 | allows suffixes on ordinals, and assumes D/M/Y rather than M/D/Y.
33 |
34 | Testing
35 | =======
36 | Run 'tox' with tox installed.
37 |
38 | Todo
39 | ====
40 |
41 | Improve date parsing to take more inputs like my traintimes.org.uk PHP, such as
42 | "next Friday".
43 |
44 |
45 | Any queries or comments, do get in touch. Something's probably broken, as I tried
46 | to tidy up the code a little for public release :)
47 |
48 | Matthew Somerville.
49 |
--------------------------------------------------------------------------------
/django_date_extensions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dracos/django-date-extensions/2694f13566b0f097bf8e31c4c6628b85b75b98eb/django_date_extensions/__init__.py
--------------------------------------------------------------------------------
/django_date_extensions/fields.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import time
3 | import re
4 | from functools import total_ordering
5 |
6 | from django.db import models
7 | from django import forms
8 | from django.forms import ValidationError
9 | from django.utils import dateformat
10 | try:
11 | from django.utils.encoding import python_2_unicode_compatible
12 | except ImportError:
13 | # Django 3+, no longer present, we know wre are Python 3, so can be null
14 | def python_2_unicode_compatible(f): return f
15 |
16 | from . import settings
17 | from .widgets import PrettyDateInput
18 |
19 |
20 | @total_ordering
21 | @python_2_unicode_compatible
22 | class ApproximateDate(object):
23 | """A date object that accepts 0 for month or day to mean we don't
24 | know when it is within that month/year."""
25 | def __init__(self, year=0, month=0, day=0, future=False, past=False):
26 | if future and past:
27 | raise ValueError("Can't be both future and past")
28 | elif future or past:
29 | if year or month or day:
30 | raise ValueError("Future or past dates can have no year, month or day")
31 | elif year and month and day:
32 | datetime.date(year, month, day)
33 | elif year and month:
34 | datetime.date(year, month, 1)
35 | elif year and day:
36 | raise ValueError("You cannot specify just a year and a day")
37 | elif year:
38 | datetime.date(year, 1, 1)
39 | else:
40 | raise ValueError("You must specify a year")
41 |
42 | self.future = future
43 | self.past = past
44 | self.year = year
45 | self.month = month
46 | self.day = day
47 |
48 | def __repr__(self):
49 | if self.future or self.past:
50 | return str(self)
51 | return "{year:04d}-{month:02d}-{day:02d}".format(year=self.year, month=self.month, day=self.day)
52 |
53 | def __str__(self):
54 | if self.future:
55 | return 'future'
56 | if self.past:
57 | return 'past'
58 | elif self.year and self.month and self.day:
59 | return dateformat.format(self, settings.OUTPUT_FORMAT_DAY_MONTH_YEAR)
60 | elif self.year and self.month:
61 | return dateformat.format(self, settings.OUTPUT_FORMAT_MONTH_YEAR)
62 | elif self.year:
63 | return dateformat.format(self, settings.OUTPUT_FORMAT_YEAR)
64 |
65 | def __eq__(self, other):
66 | if isinstance(other, (datetime.date, datetime.datetime)):
67 | return (self.year, self.month, self.day) ==\
68 | (other.year, other.month, other.day)
69 |
70 | if not isinstance(other, ApproximateDate):
71 | return False
72 |
73 | return (self.year, self.month, self.day, self.future, self.past) ==\
74 | (other.year, other.month, other.day, other.future, other.past)
75 |
76 | def __ne__(self, other):
77 | return not (self == other)
78 |
79 | def __lt__(self, other):
80 | if other is None:
81 | return False
82 |
83 | if isinstance(other, ApproximateDate):
84 | if self.future or other.future:
85 | return not self.future
86 | if self.past or other.past:
87 | return not other.past
88 |
89 | return (self.year, self.month, self.day) < (other.year, other.month, other.day)
90 |
91 | def __len__(self):
92 | return len(self.__repr__())
93 |
94 |
95 | ansi_date_re = re.compile(r'^\d{4}-\d{1,2}-\d{1,2}$')
96 |
97 |
98 | class ApproximateDateField(models.CharField):
99 | """A model field to store ApproximateDate objects in the database
100 | (as a CharField because MySQLdb intercepts dates from the
101 | database and forces them to be datetime.date()s."""
102 |
103 | description = "An approximate date"
104 |
105 | def __init__(self, *args, **kwargs):
106 | kwargs['max_length'] = 10
107 | super(ApproximateDateField, self).__init__(*args, **kwargs)
108 |
109 | def deconstruct(self):
110 | name, path, args, kwargs = super(ApproximateDateField, self).deconstruct()
111 | del kwargs['max_length']
112 | return name, path, args, kwargs
113 |
114 | def to_python(self, value):
115 | if isinstance(value, ApproximateDate):
116 | return value
117 |
118 | return self.from_db_value(value)
119 |
120 | def from_db_value(self, value, *args, **kwarsg):
121 | if not value:
122 | return ''
123 |
124 | if value == 'future':
125 | return ApproximateDate(future=True)
126 | if value == 'past':
127 | return ApproximateDate(past=True)
128 |
129 | if not ansi_date_re.search(value):
130 | raise ValidationError('Enter a valid date in YYYY-MM-DD format.')
131 |
132 | year, month, day = map(int, value.split('-'))
133 | try:
134 | return ApproximateDate(year, month, day)
135 | except ValueError as e:
136 | msg = 'Invalid date: %s' % str(e)
137 | raise ValidationError(msg)
138 |
139 | def get_prep_value(self, value):
140 | if not value:
141 | return ''
142 | if isinstance(value, ApproximateDate):
143 | return repr(value)
144 | if isinstance(value, datetime.date):
145 | return dateformat.format(value, "Y-m-d")
146 | if value == 'future':
147 | return 'future'
148 | if value == 'past':
149 | return 'past'
150 | if not ansi_date_re.search(value):
151 | raise ValidationError('Enter a valid date in YYYY-MM-DD format.')
152 | return value
153 |
154 | def value_to_string(self, obj):
155 | value = self.value_from_object(obj)
156 | return self.get_prep_value(value)
157 |
158 | def formfield(self, **kwargs):
159 | defaults = {'form_class': ApproximateDateFormField}
160 | defaults.update(kwargs)
161 | return super(ApproximateDateField, self).formfield(**defaults)
162 |
163 | # def get_db_prep_lookup(self, lookup_type, value):
164 | # pass
165 |
166 |
167 | # TODO: Expand to work more like my PHP strtotime()-using function
168 | class ApproximateDateFormField(forms.fields.Field):
169 | def __init__(self, max_length=10, empty_value='', *args, **kwargs):
170 | super(ApproximateDateFormField, self).__init__(*args, **kwargs)
171 |
172 | def clean(self, value):
173 | super(ApproximateDateFormField, self).clean(value)
174 | if not value:
175 | return None
176 | if value == 'future':
177 | return ApproximateDate(future=True)
178 | if value == 'past':
179 | return ApproximateDate(past=True)
180 | if isinstance(value, ApproximateDate):
181 | return value
182 | value = re.sub(r'(?<=\d)(st|nd|rd|th)', '', value.strip())
183 | for date_format in settings.DATE_INPUT_FORMATS:
184 | try:
185 | return ApproximateDate(*time.strptime(value, date_format)[:3])
186 | except ValueError:
187 | continue
188 | for month_format in settings.MONTH_INPUT_FORMATS:
189 | try:
190 | match = time.strptime(value, month_format)
191 | return ApproximateDate(match[0], match[1], 0)
192 | except ValueError:
193 | continue
194 | for year_format in settings.YEAR_INPUT_FORMATS:
195 | try:
196 | return ApproximateDate(time.strptime(value, year_format)[0], 0, 0)
197 | except ValueError:
198 | continue
199 | raise ValidationError('Please enter a valid date.')
200 |
201 |
202 | # PrettyDateField - same as DateField but accepts slightly more input,
203 | # like ApproximateDateFormField above. If initialised with future=True,
204 | # it will assume a date without year means the current year (or the next
205 | # year if the day is before the current date). If future=False, it does
206 | # the same but in the past.
207 | class PrettyDateField(forms.fields.Field):
208 | widget = PrettyDateInput
209 |
210 | def __init__(self, future=None, *args, **kwargs):
211 | self.future = future
212 | super(PrettyDateField, self).__init__(*args, **kwargs)
213 |
214 | def clean(self, value):
215 | """
216 | Validates that the input can be converted to a date. Returns a Python
217 | datetime.date object.
218 | """
219 | super(PrettyDateField, self).clean(value)
220 | if not value:
221 | return None
222 | if value == 'future':
223 | return ApproximateDate(future=True)
224 | if value == 'past':
225 | return ApproximateDate(past=True)
226 | if isinstance(value, datetime.datetime):
227 | return value.date()
228 | if isinstance(value, datetime.date):
229 | return value
230 | value = re.sub(r'(?<=\d)(st|nd|rd|th)', '', value.strip())
231 | for date_input_format in settings.DATE_INPUT_FORMATS:
232 | try:
233 | return datetime.date(*time.strptime(value, date_input_format)[:3])
234 | except ValueError:
235 | continue
236 |
237 | if self.future is None:
238 | raise ValidationError('Please enter a valid date.')
239 |
240 | # Allow year to be omitted. Do the sensible thing, either past or future.
241 | for day_month_input_format in settings.DAY_MONTH_INPUT_FORMATS:
242 | try:
243 | t = time.strptime(value, day_month_input_format)
244 | month, day, yday = t[1], t[2], t[7]
245 | year = datetime.date.today().year
246 | if self.future and yday < int(datetime.date.today().strftime('%j')):
247 | year += 1
248 | if not self.future and yday > int(datetime.date.today().strftime('%j')):
249 | year -= 1
250 | return datetime.date(year, month, day)
251 | except ValueError:
252 | continue
253 |
254 | raise ValidationError('Please enter a valid date.')
255 |
--------------------------------------------------------------------------------
/django_date_extensions/models.py:
--------------------------------------------------------------------------------
1 | # Require to run the tests using `./manage.py test ...`
2 |
--------------------------------------------------------------------------------
/django_date_extensions/settings.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 | OUTPUT_FORMAT_DAY_MONTH_YEAR = getattr(settings, 'DATE_EXTENSIONS_OUTPUT_FORMAT_DAY_MONTH_YEAR', "jS F Y")
4 |
5 | OUTPUT_FORMAT_MONTH_YEAR = getattr(settings, 'DATE_EXTENSIONS_OUTPUT_FORMAT_MONTH_YEAR', "F Y")
6 |
7 | OUTPUT_FORMAT_YEAR = getattr(settings, 'DATE_EXTENSIONS_OUTPUT_FORMAT_YEAR', "Y")
8 |
9 | # The same as the built-in Django one, but with the d/m/y ones the right way round ;)
10 | DATE_INPUT_FORMATS = getattr(settings, 'DATE_EXTENSIONS_DATE_INPUT_FORMATS', (
11 | '%Y-%m-%d', # '2006-10-25',
12 | '%d/%m/%Y', '%d/%m/%y', # '25/10/2006', '25/10/06'
13 | '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006'
14 | '%d %b %Y', '%d %b, %Y', # '25 Oct 2006', '25 Oct, 2006'
15 | '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006'
16 | '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006'
17 | ))
18 |
19 | MONTH_INPUT_FORMATS = getattr(settings, 'DATE_EXTENSIONS_MONTH_INPUT_FORMATS', (
20 | '%m/%Y', '%m-%Y', # '10/2006', '10-2006'
21 | '%b %Y', '%Y %b', # 'Oct 2006', '2006 Oct'
22 | '%B %Y', '%Y %B', # 'October 2006', '2006 October'
23 | ))
24 |
25 | YEAR_INPUT_FORMATS = getattr(settings, 'DATE_EXTENSIONS_YEAR_INPUT_FORMATS', (
26 | '%Y', # '2006'
27 | ))
28 |
29 | DAY_MONTH_INPUT_FORMATS = getattr(settings, 'DATE_EXTENSIONS_DAY_MONTH_INPUT_FORMATS', (
30 | '%m-%d', '%d/%m', # '10-25', '25/10'
31 | '%b %d', '%d %b', # 'Oct 25', '25 Oct'
32 | '%B %d', '%d %B', # 'October 25', '25 October'
33 | ))
34 |
--------------------------------------------------------------------------------
/django_date_extensions/tests.py:
--------------------------------------------------------------------------------
1 | from datetime import date, datetime
2 | import os
3 | import unittest
4 |
5 | from django.db import models
6 | from django.core import serializers
7 | from django import forms
8 | from django.test import TestCase, override_settings
9 | from django import VERSION as DJANGO_VERSION
10 | from django.utils.encoding import force_text
11 |
12 | from .fields import ApproximateDate, ApproximateDateField
13 |
14 | os.environ['DJANGO_SETTINGS_MODULE'] = 'example.settings'
15 |
16 |
17 | class ApproxDateModel(models.Model):
18 | start = ApproximateDateField()
19 | can_be_null = ApproximateDateField(null=True)
20 |
21 | def __unicode__(self):
22 | return u'%s' % str(self.start)
23 |
24 |
25 | class ApproxDateForm(forms.ModelForm):
26 | class Meta:
27 | model = ApproxDateModel
28 | fields = ('start', 'can_be_null')
29 |
30 |
31 | class PastAndFuture(unittest.TestCase):
32 |
33 | def test_setting_both(self):
34 | self.assertRaises(ValueError, ApproximateDate, past=True, future=True)
35 |
36 | def test_setting_with_dates(self):
37 | self.assertRaises(ValueError, ApproximateDate, future=True, year=2000)
38 | self.assertRaises(ValueError, ApproximateDate, past=True, year=2000)
39 |
40 | def test_stringification(self):
41 |
42 | self.assertEqual(str(ApproximateDate(future=True)), 'future')
43 | self.assertEqual(str(ApproximateDate(past=True)), 'past')
44 |
45 | self.assertEqual(repr(ApproximateDate(future=True)), 'future')
46 | self.assertEqual(repr(ApproximateDate(past=True)), 'past')
47 |
48 |
49 | class CompareDates(unittest.TestCase):
50 |
51 | def test_compare(self):
52 |
53 | past = ApproximateDate(past=True)
54 | past_too = ApproximateDate(past=True)
55 | y_past = ApproximateDate(year=2000)
56 | y_future = ApproximateDate(year=2100)
57 | future = ApproximateDate(future=True)
58 | future_too = ApproximateDate(future=True)
59 |
60 | # check that we can be compared to None, '' and u''
61 | for bad_val in ('', u'', None):
62 | self.assertFalse(y_past in (bad_val,))
63 | self.assertFalse(y_past == bad_val)
64 | self.assertTrue(y_past != bad_val)
65 |
66 | # sanity check
67 | self.assertTrue(y_past == y_past)
68 | self.assertTrue(y_future == y_future)
69 |
70 | self.assertFalse(y_past != y_past)
71 | self.assertFalse(y_future != y_future)
72 |
73 | self.assertTrue(y_past != y_future)
74 | self.assertTrue(y_future != y_past)
75 |
76 | self.assertTrue(y_future > y_past)
77 | self.assertTrue(y_future >= y_past)
78 | self.assertFalse(y_past > y_future)
79 | self.assertFalse(y_past >= y_future)
80 |
81 | self.assertTrue(y_past < y_future)
82 | self.assertTrue(y_past <= y_future)
83 | self.assertFalse(y_future < y_past)
84 | self.assertFalse(y_future <= y_past)
85 |
86 | # Future dates are always greater
87 | self.assertTrue(y_past < future)
88 | self.assertTrue(y_past <= future)
89 | self.assertTrue(y_past != future)
90 | self.assertTrue(y_future < future)
91 | self.assertTrue(y_future <= future)
92 | self.assertTrue(y_future != future)
93 |
94 | self.assertTrue(future > y_past)
95 | self.assertTrue(future >= y_past)
96 | self.assertTrue(future != y_past)
97 | self.assertTrue(future > y_future)
98 | self.assertTrue(future >= y_future)
99 | self.assertTrue(future != y_future)
100 |
101 | # Past dates are always lesser
102 | self.assertTrue(y_past > past)
103 | self.assertTrue(y_past >= past)
104 | self.assertTrue(y_past != past)
105 | self.assertTrue(y_future > past)
106 | self.assertTrue(y_future >= past)
107 | self.assertTrue(y_future != past)
108 |
109 | self.assertTrue(past < y_past)
110 | self.assertTrue(past <= y_past)
111 | self.assertTrue(past != y_past)
112 | self.assertTrue(past < y_future)
113 | self.assertTrue(past <= y_future)
114 | self.assertTrue(past != y_future)
115 |
116 | # Past and future comparisons
117 | self.assertTrue(past < future)
118 | self.assertTrue(past <= future)
119 | self.assertTrue(past != future)
120 |
121 | self.assertTrue(future > past)
122 | self.assertTrue(future >= past)
123 | self.assertTrue(future != past)
124 |
125 | # Future and past dates are equal to themselves (so that sorting is sane)
126 | self.assertFalse(future < future)
127 | self.assertTrue(future <= future)
128 | self.assertTrue(future == future)
129 | self.assertTrue(future >= future)
130 | self.assertFalse(future > future)
131 | self.assertTrue(future == future_too)
132 | self.assertFalse(future != future_too)
133 |
134 | self.assertFalse(past < past)
135 | self.assertTrue(past <= past)
136 | self.assertTrue(past == past)
137 | self.assertTrue(past >= past)
138 | self.assertFalse(past > past)
139 | self.assertTrue(past == past_too)
140 | self.assertFalse(past != past_too)
141 |
142 | def test_compare_date(self):
143 | """
144 | You can compare Approximate date objects to regular date ones.
145 | """
146 | self.assertEqual(ApproximateDate(2008, 9, 3), date(2008, 9, 3))
147 | self.assertTrue(ApproximateDate(2008, 9, 3) < date(2009, 9, 3))
148 | self.assertTrue(ApproximateDate(2007) < date(2007, 9, 3))
149 |
150 |
151 | class Lengths(unittest.TestCase):
152 | known_lengths = (
153 | ({'year': 1999}, 10),
154 | ({'year': 1999, 'month': 1}, 10),
155 | ({'year': 1999, 'month': 1, 'day': 1}, 10),
156 | ({'future': True}, 6),
157 | ({'past': True}, 4),
158 | )
159 |
160 | def test_length(self):
161 | for kwargs, length in self.known_lengths:
162 | approx = ApproximateDate(**kwargs)
163 | self.assertEqual(len(approx), length)
164 |
165 |
166 | class ApproxDateFiltering(unittest.TestCase):
167 | def setUp(self):
168 | for year in [2000, 2001, 2002, 2003, 2004]:
169 | ApproxDateModel.objects.create(start=ApproximateDate(year=year))
170 |
171 | def test_filtering_with_python_date(self):
172 | qs = ApproxDateModel.objects.filter(start__gt=date.today())
173 | # force evaluate queryset
174 | list(qs)
175 |
176 | def test_filtering_with_python_datetime(self):
177 | qs = ApproxDateModel.objects.filter(start__gt=datetime.now())
178 | # force evaluate queryset
179 | list(qs)
180 |
181 |
182 | class ApproxDateI18n(unittest.TestCase):
183 | @override_settings(LANGUAGE_CODE='ru')
184 | def test_date_in_russian(self):
185 | date = ApproximateDate(year=2000, month=5)
186 | self.assertEqual(force_text(date), u'\u041c\u0430\u0439 2000')
187 |
188 |
189 | class ApproximateDateFieldTesting(TestCase):
190 | def test_deconstruction(self):
191 | f = ApproximateDateField()
192 | name, path, args, kwargs = f.deconstruct()
193 | new_instance = ApproximateDateField(*args, **kwargs)
194 | self.assertEqual(f.max_length, new_instance.max_length)
195 |
196 | def test_empty_fields(self):
197 | a1 = ApproxDateModel.objects.create(start="")
198 |
199 | if DJANGO_VERSION[0] < 2:
200 | self.assertEqual(0, ApproxDateModel.objects.filter(start=None).count())
201 | else:
202 | self.assertEqual(1, ApproxDateModel.objects.filter(start=None).count())
203 | self.assertEqual(1, ApproxDateModel.objects.filter(start=a1.start).count())
204 | self.assertEqual(1, ApproxDateModel.objects.filter(start="").count())
205 | self.assertEqual(1, ApproxDateModel.objects.filter(start=a1.start or "").count())
206 |
207 | def test_serialization(self):
208 | a = ApproxDateModel.objects.create(start=ApproximateDate(year=2020, month=12))
209 | data = serializers.serialize("xml", [a])
210 | self.assertIn('2020-12-00', data)
211 |
212 |
213 | class ApproximateDateFormTesting(unittest.TestCase):
214 | def test_form(self):
215 | ApproxDateForm()
216 |
217 |
218 | if __name__ == "__main__":
219 | unittest.main()
220 |
--------------------------------------------------------------------------------
/django_date_extensions/widgets.py:
--------------------------------------------------------------------------------
1 | from datetime import date
2 |
3 | from django.utils import dateformat
4 | from django.forms import widgets
5 |
6 | from . import settings
7 |
8 |
9 | class PrettyDateInput(widgets.Input):
10 | input_type = 'text'
11 |
12 | def render(self, name, value, attrs=None):
13 | if value is None:
14 | value = ''
15 | elif isinstance(value, date):
16 | value = dateformat.format(value, settings.OUTPUT_FORMAT_DAY_MONTH_YEAR)
17 | return super(PrettyDateInput, self).render(name, value, attrs)
18 |
--------------------------------------------------------------------------------
/example/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dracos/django-date-extensions/2694f13566b0f097bf8e31c4c6628b85b75b98eb/example/__init__.py
--------------------------------------------------------------------------------
/example/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django_date_extensions.fields import PrettyDateField, ApproximateDateFormField
3 |
4 |
5 | class DatesForm(forms.Form):
6 | near_future = PrettyDateField(future=True)
7 | near_past = PrettyDateField(future=False)
8 | just_a_date = PrettyDateField()
9 | approximate = ApproximateDateFormField()
10 |
11 | def clean(self):
12 | self.safe_cleaned_data = self.cleaned_data
13 | return self.cleaned_data
14 |
--------------------------------------------------------------------------------
/example/settings.py:
--------------------------------------------------------------------------------
1 |
2 | # Set DEBUG to true so that we don't need to provide a 500.html template
3 | # (django's debug on will be used instead).
4 | DEBUG = True
5 |
6 | SECRET_KEY = 'x9i*)8dp_i=r1o(p%0-g*^sz@_(631+_tjr=e-t04vj2!@l$8a'
7 | ROOT_URLCONF = 'example.urls'
8 | INSTALLED_APPS = (
9 | 'django_date_extensions',
10 | )
11 |
12 | DATABASES = {
13 | 'default': {
14 | 'ENGINE': 'django.db.backends.sqlite3',
15 | 'NAME': '',
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/templates/form.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | django-date-extensions
4 |
15 |
16 |
17 | Example of DateField extensions
18 |
19 | Prefill with some valid data
20 |
21 |
37 |
38 | Written by Matthew Somerville.
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/example/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 | from .views import view
3 |
4 | urlpatterns = [
5 | url(r'^$', view),
6 | ]
7 |
--------------------------------------------------------------------------------
/example/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from .forms import DatesForm
3 |
4 |
5 | def view(request):
6 | dates_form = DatesForm(request.GET or None)
7 | dates_form.is_valid()
8 | return render(request, 'form.html', {'form': dates_form})
9 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length=119
3 |
4 | [bdist_wheel]
5 | universal = 1
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='django_date_extensions',
5 | version='3.1.2',
6 | url='https://github.com/dracos/django-date-extensions',
7 | packages=['django_date_extensions'],
8 | license='BSD',
9 | description=(
10 | "This code adds a few small extensions to Django's DateField,"
11 | "to handle both approximate dates (e.g. 'March 1963') and default year dates"
12 | "(e.g. assume '24th June' is the most recent such)."),
13 | long_description=open('README.txt').read(),
14 | author='Matthew Somerville',
15 | author_email='matthew-pypi@dracos.co.uk',
16 | requires=[
17 | 'Django',
18 | ],
19 | classifiers=[
20 | 'Development Status :: 5 - Production/Stable',
21 | 'Environment :: Web Environment',
22 | 'Framework :: Django',
23 | 'License :: OSI Approved :: BSD License',
24 | 'Intended Audience :: Developers',
25 | 'Programming Language :: Python :: 2.7',
26 | 'Programming Language :: Python :: 3',
27 | 'Topic :: Database',
28 | 'Topic :: Internet :: WWW/HTTP',
29 | ],
30 | )
31 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = flake8, py{27,39}-1.11, py39-{2.2,3.2}
3 |
4 | [testenv]
5 | commands =
6 | flake8: flake8 django_date_extensions example manage.py setup.py
7 | py{27,39}: python -Wall manage.py test django_date_extensions
8 | deps =
9 | flake8: flake8
10 | 1.11: Django>=1.11,<2.0
11 | 2.2: Django>=2.2,<3.0
12 | 3.2: Django>=3.2,<4.0
13 | passenv = CFLAGS
14 | setenv =
15 | PYTHONDONTWRITEBYTECODE=1
16 |
17 | [testenv:flake8]
18 | skip_install = True
19 |
--------------------------------------------------------------------------------