Merge pull request #627 from babybuddy/v2

v2
This commit is contained in:
Christopher Charbonneau Wells 2023-05-03 06:38:43 -07:00 committed by GitHub
commit 8545fb360b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
171 changed files with 24210 additions and 31299 deletions

View File

@ -25,4 +25,4 @@ runs:
run: pipenv graph
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18

View File

@ -1,9 +1,11 @@
name: "CodeQL"
on:
push:
branches: [ master ]
branches:
- master
pull_request:
branches: [ master ]
branches:
- master
schedule:
- cron: '33 0 * * 3'
jobs:

View File

@ -6,7 +6,7 @@ name = "pypi"
[packages]
boto3 = "*"
dj-database-url = "*"
django = ">=4.0.6"
django = "*"
django-axes = "*"
django-filter = "*"
django-imagekit = "*"
@ -25,6 +25,7 @@ uritemplate = "*"
whitenoise = "*"
django-taggit = "*"
django-qr-code = "*"
django-dbsettings = "*"
[dev-packages]
coveralls = "*"
@ -33,3 +34,4 @@ mkdocs = "*"
mkdocs-material = "*"
tblib = "*"
black = "*"
pysnooper = "*"

View File

@ -99,7 +99,7 @@ class TemperatureFilter(TimeFieldFilter, TagsFieldFilter):
class TimerFilter(StartEndFieldFilter):
class Meta(StartEndFieldFilter.Meta):
model = models.Timer
fields = sorted(StartEndFieldFilter.Meta.fields + ["active", "user"])
fields = sorted(StartEndFieldFilter.Meta.fields + ["user"])
class TummyTimeFilter(StartEndFieldFilter, TagsFieldFilter):

View File

@ -77,16 +77,12 @@ class CoreModelWithDurationSerializer(CoreModelSerializer):
timer = attrs["timer"]
attrs.pop("timer")
if timer.end:
end = timer.end
else:
end = timezone.now()
if timer.child:
attrs["child"] = timer.child
# Overwrites values provided directly!
attrs["start"] = timer.start
attrs["end"] = end
attrs["end"] = timezone.now()
# The "child", "start", and "end" field should all be set at this
# point. If one is not, model validation will fail because they are
@ -103,7 +99,7 @@ class CoreModelWithDurationSerializer(CoreModelSerializer):
# Only actually stop the timer if all validation passed.
if timer:
timer.stop(attrs["end"])
timer.stop()
return attrs
@ -232,10 +228,11 @@ class TimerSerializer(CoreModelSerializer):
queryset=get_user_model().objects.all(),
required=False,
)
duration = serializers.DurationField(read_only=True, required=False)
class Meta:
model = models.Timer
fields = ("id", "child", "name", "start", "end", "duration", "active", "user")
fields = ("id", "child", "name", "start", "duration", "user")
def validate(self, attrs):
attrs = super(TimerSerializer, self).validate(attrs)

View File

@ -47,7 +47,6 @@ class TestBase:
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
timer.refresh_from_db()
self.assertTrue(timer.active)
child = models.Child.objects.first()
self.timer_test_data["child"] = child.id
@ -55,11 +54,9 @@ class TestBase:
self.endpoint, self.timer_test_data, format="json"
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
timer.refresh_from_db()
self.assertFalse(timer.active)
obj = self.model.objects.get(pk=response.data["id"])
self.assertEqual(obj.start, start)
self.assertEqual(obj.end, timer.end)
self.assertIsNotNone(obj.end)
def test_post_with_timer_with_child(self):
if not self.timer_test_data:
@ -73,12 +70,10 @@ class TestBase:
self.endpoint, self.timer_test_data, format="json"
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
timer.refresh_from_db()
self.assertFalse(timer.active)
obj = self.model.objects.get(pk=response.data["id"])
self.assertEqual(obj.child, timer.child)
self.assertIsNotNone(obj.child)
self.assertEqual(obj.start, start)
self.assertEqual(obj.end, timer.end)
self.assertIsNotNone(obj.end)
class BMIAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
@ -703,19 +698,7 @@ class TimerAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
def test_get(self):
response = self.client.get(self.endpoint)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data["results"][0],
{
"id": 1,
"child": None,
"name": "Fake timer",
"start": "2017-11-17T23:30:00-05:00",
"end": "2017-11-18T00:30:00-05:00",
"duration": "01:00:00",
"active": False,
"user": 1,
},
)
self.assertEqual(response.data["results"][0]["id"], 1)
def test_post(self):
data = {"name": "New fake timer", "user": 1}
@ -743,31 +726,19 @@ class TimerAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, entry)
self.assertEqual(response.data["name"], entry["name"])
def test_start_stop_timer(self):
def test_start_restart_timer(self):
endpoint = "{}{}/".format(self.endpoint, 1)
response = self.client.get(endpoint)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(response.data["active"])
response = self.client.patch(f"{endpoint}restart/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data["active"])
# Restart twice is allowed
response = self.client.patch(f"{endpoint}restart/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data["active"])
response = self.client.patch(f"{endpoint}stop/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(response.data["active"])
# Stopping twice is allowed, too
response = self.client.patch(f"{endpoint}stop/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(response.data["active"])
class TummyTimeAPITestCase(TestBase.BabyBuddyAPITestCaseBase):

View File

@ -118,12 +118,6 @@ class TimerViewSet(viewsets.ModelViewSet):
ordering_fields = ("duration", "end", "start")
ordering = "-start"
@action(detail=True, methods=["patch"])
def stop(self, request, pk=None):
timer = self.get_object()
timer.stop()
return Response(self.serializer_class(timer).data)
@action(detail=True, methods=["patch"])
def restart(self, request, pk=None):
timer = self.get_object()

View File

@ -393,9 +393,6 @@
{
"name": "Fake timer",
"start": "2017-11-18T04:30:00Z",
"end": "2017-11-18T05:30:00Z",
"duration": "01:00:00",
"active": false,
"user": 1
}
},

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Add custom "short" version of `MONTH_DAY_FORMAT`. This customization will
# only work with the locale format locale specified by this file.
SHORT_MONTH_DAY_FORMAT = "M j"

View File

@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
from django.conf.locale.pt import formats
# Limit datetime input formats to those support by moment.
formats_supported = list(
filter(
lambda dt_format: not dt_format.startswith("%Y-%m-%d"),
formats.DATETIME_INPUT_FORMATS,
)
)
DATETIME_INPUT_FORMATS = formats_supported

View File

@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
from django.conf.locale.tr import formats
# Add formats supported by moment.
DATETIME_INPUT_FORMATS = [
"%d.%m.%Y %H:%M:%S",
"%d.%m.%Y %H:%M",
*formats.DATETIME_INPUT_FORMATS,
]

View File

@ -5,68 +5,9 @@ import pytz
from django.conf import settings
from django.utils import timezone, translation
from django.conf.locale.en import formats as formats_en_us
from django.conf.locale.en_GB import formats as formats_en_gb
from django.contrib.auth.middleware import RemoteUserMiddleware
def update_en_us_date_formats():
"""
Update the datetime formats for the en-US locale. This is handled here and
not using `FORMAT_MODULE_PATH` because the processing of format modules
does not allow us to distinguish appropriately between en-US and en-GB
based on user settings.
"""
if settings.USE_24_HOUR_TIME_FORMAT:
formats_en_us.DATETIME_FORMAT = "N j, Y, H:i:s"
custom_input_formats = [
"%m/%d/%Y %H:%M:%S", # '10/25/2006 14:30:59'
"%m/%d/%Y %H:%M", # '10/25/2006 14:30'
]
formats_en_us.SHORT_DATETIME_FORMAT = "m/d/Y G:i:s"
formats_en_us.TIME_FORMAT = "H:i:s"
else:
# These formats are added to support the locale style of Baby Buddy's
# frontend library, which uses momentjs.
custom_input_formats = [
"%m/%d/%Y %I:%M:%S %p", # '10/25/2006 2:30:59 PM'
"%m/%d/%Y %I:%M %p", # '10/25/2006 2:30 PM'
]
# Add custom "short" version of `MONTH_DAY_FORMAT`.
formats_en_us.SHORT_MONTH_DAY_FORMAT = "M j"
# Append all other input formats from the base locale.
formats_en_us.DATETIME_INPUT_FORMATS = (
custom_input_formats + formats_en_us.DATETIME_INPUT_FORMATS
)
def update_en_gb_date_formats():
if settings.USE_24_HOUR_TIME_FORMAT:
# 25 October 2006 14:30:00
formats_en_gb.DATETIME_FORMAT = "j F Y H:i:s"
custom_input_formats = [
"%d/%m/%Y %H:%M:%S", # '25/10/2006 14:30:59'
"%d/%m/%Y %H:%M", # '25/10/2006 14:30'
]
formats_en_gb.SHORT_DATETIME_FORMAT = "d/m/Y H:i"
formats_en_gb.TIME_FORMAT = "H:i"
else:
formats_en_gb.DATETIME_FORMAT = "j F Y f a" # 25 October 2006 2:30 p.m
# These formats are added to support the locale style of Baby Buddy's
# frontend library, which uses momentjs.
custom_input_formats = [
"%d/%m/%Y %I:%M:%S %p", # '25/10/2006 2:30:59 PM'
"%d/%m/%Y %I:%M %p", # '25/10/2006 2:30 PM'
]
# Append all other input formats from the base locale.
formats_en_gb.DATETIME_INPUT_FORMATS = (
custom_input_formats + formats_en_gb.DATETIME_INPUT_FORMATS
)
class UserLanguageMiddleware:
"""
Customizes settings based on user language setting.
@ -85,11 +26,6 @@ class UserLanguageMiddleware:
language = settings.LANGUAGE_CODE
if language:
if language == "en-US":
update_en_us_date_formats()
elif language == "en-GB":
update_en_gb_date_formats()
# Set the language before generating the response.
translation.activate(language)
@ -104,10 +40,9 @@ class UserLanguageMiddleware:
class UserTimezoneMiddleware:
"""
Sets the timezone based on a user specific setting that falls back on
`settings.TIME_ZONE`. This middleware must run after
`django.contrib.auth.middleware.AuthenticationMiddleware` because it uses
the request.user object.
Sets the timezone based on a user specific setting. This middleware must run after
`django.contrib.auth.middleware.AuthenticationMiddleware` because it uses the
request.user object.
"""
def __init__(self, get_response):

View File

@ -37,6 +37,7 @@ INSTALLED_APPS = [
"storages",
"import_export",
"qr_code",
"dbsettings",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
@ -78,7 +79,7 @@ ROOT_URLCONF = "babybuddy.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": ["babybuddy/templates/error"],
"DIRS": ["babybuddy/templates", "babybuddy/templates/error"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
@ -158,7 +159,7 @@ if REVERSE_PROXY_AUTH:
USE_TZ = True
TIME_ZONE = os.environ.get("TIME_ZONE") or "UTC"
TIME_ZONE = "UTC"
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
@ -201,16 +202,6 @@ USE_L10N = True
FORMAT_MODULE_PATH = ["babybuddy.formats"]
# 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 = bool(
strtobool(os.environ.get("USE_24_HOUR_TIME_FORMAT") or "False")
)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
@ -364,7 +355,5 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
BABY_BUDDY = {
"ALLOW_UPLOADS": bool(strtobool(os.environ.get("ALLOW_UPLOADS") or "True")),
"NAP_START_MAX": os.environ.get("NAP_START_MAX") or "18:00",
"NAP_START_MIN": os.environ.get("NAP_START_MIN") or "06:00",
"READ_ONLY_GROUP_NAME": "read_only",
}

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from datetime import time
from django.utils.translation import gettext_lazy as _
import dbsettings
from .widgets import TimeInput
class NapSettings(dbsettings.Group):
nap_start_min = dbsettings.TimeValue(
default=time(6),
description=_("Default minimum nap start time"),
help_text=_(
"The minimum default time that a sleep entry is consider a nap. If set the "
"nap property will be preselected if the start time is within the bounds."
),
widget=TimeInput,
)
nap_start_max = dbsettings.TimeValue(
default=time(18),
description=_("Default maximum nap start time"),
help_text=_(
"The maximum default time that a sleep entry is consider a nap. If set the "
"nap property will be preselected if the start time is within the bounds."
),
widget=TimeInput,
)

View File

@ -1,9 +1,6 @@
if (typeof jQuery === 'undefined') {
throw new Error('Baby Buddy requires jQuery.')
}
if (typeof moment === 'undefined') {
throw new Error('Baby Buddy requires moment.js.')
}
/**
* Baby Buddy Namespace
@ -16,42 +13,6 @@ var BabyBuddy = function () {
return {};
}();
/**
* Datetime Picker.
*
* Provides modifications and defaults for the base datetime picker widget.
*
* @type {{init: BabyBuddy.DatetimePicker.init}}
*/
BabyBuddy.DatetimePicker = function ($, moment) {
return {
init: function (element, options) {
var defaultOptions = {
buttons: { showToday: true, showClose: true },
defaultDate: 'now',
focusOnShow: false,
format: 'L LT',
ignoreReadonly: true,
locale: moment.locale(),
useCurrent: false,
icons: {
time: 'icon-clock',
date: 'icon-calendar',
up: 'icon-arrow-up',
down: 'icon-arrow-down',
previous: 'icon-angle-circled-left',
next: 'icon-angle-circled-right',
today: 'icon-today',
clear: 'icon-delete',
close: 'icon-cancel'
},
viewMode: 'times',
};
element.datetimepicker($.extend(defaultOptions, options));
}
};
}(jQuery, moment);
/**
* Pull to refresh.
*

View File

@ -174,7 +174,7 @@
this.apiTagsUrl = widget.getAttribute('data-tags-url');
this.createTagInputs = widget.querySelector('.create-tag-inputs');
this.addTagInput = this.createTagInputs.querySelector('input[type="text"]');
this.addTagButton = this.createTagInputs.querySelector('.btn-add-new-tag');
this.addTagButton = this.createTagInputs.querySelector('#add-tag');
this.addTagInput.value = "";

View File

@ -1,3 +0,0 @@
@import '../../../node_modules/bootstrap/scss/functions';
// Baby Buddy site-wide custom functions.

View File

@ -1,3 +0,0 @@
@import '../../../node_modules/bootstrap/scss/mixins';
// Baby Buddy site-wide custom mixins.

View File

@ -1,5 +1,136 @@
@import 'functions';
@import '../../../node_modules/bootstrap/scss/variables';
@import 'themes/blueorange';
@use 'sass:color';
// Baby Buddy site-wide variables.
// Theme variables.
// Color system
$white: #fff;
$gray-100: #f8f9fa;
$gray-200: #e9ecef;
$gray-300: #dee2e6;
$gray-400: #ced4da;
$gray-500: #adb5bd;
$gray-600: #6c757d;
$gray-700: #495057;
$gray-800: #343a40;
$gray-900: #212529;
$black: #000;
$primary: #37abe9;
$danger: #a72431;
$error: $danger;
$secondary: #ff8f00;
$warning: #ffbe42;
$success: #239556;
$debug: #5abccc;
$info: #44c4dd;
$light: $gray-100;
$dark: $gray-800;
// Body
// Settings for the `<body>` element.
$body-bg: $gray-900;
$body-color: $gray-400;
// Links
// Style anchor elements.
$link-color: $info;
$link-decoration: none;
$link-hover-color: color.adjust($link-color, $lightness: -15%);
// Components
// Define common padding and border radius sizes and more.
$border-color: $gray-200;
$component-active-color: $gray-400;
$component-active-bg: $primary;
// Fonts
// Font, line-height, and color for body text, headings, and more.
$text-muted: $gray-600 !default;
$blockquote-small-color: $gray-600 !default;
$hr-border-color: rgba($black, .1) !default;
$mark-bg: #fcf8e3 !default;
// Forms
$input-bg: $white;
$input-disabled-bg: $gray-600;
$input-color: $black;
$input-border-color: rgba($gray-600, .15);
$input-group-addon-bg: $gray-500;
// Tables
$table-cell-padding-y: 0.75rem;
$table-cell-padding-x: 0.75rem;
$table-striped-bg: $dark;
// Dropdowns
// Dropdown menu container and contents.
$dropdown-bg: $gray-700;
$dropdown-divider-bg: $gray-800;
$dropdown-link-color: $body-color;
$dropdown-link-hover-color: color.adjust($body-color, $lightness: -5%);
$dropdown-link-hover-bg: $primary;
$dropdown-link-active-color: $component-active-color;
$dropdown-link-active-bg: $component-active-bg;
$dropdown-header-color: color.adjust($body-color, $lightness: -25%);
// Pagination
$pagination-color: $link-color;
$pagination-bg: $dark;
$pagination-border-color: color.adjust($dark, $lightness: 5%);
$pagination-hover-color: $link-hover-color;
$pagination-hover-bg: $gray-900;
$pagination-hover-border-color: $gray-800;
$pagination-active-color: $body-color;
$pagination-active-bg: $primary;
$pagination-active-border-color: $primary;
$pagination-disabled-color: $gray-600;
$pagination-disabled-bg: $pagination-bg;
$pagination-disabled-border-color: $pagination-border-color;
// Cards
$card-bg: $dark;
$card-cap-bg: rgba($light, .05);
// Progress bars
$progress-bg: $gray-600;
// List group
$list-group-bg: $dark;
$list-group-hover-bg: color.adjust($list-group-bg, $lightness: -5%);
$list-group-active-color: $component-active-color !default;
$list-group-active-bg: $component-active-bg !default;
$list-group-active-border-color: $list-group-active-bg !default;
$list-group-action-color: $gray-400;
$list-group-action-hover-color: $list-group-action-color;
// Breadcrumbs
$breadcrumb-active-color: $gray-600;
$breadcrumb-bg: none;
$breadcrumb-padding-y: 0.75rem;
$breadcrumb-padding-x: 1rem;

View File

@ -1,8 +1,5 @@
@import 'functions';
@import 'variables';
@import 'mixins';
@import '../../../node_modules/bootstrap/scss/bootstrap';
@import '../../../node_modules/tempusdominus-bootstrap-4/src/sass/tempusdominus-bootstrap-4';
@import '../../../**/static_src/scss/*';
@import '../fontello/css/babybuddy';

View File

@ -1,3 +1,5 @@
@use 'sass:map';
// Baby Buddy form style customizations.
// BB form fields do not follow typical BS4 style that enables this display.
@ -18,40 +20,6 @@
pointer-events: none;
}
.bootstrap-datetimepicker-widget {
// Set default text color for datetime picker.
color: $gray-400;
// Ensure widget appears above other elements (e.g. .submit-primary).
z-index: 1050;
}
// Datetime picker input styles.
.input-group {
&.datetimepicker {
// Make calendar icon large and use the primary theme color.
.input-group-text {
background: none;
border: 0;
color: theme-color('primary');
padding-left: 0;
padding-right: 0;
}
// Style readonly input to look less like an input element.
.datetimepicker-input {
&[readonly] {
background: none;
border: 0;
color: $white;
cursor: pointer;
font-weight: bold;
}
}
}
}
// Use a full width, fixed button on smaller screens.
.submit-primary {
display: block;

View File

@ -1,3 +1,5 @@
@use 'sass:map';
// Baby Buddy site-wide custom styles.
// Remove extra margin below site breadcrumb.
@ -7,7 +9,7 @@
// Extra-small button.
.btn-xs {
@include button-size(.2rem, .12rem, .75rem, 1, .2rem);
@include button-size(.2rem, .12rem, .75rem, .2rem);
}
// Right align main dropdown menu.
@ -18,33 +20,10 @@
// PullToRefresh elements.
.ptr--ptr {
background: theme-color('dark');
background: map.get($theme-colors, 'dark');
.ptr--text, .ptr--icon {
// "!important" must be used to override inline styling from JS.
color: theme-color('light') !important;
}
}
// Basic table of model instances.
.table-instances {
tr {
td, th {
vertical-align: middle;
}
&.odd {
background: $table-accent-bg;
}
&.even {
background: none;
}
&.row-details {
td {
color: $gray-500;
padding-top: 0;
border-top: 0;
}
}
color: map.get($theme-colors, 'light') !important;
}
}
@ -55,5 +34,5 @@
// All modals
.modal-content {
color: theme-color('dark');
color: map.get($theme-colors, 'dark');
}

View File

@ -1,149 +0,0 @@
@use 'sass:color';
// Blue Orange theme variables.
// Color system
$blue: #37abe9;
$indigo: #472395;
$purple: #712395;
$pink: #952393;
$red: #a72431;
$orange: #ff8f00;
$yellow: #ffbe42;
$green: #239556;
$teal: #5abccc;
$cyan: #44c4dd;
$theme-colors: (
primary: $blue,
secondary: $orange,
success: $green,
info: $cyan,
debug: $cyan,
warning: $yellow,
danger: $red,
error: $red,
light: $gray-100,
dark: $gray-800
);
// Body
// Settings for the `<body>` element.
$body-bg: $gray-900;
$body-color: $gray-400;
// Links
// Style anchor elements.
$link-color: theme-color('info');
$link-hover-color: color.adjust($link-color, $lightness: -15%);
// Components
// Define common padding and border radius sizes and more.
$border-color: $gray-200;
$component-active-color: $gray-400;
$component-active-bg: theme-color('primary');
// Fonts
// Font, line-height, and color for body text, headings, and more.
$text-muted: $gray-600 !default;
$blockquote-small-color: $gray-600 !default;
$hr-border-color: rgba($black, .1) !default;
$mark-bg: #fcf8e3 !default;
// Tables
// Customizes the `.table` component with basic values, each used across all table variations.
$table-border-color: $gray-800;
$table-color: $body-color;
$table-head-bg: theme-color('primary');
$table-hover-color: $body-color;
$table-inverse-bg: theme-color('primary');
$table-accent-bg: rgba(theme-color('primary'), .1);
// Forms
$input-bg: $white;
$input-disabled-bg: $gray-600;
$input-color: $black;
$input-border-color: rgba($gray-600, .15);
$input-group-addon-bg: $gray-500;
// Dropdowns
// Dropdown menu container and contents.
$dropdown-bg: $gray-700;
$dropdown-divider-bg: $gray-800;
$dropdown-link-color: $body-color;
$dropdown-link-hover-color: color.adjust($body-color, $lightness: -5%);
$dropdown-link-hover-bg: theme-color('primary');
$dropdown-link-active-color: $component-active-color;
$dropdown-link-active-bg: $component-active-bg;
$dropdown-header-color: color.adjust($body-color, $lightness: -25%);
// Pagination
$pagination-color: $link-color;
$pagination-bg: theme-color('dark');
$pagination-border-color: color.adjust(theme-color('dark'), $lightness: 5%);
$pagination-hover-color: $link-hover-color;
$pagination-hover-bg: $gray-900;
$pagination-hover-border-color: $gray-800;
$pagination-active-color: $body-color;
$pagination-active-bg: theme-color('primary');
$pagination-active-border-color: theme-color('primary');
$pagination-disabled-color: $gray-600;
$pagination-disabled-bg: $pagination-bg;
$pagination-disabled-border-color: $pagination-border-color;
// Jumbotron
$jumbotron-bg: theme-color('dark');
// Cards
$card-bg: theme-color('dark');
$card-cap-bg: rgba(theme-color('light'), .05);
// Progress bars
$progress-bg: $gray-600;
// List group
$list-group-bg: theme-color('dark');
$list-group-hover-bg: color.adjust($list-group-bg, $lightness: -5%);
$list-group-active-color: $component-active-color !default;
$list-group-active-bg: $component-active-bg !default;
$list-group-active-border-color: $list-group-active-bg !default;
$list-group-action-color: $gray-400;
$list-group-action-hover-color: $list-group-action-color;
// Breadcrumbs
$breadcrumb-bg: none;
$breadcrumb-active-color: $gray-600;
// Datetimepicker library (Tempus Dominus)
$bs-datetimepicker-btn-hover-bg: $gray-800;

View File

@ -42,8 +42,6 @@
{% block page %}{% endblock %}
<script src="{% static "babybuddy/js/vendor.js" %}"></script>
<script>moment.locale('{{ LOCALE }}');</script>
<script>moment.tz.setDefault('{{ TIMEZONE }}');</script>
<script src="{% static "babybuddy/js/app.js" %}"></script>
{% if user.is_authenticated %}
<script>BabyBuddy.PullToRefresh.init()</script>

View File

@ -8,14 +8,14 @@
</label>
<div class="col-xs-10 col-sm-auto">
{% if 'choice' or 'boolean' in field|field_type %}
{{ field|add_class:"custom-select custom-select-sm" }}
{{ field|add_class:"form-select form-select-sm" }}
{% else %}
{{ field|add_class:"form-control form-control-sm" }}
{% endif %}
</div>
{% endfor %}
<div class="col-xs-12 col-sm-auto mt-3 mt-sm-0">
<button type="submit" class="btn btn-sm btn-primary mr-2">{% trans "Filter" %}</button>
<button type="submit" class="btn btn-sm btn-primary me-2">{% trans "Filter" %}</button>
<a href="{{ request.path }}" class="btn btn-sm btn-error">{% trans "Reset" %}</a>
</div>
</div>
@ -24,7 +24,7 @@
<p>
<a class="btn btn-dark btn-sm"
data-toggle="collapse"
data-bs-toggle="collapse"
href="#filter_form"
role="button"
aria-expanded="false"

View File

@ -7,7 +7,7 @@
{% csrf_token %}
{% for field in form %}
{{ field.widget }}
<div class="form-group row">
<div class="row">
{% include 'babybuddy/form_field.html' %}
</div>
{% endfor %}

View File

@ -1,38 +1,24 @@
{% load widget_tweaks %}
<label for="id_{{ field.name }}" class="col-sm-2 col-form-label{% if field|field_type == 'booleanfield' %} boolean-label{% endif %}">
{% if field|field_type != "booleanfield" %}
{{ field.label }}
{% endif %}
</label>
<div class="col-sm-10{% if field|widget_type == 'childradioselect' %} overflow-auto"{% endif %}">
{% if field|field_type == "booleanfield" %}
<div class="btn-group-toggle" data-toggle="buttons">
<label for="id_{{ field.name }}" class="btn btn-outline-light btn-no-hover{% if field.value %} active{% endif %}">
{% if field.errors %}
{{ field|add_class:"is-invalid" }}
{% else %}
{{ field }}
{% endif %}
<div class="row mb-3">
<label for="id_{{ field.name }}" class="col-sm-2 col-form-label{% if field|field_type == 'booleanfield' %} boolean-label{% endif %}">
{{ field.label }}
</label>
</div>
{% elif field|field_type == "datetimefield" or field|field_type == "datefield" %}
<div class="input-group input-group-lg datetimepicker" id="datetimepicker_{{ field.name }}" data-target-input="nearest">
<div class="input-group-prepend px-2 rounded-left bg-dark" data-target="#datetimepicker_{{ field.name }}" data-toggle="datetimepicker">
<span class="input-group-text"><i class="icon-calendar"></i></span>
</div>
<div class="col-sm-10{% if field|widget_type == 'childradioselect' %} overflow-auto"{% endif %}">
{% if field|field_type == "booleanfield" %}
{% if field.errors %}
{{ field|add_class:"datetimepicker-input form-control form-control-lg is-invalid" }}
{{ field|add_class:"btn-check is-invalid" }}
{% else %}
{{ field|add_class:"datetimepicker-input form-control form-control-lg" }}
{{ field|add_class:"btn-check" }}
{% endif %}
</div>
<label for="id_{{ field.name }}" class="btn btn-outline-light btn-no-hover{% if field.value %} active{% endif %}">
{{ field.label }}
</label>
{% elif 'choice' in field|field_type %}
{% if field.errors %}
{{ field|add_class:"custom-select is-invalid" }}
{{ field|add_class:"form-select is-invalid" }}
{% else %}
{{ field|add_class:"custom-select" }}
{{ field|add_class:"form-select" }}
{% endif %}
{% else %}
{% if field.errors %}
@ -47,4 +33,5 @@
{% if field.errors %}
<div class="invalid-feedback">{% for error in field.errors %}{{ error }}{% endfor %}</div>
{% endif %}
</div>
</div>

View File

@ -5,9 +5,7 @@
{% for message in messages %}
<div class="alert{% if message.tags %} alert-{{ message.tags }}{% endif %} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}

View File

@ -3,13 +3,15 @@
{% block nav %}
<nav class="navbar navbar-expand-md navbar-dark bg-dark sticky-top">
<a class="navbar-brand mr-2" href={% url "babybuddy:root-router" %}>
<img src="{% static "babybuddy/logo/icon-brand.png" %}" width="30" height="30" class="d-inline-block align-top" alt="">
<div class="container-fluid">
<a class="navbar-brand me-2" href={% url "babybuddy:root-router" %}>
<img src="{% static "babybuddy/logo/icon-brand.png" %}" width="30" height="30"
class="d-inline-block align-top" alt="">
<span class="d-none d-lg-inline-block">
<span class="text-primary">Baby</span> Buddy
</span>
</a>
<div class="d-lg-none d-md-none d-flex mr-auto p-0 ml-2">
<div class="d-lg-none d-md-none d-flex me-auto p-0 ms-2">
<div>
<a class="text-muted"
href="{% url 'dashboard:dashboard' %}"
@ -25,23 +27,19 @@
&nbsp;
</div>
</div>
<div class="d-lg-none d-md-none d-flex ml-auto p-0 mr-2">
<div class="d-lg-none d-md-none d-flex ms-auto p-0 me-2">
{% quick_timer_nav %}
<div class="dropdown show">
<a class="text-success"
href="#"
role="button"
id="nav-quick-add-link"
data-toggle="dropdown"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"><i class="icon-2x icon-add" aria-hidden="true"></i>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="nav-quick-add-link">
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="nav-quick-add-link">
{% if perms.core.add_diaperchange %}
<a class="dropdown-item p-2" href="{% url 'core:diaperchange-add' %}">
<i class="icon-diaperchange" aria-hidden="true"></i>
@ -72,17 +70,18 @@
{% trans "Tummy Time" %}
</a>
{% endif %}
</div>
</div>
</div>
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbar-app" aria-controls="navbar-app"
</div>
</div>
</div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbar-app" aria-controls="navbar-app"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbar-app">
<ul class="navbar-nav mr-auto">
<ul class="navbar-nav me-auto">
<li class="nav-item{% if request.path == '/' %} active{% endif %}">
<a class="nav-link" href="{% url 'dashboard:dashboard' %}">
<i class="icon-dashboard" aria-hidden="true"></i>
@ -101,7 +100,7 @@
<a id="nav-children-menu-link"
class="nav-link dropdown-toggle"
href="#"
data-toggle="dropdown"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"><i class="icon-child" aria-hidden="true"></i>
{% trans "Children" %}
@ -116,7 +115,7 @@
</a>
{% endif %}
{% if perms.core.add_child %}
<a class="dropdown-item pl-5{% if request.path == '/children/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/children/add/' %} active{% endif %}"
href="{% url 'core:child-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "Child" %}
</a>
@ -130,7 +129,7 @@
</a>
{% endif %}
{% if perms.core.add_note %}
<a class="dropdown-item pl-5{% if request.path == '/notes/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/notes/add/' %} active{% endif %}"
href="{% url 'core:note-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "Note" %}
</a>
@ -143,7 +142,7 @@
<a id="nav-measurements-menu-link"
class="nav-link dropdown-toggle"
href="#"
data-toggle="dropdown"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"><i class="icon-measurements" aria-hidden="true"></i>
{% trans "Measurements" %}
@ -158,7 +157,7 @@
</a>
{% endif %}
{% if perms.core.add_bmi %}
<a class="dropdown-item pl-5{% if request.path == '/bmi/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/bmi/add/' %} active{% endif %}"
href="{% url 'core:bmi-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "BMI entry" %}
</a>
@ -172,7 +171,7 @@
</a>
{% endif %}
{% if perms.core.add_head_circumference %}
<a class="dropdown-item pl-5{% if request.path == '/head-circumference/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/head-circumference/add/' %} active{% endif %}"
href="{% url 'core:head-circumference-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "Head Circumference entry" %}
</a>
@ -186,7 +185,7 @@
</a>
{% endif %}
{% if perms.core.add_height %}
<a class="dropdown-item pl-5{% if request.path == '/height/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/height/add/' %} active{% endif %}"
href="{% url 'core:height-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "Height entry" %}
</a>
@ -200,7 +199,7 @@
</a>
{% endif %}
{% if perms.core.add_temperature %}
<a class="dropdown-item pl-5{% if request.path == '/temperature/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/temperature/add/' %} active{% endif %}"
href="{% url 'core:temperature-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "Temperature reading" %}
</a>
@ -214,7 +213,7 @@
</a>
{% endif %}
{% if perms.core.add_weight %}
<a class="dropdown-item pl-5{% if request.path == '/weight/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/weight/add/' %} active{% endif %}"
href="{% url 'core:weight-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "Weight entry" %}
</a>
@ -222,12 +221,11 @@
</div>
</li>
<li class="nav-item dropdown">
<a id="nav-activity-menu-link"
class="nav-link dropdown-toggle"
href="#"
data-toggle="dropdown"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"><i class="icon-activities" aria-hidden="true"></i>
{% trans "Activities" %}
@ -236,12 +234,13 @@
{% if perms.core.view_diaperchange %}
<a class="dropdown-item{% if request.path == '/changes/' %} active{% endif %}"
href="{% url 'core:diaperchange-list' %}"><i class="icon-diaperchange" aria-hidden="true"></i>
href="{% url 'core:diaperchange-list' %}"><i class="icon-diaperchange"
aria-hidden="true"></i>
{% trans "Changes" %}
</a>
{% endif %}
{% if perms.core.add_diaperchange %}
<a class="dropdown-item pl-5{% if request.path == '/changes/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/changes/add/' %} active{% endif %}"
href="{% url 'core:diaperchange-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "Change" %}
</a>
@ -254,7 +253,7 @@
</a>
{% endif %}
{% if perms.core.add_feeding %}
<a class="dropdown-item pl-5{% if request.path == '/feedings/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/feedings/add/' %} active{% endif %}"
href="{% url 'core:feeding-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "Feeding" %}
</a>
@ -268,7 +267,7 @@
</a>
{% endif %}
{% if perms.core.add_pumping %}
<a class="dropdown-item pl-5{% if request.path == '/pumping/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/pumping/add/' %} active{% endif %}"
href="{% url 'core:pumping-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "Pumping entry" %}
</a>
@ -281,7 +280,7 @@
</a>
{% endif %}
{% if perms.core.add_sleep %}
<a class="dropdown-item pl-5{% if request.path == '/sleep/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/sleep/add/' %} active{% endif %}"
href="{% url 'core:sleep-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "Sleep entry" %}
</a>
@ -289,12 +288,13 @@
{% if perms.core.view_tummytime %}
<a class="dropdown-item{% if request.path == '/tummy-time/' %} active{% endif %}"
href="{% url 'core:tummytime-list' %}"><i class="icon-tummytime" aria-hidden="true"></i>
href="{% url 'core:tummytime-list' %}"><i class="icon-tummytime"
aria-hidden="true"></i>
{% trans "Tummy Time" %}
</a>
{% endif %}
{% if perms.core.add_tummytime %}
<a class="dropdown-item pl-5{% if request.path == '/tummy-time/add/' %} active{% endif %}"
<a class="dropdown-item ps-5{% if request.path == '/tummy-time/add/' %} active{% endif %}"
href="{% url 'core:tummytime-add' %}"><i class="icon-add" aria-hidden="true"></i>
{% trans "Tummy Time entry" %}
</a>
@ -308,21 +308,23 @@
</ul>
{% if request.user %}
<ul class="navbar-nav ml-auto">
<ul class="navbar-nav ms-auto">
<li class="nav-item dropdown">
<a id="nav-user-menu-link"
class="nav-link dropdown-toggle"
href="#"
data-toggle="dropdown"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<i class="icon-user" aria-hidden="true"></i>
{% firstof user.get_full_name user.get_username %}
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="nav-user-menu-link">
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="nav-user-menu-link">
<h6 class="dropdown-header">{% trans "User" %}</h6>
<a href="{% url 'babybuddy:user-settings' %}" class="dropdown-item">{% trans "Settings" %}</a>
<a href="{% url 'babybuddy:user-password' %}" class="dropdown-item">{% trans "Password" %}</a>
<a href="{% url 'babybuddy:user-settings' %}"
class="dropdown-item">{% trans "Settings" %}</a>
<a href="{% url 'babybuddy:user-password' %}"
class="dropdown-item">{% trans "Password" %}</a>
<a href="{% url 'babybuddy:user-add-device' %}" class="dropdown-item">{% trans "Add a device" %}</a>
<form action="{% url 'babybuddy:logout' %}" role="form" method="post">
{% csrf_token %}
@ -333,8 +335,12 @@
<h6 class="dropdown-header">{% trans "Site" %}</h6>
<a href="{% url 'api:api-root' %}" class="dropdown-item">{% trans "API Browser" %}</a>
{% if request.user.is_staff %}
<a href="{% url 'babybuddy:user-list' %}" class="dropdown-item">{% trans "Users" %}</a>
<a href="{% url 'admin:index' %}" class="dropdown-item">{% trans "Database Admin" %}</a>
<a href="{% url 'babybuddy:site_settings' %}"
class="dropdown-item">{% trans "Settings" %}</a>
<a href="{% url 'babybuddy:user-list' %}"
class="dropdown-item">{% trans "Users" %}</a>
<a href="{% url 'admin:index' %}"
class="dropdown-item">{% trans "Database Admin" %}</a>
{% endif %}
<h6 class="dropdown-header">{% trans "Support" %}</h6>
<a href="https://github.com/babybuddy/babybuddy" class="dropdown-item">
@ -347,5 +353,6 @@
</ul>
{% endif %}
</div>
</div>
</nav>
{% endblock %}

View File

@ -8,7 +8,7 @@
<li class="page-item">
<a class="page-link" href="{% relative_url 'page' page_obj.previous_page_number %}" aria-label="Previous">
<i class="icon-angle-circled-left" aria-hidden="true"></i>
<span class="sr-only">{% trans "Previous" %}</span>
<span class="visually-hidden">{% trans "Previous" %}</span>
</a>
</li>
{% endif %}
@ -25,7 +25,7 @@
<li class="page-item">
<a class="page-link" href="{% relative_url 'page' page_obj.next_page_number %}" aria-label="Next">
<i class="icon-angle-circled-right" aria-hidden="true"></i>
<span class="sr-only">{% trans "Next" %}</span>
<span class="visually-hidden">{% trans "Next" %}</span>
</a>
</li>
{% endif %}

View File

@ -12,7 +12,7 @@
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'babybuddy:user-list' %}">{% trans "Users" %}</a></li>
{% if object %}
<li class="breadcrumb-item font-weight-bold">{{ object }}</li>
<li class="breadcrumb-item fw-bold">{{ object }}</li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Update" %}</li>
{% else %}
<li class="breadcrumb-item active" aria-current="page">{% trans "Create User" %}</li>

View File

@ -11,8 +11,8 @@
<h1>Users</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-striped table-hover user-list">
<thead class="thead-inverse">
<table class="table table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "User" %}</th>
<th>{% trans "First Name" %}</th>

View File

@ -8,7 +8,8 @@
{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="px-2 py-5 bg-dark rounded-3">
<div class="container-fluid">
<h1 class="display-3">{% trans "Welcome to Baby Buddy!" %}</h1>
<p class="lead">
{% blocktrans trimmed%}
@ -17,7 +18,8 @@
{% endblocktrans %}
</p>
<hr class="my-4">
<div class="card-deck">
<div class="row gy-4">
<div class="col-12 col-md-6 col-lg-3">
<div class="card card-diaperchange">
<div class="card-header text-center">
<i class="icon-2x icon-diaperchange" aria-hidden="true"></i>
@ -26,6 +28,8 @@
<h3 class="card-title text-center">{% trans "Diaper Changes" %}</h3>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="card card-feeding">
<div class="card-header text-center">
<i class="icon-2x icon-feeding" aria-hidden="true"></i>
@ -34,6 +38,8 @@
<h3 class="card-title text-center">{% trans "Feedings" %}</h3>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="card card-sleep">
<div class="card-header text-center">
<i class="icon-2x icon-sleep" aria-hidden="true"></i>
@ -42,6 +48,8 @@
<h3 class="card-title text-center">{% trans "Sleep" %}</h3>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="card card-tummytime">
<div class="card-header text-center">
<i class="icon-2x icon-tummytime" aria-hidden="true"></i>
@ -51,6 +59,7 @@
</div>
</div>
</div>
</div>
<hr class="my-4">
<p class="lead">
{% blocktrans trimmed %}
@ -70,4 +79,5 @@
{% endif %}
</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends 'babybuddy/page.html' %}
{% load i18n %}
{% block title %}{% trans "Site Settings" %}{% endblock %}
{% block breadcrumbs %}
<li class="breadcrumb-item">{% trans "Site" %}</li>
<li class="breadcrumb-item active">{% trans "Settings" %}</li>
{% endblock %}
{% block content %}
<h1>{% trans "Site Settings" %}</h1>
{% include 'babybuddy/form.html' %}
{% endblock %}

View File

@ -10,7 +10,8 @@
<div class="alert alert-danger" role="alert">
{{ main }} {{ reason }}
</div>
<div class="jumbotron">
<div class="px-2 py-5 bg-dark rounded-3">
<div class="container-fluid">
<h2>{% trans "How to Fix" %}</h2>
{% blocktrans trimmed with origin=origin %}
Add <samp>{{ origin }}</samp> to the <code>CSRF_TRUSTED_ORIGINS</code>
@ -18,4 +19,5 @@
with commas.
{% endblocktrans %}
</div>
</div>
{% endblock %}

View File

@ -8,7 +8,7 @@
<div class="row justify-content-md-center">
<div class="col-lg-12 mb-4">
<div class="d-sm-flex">
<img class="d-inline-block align-self-top mr-2 mt-2 text-center" src="{% static "babybuddy/logo/logo-sad.png" %}" width="65" height="65">
<img class="d-inline-block align-self-top me-2 mt-2 text-center" src="{% static "babybuddy/logo/logo-sad.png" %}" width="65" height="65">
<div class="p-2 flex-grow-1">
{% block content %}{% endblock %}
<a href="{% url "babybuddy:root-router" %}" class="btn btn-outline-primary mt-3">{% trans "Return to Baby Buddy" %}</a>

View File

@ -7,7 +7,7 @@
<div id="view-{{ request.resolver_match.view_name }}" class="container">
<div class="text-center pt-3">
<img src="{% static "babybuddy/logo/icon-brand.png" %}" width="65" height="65" class="d-inline-block align-top" alt="">
<h1 class="d-none d-md-inline-block display-4 ml-2">
<h1 class="d-none d-md-inline-block display-4 ms-2">
<span class="text-primary">Baby</span> Buddy
</h1>
</a>

View File

@ -8,23 +8,19 @@
<input type="hidden" name="next" value="{{ next }}">
<label class="sr-only" for="username-input-group">
<label class="visually-hidden" for="username-input-group">
{{ form.username.label }}
</label>
<div class="input-group mb-3 fade-in">
<div class="input-group-prepend">
<span class="input-group-text"><i class="icon-user" aria-hidden="true"></i></span>
</div>
<span class="input-group-text text-muted"><i class="icon-user" aria-hidden="true"></i></span>
{% render_field form.username name='username' class+='form-control' id='username-input-group' placeholder=form.username.label %}
</div>
<label class="sr-only" for="password-input-group">
<label class="visually-hidden" for="password-input-group">
{{ form.password.label }}
</label>
<div class="input-group mb-3 fade-in">
<div class="input-group-prepend">
<span class="input-group-text"><i class="icon-lock" aria-hidden="true"></i></span>
</div>
<span class="input-group-text text-muted"><i class="icon-lock" aria-hidden="true"></i></span>
{% render_field form.password name='password' class+='form-control' id='password-input-group' placeholder=form.password.label %}
</div>

View File

@ -22,23 +22,19 @@
<p class="mb-0">{% trans "Enter your new password in each field below." %}</p>
</div>
<label class="sr-only" for="password1-input-group">
<label class="visually-hidden" for="password1-input-group">
{{ form.new_password1.label }}
</label>
<div class="input-group mb-3 fade-in">
<div class="input-group-prepend">
<span class="input-group-text"><i class="icon-lock" aria-hidden="true"></i></span>
</div>
<span class="input-group-text text-muted"><i class="icon-lock" aria-hidden="true"></i></span>
{% render_field form.new_password1 name='new_password1' class+='form-control' id='password1-input-group' %}
</div>
<label class="sr-only" for="password2-input-group">
<label class="visually-hidden" for="password2-input-group">
{{ form.new_password2.label }}
</label>
<div class="input-group mb-3 fade-in">
<div class="input-group-prepend">
<span class="input-group-text"><i class="icon-lock" aria-hidden="true"></i></span>
</div>
<span class="input-group-text text-muted"><i class="icon-lock" aria-hidden="true"></i></span>
{% render_field form.new_password2 name='new_password2' class+='form-control' id='password2-input-group' %}
</div>

View File

@ -17,13 +17,11 @@
<form method="post">
{% csrf_token %}
<label class="sr-only" for="email-input-group">
<label class="visually-hidden" for="email-input-group">
{{ form.email.label }}
</label>
<div class="input-group mb-3 fade-in">
<div class="input-group-prepend">
<span class="input-group-text"><i class="icon-mail" aria-hidden="true"></i></span>
</div>
<span class="input-group-text text-muted"><i class="icon-mail" aria-hidden="true"></i></span>
{% render_field form.email name='email' class+='form-control' id='email-input-group' placeholder=form.email.label %}
</div>

View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
import datetime
from django.test import TestCase
from django.utils.formats import date_format
class FormatsTestCase(TestCase):
def test_short_month_day_format(self):
dt = datetime.datetime(year=2021, month=7, day=31, hour=5, minute=5, second=5)
self.assertEqual(date_format(dt, "SHORT_MONTH_DAY_FORMAT"), "Jul 31")

View File

@ -1,70 +0,0 @@
# -*- coding: utf-8 -*-
import datetime
from django.core.exceptions import ValidationError
from django.forms.fields import DateTimeField
from django.test import TestCase, override_settings # , tag
from django.utils.formats import date_format, time_format
from babybuddy.middleware import update_en_gb_date_formats
class GbFormatsTestCase(TestCase):
@override_settings(LANGUAGE_CODE="en-GB")
def test_datetime_input_formats(self):
update_en_gb_date_formats()
field = DateTimeField()
supported_custom_examples = [
"20/01/2020",
"20/01/2020 9:30 AM",
"20/01/2020 9:30:03 AM",
"01/10/2020 11:30 PM",
"01/10/2020 11:30:03 AM",
]
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!")
# @tag('isolate')
@override_settings(LANGUAGE_CODE="en-GB", USE_24_HOUR_TIME_FORMAT=True)
def test_use_24_hour_time_format(self):
update_en_gb_date_formats()
field = DateTimeField()
supported_custom_examples = [
"25/10/2006 2:30:59",
"25/10/2006 2:30",
"25/10/2006 14:30:59",
"25/10/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(year=2011, month=11, day=4, hour=23, minute=5, second=59)
self.assertEqual(date_format(dt, "DATETIME_FORMAT"), "4 November 2011 23:05:59")
dt = datetime.datetime(year=2011, month=11, day=4, hour=2, minute=5, second=59)
self.assertEqual(date_format(dt, "SHORT_DATETIME_FORMAT"), "04/11/2011 02:05")
t = datetime.time(hour=16, minute=2, second=25)
self.assertEqual(time_format(t), "16:02")
# def test_short_month_day_format(self):
# update_en_gb_date_formats()
# dt = datetime.datetime(year=2021, month=7, day=31, hour=5, minute=5,
# second=5)
# self.assertEqual(date_format(dt, 'SHORT_MONTH_DAY_FORMAT'), '31 Jul')

View File

@ -1,67 +0,0 @@
# -*- coding: utf-8 -*-
import datetime
from django.core.exceptions import ValidationError
from django.forms.fields import DateTimeField
from django.test import TestCase, override_settings, tag
from django.utils.formats import date_format, time_format
from babybuddy.middleware import update_en_us_date_formats
class FormatsTestCase(TestCase):
def test_datetime_input_formats(self):
update_en_us_date_formats()
field = DateTimeField()
supported_custom_examples = [
"01/20/2020 9:30 AM",
"01/20/2020 9:30:03 AM",
"10/01/2020 11:30 PM",
"10/01/2020 11:30:03 AM",
]
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!")
@tag("isolate")
@override_settings(LANGUAGE_CODE="en-US", USE_24_HOUR_TIME_FORMAT=True)
def test_use_24_hour_time_format(self):
update_en_us_date_formats()
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(year=2011, month=11, day=4, hour=23, minute=5, second=59)
self.assertEqual(date_format(dt, "DATETIME_FORMAT"), "Nov. 4, 2011, 23:05:59")
dt = datetime.datetime(year=2011, month=11, day=4, hour=2, minute=5, second=59)
self.assertEqual(date_format(dt, "SHORT_DATETIME_FORMAT"), "11/04/2011 2:05:59")
t = datetime.time(hour=16, minute=2, second=25)
self.assertEqual(time_format(t), "16:02:25")
def test_short_month_day_format(self):
update_en_us_date_formats()
dt = datetime.datetime(year=2021, month=7, day=31, hour=5, minute=5, second=5)
self.assertEqual(date_format(dt, "SHORT_MONTH_DAY_FORMAT"), "Jul 31")

View File

@ -222,11 +222,10 @@ class FormsTestCase(TestCase):
page = self.c.post("/user/settings/", data=params, follow=True)
self.assertContains(page, "Paramètres utilisateur")
@override_settings(TIME_ZONE="US/Eastern")
def test_user_settings_timezone(self):
self.c.login(**self.credentials)
self.assertEqual(timezone.get_default_timezone_name(), "US/Eastern")
self.assertEqual(timezone.get_default_timezone_name(), "UTC")
params = self.settings_template.copy()
params["timezone"] = "US/Pacific"
page = self.c.post("/user/settings/", data=params, follow=True)

View File

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import Client as HttpClient, TestCase
from faker import Faker
from core.models import Sleep
class SiteSettingsTestCase(TestCase):
@classmethod
def setUpClass(cls):
super(SiteSettingsTestCase, cls).setUpClass()
fake = Faker()
call_command("migrate", verbosity=0)
call_command("fake", verbosity=0)
cls.c = HttpClient()
fake_user = fake.simple_profile()
cls.credentials = {
"username": fake_user["username"],
"password": fake.password(),
}
cls.user = get_user_model().objects.create_user(
is_superuser=True, is_staff=True, **cls.credentials
)
def test_default_settings(self):
self.c.login(**self.credentials)
page = self.c.get("/settings/")
self.assertEqual(page.status_code, 200)
self.assertEqual(
page.context["form"]["core.models__Sleep__nap_start_max"].value(),
"18:00:00",
)
self.assertEqual(
page.context["form"]["core.models__Sleep__nap_start_min"].value(),
"06:00:00",
)
def test_nap_start_settings(self):
self.c.login(**self.credentials)
self.assert_naps()
params = {
"core.models__Sleep__nap_start_max": "20:00:00",
"core.models__Sleep__nap_start_min": "09:00:00",
}
page = self.c.post("/settings/", params, follow=True)
self.assertEqual(page.status_code, 200)
self.assertEqual(
Sleep.settings.nap_start_max.strftime("%H:%M:%S"),
params["core.models__Sleep__nap_start_max"],
)
self.assertEqual(
Sleep.settings.nap_start_min.strftime("%H:%M:%S"),
params["core.models__Sleep__nap_start_min"],
)
self.assert_naps()
def assert_naps(self):
"""
Asserts sleep instances filtered with nap start min and max match nap instances.
"""
instances = Sleep.objects.filter(
start__time__range=(
Sleep.settings.nap_start_min.strftime("%H:%M:%S"),
Sleep.settings.nap_start_max.strftime("%H:%M:%S"),
)
)
naps = Sleep.naps.all()
self.assertQuerySetEqual(instances, naps)

View File

@ -44,6 +44,7 @@ app_patterns = [
path("user/password/", views.UserPassword.as_view(), name="user-password"),
path("user/settings/", views.UserSettings.as_view(), name="user-settings"),
path("user/add-device/", views.UserAddDevice.as_view(), name="user-add-device"),
path("settings/", include("dbsettings.urls")),
]
urlpatterns = [

29
babybuddy/widgets.py Normal file
View File

@ -0,0 +1,29 @@
import datetime
from django.forms import widgets
class DateTimeBaseInput(widgets.DateTimeBaseInput):
def format_value(self, value):
if isinstance(value, datetime.datetime):
value = value.isoformat()
return value
class DateTimeInput(DateTimeBaseInput):
input_type = "datetime-local"
def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super().build_attrs(base_attrs, extra_attrs)
# Default to seconds granularity. Required for client validation in Safari.
if "step" not in attrs:
attrs["step"] = 1
return attrs
class DateInput(DateTimeBaseInput):
input_type = "date"
class TimeInput(DateTimeBaseInput):
input_type = "time"

View File

@ -216,8 +216,8 @@ class TemperatureAdmin(ImportExportMixin, ExportActionMixin, admin.ModelAdmin):
@admin.register(models.Timer)
class TimerAdmin(admin.ModelAdmin):
list_display = ("name", "child", "start", "end", "duration", "active", "user")
list_filter = ("child", "active", "user")
list_display = ("name", "child", "start", "duration", "user")
list_filter = ("child", "user")
search_fields = ("child__first_name", "child__last_name", "name", "user")

View File

@ -7,6 +7,7 @@ from django.utils.translation import gettext as _
from taggit.forms import TagField
from babybuddy.widgets import DateInput, DateTimeInput
from core import models
from core.widgets import TagsEditor, ChildRadioSelect
@ -44,7 +45,7 @@ def set_initial_values(kwargs, form_type):
if timer_id:
timer = models.Timer.objects.get(id=timer_id)
kwargs["initial"].update(
{"timer": timer, "start": timer.start, "end": timer.end or timezone.now()}
{"timer": timer, "start": timer.start, "end": timezone.now()}
)
# Set type and method values for Feeding instance based on last feed.
@ -83,7 +84,7 @@ class CoreModelForm(forms.ModelForm):
instance = super(CoreModelForm, self).save(commit=False)
if self.timer_id:
timer = models.Timer.objects.get(id=self.timer_id)
timer.stop(instance.end)
timer.stop()
if commit:
instance.save()
self.save_m2m()
@ -97,12 +98,7 @@ class ChildForm(forms.ModelForm):
if settings.BABY_BUDDY["ALLOW_UPLOADS"]:
fields.append("picture")
widgets = {
"birth_date": forms.DateInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_birth_date",
}
),
"birth_date": DateInput(),
}
@ -144,12 +140,7 @@ class PumpingForm(CoreModelForm, TaggableModelForm):
fields = ["child", "amount", "time", "notes", "tags"]
widgets = {
"child": ChildRadioSelect,
"time": forms.DateTimeInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_time",
}
),
"time": DateTimeInput(),
"notes": forms.Textarea(attrs={"rows": 5}),
}
@ -160,12 +151,7 @@ class DiaperChangeForm(CoreModelForm, TaggableModelForm):
fields = ["child", "time", "wet", "solid", "color", "amount", "notes", "tags"]
widgets = {
"child": ChildRadioSelect(),
"time": forms.DateTimeInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_time",
}
),
"time": DateTimeInput(),
"notes": forms.Textarea(attrs={"rows": 5}),
}
@ -176,18 +162,8 @@ class FeedingForm(CoreModelForm, TaggableModelForm):
fields = ["child", "start", "end", "type", "method", "amount", "notes", "tags"]
widgets = {
"child": ChildRadioSelect,
"start": forms.DateTimeInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_start",
}
),
"end": forms.DateTimeInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_end",
}
),
"start": DateTimeInput(),
"end": DateTimeInput(),
"notes": forms.Textarea(attrs={"rows": 5}),
}
@ -198,12 +174,7 @@ class NoteForm(CoreModelForm, TaggableModelForm):
fields = ["child", "note", "time", "tags"]
widgets = {
"child": ChildRadioSelect,
"time": forms.DateTimeInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_time",
}
),
"time": DateTimeInput(),
}
@ -213,18 +184,8 @@ class SleepForm(CoreModelForm, TaggableModelForm):
fields = ["child", "start", "end", "notes", "tags"]
widgets = {
"child": ChildRadioSelect,
"start": forms.DateTimeInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_start",
}
),
"end": forms.DateTimeInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_end",
}
),
"start": DateTimeInput(),
"end": DateTimeInput(),
"notes": forms.Textarea(attrs={"rows": 5}),
}
@ -235,12 +196,7 @@ class TemperatureForm(CoreModelForm, TaggableModelForm):
fields = ["child", "temperature", "time", "notes", "tags"]
widgets = {
"child": ChildRadioSelect,
"time": forms.DateTimeInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_time",
}
),
"time": DateTimeInput(),
"notes": forms.Textarea(attrs={"rows": 5}),
}
@ -251,12 +207,7 @@ class TimerForm(CoreModelForm):
fields = ["child", "name", "start"]
widgets = {
"child": ChildRadioSelect,
"start": forms.DateTimeInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_start",
}
),
"start": DateTimeInput(),
}
def __init__(self, *args, **kwargs):
@ -276,18 +227,8 @@ class TummyTimeForm(CoreModelForm, TaggableModelForm):
fields = ["child", "start", "end", "milestone", "tags"]
widgets = {
"child": ChildRadioSelect,
"start": forms.DateTimeInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_start",
}
),
"end": forms.DateTimeInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_end",
}
),
"start": DateTimeInput(),
"end": DateTimeInput(),
}
@ -297,12 +238,7 @@ class WeightForm(CoreModelForm, TaggableModelForm):
fields = ["child", "weight", "date", "notes", "tags"]
widgets = {
"child": ChildRadioSelect,
"date": forms.DateInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_date",
}
),
"date": DateInput(),
"notes": forms.Textarea(attrs={"rows": 5}),
}
@ -313,12 +249,7 @@ class HeightForm(CoreModelForm, TaggableModelForm):
fields = ["child", "height", "date", "notes", "tags"]
widgets = {
"child": ChildRadioSelect,
"date": forms.DateInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_date",
}
),
"date": DateInput(),
"notes": forms.Textarea(attrs={"rows": 5}),
}
@ -329,12 +260,7 @@ class HeadCircumferenceForm(CoreModelForm, TaggableModelForm):
fields = ["child", "head_circumference", "date", "notes", "tags"]
widgets = {
"child": ChildRadioSelect,
"date": forms.DateInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_date",
}
),
"date": DateInput(),
"notes": forms.Textarea(attrs={"rows": 5}),
}
@ -345,12 +271,7 @@ class BMIForm(CoreModelForm, TaggableModelForm):
fields = ["child", "bmi", "date", "notes", "tags"]
widgets = {
"child": ChildRadioSelect,
"date": forms.DateInput(
attrs={
"autocomplete": "off",
"data-target": "#datetimepicker_date",
}
),
"date": DateInput(),
"notes": forms.Textarea(attrs={"rows": 5}),
}

View File

@ -0,0 +1,62 @@
# Generated by Django 4.0.4 on 2022-06-10 03:34
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("core", "0025_pumping_tags"),
]
operations = [
migrations.AlterField(
model_name="feeding",
name="end",
field=models.DateTimeField(
default=django.utils.timezone.localtime, verbose_name="End time"
),
),
migrations.AlterField(
model_name="feeding",
name="start",
field=models.DateTimeField(
default=django.utils.timezone.localtime, verbose_name="Start time"
),
),
migrations.AlterField(
model_name="pumping",
name="time",
field=models.DateTimeField(
default=django.utils.timezone.localtime, verbose_name="Time"
),
),
migrations.AlterField(
model_name="sleep",
name="end",
field=models.DateTimeField(
default=django.utils.timezone.localtime, verbose_name="End time"
),
),
migrations.AlterField(
model_name="sleep",
name="start",
field=models.DateTimeField(
default=django.utils.timezone.localtime, verbose_name="Start time"
),
),
migrations.AlterField(
model_name="tummytime",
name="end",
field=models.DateTimeField(
default=django.utils.timezone.localtime, verbose_name="End time"
),
),
migrations.AlterField(
model_name="tummytime",
name="start",
field=models.DateTimeField(
default=django.utils.timezone.localtime, verbose_name="Start time"
),
),
]

View File

@ -0,0 +1,37 @@
from django.db import migrations
def delete_inactive_timers(apps, schema_editor):
from core import models
for timer in models.Timer.objects.filter(active=False):
timer.delete()
class Migration(migrations.Migration):
dependencies = [
("core", "0026_alter_feeding_end_alter_feeding_start_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="timer",
options={
"default_permissions": ("view", "add", "change", "delete"),
"ordering": ["-start"],
"verbose_name": "Timer",
"verbose_name_plural": "Timers",
},
),
migrations.RemoveField(
model_name="timer",
name="duration",
),
migrations.RemoveField(
model_name="timer",
name="end",
),
migrations.RunPython(
delete_inactive_timers, reverse_code=migrations.RunPython.noop
),
]

View File

@ -2,7 +2,6 @@
import re
from datetime import timedelta
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
@ -13,6 +12,7 @@ from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager as TaggitTaggableManager
from taggit.models import GenericTaggedItemBase, TagBase
from babybuddy.site_settings import NapSettings
from core.utils import random_color
@ -273,8 +273,15 @@ class Feeding(models.Model):
related_name="feeding",
verbose_name=_("Child"),
)
start = models.DateTimeField(blank=False, null=False, verbose_name=_("Start time"))
end = models.DateTimeField(blank=False, null=False, verbose_name=_("End time"))
start = models.DateTimeField(
blank=False,
default=timezone.localtime,
null=False,
verbose_name=_("Start time"),
)
end = models.DateTimeField(
blank=False, default=timezone.localtime, null=False, verbose_name=_("End time")
)
duration = models.DurationField(
editable=False, null=True, verbose_name=_("Duration")
)
@ -426,7 +433,9 @@ class Pumping(models.Model):
verbose_name=_("Child"),
)
amount = models.FloatField(blank=False, null=False, verbose_name=_("Amount"))
time = models.DateTimeField(blank=False, null=False, verbose_name=_("Time"))
time = models.DateTimeField(
blank=False, default=timezone.localtime, null=False, verbose_name=_("Time")
)
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
tags = TaggableManager(blank=True, through=Tagged)
@ -451,8 +460,15 @@ class Sleep(models.Model):
"Child", on_delete=models.CASCADE, related_name="sleep", verbose_name=_("Child")
)
napping = models.BooleanField(editable=False, null=True, verbose_name=_("Napping"))
start = models.DateTimeField(blank=False, null=False, verbose_name=_("Start time"))
end = models.DateTimeField(blank=False, null=False, verbose_name=_("End time"))
start = models.DateTimeField(
blank=False,
default=timezone.localtime,
null=False,
verbose_name=_("Start time"),
)
end = models.DateTimeField(
blank=False, default=timezone.localtime, null=False, verbose_name=_("End time")
)
duration = models.DurationField(
editable=False, null=True, verbose_name=_("Duration")
)
@ -461,6 +477,7 @@ class Sleep(models.Model):
objects = models.Manager()
naps = NapsManager()
settings = NapSettings(_("Nap settings"))
class Meta:
default_permissions = ("view", "add", "change", "delete")
@ -473,14 +490,12 @@ class Sleep(models.Model):
@property
def nap(self):
nap_start_min = timezone.datetime.strptime(
settings.BABY_BUDDY["NAP_START_MIN"], "%H:%M"
).time()
nap_start_max = timezone.datetime.strptime(
settings.BABY_BUDDY["NAP_START_MAX"], "%H:%M"
).time()
local_start_time = timezone.localtime(self.start).time()
return nap_start_min <= local_start_time <= nap_start_max
return (
Sleep.settings.nap_start_min
<= local_start_time
<= Sleep.settings.nap_start_max
)
def save(self, *args, **kwargs):
if self.start and self.end:
@ -543,12 +558,6 @@ class Timer(models.Model):
start = models.DateTimeField(
default=timezone.now, blank=False, verbose_name=_("Start time")
)
end = models.DateTimeField(
blank=True, editable=False, null=True, verbose_name=_("End time")
)
duration = models.DurationField(
editable=False, null=True, verbose_name=_("Duration")
)
active = models.BooleanField(default=True, editable=False, verbose_name=_("Active"))
user = models.ForeignKey(
"auth.User",
@ -561,7 +570,7 @@ class Timer(models.Model):
class Meta:
default_permissions = ("view", "add", "change", "delete")
ordering = ["-active", "-start", "-end"]
ordering = ["-start"]
verbose_name = _("Timer")
verbose_name_plural = _("Timers")
@ -584,42 +593,24 @@ class Timer(models.Model):
return self.user.get_full_name()
return self.user.get_username()
@classmethod
def from_db(cls, db, field_names, values):
instance = super(Timer, cls).from_db(db, field_names, values)
if not instance.duration:
instance.duration = timezone.now() - instance.start
return instance
def duration(self):
return timezone.now() - self.start
def restart(self):
"""Restart the timer."""
self.start = timezone.now()
self.end = None
self.duration = None
self.active = True
self.save()
def stop(self, end=None):
"""Stop the timer."""
if not end:
end = timezone.now()
self.end = end
self.save()
def stop(self):
"""Stop (delete) the timer."""
self.delete()
def save(self, *args, **kwargs):
self.active = self.end is None
self.name = self.name or None
if self.start and self.end:
self.duration = self.end - self.start
else:
self.duration = None
super(Timer, self).save(*args, **kwargs)
def clean(self):
validate_time(self.start, "start")
if self.end:
validate_time(self.end, "end")
validate_duration(self)
class TummyTime(models.Model):
@ -630,8 +621,15 @@ class TummyTime(models.Model):
related_name="tummy_time",
verbose_name=_("Child"),
)
start = models.DateTimeField(blank=False, null=False, verbose_name=_("Start time"))
end = models.DateTimeField(blank=False, null=False, verbose_name=_("End time"))
start = models.DateTimeField(
blank=False,
default=timezone.localtime,
null=False,
verbose_name=_("Start time"),
)
end = models.DateTimeField(
blank=False, default=timezone.localtime, null=False, verbose_name=_("End time")
)
duration = models.DurationField(
editable=False, null=True, verbose_name=_("Duration")
)

View File

@ -10,7 +10,7 @@ BabyBuddy.Timer = function ($) {
var runIntervalId = null;
var timerId = null;
var timerElement = null;
var lastUpdate = moment();
var lastUpdate = new Date();
var hidden = null;
var Timer = {
@ -18,14 +18,14 @@ BabyBuddy.Timer = function ($) {
timerId = timer_id;
timerElement = $('#' + element_id);
if (timerElement.length == 0) {
if (timerElement.length === 0) {
console.error('BBTimer: Timer element not found.');
return false;
}
if (timerElement.find('.timer-seconds').length == 0
|| timerElement.find('.timer-minutes').length == 0
|| timerElement.find('.timer-hours').length == 0) {
if (timerElement.find('.timer-seconds').length === 0
|| timerElement.find('.timer-minutes').length === 0
|| timerElement.find('.timer-hours').length === 0) {
console.error('BBTimer: Element does not contain expected children.');
return false;
}
@ -48,7 +48,7 @@ BabyBuddy.Timer = function ($) {
},
handleVisibilityChange: function() {
if (!document[hidden] && moment().diff(lastUpdate) > 10000) {
if (!document[hidden] && (new Date()) - lastUpdate > 1) {
Timer.update();
}
},
@ -83,19 +83,18 @@ BabyBuddy.Timer = function ($) {
$.get('/api/timers/' + timerId + '/', function(data) {
if (data && 'duration' in data) {
clearInterval(runIntervalId);
var duration = moment.duration(data.duration);
timerElement.find('.timer-hours').text(duration.hours());
timerElement.find('.timer-minutes').text(duration.minutes());
timerElement.find('.timer-seconds').text(duration.seconds());
lastUpdate = moment();
if (data['active']) {
var duration = data.duration.split(/[\s:.]/)
if (duration.length === 5) {
duration[0] = parseInt(duration[0]) * 24 + parseInt(duration[1]);
duration[1] = duration[2];
duration[2] = duration[3];
}
timerElement.find('.timer-hours').text(parseInt(duration[0]));
timerElement.find('.timer-minutes').text(parseInt(duration[1]));
timerElement.find('.timer-seconds').text(parseInt(duration[2]));
lastUpdate = new Date()
runIntervalId = setInterval(Timer.tick, 1000);
}
else {
timerElement.addClass('timer-stopped');
}
}
});
}
};

View File

@ -4,21 +4,6 @@
}
}
#view-core\:child-list {
.picture-column {
text-align: center;
width: 65px;
min-width: 65px;
}
.child-list {
th,
td {
vertical-align: middle;
}
}
}
@include media-breakpoint-up(md) {
#view-core\:child {
.child-detail-column {

View File

@ -1,3 +1,5 @@
@use 'sass:map';
// Adapted for Bootstrap 4 from https://www.bootply.com/SzXin8KDZJ.
$card-shadow: rgba(0, 0, 0, .175);
@ -12,7 +14,7 @@ $card-shadow: rgba(0, 0, 0, .175);
position: absolute;
content: ' ';
width: 3px;
background-color: theme-color('primary');
background-color: map.get($theme-colors, 'primary');
left: 50%;
margin-left: -1.5px;
@ -34,7 +36,7 @@ $card-shadow: rgba(0, 0, 0, .175);
}
.card {
width: 47%;
width: 46%;
float: left;
position: relative;
box-shadow: 0 1px 6px $card-shadow;
@ -45,8 +47,8 @@ $card-shadow: rgba(0, 0, 0, .175);
right: -15px;
display: inline-block;
border-top: 15px solid transparent;
border-left: 15px solid theme-color('dark');
border-right: 0 solid theme-color('dark');
border-left: 15px solid map.get($theme-colors, 'dark');
border-right: 0 solid map.get($theme-colors, 'dark');
border-bottom: 15px solid transparent;
content: ' ';
}
@ -57,8 +59,8 @@ $card-shadow: rgba(0, 0, 0, .175);
right: -14px;
display: inline-block;
border-top: 14px solid transparent;
border-left: 14px solid theme-color('dark');
border-right: 0 solid theme-color('dark');
border-left: 14px solid map.get($theme-colors, 'dark');
border-right: 0 solid map.get($theme-colors, 'dark');
border-bottom: 14px solid transparent;
content: ' ';
}
@ -95,7 +97,7 @@ $card-shadow: rgba(0, 0, 0, .175);
top: 16px;
left: 50%;
margin-left: -25px;
background-color: theme-color('dark');
background-color: map.get($theme-colors, 'dark');
z-index: 100;
border-radius: 50%;
@ -110,7 +112,7 @@ $card-shadow: rgba(0, 0, 0, .175);
top: 26px;
left: 50%;
margin-left: -25px;
background-color: theme-color('dark');
background-color: map.get($theme-colors, 'dark');
z-index: 100;
border-radius: 50%;
}

View File

@ -1,16 +1,7 @@
@use 'sass:map';
#timer-status {
font-size: $h1-font-size;
font-weight: $display1-weight;
&.timer-stopped {
color: theme-color('danger');
}
}
@include media-breakpoint-up(sm) {
#view-core\:timer-detail {
#timer-status {
font-size: $display1-size;
}
color: map.get($theme-colors, 'dark');
}
}

View File

@ -28,12 +28,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_date'), {
format: 'L',
extraFormats: ['YYYY-MM-DD']
});
</script>
{% endblock %}

View File

@ -18,8 +18,8 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Actions" %}</th>
<th>{% trans "Date" %}</th>
@ -32,8 +32,7 @@
</thead>
<tbody>
{% for bmi in object_list %}
{% cycle "odd" "even" as row_class silent %}
<tr class="{{ row_class }}">
<tr>
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="{% trans "Actions" %}">
@ -62,7 +61,7 @@
</tr>
{% if bmi.notes %}
<tr class="{{ row_class }} row-details">
<td colspan="5"><i class="icon-note mr-2" aria-hidden="true"></i>{{ bmi.notes }}</td>
<td colspan="5"><i class="icon-note me-2" aria-hidden="true"></i>{{ bmi.notes }}</td>
</tr>
{% endif %}
{% empty %}

View File

@ -5,7 +5,7 @@
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'core:child-list' %}">{% trans "Children" %}</a></li>
<li class="breadcrumb-item font-weight-bold"><a href="{% url 'core:child' object.slug %}">{{ object }}</a></li>
<li class="breadcrumb-item fw-bold"><a href="{% url 'core:child' object.slug %}">{{ object }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Delete" %}</li>
{% endblock %}

View File

@ -5,7 +5,7 @@
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'core:child-list' %}">{% trans "Children" %}</a></li>
<li class="breadcrumb-item font-weight-bold">
<li class="breadcrumb-item fw-bold">
{% child_quick_switch object 'core:child' %}
</li>
{% endblock %}

View File

@ -12,7 +12,7 @@
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'core:child-list' %}">{% trans "Children" %}</a></li>
{% if object %}
<li class="breadcrumb-item font-weight-bold"><a href="{% url 'core:child' object.slug %}">{{ object }}</a></li>
<li class="breadcrumb-item fw-bold"><a href="{% url 'core:child' object.slug %}">{{ object }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Update" %}</li>
{% else %}
<li class="breadcrumb-item active" aria-current="page">{% trans "Add a Child" %}</li>
@ -29,13 +29,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_birth_date'), {
format: 'L',
viewMode: 'years',
extraFormats: ['YYYY-MM-DD']
});
</script>
{% endblock %}

View File

@ -18,10 +18,10 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-striped table-hover child-list">
<thead class="thead-inverse">
<table class="table table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th class="picture-column"><i class="icon-camera" aria-hidden="true"></i></th>
<th><i class="icon-camera" aria-hidden="true"></i></th>
<th>{% trans "First Name" %}</th>
<th>{% trans "Last Name" %}</th>
<th>{% trans "Birth Date" %}</th>
@ -31,8 +31,12 @@
<tbody>
{% for child in object_list %}
<tr>
<td class="picture-column">
<td>
{% if child.picture %}
{% include "core/child_thumbnail.html" %}
{% else %}
<img src="{% static 'babybuddy/img/core/child-placeholder.png' %}" width="40" height="40" class="img-fluid rounded-circle" />
{% endif %}
</td>
<th scope="row">
<a href="{% url 'core:child' child.slug %}">{{ child.first_name }}</a>

View File

@ -2,8 +2,8 @@
<a href="{% url 'core:child' current_child.slug %}">{{ current_child }}</a>
{% if children.count > 0 %}
<a href="#" class="ml-1 pl-1 pr-1 dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">{% trans 'Switch child' %}</span>
<a href="#" class="ms-1 ps-1 pe-1 dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="visually-hidden">{% trans 'Switch child' %}</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M3.626 6.832A.5.5 0 0 1 4 6h8a.5.5 0 0 1 .374.832l-4 4.5a.5.5 0 0 1-.748 0l-4-4.5z"></path>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2z"></path>
@ -14,7 +14,7 @@
{% for child in children %}
<a class="dropdown-item d-flex align-items-center" href="{% url target_url child.slug %}">
{% include "core/child_thumbnail.html" %}
<span class="text-wrap ml-2">{{ child }}</span>
<span class="text-wrap ms-2">{{ child }}</span>
</a>
{% endfor %}
</div>

View File

@ -1,10 +1,7 @@
{% load imagekit static %}
{% if widget.wrap_label %}
<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %} class="btn btn-outline-light btn-no-hover">
{% endif %}
{% include "django/forms/widgets/input.html" %}
{% if widget.wrap_label %}
<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %} class="btn btn-outline-light btn-no-hover">
{% include "core/child_thumbnail.html" with child=widget %}
{{ widget.label }}</label>
{% endif %}
{{ widget.label }}
</label>

View File

@ -28,11 +28,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_time'), {
format: '{% datetimepicker_format %}'
});
</script>
{% endblock %}

View File

@ -18,8 +18,8 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Actions" %}</th>
<th>{% trans "Time" %}</th>
@ -34,8 +34,7 @@
</thead>
<tbody>
{% for change in object_list %}
{% cycle "odd" "even" as row_class silent %}
<tr class="{{ row_class }}">
<tr>
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="{% trans "Actions" %}">
@ -69,7 +68,7 @@
</tr>
{% if change.notes %}
<tr class="{{ row_class }} row-details">
<td colspan="7"><i class="icon-note mr-2" aria-hidden="true"></i>{{ change.notes }}</td>
<td colspan="7"><i class="icon-note me-2" aria-hidden="true"></i>{{ change.notes }}</td>
</tr>
{% endif %}
{% empty %}

View File

@ -28,26 +28,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), {
format: '{% datetimepicker_format %}'
});
BabyBuddy.DatetimePicker.init($('#datetimepicker_end'), {
format: '{% datetimepicker_format %}'
});
$('#id_type').change(function() {
var feed_type=$('#id_type').val();
if (feed_type === 'formula' || feed_type === 'fortified breast milk') {
$('#id_method').val('bottle');
}
});
$('#id_method').change(function() {
var method = $('#id_method').val();
if (['left breast', 'right breast', 'both breasts'].includes(method)) {
$('#id_type').val('breast milk');
}
});
</script>
{% endblock %}

View File

@ -18,8 +18,8 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Actions" %}</th>
<th>{% trans "Date" %}</th>
@ -36,8 +36,7 @@
</thead>
<tbody>
{% for feeding in object_list %}
{% cycle "odd" "even" as row_class silent %}
<tr class="{{ row_class }}">
<tr>
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="{% trans "Actions" %}">
@ -73,7 +72,7 @@
</tr>
{% if feeding.notes %}
<tr class="{{ row_class }} row-details">
<td colspan="8"><i class="icon-note mr-2" aria-hidden="true"></i>{{ feeding.notes }}</td>
<td colspan="8"><i class="icon-note me-2" aria-hidden="true"></i>{{ feeding.notes }}</td>
</tr>
{% endif %}
{% empty %}

View File

@ -28,12 +28,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_date'), {
format: 'L',
extraFormats: ['YYYY-MM-DD']
});
</script>
{% endblock %}

View File

@ -18,8 +18,8 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Actions" %}</th>
<th>{% trans "Date" %}</th>
@ -32,8 +32,7 @@
</thead>
<tbody>
{% for head_circumference in object_list %}
{% cycle "odd" "even" as row_class silent %}
<tr class="{{ row_class }}">
<tr>
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="{% trans "Actions" %}">
@ -62,7 +61,7 @@
</tr>
{% if head_circumference.notes %}
<tr class="{{ row_class }} row-details">
<td colspan="5"><i class="icon-note mr-2" aria-hidden="true"></i>{{ head_circumference.notes }}</td>
<td colspan="5"><i class="icon-note me-2" aria-hidden="true"></i>{{ head_circumference.notes }}</td>
</tr>
{% endif %}
{% empty %}

View File

@ -28,12 +28,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_date'), {
format: 'L',
extraFormats: ['YYYY-MM-DD']
});
</script>
{% endblock %}

View File

@ -18,8 +18,8 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Actions" %}</th>
<th>{% trans "Date" %}</th>
@ -32,8 +32,7 @@
</thead>
<tbody>
{% for height in object_list %}
{% cycle "odd" "even" as row_class silent %}
<tr class="{{ row_class }}">
<tr>
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="{% trans "Actions" %}">
@ -62,7 +61,7 @@
</tr>
{% if height.notes %}
<tr class="{{ row_class }} row-details">
<td colspan="5"><i class="icon-note mr-2" aria-hidden="true"></i>{{ height.notes }}</td>
<td colspan="5"><i class="icon-note me-2" aria-hidden="true"></i>{{ height.notes }}</td>
</tr>
{% endif %}
{% empty %}

View File

@ -28,11 +28,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_time'), {
format: '{% datetimepicker_format %}'
});
</script>
{% endblock %}

View File

@ -18,8 +18,8 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Actions" %}</th>
<th>{% trans "Time" %}</th>

View File

@ -28,11 +28,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_time'), {
format: '{% datetimepicker_format %}'
});
</script>
{% endblock %}

View File

@ -18,8 +18,8 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Actions" %}</th>
<th>{% trans "Time" %}</th>
@ -32,8 +32,7 @@
</thead>
<tbody>
{% for pumping in object_list %}
{% cycle "odd" "even" as row_class silent %}
<tr class="{{ row_class }}">
<tr>
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="{% trans "Actions" %}">
@ -62,7 +61,7 @@
</tr>
{% if pumping.notes %}
<tr class="{{ row_class }} row-details">
<td colspan="4"><i class="icon-note mr-2" aria-hidden="true"></i>{{ pumping.notes }}</td>
<td colspan="4"><i class="icon-note me-2" aria-hidden="true"></i>{{ pumping.notes }}</td>
</tr>
{% endif %}
{% empty %}

View File

@ -20,13 +20,13 @@
{% for child in children %}
<button class="dropdown-item d-flex align-items-center" type="submit" name="child" value="{{ child.pk }}">
{% include "core/child_thumbnail.html" %}
<span class="text-wrap ml-2">{{ child }}</span>
<span class="text-wrap ms-2">{{ child }}</span>
</button>
{% endfor %}
</div>
</div>
{% else %}
<label class="sr-only">{% trans "Quick Start Timer" %}</label>
<label class="visually-hidden">{% trans "Quick Start Timer" %}</label>
<button class="btn m-0 p-0 text-success">
<i class="icon-2x icon-timer" aria-hidden="true"></i>
</button>

View File

@ -28,15 +28,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), {
defaultDate: false,
format: '{% datetimepicker_format %}'
});
BabyBuddy.DatetimePicker.init($('#datetimepicker_end'), {
format: '{% datetimepicker_format %}'
});
</script>
{% endblock %}

View File

@ -18,8 +18,8 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Actions" %}</th>
<th>{% trans "Start" %}</th>
@ -34,8 +34,7 @@
</thead>
<tbody>
{% for sleep in object_list %}
{% cycle "odd" "even" as row_class silent %}
<tr class="{{ row_class }}">
<tr>
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="{% trans "Actions" %}">
@ -66,7 +65,7 @@
</tr>
{% if sleep.notes %}
<tr class="{{ row_class }} row-details">
<td colspan="7"><i class="icon-note mr-2" aria-hidden="true"></i>{{ sleep.notes }}</td>
<td colspan="7"><i class="icon-note me-2" aria-hidden="true"></i>{{ sleep.notes }}</td>
</tr>
{% endif %}
{% empty %}

View File

@ -28,11 +28,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_time'), {
format: '{% datetimepicker_format %}'
});
</script>
{% endblock %}

View File

@ -18,8 +18,8 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Actions" %}</th>
<th>{% trans "Time" %}</th>
@ -32,8 +32,7 @@
</thead>
<tbody>
{% for temperature in object_list %}
{% cycle "odd" "even" as row_class silent %}
<tr class="{{ row_class }}">
<tr>
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="{% trans "Actions" %}">
@ -62,7 +61,7 @@
</tr>
{% if temperature.notes %}
<tr class="{{ row_class }} row-details">
<td colspan="5"><i class="icon-note mr-2" aria-hidden="true"></i>{{ temperature.notes }}</td>
<td colspan="5"><i class="icon-note me-2" aria-hidden="true"></i>{{ temperature.notes }}</td>
</tr>
{% endif %}
{% empty %}

View File

@ -7,7 +7,7 @@
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'core:timer-list' %}">{% trans "Timers" %}</a></li>
<li class="breadcrumb-item font-weight-bold"><a href="{% url 'core:timer-detail' object.id %}">{{ object }}</a></li>
<li class="breadcrumb-item fw-bold"><a href="{% url 'core:timer-detail' object.id %}">{{ object }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Delete" %}</li>
{% endblock %}

View File

@ -1,26 +0,0 @@
{% extends 'babybuddy/page.html' %}
{% load humanize i18n widget_tweaks %}
{% block title %}
{% blocktrans %}Delete All Inactive Timers{% endblocktrans %}
{% endblock %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'core:timer-list' %}">{% trans "Timers" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Delete Inactive" %}</li>
{% endblock %}
{% block content %}
<form role="form" method="post">
{% csrf_token %}
<h1>
{% blocktrans trimmed with number=timer_count|apnumber|intcomma count counter=timer_count %}
Are you sure you want to delete {{ number }} inactive timer?
{% plural %}
Are you sure you want to delete {{ number }} inactive timers?
{% endblocktrans %}
</h1>
<input type="submit" value="{% trans "Delete" %}" class="btn btn-danger" />
<a href={% url "babybuddy:root-router" %} class="btn btn-default">{% trans "Cancel" %}</a>
</form>
{% endblock %}

View File

@ -6,13 +6,14 @@
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'core:timer-list' %}">{% trans "Timers" %}</a></li>
<li class="breadcrumb-item font-weight-bold">{{ object }}</li>
<li class="breadcrumb-item fw-bold">{{ object }}</li>
{% endblock %}
{% block content %}
<div class="jumbotron text-center">
<h1 {% if not object.active %}class="timer-stopped" {% endif %}id="timer-status">
<div class="p-5 mb-4 bg-dark rounded-3 text-center">
<div class="container-fluid py-1">
<h1 id="timer-status"
class="display-1">
<span class="timer-hours">{{ object.duration|hours }}</span>h
<span class="timer-minutes">{{ object.duration|minutes }}</span>m
<span class="timer-seconds">{{ object.duration|seconds }}</span>s
@ -26,9 +27,6 @@
<p class="lead text-secondary">
{% trans "Started" %} {{ object.start }}
{% if not object.active %}
/ {% trans "Stopped" %} {{ object.end }}
{% endif %}
</p>
<p class="text-muted">
{% blocktrans trimmed with user=object.user_username %}
@ -36,8 +34,9 @@
{% endblocktrans %}
</p>
<div class="d-grid gap-4 mb-4">
{% if perms.core.add_feeding %}
<a class="btn btn-success btn-lg btn-block mb-3"
<a class="btn btn-success btn-lg"
href="{% instance_add_url 'core:feeding-add' %}"
role="button"><i class="icon-feeding" aria-hidden="true"></i>
{% trans "Feeding" %}
@ -45,7 +44,7 @@
{% endif %}
{% if perms.core.add_sleep %}
<a class="btn btn-success btn-lg btn-block mb-3"
<a class="btn btn-success btn-lg"
href="{% instance_add_url 'core:sleep-add' %}"
role="button"><i class="icon-sleep" aria-hidden="true"></i>
{% trans "Sleep" %}
@ -53,12 +52,13 @@
{% endif %}
{% if perms.core.add_tummytime %}
<a class="btn btn-success btn-lg btn-block mb-3"
<a class="btn btn-success btn-lg"
href="{% instance_add_url 'core:tummytime-add' %}"
role="button"><i class="icon-tummytime" aria-hidden="true"></i>
{% trans "Tummy Time" %}
</a>
{% endif %}
</div>
<div class="center-block" role="group" aria-label="{% trans "Timer actions" %}">
{% if perms.core.delete_timer %}
@ -74,26 +74,17 @@
<form action="{% url 'core:timer-restart' timer.id %}" role="form" method="post" class="d-inline">
{% csrf_token %}
<label class="sr-only">{% trans "Restart timer" %}</label>
<label class="visually-hidden">{% trans "Restart timer" %}</label>
<button type="submit" class="btn btn-lg btn-secondary"><i class="icon-refresh" aria-hidden="true"></i></button>
</form>
{% if object.active %}
<form action="{% url 'core:timer-stop' timer.id %}" role="form" method="post" class="d-inline">
{% csrf_token %}
<label class="sr-only">{% trans "Delete timer" %}</label>
<button type="submit" class="btn btn-lg btn-warning"><i class="icon-stop" aria-hidden="true"></i></button>
</form>
{% endif %}
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block javascript %}
{% if object.active %}
<script type="application/javascript">
BabyBuddy.Timer.run({{ timer.id }}, 'timer-status');
</script>
{% endif %}
{% endblock %}

View File

@ -6,7 +6,7 @@
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'core:timer-list' %}">{% trans "Timers" %}</a></li>
{% if object %}
<li class="breadcrumb-item font-weight-bold"><a href="{% url 'core:timer-detail' object.id %}">{{ object }}</a></li>
<li class="breadcrumb-item fw-bold"><a href="{% url 'core:timer-detail' object.id %}">{{ object }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Update" %}</li>
{% else %}
<li class="breadcrumb-item active" aria-current="page">{% trans "Start" %}</li>
@ -23,11 +23,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), {
format: '{% datetimepicker_format 'L LTS' %}'
});
</script>
{% endblock %}

View File

@ -18,17 +18,14 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Start" %}</th>
<th>{% trans "Name" %}</th>
{% if not unique_child %}
<th>{% trans "Child" %}</th>
{% endif %}
<th>{% trans "Duration" %}</th>
<th>{% trans "End" %}</th>
<th>{% trans "Active" %}</th>
<th>{% trans "User" %}</th>
</tr>
</thead>
@ -44,13 +41,6 @@
{% endif %}
</td>
{% endif %}
<td>{{ timer.duration|duration_string }}</td>
<td>
{% if timer.end %}
{{ timer.end|datetime_short }}
{% endif %}
</td>
<td>{{ timer.active|bool_icon }}</td>
<td>{{ timer.user_username }}</td>
</tr>
{% empty %}
@ -62,11 +52,4 @@
</table>
</div>
{% include 'babybuddy/paginator.html' %}
{% if object_list and perms.core.delete_timer %}
<a href="{% url 'core:timer-delete-inactive' %}" class="btn btn-sm btn-danger">
<i class="icon-delete" aria-hidden="true"></i> {% trans "Delete Inactive Timers" %}
</a>
{% endif %}
{% endblock %}

View File

@ -4,7 +4,7 @@
<a id="nav-timer-menu-link"
class="nav-link dropdown-toggle"
href="#"
data-toggle="dropdown"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"><i class="icon-timer" aria-hidden="true"></i>
{% trans "Timers" %}
@ -29,7 +29,7 @@
{% for child in children %}
<button class="dropdown-item d-flex align-items-center" type="submit" name="child" value="{{ child.pk }}">
{% include "core/child_thumbnail.html" %}
<span class="text-wrap ml-2">{{ child }}</span>
<span class="text-wrap ms-2">{{ child }}</span>
</button>
{% endfor %}
{% else %}
@ -41,7 +41,7 @@
{% endif %}
{% if timers %}
<div class="dropdown-divider"></div>
<h6 class="dropdown-header">{% trans "Active Timers" %}</h6>
<h6 class="dropdown-header">{% trans "Timers" %}</h6>
{% for timer in timers %}
<a class="dropdown-item" href="{% url 'core:timer-detail' timer.id %}">
{{ timer.title_with_child }}

View File

@ -28,15 +28,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_start'), {
defaultDate: false,
format: '{% datetimepicker_format 'L LTS' %}'
});
BabyBuddy.DatetimePicker.init($('#datetimepicker_end'), {
format: '{% datetimepicker_format 'L LTS' %}'
});
</script>
{% endblock %}

View File

@ -17,8 +17,8 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Actions" %}</th>
<th>{% trans "Start" %}</th>

View File

@ -28,12 +28,3 @@
{% endif %}
{% include 'babybuddy/form.html' %}
{% endblock %}
{% block javascript %}
<script type="text/javascript">
BabyBuddy.DatetimePicker.init($('#datetimepicker_date'), {
format: 'L',
extraFormats: ['YYYY-MM-DD']
});
</script>
{% endblock %}

View File

@ -18,8 +18,8 @@
</h1>
{% include 'babybuddy/filter.html' %}
<div class="table-responsive">
<table class="table table-instances table-striped table-hover">
<thead class="thead-inverse">
<table class="table table-instances table-borderless table-striped table-hover align-middle">
<thead>
<tr>
<th>{% trans "Actions" %}</th>
<th>{% trans "Date" %}</th>
@ -32,8 +32,7 @@
</thead>
<tbody>
{% for weight in object_list %}
{% cycle "odd" "even" as row_class silent %}
<tr class="{{ row_class }}">
<tr>
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="{% trans "Actions" %}">
@ -62,7 +61,7 @@
</tr>
{% if weight.notes %}
<tr class="{{ row_class }} row-details">
<td colspan="5"><i class="icon-note mr-2" aria-hidden="true"></i>{{ weight.notes }}</td>
<td colspan="5"><i class="icon-note me-2" aria-hidden="true"></i>{{ weight.notes }}</td>
</tr>
{% endif %}
{% empty %}

View File

@ -5,39 +5,31 @@
{{ k }}="{{ v }}"
{% endfor %}>
{% csrf_token %}
<span class="prototype-tag btn badge badge-pill cursor-pointer mr-1" style="display: none;">
<span class="prototype-tag btn badge badge-pill cursor-pointer me-1" style="display: none;">
UNINITIALIZED PROTOTYPE
<span class="add-remove-icon pl-1 pr-1">+ or -</span>
<span class="add-remove-icon ps-1 pe-1">+ or -</span>
</span>
<div class="current_tags" style="min-height: 2em;">
{% for t in widget.value %}
<span data-value="{{ t.name }}" data-color="{{ t.color }}" class="tag btn badge badge-pill cursor-pointer mr-1" style="background-color: {{ t.color }};">
<span data-value="{{ t.name }}" data-color="{{ t.color }}" class="tag btn badge badge-pill cursor-pointer me-1" style="background-color: {{ t.color }};">
{{ t.name }}
<span class="add-remove-icon pl-1 pr-1">-</span>
<span class="add-remove-icon ps-1 pe-1">-</span>
</span>
{% endfor %}
</div>
<div class="new-tags">
<div class="create-tag-inputs input-group">
<input class="form-control" type="text" name="" placeholder="{% trans "Tag name" %}">
<div class="input-group-append">
<button class="btn btn-outline-primary bg-dark btn-add-new-tag" type="button">{% trans "Add" %}</button>
</div>
<button id="add-tag" class="btn btn-outline-primary bg-dark" type="button">{% trans "Add" %}</button>
</div>
{% if widget.tag_suggestions.quick %}
<span>{% trans "Recently used:" %}</span>
{% for t in widget.tag_suggestions.quick %}
<span data-value="{{ t.name }}" data-color="{{ t.color }}" class="tag btn badge badge-pill cursor-pointer mr-1" style="background-color: {{ t.color }};">
<span data-value="{{ t.name }}" data-color="{{ t.color }}" class="tag btn badge badge-pill cursor-pointer me-1" style="background-color: {{ t.color }};">
{{ t.name }}
<span class="add-remove-icon pl-1 pr-1">+</span>
<span class="add-remove-icon ps-1 pe-1">+</span>
</span>
{% endfor %}
{% else %}
<style>
.help-block{
display: none;
}
</style>
{%endif%}
</div>
<input
@ -51,7 +43,7 @@
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{% trans "Error" context "Error modal" %}</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
@ -62,7 +54,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">
{% trans "Close" context "Error modal" %}
</button>
</div>

View File

@ -4,14 +4,14 @@
{% if date_previous %}
<a class="btn btn-sm btn-default" href="?date={{ date_previous|date:"Y-m-d" }}" aria-label="{% trans "Previous" %}">
<i class="icon-2x icon-angle-circled-left" aria-hidden="true"></i>
<span class="sr-only">{% trans "Previous" %}</span>
<span class="visually-hidden">{% trans "Previous" %}</span>
</a>
{% endif %}
{{ date|date }}
{% if date_next %}
<a class="btn btn-sm btn-default" href="?date={{ date_next|date:"Y-m-d" }}" aria-label="{% trans "Next" %}">
<i class="icon-2x icon-angle-circled-right" aria-hidden="true"></i>
<span class="sr-only">{% trans "Next" %}</span>
<span class="visually-hidden">{% trans "Next" %}</span>
</a>
{% endif %}
</h3>
@ -22,7 +22,7 @@
<div class="timeline-badge {% if object.type == "start" %}bg-success{% elif object.type == "end" %}bg-danger{% else %}bg-info{% endif %}">
<i class="icon-{{ object.model_name }}"></i>
</div>
<div class="card text-right">
<div class="card text-end">
<div class="card-body">
{{ object.event }}
{% for detail in object.details %}
@ -70,14 +70,14 @@
{% if date_previous %}
<a class="btn btn-sm btn-default" href="?date={{ date_previous|date:"Y-m-d" }}" aria-label="{% trans "Previous" %}">
<i class="icon-2x icon-angle-circled-left" aria-hidden="true"></i>
<span class="sr-only">{% trans "Previous" %}</span>
<span class="visually-hidden">{% trans "Previous" %}</span>
</a>
{% endif %}
{{ date|date }}
{% if date_next %}
<a class="btn btn-sm btn-default" href="?date={{ date_next|date:"Y-m-d" }}" aria-label="{% trans "Next" %}">
<i class="icon-2x icon-angle-circled-right" aria-hidden="true"></i>
<span class="sr-only">{% trans "Next" %}</span>
<span class="visually-hidden">{% trans "Next" %}</span>
</a>
{% endif %}
</h3>

View File

@ -4,7 +4,7 @@
{% block title %}{% trans "Timeline" %}{% endblock %}
{% block breadcrumbs %}
<li class="breadcrumb-item font-weight-bold">{% trans "Timeline" %}</li>
<li class="breadcrumb-item fw-bold">{% trans "Timeline" %}</li>
{% endblock %}
{% block content %}

View File

@ -7,40 +7,6 @@ from django.utils.translation import gettext_lazy as _
register = template.Library()
@register.simple_tag(takes_context=True)
def datetimepicker_format(context, format_string="L LT"):
"""
Return a datetime format string for momentjs, with support for 24 hour time
override setting.
:param context: caller context data
:param format_string: the default format string (locale based)
:return: the format string to use, as 24 hour time if configured.
"""
try:
user = context["request"].user
if hasattr(user, "settings") and user.settings.language:
language = user.settings.language
else:
language = settings.LANGUAGE_CODE
except KeyError:
language = None
if settings.USE_24_HOUR_TIME_FORMAT:
if format_string == "L LT":
format_string = "L HH:mm"
elif format_string == "L LTS":
format_string = "L HH:mm:ss"
elif language and language == "en-GB":
# Force 12-hour format if 24 hour format is not configured for en-GB
# (Django default is 12H, momentjs default is 24H).
if format_string == "L LT":
format_string = "L h:mm a"
elif format_string == "L LTS":
format_string = "L h:mm:ss a"
return format_string
@register.filter()
def datetime_short(date):
"""

View File

@ -8,15 +8,14 @@ register = template.Library()
@register.inclusion_tag("core/timer_nav.html", takes_context=True)
def timer_nav(context, active=True):
def timer_nav(context):
"""
Get a list of active Timer instances to include in the nav menu.
Get a list of Timer instances to include in the nav menu.
:param context: Django's context data.
:param active: the state of Timers to filter.
:returns: a dictionary with timers data.
"""
request = context["request"] or None
timers = Timer.objects.filter(active=active)
timers = Timer.objects.filter()
children = Child.objects.all()
perms = context["perms"] or None
# The 'next' parameter is currently not used.

View File

@ -122,32 +122,13 @@ class InitialValuesTestCase(FormsTestCaseBase):
self.assertEqual(page.context["form"].initial["type"], f_three.type)
self.assertEqual(page.context["form"].initial["method"], f_three.method)
def test_timer_set(self):
def test_timer_form_field_set(self):
self.timer.stop()
page = self.c.get("/sleep/add/")
self.assertTrue("start" not in page.context["form"].initial)
self.assertTrue("end" not in page.context["form"].initial)
page = self.c.get("/sleep/add/?timer={}".format(self.timer.id))
self.assertEqual(page.context["form"].initial["start"], self.timer.start)
self.assertEqual(page.context["form"].initial["end"], self.timer.end)
def test_timer_stop_on_save(self):
end = timezone.localtime()
params = {
"child": self.child.id,
"start": self.localtime_string(self.timer.start),
"end": self.localtime_string(end),
}
page = self.c.post(
"/sleep/add/?timer={}".format(self.timer.id), params, follow=True
)
self.assertEqual(page.status_code, 200)
self.timer.refresh_from_db()
self.assertFalse(self.timer.active)
self.assertEqual(self.localtime_string(self.timer.end), params["end"])
class BMIFormsTestCase(FormsTestCaseBase):
@classmethod
@ -783,29 +764,6 @@ class TimerFormsTestCase(FormsTestCaseBase):
self.timer.refresh_from_db()
self.assertEqual(self.localtime_string(self.timer.start), params["start"])
def test_edit_stopped(self):
self.timer.stop()
params = {
"name": "Edit stopped timer",
"start": self.localtime_string(self.timer.start),
"end": self.localtime_string(self.timer.end),
}
page = self.c.post(
"/timers/{}/edit/".format(self.timer.id), params, follow=True
)
self.assertEqual(page.status_code, 200)
def test_delete_inactive(self):
models.Timer.objects.create(user=self.user)
self.assertEqual(models.Timer.objects.count(), 2)
self.timer.stop()
page = self.c.post("/timers/delete-inactive/", follow=True)
self.assertEqual(page.status_code, 200)
messages = list(page.context["messages"])
self.assertEqual(len(messages), 1)
self.assertEqual(str(messages[0]), "All inactive timers deleted.")
self.assertEqual(models.Timer.objects.count(), 1)
class ValidationsTestCase(FormsTestCaseBase):
def test_validate_date(self):

View File

@ -270,11 +270,9 @@ class TimerTestCase(TestCase):
)
self.user = get_user_model().objects.first()
self.named = models.Timer.objects.create(
name="Named", end=timezone.localtime(), user=self.user, child=child
)
self.unnamed = models.Timer.objects.create(
end=timezone.localtime(), user=self.user
name="Named", user=self.user, child=child
)
self.unnamed = models.Timer.objects.create(user=self.user)
def test_timer_create(self):
self.assertEqual(self.named, models.Timer.objects.get(name="Named"))
@ -302,19 +300,7 @@ class TimerTestCase(TestCase):
def test_timer_restart(self):
self.named.restart()
self.assertIsNone(self.named.end)
self.assertIsNone(self.named.duration)
self.assertTrue(self.named.active)
def test_timer_stop(self):
stop_time = timezone.localtime()
self.unnamed.stop(end=stop_time)
self.assertEqual(self.unnamed.end, stop_time)
self.assertEqual(
self.unnamed.duration.seconds,
(self.unnamed.end - self.unnamed.start).seconds,
)
self.assertFalse(self.unnamed.active)
self.assertGreaterEqual(timezone.localtime(), self.named.start)
def test_timer_duration(self):
timer = models.Timer.objects.create(user=get_user_model().objects.first())
@ -322,9 +308,9 @@ class TimerTestCase(TestCase):
timer.save()
timer.refresh_from_db()
self.assertEqual(timer.duration.seconds, timezone.timedelta(minutes=30).seconds)
timer.stop()
self.assertEqual(timer.duration.seconds, timezone.timedelta(minutes=30).seconds)
self.assertEqual(
timer.duration().seconds, timezone.timedelta(minutes=30).seconds
)
class TummyTimeTestCase(TestCase):

Some files were not shown because too many files have changed in this diff Show More