Merge pull request #150 from babybuddy/148-24h-time

Add support for 24 hour time override (#148)
This commit is contained in:
Christopher Charbonneau Wells 2020-07-26 20:09:46 -07:00 committed by GitHub
commit dda1cd4750
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 185 additions and 49 deletions

View File

@ -284,6 +284,7 @@ take precedence over the contents of an `.env` file.**
- [`NAP_START_MIN`](#nap_start_min) - [`NAP_START_MIN`](#nap_start_min)
- [`SECRET_KEY`](#secret_key) - [`SECRET_KEY`](#secret_key)
- [`TIME_ZONE`](#time_zone) - [`TIME_ZONE`](#time_zone)
- [`USE_24_HOUR_TIME_FORMAT`](#use_24_hour_time_format)
### `ALLOWED_HOSTS` ### `ALLOWED_HOSTS`
@ -369,6 +370,21 @@ The default time zone to use for the instance. See [List of tz database time zon
for all possible values. This value can be overridden per use from the user for all possible values. This value can be overridden per use from the user
settings form. settings form.
### `USE_24_HOUR_TIME_FORMAT`
*Default: False*
Whether to force 24-hour time format for locales that do not ordinarily use it
(e.g. `en`). Support for this feature must implemented on a per-locale basis.
See format files under [`babybuddy/formats`](babybuddy/formats) for supported
locales.
Note: This value for this setting is interpreted as a boolean from a string
using Python's built-in [`strtobool`](https://docs.python.org/3/distutils/apiref.html#distutils.util.strtobool)
tool. Only certain strings are supported (e.g. "True" for `True` and "False" for
`False`), other unrecognized strings will cause a `ValueError` and prevent Baby
Buddy from loading.
## Languages ## Languages
Baby Buddy includes translation support as of v1.2.2. Language can be set on a Baby Buddy includes translation support as of v1.2.2. Language can be set on a

View File

@ -1,24 +1,22 @@
# This files adds two new date input formats to support Baby Buddy's frontend from django.conf import settings
# library formats: from django.conf.locale.en import formats
# - %m/%d/%Y %I:%M:%S %p
# - %m/%d/%Y %I:%M %p # Override the regular locale settings to support 24 hour time.
# if settings.USE_24_HOUR_TIME_FORMAT:
# The remaining formats come from the base Django formats. DATETIME_FORMAT = 'N j, Y, H:i:s'
# CUSTOM_INPUT_FORMATS = [
# See django.cong.locale.en. '%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59'
DATETIME_INPUT_FORMATS = [ '%m/%d/%Y %H:%M', # '10/25/2006 14:30'
'%m/%d/%Y %I:%M:%S %p', # '10/25/2006 2:30:59 PM' (new) ]
'%m/%d/%Y %I:%M %p', # '10/25/2006 2:30 PM' (new) SHORT_DATETIME_FORMAT = 'm/d/Y G:i:s'
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' TIME_FORMAT = 'H:i:s'
'%Y-%m-%d %H:%M:%S.%f', # '2006-10-25 14:30:59.000200' else:
'%Y-%m-%d %H:%M', # '2006-10-25 14:30' # These formats are added to support the locale style of Baby Buddy's
'%Y-%m-%d', # '2006-10-25' # frontend library, which uses momentjs.
'%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59' CUSTOM_INPUT_FORMATS = [
'%m/%d/%Y %H:%M:%S.%f', # '10/25/2006 14:30:59.000200' '%m/%d/%Y %I:%M:%S %p', # '10/25/2006 2:30:59 PM'
'%m/%d/%Y %H:%M', # '10/25/2006 14:30' '%m/%d/%Y %I:%M %p', # '10/25/2006 2:30 PM'
'%m/%d/%Y', # '10/25/2006' ]
'%m/%d/%y %H:%M:%S', # '10/25/06 14:30:59'
'%m/%d/%y %H:%M:%S.%f', # '10/25/06 14:30:59.000200' # Append all other input formats from the base locale.
'%m/%d/%y %H:%M', # '10/25/06 14:30' DATETIME_INPUT_FORMATS = CUSTOM_INPUT_FORMATS + formats.DATETIME_INPUT_FORMATS
'%m/%d/%y', # '10/25/06'
]

View File

@ -1,4 +1,5 @@
import os import os
from distutils.util import strtobool
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from dotenv import load_dotenv, find_dotenv from dotenv import load_dotenv, find_dotenv
@ -166,6 +167,14 @@ LANGUAGES = [
USE_L10N = True USE_L10N = True
# Custom setting that can be used to override the locale-based time set by
# USE_L10N _for specific locales_ to use 24-hour format. In order for this to
# work with a given locale it must be set at the FORMAT_MODULE_PATH with
# conditionals on this setting. See babybuddy/forms/en/formats.py for an example
# implementation for the English locale.
USE_24_HOUR_TIME_FORMAT = strtobool(os.environ.get('USE_24_HOUR_TIME_FORMAT') or 'False')
FORMAT_MODULE_PATH = ['babybuddy.formats'] FORMAT_MODULE_PATH = ['babybuddy.formats']

View File

@ -3,7 +3,8 @@ import datetime
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms.fields import DateTimeField from django.forms.fields import DateTimeField
from django.test import TestCase from django.test import TestCase, override_settings, tag
from django.utils.formats import date_format, time_format
class FormatsTestCase(TestCase): class FormatsTestCase(TestCase):
@ -17,8 +18,43 @@ class FormatsTestCase(TestCase):
] ]
for example in supported_custom_examples: for example in supported_custom_examples:
result = field.to_python(example) try:
self.assertIsInstance(result, datetime.datetime) result = field.to_python(example)
self.assertIsInstance(result, datetime.datetime)
except ValidationError:
self.fail('Format of "{}" not recognized!'.format(example))
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
field.to_python('invalid date string!') field.to_python('invalid date string!')
@tag('isolate')
@override_settings(LANGUAGE_CODE='en', USE_24_HOUR_TIME_FORMAT=True)
def test_use_24_hour_time_format_en(self):
field = DateTimeField()
supported_custom_examples = [
'10/25/2006 2:30:59',
'10/25/2006 2:30',
'10/25/2006 14:30:59',
'10/25/2006 14:30',
]
for example in supported_custom_examples:
try:
result = field.to_python(example)
self.assertIsInstance(result, datetime.datetime)
except ValidationError:
self.fail('Format of "{}" not recognized!'.format(example))
with self.assertRaises(ValidationError):
field.to_python('invalid date string!')
dt = datetime.datetime.fromisoformat('2011-11-04 23:05:59')
self.assertEqual(
date_format(dt, 'DATETIME_FORMAT'), 'Nov. 4, 2011, 23:05:59')
dt = datetime.datetime.fromisoformat('2011-11-04 02:05:59')
self.assertEqual(
date_format(dt, 'SHORT_DATETIME_FORMAT'), '11/04/2011 2:05:59')
t = datetime.time.fromisoformat('16:02:25')
self.assertEqual(time_format(t), '16:02:25')

View File

@ -1,5 +1,5 @@
{% extends 'babybuddy/page.html' %} {% extends 'babybuddy/page.html' %}
{% load i18n %} {% load datetimepicker i18n %}
{% block title %} {% block title %}
{% if request.resolver_match.url_name == 'diaperchange-update' %} {% if request.resolver_match.url_name == 'diaperchange-update' %}
@ -31,6 +31,8 @@
{% block javascript %} {% block javascript %}
<script type="text/javascript"> <script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_time')); BabyBuddy.DatetimePicker.init($('#datetimepicker_time'), {
format: '{% datetimepicker_format %}'
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,5 @@
{% extends 'babybuddy/page.html' %} {% extends 'babybuddy/page.html' %}
{% load i18n %} {% load datetimepicker i18n %}
{% block title %} {% block title %}
{% if request.resolver_match.url_name == 'feeding-update' %} {% if request.resolver_match.url_name == 'feeding-update' %}
@ -32,12 +32,15 @@
{% block javascript %} {% block javascript %}
<script type="text/javascript"> <script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), { BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), {
defaultDate: false defaultDate: false,
format: '{% datetimepicker_format %}'
});
BabyBuddy.DatetimePicker.init($('#datetimepicker_end'), {
format: '{% datetimepicker_format %}'
}); });
BabyBuddy.DatetimePicker.init($('#datetimepicker_end'));
$('#id_type').change(function() { $('#id_type').change(function() {
var feed_type=$('#id_type').val(); var feed_type=$('#id_type').val();
if (feed_type=='formula'||feed_type=='fortified breast milk') { if (feed_type === 'formula' || feed_type === 'fortified breast milk') {
$('#id_method').val('bottle'); $('#id_method').val('bottle');
} }
}); });

View File

@ -1,5 +1,5 @@
{% extends 'babybuddy/page.html' %} {% extends 'babybuddy/page.html' %}
{% load i18n %} {% load datetimepicker i18n %}
{% block title %} {% block title %}
{% if request.resolver_match.url_name == 'sleep-update' %} {% if request.resolver_match.url_name == 'sleep-update' %}
@ -32,8 +32,11 @@
{% block javascript %} {% block javascript %}
<script type="text/javascript"> <script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), { BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), {
defaultDate: false defaultDate: false,
format: '{% datetimepicker_format %}'
});
BabyBuddy.DatetimePicker.init($('#datetimepicker_end'), {
format: '{% datetimepicker_format %}'
}); });
BabyBuddy.DatetimePicker.init($('#datetimepicker_end'));
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,5 @@
{% extends 'babybuddy/page.html' %} {% extends 'babybuddy/page.html' %}
{% load i18n %} {% load datetimepicker i18n %}
{% block title %} {% block title %}
{% if object %} {% if object %}
@ -31,6 +31,8 @@
{% block javascript %} {% block javascript %}
<script type="text/javascript"> <script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_time')); BabyBuddy.DatetimePicker.init($('#datetimepicker_time'), {
format: '{% datetimepicker_format %}'
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,5 @@
{% extends 'babybuddy/page.html' %} {% extends 'babybuddy/page.html' %}
{% load duration i18n %} {% load datetimepicker duration i18n %}
{% block title %}{% trans "Timer" %}{% endblock %} {% block title %}{% trans "Timer" %}{% endblock %}
@ -27,7 +27,7 @@
{% block javascript %} {% block javascript %}
<script type="text/javascript"> <script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), { BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), {
format: 'L LTS' format: '{% datetimepicker_format 'L LTS' %}'
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,5 @@
{% extends 'babybuddy/page.html' %} {% extends 'babybuddy/page.html' %}
{% load i18n %} {% load datetimepicker i18n %}
{% block title %} {% block title %}
{% if request.resolver_match.url_name == 'tummytime-update' %} {% if request.resolver_match.url_name == 'tummytime-update' %}
@ -33,10 +33,10 @@
<script type="text/javascript"> <script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), { BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), {
defaultDate: false, defaultDate: false,
format: 'L LTS' format: '{% datetimepicker_format 'L LTS' %}'
}); });
BabyBuddy.DatetimePicker.init($('#datetimepicker_end'), { BabyBuddy.DatetimePicker.init($('#datetimepicker_end'), {
format: 'L LTS' format: '{% datetimepicker_format 'L LTS' %}'
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from django import template
from django.conf import settings
register = template.Library()
@register.simple_tag()
def datetimepicker_format(format_string='L LT'):
"""
Return a datetime format string for momentjs, with support for 24 hour time
override setting.
:param format_string: the default format string (locale based)
:return: the format string to use, as 24 hour time if configured.
"""
if settings.USE_24_HOUR_TIME_FORMAT:
if format_string == 'L LT':
return 'L HH:mm'
elif format_string == 'L LTS':
return 'L HH:mm:ss'
return format_string

View File

@ -4,7 +4,7 @@ from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from core.models import Child, Timer from core.models import Child, Timer
from core.templatetags import bootstrap, duration, timers from core.templatetags import bootstrap, datetimepicker, duration, timers
class TemplateTagsTestCase(TestCase): class TemplateTagsTestCase(TestCase):
@ -60,3 +60,16 @@ class TemplateTagsTestCase(TestCase):
url = timers.instance_add_url({'timer': timer}, 'core:sleep-add') url = timers.instance_add_url({'timer': timer}, 'core:sleep-add')
self.assertEqual(url, '/sleep/add/?timer={}&child={}'.format( self.assertEqual(url, '/sleep/add/?timer={}&child={}'.format(
timer.id, child.slug)) timer.id, child.slug))
def test_datetimepicker_format(self):
self.assertEqual(datetimepicker.datetimepicker_format(), 'L LT')
self.assertEqual(datetimepicker.datetimepicker_format('L LT'), 'L LT')
self.assertEqual(
datetimepicker.datetimepicker_format('L LTS'), 'L LTS')
with self.settings(USE_24_HOUR_TIME_FORMAT=True):
self.assertEqual(datetimepicker.datetimepicker_format(), 'L HH:mm')
self.assertEqual(
datetimepicker.datetimepicker_format('L LT'), 'L HH:mm')
self.assertEqual(
datetimepicker.datetimepicker_format('L LTS'), 'L HH:mm:ss')

View File

@ -58,6 +58,11 @@ module.exports = {
'babybuddy.scss' 'babybuddy.scss'
] ]
}, },
testsConfig: {
isolated: [
'babybuddy.tests.tests_formats.FormatsTestCase.test_use_24_hour_time_format_en'
],
},
watchConfig: { watchConfig: {
scriptsGlob: [ scriptsGlob: [
'*/static_src/js/**/*.js', '*/static_src/js/**/*.js',

View File

@ -2,6 +2,7 @@ var gulp = require('gulp');
var concat = require('gulp-concat'); var concat = require('gulp-concat');
var del = require('del'); var del = require('del');
var es = require('child_process').execSync;
var flatten = require('gulp-flatten'); var flatten = require('gulp-flatten');
var pump = require('pump'); var pump = require('pump');
var sass = require('gulp-sass'); var sass = require('gulp-sass');
@ -40,12 +41,23 @@ function coverage(cb) {
'coverage', 'coverage',
'run', 'run',
'manage.py', 'manage.py',
'test' 'test',
'--exclude-tag',
'isolate'
], ],
{ {
stdio: 'inherit' stdio: 'inherit'
} }
).on('exit', cb); ).on('exit', function() {
// Add coverage for isolated tests.
config.testsConfig.isolated.forEach(function(test_name) {
es(
'pipenv run coverage run -a manage.py test ' + test_name,
{stdio: 'inherit'}
);
})
cb();
});
} }
/** /**
@ -160,14 +172,30 @@ function styles(cb) {
} }
/** /**
* Runs all tests. * Runs all tests _not_ tagged "isolate".
* *
* @param cb * @param cb
*/ */
function test(cb) { function test(cb) {
var command = ['run', 'python', 'manage.py', 'test']; var command = [
'run',
'python',
'manage.py',
'test',
'--exclude-tag',
'isolate'
];
command = command.concat(process.argv.splice(3)); command = command.concat(process.argv.splice(3));
spawn('pipenv', command, { stdio: 'inherit' }).on('exit', cb); spawn('pipenv', command, { stdio: 'inherit' }).on('exit', function() {
// Run isolated tests.
config.testsConfig.isolated.forEach(function(test_name) {
es(
'pipenv run python manage.py test ' + test_name,
{stdio: 'inherit'}
);
})
cb();
});
} }
/** /**