mirror of https://github.com/snachodog/mybuddy.git
commit
33005b930e
1
Pipfile
1
Pipfile
|
@ -22,6 +22,7 @@ python-dotenv = "*"
|
||||||
pyyaml = "*"
|
pyyaml = "*"
|
||||||
uritemplate = "*"
|
uritemplate = "*"
|
||||||
whitenoise = "*"
|
whitenoise = "*"
|
||||||
|
django-taggit = "==2.1.0"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
|
|
|
@ -6,6 +6,8 @@ from rest_framework.exceptions import ValidationError
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from taggit.serializers import TagListSerializerField, TaggitSerializer
|
||||||
|
|
||||||
from core import models
|
from core import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -125,10 +127,12 @@ class FeedingSerializer(CoreModelWithDurationSerializer):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class NoteSerializer(CoreModelSerializer):
|
class NoteSerializer(TaggitSerializer, CoreModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Note
|
model = models.Note
|
||||||
fields = ("id", "child", "note", "time")
|
fields = ("id", "child", "note", "time", "tags")
|
||||||
|
|
||||||
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class SleepSerializer(CoreModelWithDurationSerializer):
|
class SleepSerializer(CoreModelWithDurationSerializer):
|
||||||
|
@ -196,3 +200,14 @@ class BMISerializer(CoreModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.BMI
|
model = models.BMI
|
||||||
fields = ("id", "child", "bmi", "date", "notes")
|
fields = ("id", "child", "bmi", "date", "notes")
|
||||||
|
|
||||||
|
|
||||||
|
class TagsSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.Tag
|
||||||
|
fields = ("slug", "name", "color", "last_used")
|
||||||
|
extra_kwargs = {
|
||||||
|
"slug": {"required": False, "read_only": True},
|
||||||
|
"color": {"required": False},
|
||||||
|
"last_used": {"required": False, "read_only": True},
|
||||||
|
}
|
||||||
|
|
56
api/tests.py
56
api/tests.py
|
@ -251,13 +251,14 @@ class NoteAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
response = self.client.get(self.endpoint)
|
response = self.client.get(self.endpoint)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(
|
self.assertDictEqual(
|
||||||
response.data["results"][0],
|
response.data["results"][0],
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"child": 1,
|
"child": 1,
|
||||||
"note": "Fake note.",
|
"note": "Fake note.",
|
||||||
"time": "2017-11-17T22:45:00-05:00",
|
"time": "2017-11-17T22:45:00-05:00",
|
||||||
|
"tags": [],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -551,3 +552,56 @@ class WeightAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, entry)
|
self.assertEqual(response.data, entry)
|
||||||
|
|
||||||
|
|
||||||
|
class TagsAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
|
||||||
|
endpoint = reverse("api:tag-list")
|
||||||
|
model = models.Tag
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
response = self.client.get(self.endpoint)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertDictEqual(
|
||||||
|
dict(response.data["results"][0]),
|
||||||
|
{
|
||||||
|
"name": "a name",
|
||||||
|
"slug": "a-name",
|
||||||
|
"color": "#FF0000",
|
||||||
|
"last_used": "2017-11-18T11:00:00-05:00",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_post(self):
|
||||||
|
data = {"name": "new tag", "color": "#123456"}
|
||||||
|
response = self.client.post(self.endpoint, data, format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
response = self.client.get(self.endpoint)
|
||||||
|
results = response.json()["results"]
|
||||||
|
results_by_name = {r["name"]: r for r in results}
|
||||||
|
|
||||||
|
tag_data = results_by_name["new tag"]
|
||||||
|
self.assertDictContainsSubset(data, tag_data)
|
||||||
|
self.assertEqual(tag_data["slug"], "new-tag")
|
||||||
|
self.assertTrue(tag_data["last_used"])
|
||||||
|
|
||||||
|
def test_patch(self):
|
||||||
|
endpoint = f"{self.endpoint}a-name/"
|
||||||
|
|
||||||
|
modified_data = {
|
||||||
|
"name": "A different name",
|
||||||
|
"color": "#567890",
|
||||||
|
}
|
||||||
|
response = self.client.patch(
|
||||||
|
endpoint,
|
||||||
|
modified_data,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertDictContainsSubset(modified_data, response.data)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
endpoint = f"{self.endpoint}a-name/"
|
||||||
|
response = self.client.delete(endpoint)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
response = self.client.delete(endpoint)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
|
@ -18,6 +18,7 @@ router.register(r"weight", views.WeightViewSet)
|
||||||
router.register(r"height", views.HeightViewSet)
|
router.register(r"height", views.HeightViewSet)
|
||||||
router.register(r"head-circumference", views.HeadCircumferenceViewSet)
|
router.register(r"head-circumference", views.HeadCircumferenceViewSet)
|
||||||
router.register(r"bmi", views.BMIViewSet)
|
router.register(r"bmi", views.BMIViewSet)
|
||||||
|
router.register(r"tags", views.TagsViewSet)
|
||||||
|
|
||||||
app_name = "api"
|
app_name = "api"
|
||||||
|
|
||||||
|
|
|
@ -92,3 +92,10 @@ class BMIViewSet(viewsets.ModelViewSet):
|
||||||
queryset = models.BMI.objects.all()
|
queryset = models.BMI.objects.all()
|
||||||
serializer_class = serializers.BMISerializer
|
serializer_class = serializers.BMISerializer
|
||||||
filterset_fields = ("child", "date")
|
filterset_fields = ("child", "date")
|
||||||
|
|
||||||
|
|
||||||
|
class TagsViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = models.Tag.objects.all()
|
||||||
|
serializer_class = serializers.TagsSerializer
|
||||||
|
lookup_field = "slug"
|
||||||
|
filterset_fields = ("last_used", "name")
|
||||||
|
|
|
@ -462,5 +462,16 @@
|
||||||
"date": "2017-11-18",
|
"date": "2017-11-18",
|
||||||
"notes": "before feed"
|
"notes": "before feed"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "core.tag",
|
||||||
|
"pk": 1,
|
||||||
|
"fields":
|
||||||
|
{
|
||||||
|
"name": "a name",
|
||||||
|
"slug": "a-name",
|
||||||
|
"color": "#FF0000",
|
||||||
|
"last_used": "2017-11-18T16:00:00Z"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 4.0.3 on 2022-03-05 11:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("babybuddy", "0020_update_language_en_to_en_us"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="settings",
|
||||||
|
name="language",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("zh-hans", "Chinese (simplified)"),
|
||||||
|
("nl", "Dutch"),
|
||||||
|
("en-US", "English (US)"),
|
||||||
|
("en-GB", "English (UK)"),
|
||||||
|
("fr", "French"),
|
||||||
|
("fi", "Finnish"),
|
||||||
|
("de", "German"),
|
||||||
|
("it", "Italian"),
|
||||||
|
("pl", "Polish"),
|
||||||
|
("pt", "Portuguese"),
|
||||||
|
("es", "Spanish"),
|
||||||
|
("sv", "Swedish"),
|
||||||
|
("tr", "Turkish"),
|
||||||
|
],
|
||||||
|
default="en-US",
|
||||||
|
max_length=255,
|
||||||
|
verbose_name="Language",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,354 @@
|
||||||
|
(function() {
|
||||||
|
/**
|
||||||
|
* Parse a string as hexadecimal number
|
||||||
|
*/
|
||||||
|
function hexParse(x) {
|
||||||
|
return parseInt(x, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the contrasting color for any hex color
|
||||||
|
*
|
||||||
|
* Sourced from: https://vanillajstoolkit.com/helpers/getcontrast/
|
||||||
|
* - Modified with slightly softer colors
|
||||||
|
* (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com
|
||||||
|
* Derived from work by Brian Suda, https://24ways.org/2010/calculating-color-contrast/
|
||||||
|
* @param {String} A hexcolor value
|
||||||
|
* @return {String} The contrasting color (black or white)
|
||||||
|
*/
|
||||||
|
function computeComplementaryColor(hexcolor) {
|
||||||
|
|
||||||
|
// If a leading # is provided, remove it
|
||||||
|
if (hexcolor.slice(0, 1) === '#') {
|
||||||
|
hexcolor = hexcolor.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a three-character hexcode, make six-character
|
||||||
|
if (hexcolor.length === 3) {
|
||||||
|
hexcolor = hexcolor.split('').map(function (hex) {
|
||||||
|
return hex + hex;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to RGB value
|
||||||
|
let r = parseInt(hexcolor.substr(0,2),16);
|
||||||
|
let g = parseInt(hexcolor.substr(2,2),16);
|
||||||
|
let b = parseInt(hexcolor.substr(4,2),16);
|
||||||
|
|
||||||
|
// Get YIQ ratio
|
||||||
|
let yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
||||||
|
|
||||||
|
// Check contrast
|
||||||
|
return (yiq >= 128) ? '#101010' : '#EFEFEF';
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF token should always be present because it is auto-included with
|
||||||
|
// every tag-editor widget
|
||||||
|
const CSRF_TOKEN = document.querySelector('input[name="csrfmiddlewaretoken"]').value;
|
||||||
|
|
||||||
|
function doReq(method, uri, data, success, fail) {
|
||||||
|
// TODO: prefer jQuery based requests for now
|
||||||
|
|
||||||
|
const req = new XMLHttpRequest();
|
||||||
|
req.addEventListener('load', () => {
|
||||||
|
if ((req.status >= 200) && (req.status < 300)) {
|
||||||
|
success(req.responseText, req);
|
||||||
|
} else {
|
||||||
|
fail(req.responseText, req);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const name of ["error", "timeout", "abort"]) {
|
||||||
|
req.addEventListener(name, () => {
|
||||||
|
fail(req.responseText, req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
req.timeout = 20000;
|
||||||
|
|
||||||
|
req.open(method, uri);
|
||||||
|
req.setRequestHeader("Content-Type", "application/json");
|
||||||
|
req.setRequestHeader("Accept", "application/json");
|
||||||
|
req.setRequestHeader("X-CSRFTOKEN", CSRF_TOKEN);
|
||||||
|
req.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class allowing generic operations on the tag lists, like:
|
||||||
|
*
|
||||||
|
* - Adding tags to a tag list
|
||||||
|
* - Updating or creating new tags with a set name and color
|
||||||
|
* - Controlling the error modal
|
||||||
|
*/
|
||||||
|
class TaggingBase {
|
||||||
|
constructor(widget) {
|
||||||
|
this.prototype = widget.querySelector('.prototype-tag');
|
||||||
|
this.listeners = [];
|
||||||
|
|
||||||
|
this.modalElement = widget.querySelector('.tag-editor-error-modal');
|
||||||
|
this.modalBodyNode = this.modalElement.querySelector('.modal-body');
|
||||||
|
|
||||||
|
// Clean whitespace text nodes between spans
|
||||||
|
for (const n of this.modalBodyNode.childNodes) {
|
||||||
|
if (n.nodeType === Node.TEXT_NODE) {
|
||||||
|
this.modalBodyNode.removeChild(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal(msg) {
|
||||||
|
const selectedMessage = this.modalBodyNode.querySelector(`span[data-message='${msg}']`);
|
||||||
|
if (!selectedMessage) {
|
||||||
|
selectedMessage = this.modalBodyNode.childNodes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const n of this.modalBodyNode.childNodes) {
|
||||||
|
n.classList.add('d-none');
|
||||||
|
}
|
||||||
|
selectedMessage.classList.remove('d-none');
|
||||||
|
|
||||||
|
jQuery(this.modalElement).modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
addTagListUpdatedListener(c) {
|
||||||
|
this.listeners.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
callTagListUpdatedListeners() {
|
||||||
|
for (const l of this.listeners) {
|
||||||
|
l();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTag(tag, name, color, actionSymbol) {
|
||||||
|
const actionTextNode = tag.querySelector('.add-remove-icon').childNodes[0];
|
||||||
|
|
||||||
|
name = name || tag.getAttribute("data-value");
|
||||||
|
color = color || tag.getAttribute("data-color");
|
||||||
|
actionSymbol = actionSymbol || actionTextNode.textContent;
|
||||||
|
|
||||||
|
tag.childNodes[0].textContent = name;
|
||||||
|
tag.setAttribute("data-value", name);
|
||||||
|
tag.setAttribute("data-color", color);
|
||||||
|
|
||||||
|
const textColor = computeComplementaryColor(color);
|
||||||
|
tag.setAttribute('style', `background-color: ${color}; color: ${textColor};`);
|
||||||
|
actionTextNode.textContent = actionSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewTag(name, color, actionSymbol) {
|
||||||
|
const tag = this.prototype.cloneNode(true);
|
||||||
|
tag.classList.remove("prototype-tag");
|
||||||
|
tag.classList.add("tag");
|
||||||
|
this.updateTag(tag, name, color, actionSymbol);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertTag(list, tag) {
|
||||||
|
list.appendChild(tag);
|
||||||
|
this.callTagListUpdatedListeners();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the edit field allowing to dynamically create new tags.
|
||||||
|
*
|
||||||
|
* Handles user inputs for the editor. Calls the 'onInsertNewTag' callback
|
||||||
|
* when the creation of a new tag has been requested. All backend handling
|
||||||
|
* like guareteening that the requested tag exists is handled by this class,
|
||||||
|
* the only task left is to add the new tag to the tags-list when
|
||||||
|
* 'onInsertNewTag' is called.
|
||||||
|
*/
|
||||||
|
class AddNewTagControl {
|
||||||
|
/**
|
||||||
|
* @param widget
|
||||||
|
* The root DOM element of the widget
|
||||||
|
* @param taggingBase
|
||||||
|
* Reference to a common TaggingBase class to be used by this widget
|
||||||
|
* @param onInsertNewTag
|
||||||
|
* Callback that is called when a new tag should be added to the
|
||||||
|
* tags widget.
|
||||||
|
*/
|
||||||
|
constructor(widget, taggingBase, onInsertNewTag) {
|
||||||
|
this.widget = widget;
|
||||||
|
this.taggingBase = taggingBase;
|
||||||
|
|
||||||
|
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.addTagInput.value = "";
|
||||||
|
|
||||||
|
this.onInsertNewTag = onInsertNewTag;
|
||||||
|
|
||||||
|
this.addTagButton.addEventListener('click', () => this.onCreateTagClicked());
|
||||||
|
this.addTagInput.addEventListener('keydown', (e) => {
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
if (key === "enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.onCreateTagClicked();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback called when the the "Add" button of the add-tag input is
|
||||||
|
* clicked or enter is pressed in the editor.
|
||||||
|
*/
|
||||||
|
onCreateTagClicked() {
|
||||||
|
// TODO: Make promise based
|
||||||
|
|
||||||
|
const tagName = this.addTagInput.value.trim();
|
||||||
|
const uriTagName = encodeURIComponent(tagName);
|
||||||
|
|
||||||
|
const fail = (msg) => {
|
||||||
|
this.addTagInput.select();
|
||||||
|
this.taggingBase.showModal(msg || "generic");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!tagName) {
|
||||||
|
fail('invalid-tag-name');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addTag = (name, color) => {
|
||||||
|
const tag = this.taggingBase.createNewTag(name, color, "-");
|
||||||
|
this.addTagInput.value = "";
|
||||||
|
this.onInsertNewTag(tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = JSON.stringify({
|
||||||
|
'name': this.addTagInput.value
|
||||||
|
});
|
||||||
|
|
||||||
|
doReq("GET", `${this.apiTagsUrl}?name=${uriTagName}`, null,
|
||||||
|
(text) => {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
if (json.count) {
|
||||||
|
const tagJson = json.results[0];
|
||||||
|
addTag(tagJson.name, tagJson.color);
|
||||||
|
} else {
|
||||||
|
doReq("POST", this.apiTagsUrl, data,
|
||||||
|
(text) => {
|
||||||
|
const tagJson = JSON.parse(text);
|
||||||
|
addTag(tagJson.name, tagJson.color);
|
||||||
|
}, () => fail("tag-creation-failed")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, () => fail("tag-checking-failed")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaScript implementation for the tags editor.
|
||||||
|
*
|
||||||
|
* This class uses TaggingBase and AddNewTagControl to provide the custom
|
||||||
|
* tag editor controls. This mainly consists of updating the hidden
|
||||||
|
* input values with the current list of tags and adding/removing
|
||||||
|
* tags from the current-tags- or recently-used-lists.
|
||||||
|
*/
|
||||||
|
class TagsEditor {
|
||||||
|
/**
|
||||||
|
* @param tagEditorRoot
|
||||||
|
* The root DOM element of the widget.
|
||||||
|
*/
|
||||||
|
constructor(tagEditorRoot) {
|
||||||
|
this.widget = tagEditorRoot;
|
||||||
|
this.taggingBase = new TaggingBase(this.widget);
|
||||||
|
this.addTagControl = new AddNewTagControl(
|
||||||
|
this.widget, this.taggingBase, (t) => this.insertNewTag(t)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.currentTags = this.widget.querySelector('.current_tags');
|
||||||
|
this.newTags = this.widget.querySelector('.new-tags');
|
||||||
|
this.inputElement = this.widget.querySelector('input[type="hidden"]');
|
||||||
|
|
||||||
|
for (const tag of this.newTags.querySelectorAll(".tag")) {
|
||||||
|
this.configureAddTag(tag);
|
||||||
|
}
|
||||||
|
for (const tag of this.currentTags.querySelectorAll(".tag")) {
|
||||||
|
this.configureRemoveTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateInputList();
|
||||||
|
this.taggingBase.addTagListUpdatedListener(
|
||||||
|
() => this.updateInputList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new tag into the "current tag" list.
|
||||||
|
*
|
||||||
|
* Makes sure that no duplicates are present in the widget before adding
|
||||||
|
* the new tag. If a duplicate is found, the old tag is removed before
|
||||||
|
* the new one is added.
|
||||||
|
*/
|
||||||
|
insertNewTag(tag) {
|
||||||
|
const name = tag.getAttribute("data-value");
|
||||||
|
|
||||||
|
const oldTag = this.widget.querySelector(`span[data-value="${name}"]`);
|
||||||
|
if (oldTag) {
|
||||||
|
oldTag.parentNode.removeChild(oldTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.taggingBase.insertTag(this.currentTags, tag);
|
||||||
|
this.configureRemoveTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registeres a click-callback for a given node.
|
||||||
|
*
|
||||||
|
* The callback chain-calls another callback "onClicked" after
|
||||||
|
* moving the clicked tag from the old tag-list to a new tag list.
|
||||||
|
*/
|
||||||
|
registerNewCallback(tag, newParent, onClicked) {
|
||||||
|
function callback(event) {
|
||||||
|
tag.parentNode.removeChild(tag);
|
||||||
|
this.taggingBase.insertTag(newParent, tag);
|
||||||
|
|
||||||
|
tag.removeEventListener('click', callback);
|
||||||
|
onClicked(tag);
|
||||||
|
}
|
||||||
|
tag.addEventListener('click', callback.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the value of the hidden input element.
|
||||||
|
*
|
||||||
|
* Sets the value from the list of tags added to the currentTags
|
||||||
|
* DOM element.
|
||||||
|
*/
|
||||||
|
updateInputList() {
|
||||||
|
const names = [];
|
||||||
|
for (const tag of this.currentTags.querySelectorAll(".tag")) {
|
||||||
|
const name = tag.getAttribute("data-value");
|
||||||
|
names.push(`"${name}"`);
|
||||||
|
}
|
||||||
|
this.inputElement.value = names.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a tag-DOM element as a "add tag" button.
|
||||||
|
*/
|
||||||
|
configureAddTag(tag) {
|
||||||
|
this.taggingBase.updateTag(tag, null, null, "+");
|
||||||
|
this.registerNewCallback(tag, this.currentTags, () => this.configureRemoveTag(tag));
|
||||||
|
this.updateInputList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a tag-DOM element as a "remove tag" button.
|
||||||
|
*/
|
||||||
|
configureRemoveTag(tag) {
|
||||||
|
this.taggingBase.updateTag(tag, null, null, "-");
|
||||||
|
this.registerNewCallback(tag, this.newTags, () => this.configureAddTag(tag));
|
||||||
|
this.updateInputList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
for (const el of document.querySelectorAll('.babybuddy-tags-editor')) {
|
||||||
|
new TagsEditor(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
|
@ -52,3 +52,8 @@
|
||||||
.icon-2x {
|
.icon-2x {
|
||||||
font-size: 1.65em;
|
font-size: 1.65em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All modals
|
||||||
|
.modal-content {
|
||||||
|
color: theme-color('dark');
|
||||||
|
}
|
|
@ -1,9 +1,12 @@
|
||||||
{% load i18n widget_tweaks %}
|
{% load i18n widget_tweaks %}
|
||||||
|
|
||||||
|
{# Load any form-javascript files #}
|
||||||
|
{{ form.media.js }}
|
||||||
<div class="container-fluid pb-5">
|
<div class="container-fluid pb-5">
|
||||||
<form role="form" method="post" enctype="multipart/form-data">
|
<form role="form" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
|
{{ field.widget }}
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
{% include 'babybuddy/form_field.html' %}
|
{% include 'babybuddy/form_field.html' %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@ from import_export import fields, resources
|
||||||
from import_export.admin import ImportExportMixin, ExportActionMixin
|
from import_export.admin import ImportExportMixin, ExportActionMixin
|
||||||
|
|
||||||
from core import models
|
from core import models
|
||||||
|
from core.forms import TagAdminForm
|
||||||
|
|
||||||
|
|
||||||
class ImportExportResourceBase(resources.ModelResource):
|
class ImportExportResourceBase(resources.ModelResource):
|
||||||
|
@ -177,3 +178,17 @@ class WeightAdmin(ImportExportMixin, ExportActionMixin, admin.ModelAdmin):
|
||||||
"weight",
|
"weight",
|
||||||
)
|
)
|
||||||
resource_class = WeightImportExportResource
|
resource_class = WeightImportExportResource
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedItemInline(admin.StackedInline):
|
||||||
|
model = models.Tagged
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Tag)
|
||||||
|
class TagAdmin(admin.ModelAdmin):
|
||||||
|
form = TagAdminForm
|
||||||
|
inlines = [TaggedItemInline]
|
||||||
|
list_display = ["name", "slug", "color", "last_used"]
|
||||||
|
ordering = ["name", "slug"]
|
||||||
|
search_fields = ["name"]
|
||||||
|
prepopulated_fields = {"slug": ["name"]}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.forms import widgets
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from core import models
|
from core import models
|
||||||
|
from core.widgets import TagsEditor
|
||||||
|
|
||||||
|
|
||||||
def set_initial_values(kwargs, form_type):
|
def set_initial_values(kwargs, form_type):
|
||||||
|
@ -82,6 +84,7 @@ class CoreModelForm(forms.ModelForm):
|
||||||
timer.stop(instance.end)
|
timer.stop(instance.end)
|
||||||
if commit:
|
if commit:
|
||||||
instance.save()
|
instance.save()
|
||||||
|
self.save_m2m()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
@ -161,7 +164,7 @@ class FeedingForm(CoreModelForm):
|
||||||
class NoteForm(CoreModelForm):
|
class NoteForm(CoreModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Note
|
model = models.Note
|
||||||
fields = ["child", "note", "time"]
|
fields = ["child", "note", "time", "tags"]
|
||||||
widgets = {
|
widgets = {
|
||||||
"time": forms.DateTimeInput(
|
"time": forms.DateTimeInput(
|
||||||
attrs={
|
attrs={
|
||||||
|
@ -169,6 +172,7 @@ class NoteForm(CoreModelForm):
|
||||||
"data-target": "#datetimepicker_time",
|
"data-target": "#datetimepicker_time",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
"tags": TagsEditor(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -310,3 +314,8 @@ class BMIForm(CoreModelForm):
|
||||||
),
|
),
|
||||||
"notes": forms.Textarea(attrs={"rows": 5}),
|
"notes": forms.Textarea(attrs={"rows": 5}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TagAdminForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
widgets = {"color": widgets.TextInput(attrs={"type": "color"})}
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
# Generated by Django 4.0.3 on 2022-03-08 11:51
|
||||||
|
|
||||||
|
import core.models
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
("core", "0018_bmi_headcircumference_height"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Tag",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(max_length=100, unique=True, verbose_name="name"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"slug",
|
||||||
|
models.SlugField(max_length=100, unique=True, verbose_name="slug"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"color",
|
||||||
|
models.CharField(
|
||||||
|
default=core.models.random_color,
|
||||||
|
max_length=32,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.RegexValidator("^#[0-9a-fA-F]{6}$")
|
||||||
|
],
|
||||||
|
verbose_name="Color",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"last_used",
|
||||||
|
models.DateTimeField(
|
||||||
|
default=django.utils.timezone.now, verbose_name="Last used"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Tags",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Tagged",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"object_id",
|
||||||
|
models.IntegerField(db_index=True, verbose_name="object ID"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"content_type",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="%(app_label)s_%(class)s_tagged_items",
|
||||||
|
to="contenttypes.contenttype",
|
||||||
|
verbose_name="content type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tag",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="%(app_label)s_%(class)s_items",
|
||||||
|
to="core.tag",
|
||||||
|
verbose_name="Tag",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="note",
|
||||||
|
name="tags",
|
||||||
|
field=core.models.TaggableManager(
|
||||||
|
blank=True,
|
||||||
|
help_text="Click on the tags to add (+) or remove (-) tags or use the text editor to create new tags.",
|
||||||
|
through="core.Tagged",
|
||||||
|
to="core.Tag",
|
||||||
|
verbose_name="Tags",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
@ -9,6 +10,14 @@ from django.utils.text import slugify
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.text import format_lazy
|
from django.utils.text import format_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from taggit.managers import TaggableManager as TaggitTaggableManager
|
||||||
|
from taggit.models import TagBase, GenericTaggedItemBase
|
||||||
|
|
||||||
|
random.seed()
|
||||||
|
|
||||||
|
|
||||||
def validate_date(date, field_name):
|
def validate_date(date, field_name):
|
||||||
|
@ -71,6 +80,78 @@ def validate_time(time, field_name):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def random_color():
|
||||||
|
TAG_COLORS = [
|
||||||
|
"#ff0000",
|
||||||
|
"#00ff00",
|
||||||
|
"#0000ff",
|
||||||
|
"#ff00ff",
|
||||||
|
"#ffff00",
|
||||||
|
"#00ffff",
|
||||||
|
"#ff7f7f",
|
||||||
|
"#7fff7f",
|
||||||
|
"#7f7fff",
|
||||||
|
"#ff7fff",
|
||||||
|
"#ffff7f",
|
||||||
|
"#7fffff",
|
||||||
|
"#7f0000",
|
||||||
|
"#007f00",
|
||||||
|
"#00007f",
|
||||||
|
"#7f007f",
|
||||||
|
"#7f7f00",
|
||||||
|
"#007f7f",
|
||||||
|
]
|
||||||
|
return TAG_COLORS[random.randrange(0, len(TAG_COLORS))]
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(TagBase):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Tags")
|
||||||
|
|
||||||
|
color = models.CharField(
|
||||||
|
verbose_name=_("Color"),
|
||||||
|
max_length=32,
|
||||||
|
default=random_color,
|
||||||
|
validators=[RegexValidator(r"^#[0-9a-fA-F]{6}$")],
|
||||||
|
)
|
||||||
|
|
||||||
|
last_used = models.DateTimeField(
|
||||||
|
verbose_name=_("Last used"),
|
||||||
|
default=timezone.now,
|
||||||
|
blank=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Tagged(GenericTaggedItemBase):
|
||||||
|
tag = models.ForeignKey(
|
||||||
|
Tag,
|
||||||
|
verbose_name=_("Tag"),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="%(app_label)s_%(class)s_items",
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_base(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Update last_used of the used tag, whenever it is used in a
|
||||||
|
save-operation.
|
||||||
|
"""
|
||||||
|
self.tag.last_used = timezone.now()
|
||||||
|
self.tag.save()
|
||||||
|
return super().save_base(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TaggableManager(TaggitTaggableManager):
|
||||||
|
"""
|
||||||
|
Replace the default help_text with
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs["help_text"] = _(
|
||||||
|
"Click on the tags to add (+) or remove (-) tags or use the text editor to create new tags."
|
||||||
|
)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Child(models.Model):
|
class Child(models.Model):
|
||||||
model_name = "child"
|
model_name = "child"
|
||||||
first_name = models.CharField(max_length=255, verbose_name=_("First name"))
|
first_name = models.CharField(max_length=255, verbose_name=_("First name"))
|
||||||
|
@ -251,6 +332,7 @@ class Note(models.Model):
|
||||||
time = models.DateTimeField(
|
time = models.DateTimeField(
|
||||||
default=timezone.now, blank=False, verbose_name=_("Time")
|
default=timezone.now, blank=False, verbose_name=_("Time")
|
||||||
)
|
)
|
||||||
|
tags = TaggableManager(blank=True, through=Tagged)
|
||||||
|
|
||||||
objects = models.Manager()
|
objects = models.Manager()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div data-tags-url="{% url 'api:api-root' %}tags/"
|
||||||
|
{% for k, v in widget.attrs.items %}
|
||||||
|
{{ k }}="{{ v }}"
|
||||||
|
{% endfor %}>
|
||||||
|
{% csrf_token %}
|
||||||
|
<span class="prototype-tag btn badge badge-pill cursor-pointer mr-1" style="display: none;">
|
||||||
|
UNINITIALIZED PROTOTYPE
|
||||||
|
<span class="add-remove-icon pl-1 pr-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 }};">
|
||||||
|
{{ t.name }}
|
||||||
|
<span class="add-remove-icon pl-1 pr-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>
|
||||||
|
</div>
|
||||||
|
<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 }};">
|
||||||
|
{{ t.name }}
|
||||||
|
<span class="add-remove-icon pl-1 pr-1">+</span>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="{{ widget.name }}"
|
||||||
|
value="{% for t in widget.value %}"{{ t.name }}"{% if not forloop.last %},{% endif %}{% endfor %}"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="modal fade tag-editor-error-modal">
|
||||||
|
<div class="modal-dialog modal-sm" role="document">
|
||||||
|
<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">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<span data-message="generic">{% trans "An error ocurred." context "Error modal" %}</span>
|
||||||
|
<span data-message="invalid-tag-name">{% trans "Invalid tag name." context "Error modal" %}</span>
|
||||||
|
<span data-message="tag-creation-failed">{% trans "Failed to create tag." context "Error modal" %}</span>
|
||||||
|
<span data-message="tag-checking-failed">{% trans "Failed to obtain tag data." context "Error modal" %}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger" data-dismiss="modal">
|
||||||
|
{% trans "Close" context "Error modal" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,4 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from datetime import datetime
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -602,3 +603,105 @@ class WeightFormsTest(FormsTestCaseBase):
|
||||||
page = self.c.post("/weight/{}/delete/".format(self.weight.id), follow=True)
|
page = self.c.post("/weight/{}/delete/".format(self.weight.id), follow=True)
|
||||||
self.assertEqual(page.status_code, 200)
|
self.assertEqual(page.status_code, 200)
|
||||||
self.assertContains(page, "Weight entry deleted")
|
self.assertContains(page, "Weight entry deleted")
|
||||||
|
|
||||||
|
|
||||||
|
class NotesFormsTest(FormsTestCaseBase):
|
||||||
|
"""
|
||||||
|
Piggy-backs a bunch of tests for the tags-logic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(NotesFormsTest, cls).setUpClass()
|
||||||
|
|
||||||
|
cls.note = models.Note.objects.create(
|
||||||
|
child=cls.child,
|
||||||
|
note="Setup note",
|
||||||
|
time=timezone.now() - timezone.timedelta(days=2),
|
||||||
|
)
|
||||||
|
cls.note.tags.add("oldtag")
|
||||||
|
cls.oldtag = models.Tag.objects.filter(slug="oldtag").first()
|
||||||
|
|
||||||
|
def test_add_no_tags(self):
|
||||||
|
params = {
|
||||||
|
"child": self.child.id,
|
||||||
|
"note": "note with no tags",
|
||||||
|
"time": (timezone.now() - timezone.timedelta(minutes=1)).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
page = self.c.post("/notes/add/", params, follow=True)
|
||||||
|
self.assertEqual(page.status_code, 200)
|
||||||
|
self.assertContains(page, "note with no tags")
|
||||||
|
|
||||||
|
def test_add_with_tags(self):
|
||||||
|
params = {
|
||||||
|
"child": self.child.id,
|
||||||
|
"note": "this note has tags",
|
||||||
|
"time": (timezone.now() - timezone.timedelta(minutes=1)).isoformat(),
|
||||||
|
"tags": 'A,B,"setup tag"',
|
||||||
|
}
|
||||||
|
|
||||||
|
old_notes = list(models.Note.objects.all())
|
||||||
|
|
||||||
|
page = self.c.post("/notes/add/", params, follow=True)
|
||||||
|
self.assertEqual(page.status_code, 200)
|
||||||
|
self.assertContains(page, "this note has tags")
|
||||||
|
|
||||||
|
new_notes = list(models.Note.objects.all())
|
||||||
|
|
||||||
|
# Find the new tag and extract its tags
|
||||||
|
old_pks = [n.pk for n in old_notes]
|
||||||
|
new_note = [n for n in new_notes if n.pk not in old_pks][0]
|
||||||
|
new_note_tag_names = [t.name for t in new_note.tags.all()]
|
||||||
|
|
||||||
|
self.assertSetEqual(set(new_note_tag_names), {"A", "B", "setup tag"})
|
||||||
|
|
||||||
|
def test_edit(self):
|
||||||
|
old_tag_last_used = self.oldtag.last_used
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"child": self.note.child.id,
|
||||||
|
"note": "Edited note",
|
||||||
|
"time": self.localdate_string(),
|
||||||
|
"tags": "oldtag,newtag",
|
||||||
|
}
|
||||||
|
page = self.c.post("/notes/{}/".format(self.note.id), params, follow=True)
|
||||||
|
self.assertEqual(page.status_code, 200)
|
||||||
|
|
||||||
|
self.note.refresh_from_db()
|
||||||
|
self.oldtag.refresh_from_db()
|
||||||
|
self.assertEqual(self.note.note, params["note"])
|
||||||
|
self.assertContains(
|
||||||
|
page, "Note entry for {} updated".format(str(self.note.child))
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertSetEqual(
|
||||||
|
set(t.name for t in self.note.tags.all()), {"oldtag", "newtag"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Old tag remains old, because it was not added
|
||||||
|
self.assertEqual(old_tag_last_used, self.oldtag.last_used)
|
||||||
|
|
||||||
|
# Second phase: Remove all tags then add "oldtag" through posting
|
||||||
|
# which should update the last_used tag
|
||||||
|
self.note.tags.clear()
|
||||||
|
self.note.save()
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"child": self.note.child.id,
|
||||||
|
"note": "Edited note (2)",
|
||||||
|
"time": self.localdate_string(),
|
||||||
|
"tags": "oldtag",
|
||||||
|
}
|
||||||
|
page = self.c.post("/notes/{}/".format(self.note.id), params, follow=True)
|
||||||
|
self.assertEqual(page.status_code, 200)
|
||||||
|
|
||||||
|
self.note.refresh_from_db()
|
||||||
|
self.oldtag.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertLess(old_tag_last_used, self.oldtag.last_used)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
page = self.c.post("/notes/{}/delete/".format(self.note.id), follow=True)
|
||||||
|
self.assertEqual(page.status_code, 200)
|
||||||
|
self.assertContains(page, "Note entry deleted")
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
from django.forms import Media
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from django.forms import Widget
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class TagsEditor(Widget):
|
||||||
|
"""
|
||||||
|
Custom widget that provides an alternative editor for tags provided by the
|
||||||
|
taggit library.
|
||||||
|
|
||||||
|
The widget makes use of bootstrap v4 and its badge/pill feature and renders
|
||||||
|
a list of tags as badges that can be clicked to remove or add a tag to
|
||||||
|
the list of set tags. In addition, a user can dynamically add new, custom
|
||||||
|
tags, using a text editor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = ("babybuddy/js/tags_editor.js",)
|
||||||
|
|
||||||
|
input_type = "hidden"
|
||||||
|
template_name = "core/widget_tag_editor.html"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __unpack_tag(tag: models.Tag):
|
||||||
|
"""
|
||||||
|
Tiny utility function used to translate a tag to a serializable
|
||||||
|
dictionary of strings.
|
||||||
|
"""
|
||||||
|
return {"name": tag.name, "color": tag.color}
|
||||||
|
|
||||||
|
def format_value(self, value: Any) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Override format_value to provide a list of dictionaries rather than
|
||||||
|
a flat, comma-separated list of tags. This allows for the more
|
||||||
|
complex rendering of tags provided by this plugin.
|
||||||
|
"""
|
||||||
|
if value is not None and not isinstance(value, str):
|
||||||
|
value = [self.__unpack_tag(tag) for tag in value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||||
|
"""
|
||||||
|
Bootstrap integration adds form-control to the classes of the widget.
|
||||||
|
This works only for "plain" input-based widgets however. In addition,
|
||||||
|
we need to add a custom class "babybuddy-tags-editor" for the javascript
|
||||||
|
file to detect the widget and take control of its contents.
|
||||||
|
"""
|
||||||
|
attrs = super().build_attrs(base_attrs, extra_attrs)
|
||||||
|
class_string = attrs.get("class", "")
|
||||||
|
class_string = class_string.replace("form-control", "")
|
||||||
|
attrs["class"] = class_string + " babybuddy-tags-editor"
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def get_context(self, name: str, value: Any, attrs) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Adds extra information to the payload provided to the widget's template.
|
||||||
|
|
||||||
|
Specifically:
|
||||||
|
- Query a list if "recently used" tags (max 256 to not cause
|
||||||
|
DoS issues) from the database to be used for auto-completion. ("most")
|
||||||
|
- Query a smaller list of 5 tags to be made available from a quick
|
||||||
|
selection widget ("quick").
|
||||||
|
"""
|
||||||
|
most_tags = models.Tag.objects.order_by("-last_used").all()[:256]
|
||||||
|
|
||||||
|
result = super().get_context(name, value, attrs)
|
||||||
|
|
||||||
|
tag_names = set(
|
||||||
|
x["name"] for x in (result.get("widget", {}).get("value", None) or [])
|
||||||
|
)
|
||||||
|
quick_suggestion_tags = [t for t in most_tags if t.name not in tag_names][:5]
|
||||||
|
|
||||||
|
result["widget"]["tag_suggestions"] = {
|
||||||
|
"quick": [
|
||||||
|
self.__unpack_tag(t)
|
||||||
|
for t in quick_suggestion_tags
|
||||||
|
if t.name not in tag_names
|
||||||
|
],
|
||||||
|
"most": [self.__unpack_tag(t) for t in most_tags],
|
||||||
|
}
|
||||||
|
return result
|
|
@ -68,6 +68,9 @@ module.exports = {
|
||||||
'api/static_src/js/*.js',
|
'api/static_src/js/*.js',
|
||||||
'core/static_src/js/*.js',
|
'core/static_src/js/*.js',
|
||||||
'dashboard/static_src/js/*.js'
|
'dashboard/static_src/js/*.js'
|
||||||
|
],
|
||||||
|
tags_editor: [
|
||||||
|
'babybuddy/static_src/js/tags_editor.js'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
stylesConfig: {
|
stylesConfig: {
|
||||||
|
|
|
@ -199,6 +199,12 @@ function scripts(cb) {
|
||||||
concat('app.js'),
|
concat('app.js'),
|
||||||
gulp.dest(config.scriptsConfig.dest)
|
gulp.dest(config.scriptsConfig.dest)
|
||||||
], cb);
|
], cb);
|
||||||
|
|
||||||
|
pump([
|
||||||
|
gulp.src(config.scriptsConfig.tags_editor),
|
||||||
|
concat('tags_editor.js'),
|
||||||
|
gulp.dest(config.scriptsConfig.dest)
|
||||||
|
], cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Binary file not shown.
|
@ -2,7 +2,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Baby Buddy\n"
|
"Project-Id-Version: Baby Buddy\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-02-25 05:14+0000\n"
|
"POT-Creation-Date: 2022-03-02 21:15+0000\n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
@ -121,59 +121,59 @@ msgstr "Zeitzone"
|
||||||
msgid "{user}'s Settings"
|
msgid "{user}'s Settings"
|
||||||
msgstr "{user} Einstellungen"
|
msgstr "{user} Einstellungen"
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:166
|
#: babybuddy/settings/base.py:167
|
||||||
msgid "Chinese (simplified)"
|
msgid "Chinese (simplified)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:167
|
#: babybuddy/settings/base.py:168
|
||||||
msgid "Dutch"
|
msgid "Dutch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:168
|
#: babybuddy/settings/base.py:169
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "English"
|
#| msgid "English"
|
||||||
msgid "English (US)"
|
msgid "English (US)"
|
||||||
msgstr "Englisch"
|
msgstr "Englisch"
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:169
|
#: babybuddy/settings/base.py:170
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "English"
|
#| msgid "English"
|
||||||
msgid "English (UK)"
|
msgid "English (UK)"
|
||||||
msgstr "Englisch"
|
msgstr "Englisch"
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:170
|
#: babybuddy/settings/base.py:171
|
||||||
msgid "French"
|
msgid "French"
|
||||||
msgstr "Französisch"
|
msgstr "Französisch"
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:171
|
#: babybuddy/settings/base.py:172
|
||||||
msgid "Finnish"
|
msgid "Finnish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:172
|
#: babybuddy/settings/base.py:173
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr "Deutsch"
|
msgstr "Deutsch"
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:173
|
#: babybuddy/settings/base.py:174
|
||||||
msgid "Italian"
|
msgid "Italian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:174
|
#: babybuddy/settings/base.py:175
|
||||||
msgid "Polish"
|
msgid "Polish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:175
|
#: babybuddy/settings/base.py:176
|
||||||
msgid "Portuguese"
|
msgid "Portuguese"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:176
|
#: babybuddy/settings/base.py:177
|
||||||
msgid "Spanish"
|
msgid "Spanish"
|
||||||
msgstr "Spanisch"
|
msgstr "Spanisch"
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:177
|
#: babybuddy/settings/base.py:178
|
||||||
msgid "Swedish"
|
msgid "Swedish"
|
||||||
msgstr "Schwedisch"
|
msgstr "Schwedisch"
|
||||||
|
|
||||||
#: babybuddy/settings/base.py:178
|
#: babybuddy/settings/base.py:179
|
||||||
msgid "Turkish"
|
msgid "Turkish"
|
||||||
msgstr "Türkisch"
|
msgstr "Türkisch"
|
||||||
|
|
||||||
|
@ -199,7 +199,7 @@ msgstr "Reset"
|
||||||
msgid "Filters"
|
msgid "Filters"
|
||||||
msgstr "Filter"
|
msgstr "Filter"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/form.html:11
|
#: babybuddy/templates/babybuddy/form.html:14
|
||||||
#: babybuddy/templates/babybuddy/user_settings_form.html:89
|
#: babybuddy/templates/babybuddy/user_settings_form.html:89
|
||||||
msgid "Submit"
|
msgid "Submit"
|
||||||
msgstr "Senden"
|
msgstr "Senden"
|
||||||
|
@ -222,27 +222,27 @@ msgstr ""
|
||||||
msgid "Quick Start Timer"
|
msgid "Quick Start Timer"
|
||||||
msgstr "Quick-Start Timer"
|
msgstr "Quick-Start Timer"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:51 core/models.py:158
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:51 core/models.py:232
|
||||||
#: core/models.py:162
|
#: core/models.py:236
|
||||||
msgid "Diaper Change"
|
msgid "Diaper Change"
|
||||||
msgstr "Windeln wechseln"
|
msgstr "Windeln wechseln"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:57
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:57
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:274 core/models.py:227
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:274 core/models.py:301
|
||||||
#: core/models.py:231 core/templates/core/timer_detail.html:43
|
#: core/models.py:305 core/templates/core/timer_detail.html:43
|
||||||
msgid "Feeding"
|
msgid "Feeding"
|
||||||
msgstr "Mahlzeit"
|
msgstr "Mahlzeit"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:63
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:63
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:150 core/models.py:250
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:150 core/models.py:324
|
||||||
#: core/models.py:260 core/models.py:264 core/templates/core/note_list.html:29
|
#: core/models.py:335 core/models.py:339 core/templates/core/note_list.html:29
|
||||||
msgid "Note"
|
msgid "Note"
|
||||||
msgstr "Notiz"
|
msgstr "Notiz"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:69
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:69
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:281
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:281
|
||||||
#: babybuddy/templates/babybuddy/welcome.html:42 core/models.py:292
|
#: babybuddy/templates/babybuddy/welcome.html:42 core/models.py:367
|
||||||
#: core/models.py:293 core/models.py:296
|
#: core/models.py:368 core/models.py:371
|
||||||
#: core/templates/core/sleep_confirm_delete.html:7
|
#: core/templates/core/sleep_confirm_delete.html:7
|
||||||
#: core/templates/core/sleep_form.html:13 core/templates/core/sleep_list.html:4
|
#: core/templates/core/sleep_form.html:13 core/templates/core/sleep_list.html:4
|
||||||
#: core/templates/core/sleep_list.html:7 core/templates/core/sleep_list.html:12
|
#: core/templates/core/sleep_list.html:7 core/templates/core/sleep_list.html:12
|
||||||
|
@ -251,8 +251,8 @@ msgid "Sleep"
|
||||||
msgstr "Schlafen"
|
msgstr "Schlafen"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:75
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:75
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:172 core/models.py:331
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:172 core/models.py:406
|
||||||
#: core/models.py:341 core/models.py:342 core/models.py:345
|
#: core/models.py:416 core/models.py:417 core/models.py:420
|
||||||
#: core/templates/core/temperature_confirm_delete.html:7
|
#: core/templates/core/temperature_confirm_delete.html:7
|
||||||
#: core/templates/core/temperature_form.html:13
|
#: core/templates/core/temperature_form.html:13
|
||||||
#: core/templates/core/temperature_list.html:4
|
#: core/templates/core/temperature_list.html:4
|
||||||
|
@ -264,8 +264,8 @@ msgstr "Temperatur"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:81
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:81
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:294
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:294
|
||||||
#: babybuddy/templates/babybuddy/welcome.html:50 core/models.py:468
|
#: babybuddy/templates/babybuddy/welcome.html:50 core/models.py:543
|
||||||
#: core/models.py:469 core/models.py:472
|
#: core/models.py:544 core/models.py:547
|
||||||
#: core/templates/core/timer_detail.html:59
|
#: core/templates/core/timer_detail.html:59
|
||||||
#: core/templates/core/tummytime_confirm_delete.html:7
|
#: core/templates/core/tummytime_confirm_delete.html:7
|
||||||
#: core/templates/core/tummytime_form.html:13
|
#: core/templates/core/tummytime_form.html:13
|
||||||
|
@ -276,16 +276,16 @@ msgid "Tummy Time"
|
||||||
msgstr "Bauchzeit"
|
msgstr "Bauchzeit"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:87
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:87
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:186 core/models.py:494
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:186 core/models.py:569
|
||||||
#: core/models.py:503 core/models.py:504 core/models.py:507
|
#: core/models.py:578 core/models.py:579 core/models.py:582
|
||||||
#: core/templates/core/weight_confirm_delete.html:7
|
#: core/templates/core/weight_confirm_delete.html:7
|
||||||
#: core/templates/core/weight_form.html:13
|
#: core/templates/core/weight_form.html:13
|
||||||
#: core/templates/core/weight_list.html:4
|
#: core/templates/core/weight_list.html:4
|
||||||
#: core/templates/core/weight_list.html:7
|
#: core/templates/core/weight_list.html:7
|
||||||
#: core/templates/core/weight_list.html:12
|
#: core/templates/core/weight_list.html:12
|
||||||
#: core/templates/core/weight_list.html:29
|
#: core/templates/core/weight_list.html:29 reports/graphs/weight_weight.py:19
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:31
|
#: reports/graphs/weight_weight.py:30
|
||||||
#: reports/graphs/weight_weight.py:19 reports/graphs/weight_weight.py:30
|
#: reports/templates/reports/report_list.html:21
|
||||||
#: reports/templates/reports/weight_change.html:4
|
#: reports/templates/reports/weight_change.html:4
|
||||||
#: reports/templates/reports/weight_change.html:8
|
#: reports/templates/reports/weight_change.html:8
|
||||||
msgid "Weight"
|
msgid "Weight"
|
||||||
|
@ -299,20 +299,20 @@ msgid "Timeline"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:122
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:122
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:130 core/models.py:101
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:130 core/models.py:175
|
||||||
#: core/templates/core/child_confirm_delete.html:7
|
#: core/templates/core/child_confirm_delete.html:7
|
||||||
#: core/templates/core/child_detail.html:7
|
#: core/templates/core/child_detail.html:7
|
||||||
#: core/templates/core/child_form.html:13 core/templates/core/child_list.html:4
|
#: core/templates/core/child_form.html:13 core/templates/core/child_list.html:4
|
||||||
#: core/templates/core/child_list.html:7 core/templates/core/child_list.html:12
|
#: core/templates/core/child_list.html:7 core/templates/core/child_list.html:12
|
||||||
#: dashboard/templates/dashboard/child.html:7
|
#: dashboard/templates/dashboard/child.html:7
|
||||||
#: reports/templates/reports/report_base.html:7
|
#: reports/templates/reports/base.html:7
|
||||||
msgid "Children"
|
msgid "Children"
|
||||||
msgstr "Kinder"
|
msgstr "Kinder"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:136 core/models.py:100
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:136 core/models.py:174
|
||||||
#: core/models.py:134 core/models.py:190 core/models.py:248 core/models.py:276
|
#: core/models.py:208 core/models.py:264 core/models.py:322 core/models.py:351
|
||||||
#: core/models.py:328 core/models.py:359 core/models.py:452 core/models.py:492
|
#: core/models.py:403 core/models.py:434 core/models.py:527 core/models.py:567
|
||||||
#: core/models.py:519 core/models.py:546 core/models.py:572
|
#: core/models.py:594 core/models.py:621 core/models.py:647
|
||||||
#: core/templates/core/bmi_list.html:27
|
#: core/templates/core/bmi_list.html:27
|
||||||
#: core/templates/core/diaperchange_list.html:27
|
#: core/templates/core/diaperchange_list.html:27
|
||||||
#: core/templates/core/feeding_list.html:27
|
#: core/templates/core/feeding_list.html:27
|
||||||
|
@ -326,9 +326,9 @@ msgstr "Kinder"
|
||||||
msgid "Child"
|
msgid "Child"
|
||||||
msgstr "Kind"
|
msgstr "Kind"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:144 core/models.py:151
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:144 core/models.py:225
|
||||||
#: core/models.py:220 core/models.py:261 core/models.py:284 core/models.py:334
|
#: core/models.py:294 core/models.py:336 core/models.py:359 core/models.py:409
|
||||||
#: core/models.py:496 core/models.py:523 core/models.py:552 core/models.py:576
|
#: core/models.py:571 core/models.py:598 core/models.py:627 core/models.py:651
|
||||||
#: core/templates/core/note_confirm_delete.html:7
|
#: core/templates/core/note_confirm_delete.html:7
|
||||||
#: core/templates/core/note_form.html:13 core/templates/core/note_list.html:4
|
#: core/templates/core/note_form.html:13 core/templates/core/note_list.html:4
|
||||||
#: core/templates/core/note_list.html:7 core/templates/core/note_list.html:12
|
#: core/templates/core/note_list.html:7 core/templates/core/note_list.html:12
|
||||||
|
@ -347,18 +347,18 @@ msgstr "Temperatur Messung"
|
||||||
msgid "Weight entry"
|
msgid "Weight entry"
|
||||||
msgstr "Gewichtseintrag"
|
msgstr "Gewichtseintrag"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:200 core/models.py:521
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:200 core/models.py:596
|
||||||
#: core/models.py:530 core/models.py:531 core/models.py:534
|
#: core/models.py:605 core/models.py:606 core/models.py:609
|
||||||
#: core/templates/core/height_confirm_delete.html:7
|
#: core/templates/core/height_confirm_delete.html:7
|
||||||
#: core/templates/core/height_form.html:13
|
#: core/templates/core/height_form.html:13
|
||||||
#: core/templates/core/height_list.html:4
|
#: core/templates/core/height_list.html:4
|
||||||
#: core/templates/core/height_list.html:7
|
#: core/templates/core/height_list.html:7
|
||||||
#: core/templates/core/height_list.html:12
|
#: core/templates/core/height_list.html:12
|
||||||
#: core/templates/core/height_list.html:29
|
#: core/templates/core/height_list.html:29 reports/graphs/height_height.py:19
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:32
|
#: reports/graphs/height_height.py:30
|
||||||
#: reports/graphs/height_height.py:19 reports/graphs/height_height.py:30
|
|
||||||
#: reports/templates/reports/height_change.html:4
|
#: reports/templates/reports/height_change.html:4
|
||||||
#: reports/templates/reports/height_change.html:8
|
#: reports/templates/reports/height_change.html:8
|
||||||
|
#: reports/templates/reports/report_list.html:17
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Weight"
|
#| msgid "Weight"
|
||||||
msgid "Height"
|
msgid "Height"
|
||||||
|
@ -370,38 +370,36 @@ msgstr "Gewicht"
|
||||||
msgid "Height entry"
|
msgid "Height entry"
|
||||||
msgstr "Gewichtseintrag"
|
msgstr "Gewichtseintrag"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:214 core/models.py:549
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:214 core/models.py:624
|
||||||
#: core/models.py:559 core/models.py:560 core/models.py:563
|
#: core/models.py:634 core/models.py:635 core/models.py:638
|
||||||
#: core/templates/core/head_circumference_confirm_delete.html:7
|
#: core/templates/core/head_circumference_confirm_delete.html:7
|
||||||
#: core/templates/core/head_circumference_form.html:13
|
#: core/templates/core/head_circumference_form.html:13
|
||||||
#: core/templates/core/head_circumference_list.html:4
|
#: core/templates/core/head_circumference_list.html:4
|
||||||
#: core/templates/core/head_circumference_list.html:7
|
#: core/templates/core/head_circumference_list.html:7
|
||||||
#: core/templates/core/head_circumference_list.html:12
|
#: core/templates/core/head_circumference_list.html:12
|
||||||
#: core/templates/core/head_circumference_list.html:29
|
#: core/templates/core/head_circumference_list.html:29
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:33
|
|
||||||
#: reports/graphs/head_circumference_head_circumference.py:19
|
#: reports/graphs/head_circumference_head_circumference.py:19
|
||||||
#: reports/graphs/head_circumference_head_circumference.py:30
|
#: reports/graphs/head_circumference_head_circumference.py:30
|
||||||
#: reports/templates/reports/head_circumference_change.html:4
|
#: reports/templates/reports/head_circumference_change.html:4
|
||||||
#: reports/templates/reports/head_circumference_change.html:8
|
#: reports/templates/reports/head_circumference_change.html:8
|
||||||
|
#: reports/templates/reports/report_list.html:16
|
||||||
msgid "Head Circumference"
|
msgid "Head Circumference"
|
||||||
msgstr ""
|
msgstr "Kopfumfang"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:220
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:220
|
||||||
msgid "Head Circumference entry"
|
msgid "Head Circumference entry"
|
||||||
msgstr ""
|
msgstr "Kopfumfang Eintrag"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:228 core/models.py:574
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:228 core/models.py:649
|
||||||
#: core/models.py:583 core/models.py:584 core/models.py:587
|
#: core/models.py:658 core/models.py:659 core/models.py:662
|
||||||
#: core/templates/core/bmi_confirm_delete.html:7
|
#: core/templates/core/bmi_confirm_delete.html:7
|
||||||
#: core/templates/core/bmi_form.html:13 core/templates/core/bmi_list.html:4
|
#: core/templates/core/bmi_form.html:13 core/templates/core/bmi_list.html:4
|
||||||
#: core/templates/core/bmi_list.html:7 core/templates/core/bmi_list.html:12
|
#: core/templates/core/bmi_list.html:7 core/templates/core/bmi_list.html:12
|
||||||
#: core/templates/core/bmi_list.html:29
|
#: core/templates/core/bmi_list.html:29 reports/graphs/bmi_bmi.py:19
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:34
|
#: reports/graphs/bmi_bmi.py:30 reports/templates/reports/bmi_change.html:4
|
||||||
#: reports/graphs/bmi_bmi.py:19 reports/graphs/bmi_bmi.py:30
|
|
||||||
#: reports/templates/reports/bmi_change.html:4
|
|
||||||
#: reports/templates/reports/bmi_change.html:8
|
#: reports/templates/reports/bmi_change.html:8
|
||||||
msgid "BMI"
|
msgid "BMI"
|
||||||
msgstr ""
|
msgstr "BMI"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:234
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:234
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
|
@ -423,7 +421,7 @@ msgid "Change"
|
||||||
msgstr "Wechsel"
|
msgstr "Wechsel"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:268
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:268
|
||||||
#: babybuddy/templates/babybuddy/welcome.html:34 core/models.py:228
|
#: babybuddy/templates/babybuddy/welcome.html:34 core/models.py:302
|
||||||
#: core/templates/core/feeding_confirm_delete.html:7
|
#: core/templates/core/feeding_confirm_delete.html:7
|
||||||
#: core/templates/core/feeding_form.html:13
|
#: core/templates/core/feeding_form.html:13
|
||||||
#: core/templates/core/feeding_list.html:4
|
#: core/templates/core/feeding_list.html:4
|
||||||
|
@ -443,7 +441,7 @@ msgstr "Bauchzeit-Eintrag"
|
||||||
#: babybuddy/templates/babybuddy/nav-dropdown.html:325
|
#: babybuddy/templates/babybuddy/nav-dropdown.html:325
|
||||||
#: babybuddy/templates/babybuddy/user_list.html:17
|
#: babybuddy/templates/babybuddy/user_list.html:17
|
||||||
#: babybuddy/templates/babybuddy/user_password_form.html:7
|
#: babybuddy/templates/babybuddy/user_password_form.html:7
|
||||||
#: babybuddy/templates/babybuddy/user_settings_form.html:7 core/models.py:378
|
#: babybuddy/templates/babybuddy/user_settings_form.html:7 core/models.py:453
|
||||||
#: core/templates/core/timer_list.html:32
|
#: core/templates/core/timer_list.html:32
|
||||||
msgid "User"
|
msgid "User"
|
||||||
msgstr "Benutzer"
|
msgstr "Benutzer"
|
||||||
|
@ -535,7 +533,7 @@ msgstr "Benutzer löschen"
|
||||||
#: core/templates/core/tummytime_confirm_delete.html:17
|
#: core/templates/core/tummytime_confirm_delete.html:17
|
||||||
#: core/templates/core/weight_confirm_delete.html:8
|
#: core/templates/core/weight_confirm_delete.html:8
|
||||||
#: core/templates/core/weight_confirm_delete.html:17
|
#: core/templates/core/weight_confirm_delete.html:17
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:48
|
#: dashboard/templates/dashboard/child_button_group.html:27
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr "löschen"
|
msgstr "löschen"
|
||||||
|
|
||||||
|
@ -631,7 +629,7 @@ msgstr "E-Mail"
|
||||||
msgid "Staff"
|
msgid "Staff"
|
||||||
msgstr "Angestellte"
|
msgstr "Angestellte"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/user_list.html:22 core/models.py:373
|
#: babybuddy/templates/babybuddy/user_list.html:22 core/models.py:448
|
||||||
#: core/templates/core/timer_list.html:31
|
#: core/templates/core/timer_list.html:31
|
||||||
msgid "Active"
|
msgid "Active"
|
||||||
msgstr "Aktiv"
|
msgstr "Aktiv"
|
||||||
|
@ -708,7 +706,7 @@ msgstr ""
|
||||||
"Lerne und sehe die Bedürfnisse deines Babys voraus, ohne (<em>allzu viel</"
|
"Lerne und sehe die Bedürfnisse deines Babys voraus, ohne (<em>allzu viel</"
|
||||||
"em>)Spekulation indem du Baby Buddy verwendest —"
|
"em>)Spekulation indem du Baby Buddy verwendest —"
|
||||||
|
|
||||||
#: babybuddy/templates/babybuddy/welcome.html:26 core/models.py:159
|
#: babybuddy/templates/babybuddy/welcome.html:26 core/models.py:233
|
||||||
#: core/templates/core/diaperchange_confirm_delete.html:7
|
#: core/templates/core/diaperchange_confirm_delete.html:7
|
||||||
#: core/templates/core/diaperchange_form.html:13
|
#: core/templates/core/diaperchange_form.html:13
|
||||||
#: core/templates/core/diaperchange_list.html:4
|
#: core/templates/core/diaperchange_list.html:4
|
||||||
|
@ -894,105 +892,113 @@ msgstr "User API-Key neu generiert."
|
||||||
msgid "Settings saved!"
|
msgid "Settings saved!"
|
||||||
msgstr "Einstellungen gespeichert!"
|
msgstr "Einstellungen gespeichert!"
|
||||||
|
|
||||||
#: core/forms.py:115
|
#: core/forms.py:117
|
||||||
msgid "Name does not match child name."
|
msgid "Name does not match child name."
|
||||||
msgstr "Name entspricht nicht dem Kindernamen."
|
msgstr "Name entspricht nicht dem Kindernamen."
|
||||||
|
|
||||||
#: core/models.py:23
|
#: core/models.py:32
|
||||||
msgid "Date can not be in the future."
|
msgid "Date can not be in the future."
|
||||||
msgstr "Datum darf nicht in der Zukunft liegen."
|
msgstr "Datum darf nicht in der Zukunft liegen."
|
||||||
|
|
||||||
#: core/models.py:37
|
#: core/models.py:46
|
||||||
msgid "Start time must come before end time."
|
msgid "Start time must come before end time."
|
||||||
msgstr "Startzeit muss vor Endzeit sein."
|
msgstr "Startzeit muss vor Endzeit sein."
|
||||||
|
|
||||||
#: core/models.py:40
|
#: core/models.py:49
|
||||||
msgid "Duration too long."
|
msgid "Duration too long."
|
||||||
msgstr "Dauer zu lange."
|
msgstr "Dauer zu lange."
|
||||||
|
|
||||||
#: core/models.py:56
|
#: core/models.py:65
|
||||||
msgid "Another entry intersects the specified time period."
|
msgid "Another entry intersects the specified time period."
|
||||||
msgstr "Ein anderer Eintrag schneidet sich mit der angegebenen Zeitperiode."
|
msgstr "Ein anderer Eintrag schneidet sich mit der angegebenen Zeitperiode."
|
||||||
|
|
||||||
#: core/models.py:70
|
#: core/models.py:79
|
||||||
msgid "Date/time can not be in the future."
|
msgid "Date/time can not be in the future."
|
||||||
msgstr "Datum/Zeit darf nicht in der Zukunft liegen."
|
msgstr "Datum/Zeit darf nicht in der Zukunft liegen."
|
||||||
|
|
||||||
#: core/models.py:76
|
#: core/models.py:109
|
||||||
|
msgid "Tag"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:110
|
||||||
|
msgid "Tags"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:150
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr "Vorname"
|
msgstr "Vorname"
|
||||||
|
|
||||||
#: core/models.py:78
|
#: core/models.py:152
|
||||||
msgid "Last name"
|
msgid "Last name"
|
||||||
msgstr "Nachname"
|
msgstr "Nachname"
|
||||||
|
|
||||||
#: core/models.py:80
|
#: core/models.py:154
|
||||||
msgid "Birth date"
|
msgid "Birth date"
|
||||||
msgstr "Geburtsdatum"
|
msgstr "Geburtsdatum"
|
||||||
|
|
||||||
#: core/models.py:87
|
#: core/models.py:161
|
||||||
msgid "Slug"
|
msgid "Slug"
|
||||||
msgstr "Slug"
|
msgstr "Slug"
|
||||||
|
|
||||||
#: core/models.py:90
|
#: core/models.py:164
|
||||||
msgid "Picture"
|
msgid "Picture"
|
||||||
msgstr "Bild"
|
msgstr "Bild"
|
||||||
|
|
||||||
#: core/models.py:136 core/models.py:252 core/models.py:333
|
#: core/models.py:210 core/models.py:326 core/models.py:408
|
||||||
#: core/templates/core/diaperchange_list.html:25
|
#: core/templates/core/diaperchange_list.html:25
|
||||||
#: core/templates/core/note_list.html:25
|
#: core/templates/core/note_list.html:25
|
||||||
#: core/templates/core/temperature_list.html:25
|
#: core/templates/core/temperature_list.html:25
|
||||||
msgid "Time"
|
msgid "Time"
|
||||||
msgstr "Zeit"
|
msgstr "Zeit"
|
||||||
|
|
||||||
#: core/models.py:137 core/templates/core/diaperchange_list.html:60
|
#: core/models.py:211 core/templates/core/diaperchange_list.html:60
|
||||||
#: reports/graphs/diaperchange_types.py:36
|
#: reports/graphs/diaperchange_types.py:36
|
||||||
msgid "Wet"
|
msgid "Wet"
|
||||||
msgstr "Nass"
|
msgstr "Nass"
|
||||||
|
|
||||||
#: core/models.py:138 core/templates/core/diaperchange_list.html:61
|
#: core/models.py:212 core/templates/core/diaperchange_list.html:61
|
||||||
#: reports/graphs/diaperchange_types.py:30
|
#: reports/graphs/diaperchange_types.py:30
|
||||||
msgid "Solid"
|
msgid "Solid"
|
||||||
msgstr "Fest"
|
msgstr "Fest"
|
||||||
|
|
||||||
#: core/models.py:142
|
#: core/models.py:216
|
||||||
msgid "Black"
|
msgid "Black"
|
||||||
msgstr "Schwarz"
|
msgstr "Schwarz"
|
||||||
|
|
||||||
#: core/models.py:143
|
#: core/models.py:217
|
||||||
msgid "Brown"
|
msgid "Brown"
|
||||||
msgstr "Braun"
|
msgstr "Braun"
|
||||||
|
|
||||||
#: core/models.py:144
|
#: core/models.py:218
|
||||||
msgid "Green"
|
msgid "Green"
|
||||||
msgstr "Grün"
|
msgstr "Grün"
|
||||||
|
|
||||||
#: core/models.py:145
|
#: core/models.py:219
|
||||||
msgid "Yellow"
|
msgid "Yellow"
|
||||||
msgstr "Gelb"
|
msgstr "Gelb"
|
||||||
|
|
||||||
#: core/models.py:148 core/templates/core/diaperchange_list.html:30
|
#: core/models.py:222 core/templates/core/diaperchange_list.html:30
|
||||||
msgid "Color"
|
msgid "Color"
|
||||||
msgstr "Farbe"
|
msgstr "Farbe"
|
||||||
|
|
||||||
#: core/models.py:150 core/models.py:219
|
#: core/models.py:224 core/models.py:293
|
||||||
#: core/templates/core/diaperchange_list.html:31
|
#: core/templates/core/diaperchange_list.html:31
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
msgstr "Menge"
|
msgstr "Menge"
|
||||||
|
|
||||||
#: core/models.py:180
|
#: core/models.py:254
|
||||||
msgid "Wet and/or solid is required."
|
msgid "Wet and/or solid is required."
|
||||||
msgstr "Nass und/oder fest wird benötigt."
|
msgstr "Nass und/oder fest wird benötigt."
|
||||||
|
|
||||||
#: core/models.py:192 core/models.py:279 core/models.py:365 core/models.py:454
|
#: core/models.py:266 core/models.py:354 core/models.py:440 core/models.py:529
|
||||||
msgid "Start time"
|
msgid "Start time"
|
||||||
msgstr "Startzeit"
|
msgstr "Startzeit"
|
||||||
|
|
||||||
#: core/models.py:193 core/models.py:280 core/models.py:368 core/models.py:455
|
#: core/models.py:267 core/models.py:355 core/models.py:443 core/models.py:530
|
||||||
msgid "End time"
|
msgid "End time"
|
||||||
msgstr "Endzeit"
|
msgstr "Endzeit"
|
||||||
|
|
||||||
#: core/models.py:195 core/models.py:282 core/models.py:371 core/models.py:457
|
#: core/models.py:269 core/models.py:357 core/models.py:446 core/models.py:532
|
||||||
#: core/templates/core/feeding_list.html:34
|
#: core/templates/core/feeding_list.html:34
|
||||||
#: core/templates/core/sleep_list.html:30
|
#: core/templates/core/sleep_list.html:30
|
||||||
#: core/templates/core/timer_list.html:29
|
#: core/templates/core/timer_list.html:29
|
||||||
|
@ -1000,67 +1006,67 @@ msgstr "Endzeit"
|
||||||
msgid "Duration"
|
msgid "Duration"
|
||||||
msgstr "Dauer"
|
msgstr "Dauer"
|
||||||
|
|
||||||
#: core/models.py:199
|
#: core/models.py:273
|
||||||
msgid "Breast milk"
|
msgid "Breast milk"
|
||||||
msgstr "Brustmilch"
|
msgstr "Brustmilch"
|
||||||
|
|
||||||
#: core/models.py:200
|
#: core/models.py:274
|
||||||
msgid "Formula"
|
msgid "Formula"
|
||||||
msgstr "Formel"
|
msgstr "Formel"
|
||||||
|
|
||||||
#: core/models.py:201
|
#: core/models.py:275
|
||||||
msgid "Fortified breast milk"
|
msgid "Fortified breast milk"
|
||||||
msgstr "Angereicherte Brustmilch"
|
msgstr "Angereicherte Brustmilch"
|
||||||
|
|
||||||
#: core/models.py:202
|
#: core/models.py:276
|
||||||
msgid "Solid food"
|
msgid "Solid food"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/models.py:205 core/templates/core/feeding_list.html:30
|
#: core/models.py:279 core/templates/core/feeding_list.html:30
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr "Typ"
|
msgstr "Typ"
|
||||||
|
|
||||||
#: core/models.py:209
|
#: core/models.py:283
|
||||||
msgid "Bottle"
|
msgid "Bottle"
|
||||||
msgstr "Fläschchen"
|
msgstr "Fläschchen"
|
||||||
|
|
||||||
#: core/models.py:210
|
#: core/models.py:284
|
||||||
msgid "Left breast"
|
msgid "Left breast"
|
||||||
msgstr "Linke Brust"
|
msgstr "Linke Brust"
|
||||||
|
|
||||||
#: core/models.py:211
|
#: core/models.py:285
|
||||||
msgid "Right breast"
|
msgid "Right breast"
|
||||||
msgstr "Rechte Brust"
|
msgstr "Rechte Brust"
|
||||||
|
|
||||||
#: core/models.py:212
|
#: core/models.py:286
|
||||||
msgid "Both breasts"
|
msgid "Both breasts"
|
||||||
msgstr "Beide Brüste"
|
msgstr "Beide Brüste"
|
||||||
|
|
||||||
#: core/models.py:213
|
#: core/models.py:287
|
||||||
msgid "Parent fed"
|
msgid "Parent fed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/models.py:214
|
#: core/models.py:288
|
||||||
msgid "Self fed"
|
msgid "Self fed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/models.py:217 core/templates/core/feeding_list.html:29
|
#: core/models.py:291 core/templates/core/feeding_list.html:29
|
||||||
msgid "Method"
|
msgid "Method"
|
||||||
msgstr "Methode"
|
msgstr "Methode"
|
||||||
|
|
||||||
#: core/models.py:278
|
#: core/models.py:353
|
||||||
msgid "Napping"
|
msgid "Napping"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/models.py:362 core/templates/core/timer_list.html:25
|
#: core/models.py:437 core/templates/core/timer_list.html:25
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "Name"
|
msgstr "Name"
|
||||||
|
|
||||||
#: core/models.py:386 core/templates/core/timer_form.html:4
|
#: core/models.py:461 core/templates/core/timer_form.html:4
|
||||||
msgid "Timer"
|
msgid "Timer"
|
||||||
msgstr "Timer"
|
msgstr "Timer"
|
||||||
|
|
||||||
#: core/models.py:387 core/templates/core/timer_confirm_delete.html:9
|
#: core/models.py:462 core/templates/core/timer_confirm_delete.html:9
|
||||||
#: core/templates/core/timer_confirm_delete_inactive.html:9
|
#: core/templates/core/timer_confirm_delete_inactive.html:9
|
||||||
#: core/templates/core/timer_detail.html:8
|
#: core/templates/core/timer_detail.html:8
|
||||||
#: core/templates/core/timer_form.html:7 core/templates/core/timer_list.html:4
|
#: core/templates/core/timer_form.html:7 core/templates/core/timer_list.html:4
|
||||||
|
@ -1069,16 +1075,16 @@ msgstr "Timer"
|
||||||
msgid "Timers"
|
msgid "Timers"
|
||||||
msgstr "Timer"
|
msgstr "Timer"
|
||||||
|
|
||||||
#: core/models.py:390
|
#: core/models.py:465
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Timer #{id}"
|
msgid "Timer #{id}"
|
||||||
msgstr "Timer #{id}"
|
msgstr "Timer #{id}"
|
||||||
|
|
||||||
#: core/models.py:460 core/templates/core/tummytime_list.html:30
|
#: core/models.py:535 core/templates/core/tummytime_list.html:30
|
||||||
msgid "Milestone"
|
msgid "Milestone"
|
||||||
msgstr "Meilenstein"
|
msgstr "Meilenstein"
|
||||||
|
|
||||||
#: core/models.py:495 core/models.py:522 core/models.py:551 core/models.py:575
|
#: core/models.py:570 core/models.py:597 core/models.py:626 core/models.py:650
|
||||||
#: core/templates/core/bmi_list.html:25
|
#: core/templates/core/bmi_list.html:25
|
||||||
#: core/templates/core/feeding_list.html:25
|
#: core/templates/core/feeding_list.html:25
|
||||||
#: core/templates/core/head_circumference_list.html:25
|
#: core/templates/core/head_circumference_list.html:25
|
||||||
|
@ -1098,24 +1104,22 @@ msgstr "Datum"
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Delete a Sleep Entry"
|
#| msgid "Delete a Sleep Entry"
|
||||||
msgid "Delete a BMI Entry"
|
msgid "Delete a BMI Entry"
|
||||||
msgstr "Einen Schlaf-Eintrag löschen"
|
msgstr "Einen BMI-Wert löschen"
|
||||||
|
|
||||||
#: core/templates/core/bmi_form.html:8 core/templates/core/bmi_form.html:17
|
#: core/templates/core/bmi_form.html:8 core/templates/core/bmi_form.html:17
|
||||||
#: core/templates/core/bmi_form.html:27
|
#: core/templates/core/bmi_form.html:27
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Add a Sleep Entry"
|
|
||||||
msgid "Add a BMI Entry"
|
msgid "Add a BMI Entry"
|
||||||
msgstr "Schlaf-Eintrag hinzufügen"
|
msgstr "BMI-Wert hinzufügen"
|
||||||
|
|
||||||
#: core/templates/core/bmi_list.html:15
|
#: core/templates/core/bmi_list.html:15
|
||||||
msgid "Add BMI"
|
msgid "Add BMI"
|
||||||
msgstr ""
|
msgstr "BMI Wert hinzufügen"
|
||||||
|
|
||||||
#: core/templates/core/bmi_list.html:66
|
#: core/templates/core/bmi_list.html:66
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "No timer entries found."
|
#| msgid "No timer entries found."
|
||||||
msgid "No bmi entries found."
|
msgid "No bmi entries found."
|
||||||
msgstr "Keine Timer-Einträge gefunden."
|
msgstr "Keine BMI-Einträge gefunden."
|
||||||
|
|
||||||
#: core/templates/core/child_confirm_delete.html:4
|
#: core/templates/core/child_confirm_delete.html:4
|
||||||
msgid "Delete a Child"
|
msgid "Delete a Child"
|
||||||
|
@ -1165,6 +1169,7 @@ msgstr "Windelwechsel hinzufügen"
|
||||||
#: core/templates/core/feeding_form.html:17
|
#: core/templates/core/feeding_form.html:17
|
||||||
#: core/templates/core/note_form.html:17 core/templates/core/sleep_form.html:17
|
#: core/templates/core/note_form.html:17 core/templates/core/sleep_form.html:17
|
||||||
#: core/templates/core/tummytime_form.html:17
|
#: core/templates/core/tummytime_form.html:17
|
||||||
|
#: core/templates/core/widget_tag_editor.html:24
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr "Hinzufügen"
|
msgstr "Hinzufügen"
|
||||||
|
|
||||||
|
@ -1206,34 +1211,28 @@ msgid "No feedings found."
|
||||||
msgstr "Keine Mahlzeit gefunden."
|
msgstr "Keine Mahlzeit gefunden."
|
||||||
|
|
||||||
#: core/templates/core/head_circumference_confirm_delete.html:4
|
#: core/templates/core/head_circumference_confirm_delete.html:4
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Delete a Tummy Time Entry"
|
|
||||||
msgid "Delete a Head Circumference Entry"
|
msgid "Delete a Head Circumference Entry"
|
||||||
msgstr "Bauchzeit-Eintrag löschen"
|
msgstr "Kopfumfang löschen"
|
||||||
|
|
||||||
#: core/templates/core/head_circumference_form.html:8
|
#: core/templates/core/head_circumference_form.html:8
|
||||||
#: core/templates/core/head_circumference_form.html:17
|
#: core/templates/core/head_circumference_form.html:17
|
||||||
#: core/templates/core/head_circumference_form.html:27
|
#: core/templates/core/head_circumference_form.html:27
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Add a Temperature Entry"
|
|
||||||
msgid "Add a Head Circumference Entry"
|
msgid "Add a Head Circumference Entry"
|
||||||
msgstr "Temperaturmessung hinzufügen"
|
msgstr "Kopfumfang hinzufügen"
|
||||||
|
|
||||||
#: core/templates/core/head_circumference_list.html:15
|
#: core/templates/core/head_circumference_list.html:15
|
||||||
msgid "Add Head Circumference"
|
msgid "Add Head Circumference"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/core/head_circumference_list.html:66
|
#: core/templates/core/head_circumference_list.html:66
|
||||||
#, fuzzy
|
|
||||||
#| msgid "No timer entries found."
|
|
||||||
msgid "No head circumference entries found."
|
msgid "No head circumference entries found."
|
||||||
msgstr "Keine Timer-Einträge gefunden."
|
msgstr "Keine Kopfumfang-Einträge gefunden."
|
||||||
|
|
||||||
#: core/templates/core/height_confirm_delete.html:4
|
#: core/templates/core/height_confirm_delete.html:4
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Delete a Weight Entry"
|
#| msgid "Delete a Weight Entry"
|
||||||
msgid "Delete a Height Entry"
|
msgid "Delete a Height Entry"
|
||||||
msgstr "Gewichts-Eintrag löschen"
|
msgstr "Größen-Eintrag löschen"
|
||||||
|
|
||||||
#: core/templates/core/height_form.html:8
|
#: core/templates/core/height_form.html:8
|
||||||
#: core/templates/core/height_form.html:17
|
#: core/templates/core/height_form.html:17
|
||||||
|
@ -1241,19 +1240,17 @@ msgstr "Gewichts-Eintrag löschen"
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Add a Weight Entry"
|
#| msgid "Add a Weight Entry"
|
||||||
msgid "Add a Height Entry"
|
msgid "Add a Height Entry"
|
||||||
msgstr "Gewichts-Eintrag hinzufügen"
|
msgstr "Größen-Eintrag hinzufügen"
|
||||||
|
|
||||||
#: core/templates/core/height_list.html:15
|
#: core/templates/core/height_list.html:15
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Add Weight"
|
#| msgid "Add Weight"
|
||||||
msgid "Add Height"
|
msgid "Add Height"
|
||||||
msgstr "Gewicht hinzufügen"
|
msgstr "Größe hinzufügen"
|
||||||
|
|
||||||
#: core/templates/core/height_list.html:66
|
#: core/templates/core/height_list.html:66
|
||||||
#, fuzzy
|
|
||||||
#| msgid "No weight entries found."
|
|
||||||
msgid "No height entries found."
|
msgid "No height entries found."
|
||||||
msgstr "Keine Gewichts-Einträge gefunden."
|
msgstr "Keine Größen-Einträge gefunden."
|
||||||
|
|
||||||
#: core/templates/core/note_confirm_delete.html:4
|
#: core/templates/core/note_confirm_delete.html:4
|
||||||
msgid "Delete a Note"
|
msgid "Delete a Note"
|
||||||
|
@ -1450,6 +1447,44 @@ msgstr "Gewicht hinzufügen"
|
||||||
msgid "No weight entries found."
|
msgid "No weight entries found."
|
||||||
msgstr "Keine Gewichts-Einträge gefunden."
|
msgstr "Keine Gewichts-Einträge gefunden."
|
||||||
|
|
||||||
|
#: core/templates/core/widget_tag_editor.html:22
|
||||||
|
msgid "Tag name"
|
||||||
|
msgstr "Tag-Name"
|
||||||
|
|
||||||
|
#: core/templates/core/widget_tag_editor.html:27
|
||||||
|
msgid "Recently used:"
|
||||||
|
msgstr "Kürzlich verwendet:"
|
||||||
|
|
||||||
|
#: core/templates/core/widget_tag_editor.html:45
|
||||||
|
msgctxt "Error modal"
|
||||||
|
msgid "Error"
|
||||||
|
msgstr "Fehler"
|
||||||
|
|
||||||
|
#: core/templates/core/widget_tag_editor.html:50
|
||||||
|
msgctxt "Error modal"
|
||||||
|
msgid "An error ocurred."
|
||||||
|
msgstr "Ein Fehler ist aufgetreten."
|
||||||
|
|
||||||
|
#: core/templates/core/widget_tag_editor.html:51
|
||||||
|
msgctxt "Error modal"
|
||||||
|
msgid "Invalid tag name."
|
||||||
|
msgstr "Ungültiger Tag-Name."
|
||||||
|
|
||||||
|
#: core/templates/core/widget_tag_editor.html:52
|
||||||
|
msgctxt "Error modal"
|
||||||
|
msgid "Failed to create tag."
|
||||||
|
msgstr "Fehler bein erzeugen des tags."
|
||||||
|
|
||||||
|
#: core/templates/core/widget_tag_editor.html:53
|
||||||
|
msgctxt "Error modal"
|
||||||
|
msgid "Failed to obtain tag data."
|
||||||
|
msgstr "Konnte Tag nicht laden."
|
||||||
|
|
||||||
|
#: core/templates/core/widget_tag_editor.html:58
|
||||||
|
msgctxt "Error modal"
|
||||||
|
msgid "Close"
|
||||||
|
msgstr "Schließen"
|
||||||
|
|
||||||
#: core/templates/timeline/_timeline.html:33
|
#: core/templates/timeline/_timeline.html:33
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(since)s ago (%(time)s)"
|
msgid "%(since)s ago (%(time)s)"
|
||||||
|
@ -1467,7 +1502,7 @@ msgid "%(since)s since previous"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/timeline/_timeline.html:56
|
#: core/templates/timeline/_timeline.html:56
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:41
|
#: dashboard/templates/dashboard/child_button_group.html:20
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -1730,53 +1765,12 @@ msgstr "Nie"
|
||||||
msgid "Child actions"
|
msgid "Child actions"
|
||||||
msgstr "Aktionen des Kindes"
|
msgstr "Aktionen des Kindes"
|
||||||
|
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:17
|
#: dashboard/templates/dashboard/child_button_group.html:12
|
||||||
#: reports/templates/reports/report_base.html:9
|
#: reports/templates/reports/base.html:9
|
||||||
|
#: reports/templates/reports/report_list.html:4
|
||||||
msgid "Reports"
|
msgid "Reports"
|
||||||
msgstr "Reports"
|
msgstr "Reports"
|
||||||
|
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:23
|
|
||||||
msgid "Diaper Change Amounts"
|
|
||||||
msgstr "Windelwechsel Mengen"
|
|
||||||
|
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:24
|
|
||||||
#: reports/templates/reports/diaperchange_types.html:4
|
|
||||||
#: reports/templates/reports/diaperchange_types.html:8
|
|
||||||
msgid "Diaper Change Types"
|
|
||||||
msgstr "Windewechsel Typen"
|
|
||||||
|
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:25
|
|
||||||
#: reports/templates/reports/diaperchange_lifetimes.html:4
|
|
||||||
#: reports/templates/reports/diaperchange_lifetimes.html:8
|
|
||||||
msgid "Diaper Lifetimes"
|
|
||||||
msgstr "Windel-Lebensdauer"
|
|
||||||
|
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:26
|
|
||||||
#: reports/templates/reports/feeding_amounts.html:4
|
|
||||||
#: reports/templates/reports/feeding_amounts.html:8
|
|
||||||
msgid "Feeding Amounts"
|
|
||||||
msgstr "Mahlzeiten"
|
|
||||||
|
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:27
|
|
||||||
msgid "Feeding Durations (Average)"
|
|
||||||
msgstr "Mahlzeit Dauer (Durschschnitt)"
|
|
||||||
|
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:28
|
|
||||||
#: reports/templates/reports/sleep_pattern.html:4
|
|
||||||
#: reports/templates/reports/sleep_pattern.html:8
|
|
||||||
msgid "Sleep Pattern"
|
|
||||||
msgstr "Schlafrhythmus"
|
|
||||||
|
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:29
|
|
||||||
#: reports/templates/reports/sleep_totals.html:4
|
|
||||||
#: reports/templates/reports/sleep_totals.html:8
|
|
||||||
msgid "Sleep Totals"
|
|
||||||
msgstr "Schlaf Total"
|
|
||||||
|
|
||||||
#: dashboard/templates/dashboard/child_button_group.html:30
|
|
||||||
msgid "Tummy Time Durations (Sum)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: dashboard/templatetags/cards.py:288
|
#: dashboard/templatetags/cards.py:288
|
||||||
msgid "Diaper change frequency"
|
msgid "Diaper change frequency"
|
||||||
msgstr "Frequenz Windelwechsel"
|
msgstr "Frequenz Windelwechsel"
|
||||||
|
@ -1817,7 +1811,7 @@ msgstr "Gewichtsänderung pro Woche"
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Weight change per week"
|
#| msgid "Weight change per week"
|
||||||
msgid "BMI change per week"
|
msgid "BMI change per week"
|
||||||
msgstr "Gewichtsänderung pro Woche"
|
msgstr "BMI-änderung pro Woche"
|
||||||
|
|
||||||
#: dashboard/templatetags/cards.py:418
|
#: dashboard/templatetags/cards.py:418
|
||||||
msgid "Feeding frequency (past 3 days)"
|
msgid "Feeding frequency (past 3 days)"
|
||||||
|
@ -1835,7 +1829,7 @@ msgstr "Freuqenz Mahlzeiten"
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "<b>Weight</b>"
|
#| msgid "<b>Weight</b>"
|
||||||
msgid "<b>BMI</b>"
|
msgid "<b>BMI</b>"
|
||||||
msgstr "<b>Gewicht</b>"
|
msgstr "<b>BMI</b>"
|
||||||
|
|
||||||
#: reports/graphs/diaperchange_amounts.py:27
|
#: reports/graphs/diaperchange_amounts.py:27
|
||||||
msgid "Diaper change amount"
|
msgid "Diaper change amount"
|
||||||
|
@ -1955,15 +1949,61 @@ msgstr "<b>Gewicht</b>"
|
||||||
msgid "Diaper Amounts"
|
msgid "Diaper Amounts"
|
||||||
msgstr "Windel Mengen"
|
msgstr "Windel Mengen"
|
||||||
|
|
||||||
|
#: reports/templates/reports/diaperchange_lifetimes.html:4
|
||||||
|
#: reports/templates/reports/diaperchange_lifetimes.html:8
|
||||||
|
#: reports/templates/reports/report_list.html:13
|
||||||
|
msgid "Diaper Lifetimes"
|
||||||
|
msgstr "Windel-Lebensdauer"
|
||||||
|
|
||||||
|
#: reports/templates/reports/diaperchange_types.html:4
|
||||||
|
#: reports/templates/reports/diaperchange_types.html:8
|
||||||
|
#: reports/templates/reports/report_list.html:12
|
||||||
|
msgid "Diaper Change Types"
|
||||||
|
msgstr "Windewechsel Typen"
|
||||||
|
|
||||||
|
#: reports/templates/reports/feeding_amounts.html:4
|
||||||
|
#: reports/templates/reports/feeding_amounts.html:8
|
||||||
|
#: reports/templates/reports/report_list.html:14
|
||||||
|
msgid "Feeding Amounts"
|
||||||
|
msgstr "Mahlzeiten"
|
||||||
|
|
||||||
#: reports/templates/reports/feeding_duration.html:4
|
#: reports/templates/reports/feeding_duration.html:4
|
||||||
#: reports/templates/reports/feeding_duration.html:8
|
#: reports/templates/reports/feeding_duration.html:8
|
||||||
msgid "Average Feeding Durations"
|
msgid "Average Feeding Durations"
|
||||||
msgstr "Durchschnittliche Mahlzeitendauer"
|
msgstr "Durchschnittliche Mahlzeitendauer"
|
||||||
|
|
||||||
#: reports/templates/reports/report_base.html:19
|
#: reports/templates/reports/report_base.html:17
|
||||||
msgid "There is not enough data to generate this report."
|
msgid "There is not enough data to generate this report."
|
||||||
msgstr "Es gibt nicht genügend Daten um diesen Report zu generieren."
|
msgstr "Es gibt nicht genügend Daten um diesen Report zu generieren."
|
||||||
|
|
||||||
|
#: reports/templates/reports/report_list.html:10
|
||||||
|
msgid "Body Mass Index (BMI)"
|
||||||
|
msgstr "Body Mass Index (BMI)"
|
||||||
|
|
||||||
|
#: reports/templates/reports/report_list.html:11
|
||||||
|
msgid "Diaper Change Amounts"
|
||||||
|
msgstr "Windelwechsel Mengen"
|
||||||
|
|
||||||
|
#: reports/templates/reports/report_list.html:15
|
||||||
|
msgid "Feeding Durations (Average)"
|
||||||
|
msgstr "Mahlzeit Dauer (Durschschnitt)"
|
||||||
|
|
||||||
|
#: reports/templates/reports/report_list.html:18
|
||||||
|
#: reports/templates/reports/sleep_pattern.html:4
|
||||||
|
#: reports/templates/reports/sleep_pattern.html:8
|
||||||
|
msgid "Sleep Pattern"
|
||||||
|
msgstr "Schlafrhythmus"
|
||||||
|
|
||||||
|
#: reports/templates/reports/report_list.html:19
|
||||||
|
#: reports/templates/reports/sleep_totals.html:4
|
||||||
|
#: reports/templates/reports/sleep_totals.html:8
|
||||||
|
msgid "Sleep Totals"
|
||||||
|
msgstr "Schlaf Total"
|
||||||
|
|
||||||
|
#: reports/templates/reports/report_list.html:20
|
||||||
|
msgid "Tummy Time Durations (Sum)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: reports/templates/reports/tummytime_duration.html:4
|
#: reports/templates/reports/tummytime_duration.html:4
|
||||||
#: reports/templates/reports/tummytime_duration.html:8
|
#: reports/templates/reports/tummytime_duration.html:8
|
||||||
msgid "Total Tummy Time Durations"
|
msgid "Total Tummy Time Durations"
|
||||||
|
|
|
@ -7,8 +7,9 @@
|
||||||
|
|
||||||
-i https://pypi.python.org/simple
|
-i https://pypi.python.org/simple
|
||||||
asgiref==3.5.0; python_version >= '3.7'
|
asgiref==3.5.0; python_version >= '3.7'
|
||||||
boto3==1.20.52
|
backports.zoneinfo==0.2.1; python_version < '3.9'
|
||||||
botocore==1.23.52; python_version >= '3.6'
|
boto3==1.21.11
|
||||||
|
botocore==1.24.11; python_version >= '3.6'
|
||||||
defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||||
diff-match-patch==20200713; python_version >= '2.7'
|
diff-match-patch==20200713; python_version >= '2.7'
|
||||||
dj-database-url==0.5.0
|
dj-database-url==0.5.0
|
||||||
|
@ -19,11 +20,12 @@ django-imagekit==4.1.0
|
||||||
django-import-export==2.7.1
|
django-import-export==2.7.1
|
||||||
django-ipware==4.0.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
django-ipware==4.0.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||||
django-storages==1.12.3
|
django-storages==1.12.3
|
||||||
|
django-taggit==2.1.0
|
||||||
django-widget-tweaks==1.4.12
|
django-widget-tweaks==1.4.12
|
||||||
django==4.0.2
|
django==4.0.3
|
||||||
djangorestframework==3.13.1
|
djangorestframework==3.13.1
|
||||||
et-xmlfile==1.1.0; python_version >= '3.6'
|
et-xmlfile==1.1.0; python_version >= '3.6'
|
||||||
faker==12.2.0
|
faker==13.3.0
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
jmespath==0.10.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
jmespath==0.10.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||||
markuppy==1.14
|
markuppy==1.14
|
||||||
|
@ -37,8 +39,8 @@ python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0,
|
||||||
python-dotenv==0.19.2
|
python-dotenv==0.19.2
|
||||||
pytz==2021.3
|
pytz==2021.3
|
||||||
pyyaml==6.0
|
pyyaml==6.0
|
||||||
s3transfer==0.5.1; python_version >= '3.6'
|
s3transfer==0.5.2; python_version >= '3.6'
|
||||||
setuptools==60.8.2; python_version >= '3.7'
|
setuptools==60.9.3; python_version >= '3.7'
|
||||||
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||||
sqlparse==0.4.2; python_version >= '3.5'
|
sqlparse==0.4.2; python_version >= '3.5'
|
||||||
tablib[html,ods,xls,xlsx,yaml]==3.2.0; python_version >= '3.7'
|
tablib[html,ods,xls,xlsx,yaml]==3.2.0; python_version >= '3.7'
|
||||||
|
|
|
@ -10586,6 +10586,10 @@ h3 {
|
||||||
font-size: 1.65em;
|
font-size: 1.65em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
#view-core\:child .child-photo {
|
#view-core\:child .child-photo {
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
}
|
}
|
Binary file not shown.
|
@ -10586,6 +10586,10 @@ h3 {
|
||||||
font-size: 1.65em;
|
font-size: 1.65em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
#view-core\:child .child-photo {
|
#view-core\:child .child-photo {
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
|
@ -13248,46 +13248,46 @@ function transpose(out, a) {
|
||||||
};
|
};
|
||||||
},{}],64:[function(_dereq_,module,exports){
|
},{}],64:[function(_dereq_,module,exports){
|
||||||
(function (global){(function (){
|
(function (global){(function (){
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
var isBrowser = _dereq_('is-browser')
|
var isBrowser = _dereq_('is-browser')
|
||||||
var hasHover
|
var hasHover
|
||||||
|
|
||||||
if (typeof global.matchMedia === 'function') {
|
if (typeof global.matchMedia === 'function') {
|
||||||
hasHover = !global.matchMedia('(hover: none)').matches
|
hasHover = !global.matchMedia('(hover: none)').matches
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
hasHover = isBrowser
|
hasHover = isBrowser
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = hasHover
|
module.exports = hasHover
|
||||||
|
|
||||||
}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
||||||
},{"is-browser":68}],65:[function(_dereq_,module,exports){
|
},{"is-browser":68}],65:[function(_dereq_,module,exports){
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
var isBrowser = _dereq_('is-browser')
|
var isBrowser = _dereq_('is-browser')
|
||||||
|
|
||||||
function detect() {
|
function detect() {
|
||||||
var supported = false
|
var supported = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var opts = Object.defineProperty({}, 'passive', {
|
var opts = Object.defineProperty({}, 'passive', {
|
||||||
get: function() {
|
get: function() {
|
||||||
supported = true
|
supported = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
window.addEventListener('test', null, opts)
|
window.addEventListener('test', null, opts)
|
||||||
window.removeEventListener('test', null, opts)
|
window.removeEventListener('test', null, opts)
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
supported = false
|
supported = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return supported
|
return supported
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = isBrowser && detect()
|
module.exports = isBrowser && detect()
|
||||||
|
|
||||||
},{"is-browser":68}],66:[function(_dereq_,module,exports){
|
},{"is-browser":68}],66:[function(_dereq_,module,exports){
|
||||||
exports.read = function (buffer, offset, isLE, mLen, nBytes) {
|
exports.read = function (buffer, offset, isLE, mLen, nBytes) {
|
||||||
|
@ -13407,78 +13407,78 @@ if (typeof Object.create === 'function') {
|
||||||
},{}],68:[function(_dereq_,module,exports){
|
},{}],68:[function(_dereq_,module,exports){
|
||||||
module.exports = true;
|
module.exports = true;
|
||||||
},{}],69:[function(_dereq_,module,exports){
|
},{}],69:[function(_dereq_,module,exports){
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
module.exports = isMobile
|
module.exports = isMobile
|
||||||
module.exports.isMobile = isMobile
|
module.exports.isMobile = isMobile
|
||||||
module.exports.default = isMobile
|
module.exports.default = isMobile
|
||||||
|
|
||||||
var mobileRE = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i
|
var mobileRE = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i
|
||||||
|
|
||||||
var tabletRE = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino|android|ipad|playbook|silk/i
|
var tabletRE = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino|android|ipad|playbook|silk/i
|
||||||
|
|
||||||
function isMobile (opts) {
|
function isMobile (opts) {
|
||||||
if (!opts) opts = {}
|
if (!opts) opts = {}
|
||||||
var ua = opts.ua
|
var ua = opts.ua
|
||||||
if (!ua && typeof navigator !== 'undefined') ua = navigator.userAgent
|
if (!ua && typeof navigator !== 'undefined') ua = navigator.userAgent
|
||||||
if (ua && ua.headers && typeof ua.headers['user-agent'] === 'string') {
|
if (ua && ua.headers && typeof ua.headers['user-agent'] === 'string') {
|
||||||
ua = ua.headers['user-agent']
|
ua = ua.headers['user-agent']
|
||||||
}
|
}
|
||||||
if (typeof ua !== 'string') return false
|
if (typeof ua !== 'string') return false
|
||||||
|
|
||||||
var result = opts.tablet ? tabletRE.test(ua) : mobileRE.test(ua)
|
var result = opts.tablet ? tabletRE.test(ua) : mobileRE.test(ua)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!result &&
|
!result &&
|
||||||
opts.tablet &&
|
opts.tablet &&
|
||||||
opts.featureDetect &&
|
opts.featureDetect &&
|
||||||
navigator &&
|
navigator &&
|
||||||
navigator.maxTouchPoints > 1 &&
|
navigator.maxTouchPoints > 1 &&
|
||||||
ua.indexOf('Macintosh') !== -1 &&
|
ua.indexOf('Macintosh') !== -1 &&
|
||||||
ua.indexOf('Safari') !== -1
|
ua.indexOf('Safari') !== -1
|
||||||
) {
|
) {
|
||||||
result = true
|
result = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
},{}],70:[function(_dereq_,module,exports){
|
},{}],70:[function(_dereq_,module,exports){
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is this string all whitespace?
|
* Is this string all whitespace?
|
||||||
* This solution kind of makes my brain hurt, but it's significantly faster
|
* This solution kind of makes my brain hurt, but it's significantly faster
|
||||||
* than !str.trim() or any other solution I could find.
|
* than !str.trim() or any other solution I could find.
|
||||||
*
|
*
|
||||||
* whitespace codes from: http://en.wikipedia.org/wiki/Whitespace_character
|
* whitespace codes from: http://en.wikipedia.org/wiki/Whitespace_character
|
||||||
* and verified with:
|
* and verified with:
|
||||||
*
|
*
|
||||||
* for(var i = 0; i < 65536; i++) {
|
* for(var i = 0; i < 65536; i++) {
|
||||||
* var s = String.fromCharCode(i);
|
* var s = String.fromCharCode(i);
|
||||||
* if(+s===0 && !s.trim()) console.log(i, s);
|
* if(+s===0 && !s.trim()) console.log(i, s);
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* which counts a couple of these as *not* whitespace, but finds nothing else
|
* which counts a couple of these as *not* whitespace, but finds nothing else
|
||||||
* that *is* whitespace. Note that charCodeAt stops at 16 bits, but it appears
|
* that *is* whitespace. Note that charCodeAt stops at 16 bits, but it appears
|
||||||
* that there are no whitespace characters above this, and code points above
|
* that there are no whitespace characters above this, and code points above
|
||||||
* this do not map onto white space characters.
|
* this do not map onto white space characters.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = function(str){
|
module.exports = function(str){
|
||||||
var l = str.length,
|
var l = str.length,
|
||||||
a;
|
a;
|
||||||
for(var i = 0; i < l; i++) {
|
for(var i = 0; i < l; i++) {
|
||||||
a = str.charCodeAt(i);
|
a = str.charCodeAt(i);
|
||||||
if((a < 9 || a > 13) && (a !== 32) && (a !== 133) && (a !== 160) &&
|
if((a < 9 || a > 13) && (a !== 32) && (a !== 133) && (a !== 160) &&
|
||||||
(a !== 5760) && (a !== 6158) && (a < 8192 || a > 8205) &&
|
(a !== 5760) && (a !== 6158) && (a < 8192 || a > 8205) &&
|
||||||
(a !== 8232) && (a !== 8233) && (a !== 8239) && (a !== 8287) &&
|
(a !== 8232) && (a !== 8233) && (a !== 8239) && (a !== 8287) &&
|
||||||
(a !== 8288) && (a !== 12288) && (a !== 65279)) {
|
(a !== 8288) && (a !== 12288) && (a !== 65279)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
},{}],71:[function(_dereq_,module,exports){
|
},{}],71:[function(_dereq_,module,exports){
|
||||||
var rootPosition = { left: 0, top: 0 }
|
var rootPosition = { left: 0, top: 0 }
|
||||||
|
|
|
@ -0,0 +1,354 @@
|
||||||
|
(function() {
|
||||||
|
/**
|
||||||
|
* Parse a string as hexadecimal number
|
||||||
|
*/
|
||||||
|
function hexParse(x) {
|
||||||
|
return parseInt(x, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the contrasting color for any hex color
|
||||||
|
*
|
||||||
|
* Sourced from: https://vanillajstoolkit.com/helpers/getcontrast/
|
||||||
|
* - Modified with slightly softer colors
|
||||||
|
* (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com
|
||||||
|
* Derived from work by Brian Suda, https://24ways.org/2010/calculating-color-contrast/
|
||||||
|
* @param {String} A hexcolor value
|
||||||
|
* @return {String} The contrasting color (black or white)
|
||||||
|
*/
|
||||||
|
function computeComplementaryColor(hexcolor) {
|
||||||
|
|
||||||
|
// If a leading # is provided, remove it
|
||||||
|
if (hexcolor.slice(0, 1) === '#') {
|
||||||
|
hexcolor = hexcolor.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a three-character hexcode, make six-character
|
||||||
|
if (hexcolor.length === 3) {
|
||||||
|
hexcolor = hexcolor.split('').map(function (hex) {
|
||||||
|
return hex + hex;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to RGB value
|
||||||
|
let r = parseInt(hexcolor.substr(0,2),16);
|
||||||
|
let g = parseInt(hexcolor.substr(2,2),16);
|
||||||
|
let b = parseInt(hexcolor.substr(4,2),16);
|
||||||
|
|
||||||
|
// Get YIQ ratio
|
||||||
|
let yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
||||||
|
|
||||||
|
// Check contrast
|
||||||
|
return (yiq >= 128) ? '#101010' : '#EFEFEF';
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF token should always be present because it is auto-included with
|
||||||
|
// every tag-editor widget
|
||||||
|
const CSRF_TOKEN = document.querySelector('input[name="csrfmiddlewaretoken"]').value;
|
||||||
|
|
||||||
|
function doReq(method, uri, data, success, fail) {
|
||||||
|
// TODO: prefer jQuery based requests for now
|
||||||
|
|
||||||
|
const req = new XMLHttpRequest();
|
||||||
|
req.addEventListener('load', () => {
|
||||||
|
if ((req.status >= 200) && (req.status < 300)) {
|
||||||
|
success(req.responseText, req);
|
||||||
|
} else {
|
||||||
|
fail(req.responseText, req);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const name of ["error", "timeout", "abort"]) {
|
||||||
|
req.addEventListener(name, () => {
|
||||||
|
fail(req.responseText, req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
req.timeout = 20000;
|
||||||
|
|
||||||
|
req.open(method, uri);
|
||||||
|
req.setRequestHeader("Content-Type", "application/json");
|
||||||
|
req.setRequestHeader("Accept", "application/json");
|
||||||
|
req.setRequestHeader("X-CSRFTOKEN", CSRF_TOKEN);
|
||||||
|
req.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class allowing generic operations on the tag lists, like:
|
||||||
|
*
|
||||||
|
* - Adding tags to a tag list
|
||||||
|
* - Updating or creating new tags with a set name and color
|
||||||
|
* - Controlling the error modal
|
||||||
|
*/
|
||||||
|
class TaggingBase {
|
||||||
|
constructor(widget) {
|
||||||
|
this.prototype = widget.querySelector('.prototype-tag');
|
||||||
|
this.listeners = [];
|
||||||
|
|
||||||
|
this.modalElement = widget.querySelector('.tag-editor-error-modal');
|
||||||
|
this.modalBodyNode = this.modalElement.querySelector('.modal-body');
|
||||||
|
|
||||||
|
// Clean whitespace text nodes between spans
|
||||||
|
for (const n of this.modalBodyNode.childNodes) {
|
||||||
|
if (n.nodeType === Node.TEXT_NODE) {
|
||||||
|
this.modalBodyNode.removeChild(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal(msg) {
|
||||||
|
const selectedMessage = this.modalBodyNode.querySelector(`span[data-message='${msg}']`);
|
||||||
|
if (!selectedMessage) {
|
||||||
|
selectedMessage = this.modalBodyNode.childNodes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const n of this.modalBodyNode.childNodes) {
|
||||||
|
n.classList.add('d-none');
|
||||||
|
}
|
||||||
|
selectedMessage.classList.remove('d-none');
|
||||||
|
|
||||||
|
jQuery(this.modalElement).modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
addTagListUpdatedListener(c) {
|
||||||
|
this.listeners.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
callTagListUpdatedListeners() {
|
||||||
|
for (const l of this.listeners) {
|
||||||
|
l();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTag(tag, name, color, actionSymbol) {
|
||||||
|
const actionTextNode = tag.querySelector('.add-remove-icon').childNodes[0];
|
||||||
|
|
||||||
|
name = name || tag.getAttribute("data-value");
|
||||||
|
color = color || tag.getAttribute("data-color");
|
||||||
|
actionSymbol = actionSymbol || actionTextNode.textContent;
|
||||||
|
|
||||||
|
tag.childNodes[0].textContent = name;
|
||||||
|
tag.setAttribute("data-value", name);
|
||||||
|
tag.setAttribute("data-color", color);
|
||||||
|
|
||||||
|
const textColor = computeComplementaryColor(color);
|
||||||
|
tag.setAttribute('style', `background-color: ${color}; color: ${textColor};`);
|
||||||
|
actionTextNode.textContent = actionSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewTag(name, color, actionSymbol) {
|
||||||
|
const tag = this.prototype.cloneNode(true);
|
||||||
|
tag.classList.remove("prototype-tag");
|
||||||
|
tag.classList.add("tag");
|
||||||
|
this.updateTag(tag, name, color, actionSymbol);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertTag(list, tag) {
|
||||||
|
list.appendChild(tag);
|
||||||
|
this.callTagListUpdatedListeners();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the edit field allowing to dynamically create new tags.
|
||||||
|
*
|
||||||
|
* Handles user inputs for the editor. Calls the 'onInsertNewTag' callback
|
||||||
|
* when the creation of a new tag has been requested. All backend handling
|
||||||
|
* like guareteening that the requested tag exists is handled by this class,
|
||||||
|
* the only task left is to add the new tag to the tags-list when
|
||||||
|
* 'onInsertNewTag' is called.
|
||||||
|
*/
|
||||||
|
class AddNewTagControl {
|
||||||
|
/**
|
||||||
|
* @param widget
|
||||||
|
* The root DOM element of the widget
|
||||||
|
* @param taggingBase
|
||||||
|
* Reference to a common TaggingBase class to be used by this widget
|
||||||
|
* @param onInsertNewTag
|
||||||
|
* Callback that is called when a new tag should be added to the
|
||||||
|
* tags widget.
|
||||||
|
*/
|
||||||
|
constructor(widget, taggingBase, onInsertNewTag) {
|
||||||
|
this.widget = widget;
|
||||||
|
this.taggingBase = taggingBase;
|
||||||
|
|
||||||
|
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.addTagInput.value = "";
|
||||||
|
|
||||||
|
this.onInsertNewTag = onInsertNewTag;
|
||||||
|
|
||||||
|
this.addTagButton.addEventListener('click', () => this.onCreateTagClicked());
|
||||||
|
this.addTagInput.addEventListener('keydown', (e) => {
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
if (key === "enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.onCreateTagClicked();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback called when the the "Add" button of the add-tag input is
|
||||||
|
* clicked or enter is pressed in the editor.
|
||||||
|
*/
|
||||||
|
onCreateTagClicked() {
|
||||||
|
// TODO: Make promise based
|
||||||
|
|
||||||
|
const tagName = this.addTagInput.value.trim();
|
||||||
|
const uriTagName = encodeURIComponent(tagName);
|
||||||
|
|
||||||
|
const fail = (msg) => {
|
||||||
|
this.addTagInput.select();
|
||||||
|
this.taggingBase.showModal(msg || "generic");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!tagName) {
|
||||||
|
fail('invalid-tag-name');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addTag = (name, color) => {
|
||||||
|
const tag = this.taggingBase.createNewTag(name, color, "-");
|
||||||
|
this.addTagInput.value = "";
|
||||||
|
this.onInsertNewTag(tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = JSON.stringify({
|
||||||
|
'name': this.addTagInput.value
|
||||||
|
});
|
||||||
|
|
||||||
|
doReq("GET", `${this.apiTagsUrl}?name=${uriTagName}`, null,
|
||||||
|
(text) => {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
if (json.count) {
|
||||||
|
const tagJson = json.results[0];
|
||||||
|
addTag(tagJson.name, tagJson.color);
|
||||||
|
} else {
|
||||||
|
doReq("POST", this.apiTagsUrl, data,
|
||||||
|
(text) => {
|
||||||
|
const tagJson = JSON.parse(text);
|
||||||
|
addTag(tagJson.name, tagJson.color);
|
||||||
|
}, () => fail("tag-creation-failed")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, () => fail("tag-checking-failed")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaScript implementation for the tags editor.
|
||||||
|
*
|
||||||
|
* This class uses TaggingBase and AddNewTagControl to provide the custom
|
||||||
|
* tag editor controls. This mainly consists of updating the hidden
|
||||||
|
* input values with the current list of tags and adding/removing
|
||||||
|
* tags from the current-tags- or recently-used-lists.
|
||||||
|
*/
|
||||||
|
class TagsEditor {
|
||||||
|
/**
|
||||||
|
* @param tagEditorRoot
|
||||||
|
* The root DOM element of the widget.
|
||||||
|
*/
|
||||||
|
constructor(tagEditorRoot) {
|
||||||
|
this.widget = tagEditorRoot;
|
||||||
|
this.taggingBase = new TaggingBase(this.widget);
|
||||||
|
this.addTagControl = new AddNewTagControl(
|
||||||
|
this.widget, this.taggingBase, (t) => this.insertNewTag(t)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.currentTags = this.widget.querySelector('.current_tags');
|
||||||
|
this.newTags = this.widget.querySelector('.new-tags');
|
||||||
|
this.inputElement = this.widget.querySelector('input[type="hidden"]');
|
||||||
|
|
||||||
|
for (const tag of this.newTags.querySelectorAll(".tag")) {
|
||||||
|
this.configureAddTag(tag);
|
||||||
|
}
|
||||||
|
for (const tag of this.currentTags.querySelectorAll(".tag")) {
|
||||||
|
this.configureRemoveTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateInputList();
|
||||||
|
this.taggingBase.addTagListUpdatedListener(
|
||||||
|
() => this.updateInputList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new tag into the "current tag" list.
|
||||||
|
*
|
||||||
|
* Makes sure that no duplicates are present in the widget before adding
|
||||||
|
* the new tag. If a duplicate is found, the old tag is removed before
|
||||||
|
* the new one is added.
|
||||||
|
*/
|
||||||
|
insertNewTag(tag) {
|
||||||
|
const name = tag.getAttribute("data-value");
|
||||||
|
|
||||||
|
const oldTag = this.widget.querySelector(`span[data-value="${name}"]`);
|
||||||
|
if (oldTag) {
|
||||||
|
oldTag.parentNode.removeChild(oldTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.taggingBase.insertTag(this.currentTags, tag);
|
||||||
|
this.configureRemoveTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registeres a click-callback for a given node.
|
||||||
|
*
|
||||||
|
* The callback chain-calls another callback "onClicked" after
|
||||||
|
* moving the clicked tag from the old tag-list to a new tag list.
|
||||||
|
*/
|
||||||
|
registerNewCallback(tag, newParent, onClicked) {
|
||||||
|
function callback(event) {
|
||||||
|
tag.parentNode.removeChild(tag);
|
||||||
|
this.taggingBase.insertTag(newParent, tag);
|
||||||
|
|
||||||
|
tag.removeEventListener('click', callback);
|
||||||
|
onClicked(tag);
|
||||||
|
}
|
||||||
|
tag.addEventListener('click', callback.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the value of the hidden input element.
|
||||||
|
*
|
||||||
|
* Sets the value from the list of tags added to the currentTags
|
||||||
|
* DOM element.
|
||||||
|
*/
|
||||||
|
updateInputList() {
|
||||||
|
const names = [];
|
||||||
|
for (const tag of this.currentTags.querySelectorAll(".tag")) {
|
||||||
|
const name = tag.getAttribute("data-value");
|
||||||
|
names.push(`"${name}"`);
|
||||||
|
}
|
||||||
|
this.inputElement.value = names.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a tag-DOM element as a "add tag" button.
|
||||||
|
*/
|
||||||
|
configureAddTag(tag) {
|
||||||
|
this.taggingBase.updateTag(tag, null, null, "+");
|
||||||
|
this.registerNewCallback(tag, this.currentTags, () => this.configureRemoveTag(tag));
|
||||||
|
this.updateInputList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a tag-DOM element as a "remove tag" button.
|
||||||
|
*/
|
||||||
|
configureRemoveTag(tag) {
|
||||||
|
this.taggingBase.updateTag(tag, null, null, "-");
|
||||||
|
this.registerNewCallback(tag, this.newTags, () => this.configureAddTag(tag));
|
||||||
|
this.updateInputList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
for (const el of document.querySelectorAll('.babybuddy-tags-editor')) {
|
||||||
|
new TagsEditor(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
Binary file not shown.
|
@ -0,0 +1,354 @@
|
||||||
|
(function() {
|
||||||
|
/**
|
||||||
|
* Parse a string as hexadecimal number
|
||||||
|
*/
|
||||||
|
function hexParse(x) {
|
||||||
|
return parseInt(x, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the contrasting color for any hex color
|
||||||
|
*
|
||||||
|
* Sourced from: https://vanillajstoolkit.com/helpers/getcontrast/
|
||||||
|
* - Modified with slightly softer colors
|
||||||
|
* (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com
|
||||||
|
* Derived from work by Brian Suda, https://24ways.org/2010/calculating-color-contrast/
|
||||||
|
* @param {String} A hexcolor value
|
||||||
|
* @return {String} The contrasting color (black or white)
|
||||||
|
*/
|
||||||
|
function computeComplementaryColor(hexcolor) {
|
||||||
|
|
||||||
|
// If a leading # is provided, remove it
|
||||||
|
if (hexcolor.slice(0, 1) === '#') {
|
||||||
|
hexcolor = hexcolor.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a three-character hexcode, make six-character
|
||||||
|
if (hexcolor.length === 3) {
|
||||||
|
hexcolor = hexcolor.split('').map(function (hex) {
|
||||||
|
return hex + hex;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to RGB value
|
||||||
|
let r = parseInt(hexcolor.substr(0,2),16);
|
||||||
|
let g = parseInt(hexcolor.substr(2,2),16);
|
||||||
|
let b = parseInt(hexcolor.substr(4,2),16);
|
||||||
|
|
||||||
|
// Get YIQ ratio
|
||||||
|
let yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
||||||
|
|
||||||
|
// Check contrast
|
||||||
|
return (yiq >= 128) ? '#101010' : '#EFEFEF';
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF token should always be present because it is auto-included with
|
||||||
|
// every tag-editor widget
|
||||||
|
const CSRF_TOKEN = document.querySelector('input[name="csrfmiddlewaretoken"]').value;
|
||||||
|
|
||||||
|
function doReq(method, uri, data, success, fail) {
|
||||||
|
// TODO: prefer jQuery based requests for now
|
||||||
|
|
||||||
|
const req = new XMLHttpRequest();
|
||||||
|
req.addEventListener('load', () => {
|
||||||
|
if ((req.status >= 200) && (req.status < 300)) {
|
||||||
|
success(req.responseText, req);
|
||||||
|
} else {
|
||||||
|
fail(req.responseText, req);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const name of ["error", "timeout", "abort"]) {
|
||||||
|
req.addEventListener(name, () => {
|
||||||
|
fail(req.responseText, req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
req.timeout = 20000;
|
||||||
|
|
||||||
|
req.open(method, uri);
|
||||||
|
req.setRequestHeader("Content-Type", "application/json");
|
||||||
|
req.setRequestHeader("Accept", "application/json");
|
||||||
|
req.setRequestHeader("X-CSRFTOKEN", CSRF_TOKEN);
|
||||||
|
req.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class allowing generic operations on the tag lists, like:
|
||||||
|
*
|
||||||
|
* - Adding tags to a tag list
|
||||||
|
* - Updating or creating new tags with a set name and color
|
||||||
|
* - Controlling the error modal
|
||||||
|
*/
|
||||||
|
class TaggingBase {
|
||||||
|
constructor(widget) {
|
||||||
|
this.prototype = widget.querySelector('.prototype-tag');
|
||||||
|
this.listeners = [];
|
||||||
|
|
||||||
|
this.modalElement = widget.querySelector('.tag-editor-error-modal');
|
||||||
|
this.modalBodyNode = this.modalElement.querySelector('.modal-body');
|
||||||
|
|
||||||
|
// Clean whitespace text nodes between spans
|
||||||
|
for (const n of this.modalBodyNode.childNodes) {
|
||||||
|
if (n.nodeType === Node.TEXT_NODE) {
|
||||||
|
this.modalBodyNode.removeChild(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal(msg) {
|
||||||
|
const selectedMessage = this.modalBodyNode.querySelector(`span[data-message='${msg}']`);
|
||||||
|
if (!selectedMessage) {
|
||||||
|
selectedMessage = this.modalBodyNode.childNodes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const n of this.modalBodyNode.childNodes) {
|
||||||
|
n.classList.add('d-none');
|
||||||
|
}
|
||||||
|
selectedMessage.classList.remove('d-none');
|
||||||
|
|
||||||
|
jQuery(this.modalElement).modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
addTagListUpdatedListener(c) {
|
||||||
|
this.listeners.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
callTagListUpdatedListeners() {
|
||||||
|
for (const l of this.listeners) {
|
||||||
|
l();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTag(tag, name, color, actionSymbol) {
|
||||||
|
const actionTextNode = tag.querySelector('.add-remove-icon').childNodes[0];
|
||||||
|
|
||||||
|
name = name || tag.getAttribute("data-value");
|
||||||
|
color = color || tag.getAttribute("data-color");
|
||||||
|
actionSymbol = actionSymbol || actionTextNode.textContent;
|
||||||
|
|
||||||
|
tag.childNodes[0].textContent = name;
|
||||||
|
tag.setAttribute("data-value", name);
|
||||||
|
tag.setAttribute("data-color", color);
|
||||||
|
|
||||||
|
const textColor = computeComplementaryColor(color);
|
||||||
|
tag.setAttribute('style', `background-color: ${color}; color: ${textColor};`);
|
||||||
|
actionTextNode.textContent = actionSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewTag(name, color, actionSymbol) {
|
||||||
|
const tag = this.prototype.cloneNode(true);
|
||||||
|
tag.classList.remove("prototype-tag");
|
||||||
|
tag.classList.add("tag");
|
||||||
|
this.updateTag(tag, name, color, actionSymbol);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertTag(list, tag) {
|
||||||
|
list.appendChild(tag);
|
||||||
|
this.callTagListUpdatedListeners();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the edit field allowing to dynamically create new tags.
|
||||||
|
*
|
||||||
|
* Handles user inputs for the editor. Calls the 'onInsertNewTag' callback
|
||||||
|
* when the creation of a new tag has been requested. All backend handling
|
||||||
|
* like guareteening that the requested tag exists is handled by this class,
|
||||||
|
* the only task left is to add the new tag to the tags-list when
|
||||||
|
* 'onInsertNewTag' is called.
|
||||||
|
*/
|
||||||
|
class AddNewTagControl {
|
||||||
|
/**
|
||||||
|
* @param widget
|
||||||
|
* The root DOM element of the widget
|
||||||
|
* @param taggingBase
|
||||||
|
* Reference to a common TaggingBase class to be used by this widget
|
||||||
|
* @param onInsertNewTag
|
||||||
|
* Callback that is called when a new tag should be added to the
|
||||||
|
* tags widget.
|
||||||
|
*/
|
||||||
|
constructor(widget, taggingBase, onInsertNewTag) {
|
||||||
|
this.widget = widget;
|
||||||
|
this.taggingBase = taggingBase;
|
||||||
|
|
||||||
|
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.addTagInput.value = "";
|
||||||
|
|
||||||
|
this.onInsertNewTag = onInsertNewTag;
|
||||||
|
|
||||||
|
this.addTagButton.addEventListener('click', () => this.onCreateTagClicked());
|
||||||
|
this.addTagInput.addEventListener('keydown', (e) => {
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
if (key === "enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.onCreateTagClicked();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback called when the the "Add" button of the add-tag input is
|
||||||
|
* clicked or enter is pressed in the editor.
|
||||||
|
*/
|
||||||
|
onCreateTagClicked() {
|
||||||
|
// TODO: Make promise based
|
||||||
|
|
||||||
|
const tagName = this.addTagInput.value.trim();
|
||||||
|
const uriTagName = encodeURIComponent(tagName);
|
||||||
|
|
||||||
|
const fail = (msg) => {
|
||||||
|
this.addTagInput.select();
|
||||||
|
this.taggingBase.showModal(msg || "generic");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!tagName) {
|
||||||
|
fail('invalid-tag-name');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addTag = (name, color) => {
|
||||||
|
const tag = this.taggingBase.createNewTag(name, color, "-");
|
||||||
|
this.addTagInput.value = "";
|
||||||
|
this.onInsertNewTag(tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = JSON.stringify({
|
||||||
|
'name': this.addTagInput.value
|
||||||
|
});
|
||||||
|
|
||||||
|
doReq("GET", `${this.apiTagsUrl}?name=${uriTagName}`, null,
|
||||||
|
(text) => {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
if (json.count) {
|
||||||
|
const tagJson = json.results[0];
|
||||||
|
addTag(tagJson.name, tagJson.color);
|
||||||
|
} else {
|
||||||
|
doReq("POST", this.apiTagsUrl, data,
|
||||||
|
(text) => {
|
||||||
|
const tagJson = JSON.parse(text);
|
||||||
|
addTag(tagJson.name, tagJson.color);
|
||||||
|
}, () => fail("tag-creation-failed")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, () => fail("tag-checking-failed")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaScript implementation for the tags editor.
|
||||||
|
*
|
||||||
|
* This class uses TaggingBase and AddNewTagControl to provide the custom
|
||||||
|
* tag editor controls. This mainly consists of updating the hidden
|
||||||
|
* input values with the current list of tags and adding/removing
|
||||||
|
* tags from the current-tags- or recently-used-lists.
|
||||||
|
*/
|
||||||
|
class TagsEditor {
|
||||||
|
/**
|
||||||
|
* @param tagEditorRoot
|
||||||
|
* The root DOM element of the widget.
|
||||||
|
*/
|
||||||
|
constructor(tagEditorRoot) {
|
||||||
|
this.widget = tagEditorRoot;
|
||||||
|
this.taggingBase = new TaggingBase(this.widget);
|
||||||
|
this.addTagControl = new AddNewTagControl(
|
||||||
|
this.widget, this.taggingBase, (t) => this.insertNewTag(t)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.currentTags = this.widget.querySelector('.current_tags');
|
||||||
|
this.newTags = this.widget.querySelector('.new-tags');
|
||||||
|
this.inputElement = this.widget.querySelector('input[type="hidden"]');
|
||||||
|
|
||||||
|
for (const tag of this.newTags.querySelectorAll(".tag")) {
|
||||||
|
this.configureAddTag(tag);
|
||||||
|
}
|
||||||
|
for (const tag of this.currentTags.querySelectorAll(".tag")) {
|
||||||
|
this.configureRemoveTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateInputList();
|
||||||
|
this.taggingBase.addTagListUpdatedListener(
|
||||||
|
() => this.updateInputList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new tag into the "current tag" list.
|
||||||
|
*
|
||||||
|
* Makes sure that no duplicates are present in the widget before adding
|
||||||
|
* the new tag. If a duplicate is found, the old tag is removed before
|
||||||
|
* the new one is added.
|
||||||
|
*/
|
||||||
|
insertNewTag(tag) {
|
||||||
|
const name = tag.getAttribute("data-value");
|
||||||
|
|
||||||
|
const oldTag = this.widget.querySelector(`span[data-value="${name}"]`);
|
||||||
|
if (oldTag) {
|
||||||
|
oldTag.parentNode.removeChild(oldTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.taggingBase.insertTag(this.currentTags, tag);
|
||||||
|
this.configureRemoveTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registeres a click-callback for a given node.
|
||||||
|
*
|
||||||
|
* The callback chain-calls another callback "onClicked" after
|
||||||
|
* moving the clicked tag from the old tag-list to a new tag list.
|
||||||
|
*/
|
||||||
|
registerNewCallback(tag, newParent, onClicked) {
|
||||||
|
function callback(event) {
|
||||||
|
tag.parentNode.removeChild(tag);
|
||||||
|
this.taggingBase.insertTag(newParent, tag);
|
||||||
|
|
||||||
|
tag.removeEventListener('click', callback);
|
||||||
|
onClicked(tag);
|
||||||
|
}
|
||||||
|
tag.addEventListener('click', callback.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the value of the hidden input element.
|
||||||
|
*
|
||||||
|
* Sets the value from the list of tags added to the currentTags
|
||||||
|
* DOM element.
|
||||||
|
*/
|
||||||
|
updateInputList() {
|
||||||
|
const names = [];
|
||||||
|
for (const tag of this.currentTags.querySelectorAll(".tag")) {
|
||||||
|
const name = tag.getAttribute("data-value");
|
||||||
|
names.push(`"${name}"`);
|
||||||
|
}
|
||||||
|
this.inputElement.value = names.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a tag-DOM element as a "add tag" button.
|
||||||
|
*/
|
||||||
|
configureAddTag(tag) {
|
||||||
|
this.taggingBase.updateTag(tag, null, null, "+");
|
||||||
|
this.registerNewCallback(tag, this.currentTags, () => this.configureRemoveTag(tag));
|
||||||
|
this.updateInputList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a tag-DOM element as a "remove tag" button.
|
||||||
|
*/
|
||||||
|
configureRemoveTag(tag) {
|
||||||
|
this.taggingBase.updateTag(tag, null, null, "-");
|
||||||
|
this.registerNewCallback(tag, this.newTags, () => this.configureAddTag(tag));
|
||||||
|
this.updateInputList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
for (const el of document.querySelectorAll('.babybuddy-tags-editor')) {
|
||||||
|
new TagsEditor(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
Binary file not shown.
|
@ -26822,35 +26822,35 @@ return Popper;
|
||||||
return moment;
|
return moment;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/*@preserve
|
/*@preserve
|
||||||
* Tempus Dominus Bootstrap4 v5.1.2 (https://tempusdominus.github.io/bootstrap-4/)
|
* Tempus Dominus Bootstrap4 v5.1.2 (https://tempusdominus.github.io/bootstrap-4/)
|
||||||
* Copyright 2016-2018 Jonathan Peterson
|
* Copyright 2016-2018 Jonathan Peterson
|
||||||
* Licensed under MIT (https://github.com/tempusdominus/bootstrap-3/blob/master/LICENSE)
|
* Licensed under MIT (https://github.com/tempusdominus/bootstrap-3/blob/master/LICENSE)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (typeof jQuery === 'undefined') {
|
if (typeof jQuery === 'undefined') {
|
||||||
throw new Error('Tempus Dominus Bootstrap4\'s requires jQuery. jQuery must be included before Tempus Dominus Bootstrap4\'s JavaScript.');
|
throw new Error('Tempus Dominus Bootstrap4\'s requires jQuery. jQuery must be included before Tempus Dominus Bootstrap4\'s JavaScript.');
|
||||||
}
|
}
|
||||||
|
|
||||||
+function ($) {
|
+function ($) {
|
||||||
var version = $.fn.jquery.split(' ')[0].split('.');
|
var version = $.fn.jquery.split(' ')[0].split('.');
|
||||||
if ((version[0] < 2 && version[1] < 9) || (version[0] === 1 && version[1] === 9 && version[2] < 1) || (version[0] >= 4)) {
|
if ((version[0] < 2 && version[1] < 9) || (version[0] === 1 && version[1] === 9 && version[2] < 1) || (version[0] >= 4)) {
|
||||||
throw new Error('Tempus Dominus Bootstrap4\'s requires at least jQuery v3.0.0 but less than v4.0.0');
|
throw new Error('Tempus Dominus Bootstrap4\'s requires at least jQuery v3.0.0 but less than v4.0.0');
|
||||||
}
|
}
|
||||||
}(jQuery);
|
}(jQuery);
|
||||||
|
|
||||||
|
|
||||||
if (typeof moment === 'undefined') {
|
if (typeof moment === 'undefined') {
|
||||||
throw new Error('Tempus Dominus Bootstrap4\'s requires moment.js. Moment.js must be included before Tempus Dominus Bootstrap4\'s JavaScript.');
|
throw new Error('Tempus Dominus Bootstrap4\'s requires moment.js. Moment.js must be included before Tempus Dominus Bootstrap4\'s JavaScript.');
|
||||||
}
|
}
|
||||||
|
|
||||||
var version = moment.version.split('.')
|
var version = moment.version.split('.')
|
||||||
if ((version[0] <= 2 && version[1] < 17) || (version[0] >= 3)) {
|
if ((version[0] <= 2 && version[1] < 17) || (version[0] >= 3)) {
|
||||||
throw new Error('Tempus Dominus Bootstrap4\'s requires at least moment.js v2.17.0 but less than v3.0.0');
|
throw new Error('Tempus Dominus Bootstrap4\'s requires at least moment.js v2.17.0 but less than v3.0.0');
|
||||||
}
|
}
|
||||||
|
|
||||||
+function () {
|
+function () {
|
||||||
|
|
||||||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
|
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
|
||||||
|
|
||||||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
|
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
|
||||||
|
@ -27179,8 +27179,8 @@ var DateTimePicker = function ($, moment) {
|
||||||
this._int();
|
this._int();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
@ -28330,8 +28330,8 @@ var DateTimePicker = function ($, moment) {
|
||||||
return NAME;
|
return NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
}, {
|
}, {
|
||||||
|
@ -28340,8 +28340,8 @@ var DateTimePicker = function ($, moment) {
|
||||||
return DATA_KEY;
|
return DATA_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
}, {
|
}, {
|
||||||
|
@ -28350,8 +28350,8 @@ var DateTimePicker = function ($, moment) {
|
||||||
return EVENT_KEY;
|
return EVENT_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
}, {
|
}, {
|
||||||
|
@ -29537,10 +29537,10 @@ var TempusDominusBootstrap4 = function ($) {
|
||||||
return TempusDominusBootstrap4;
|
return TempusDominusBootstrap4;
|
||||||
}(DateTimePicker);
|
}(DateTimePicker);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ------------------------------------------------------------------------
|
* ------------------------------------------------------------------------
|
||||||
* jQuery
|
* jQuery
|
||||||
* ------------------------------------------------------------------------
|
* ------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
@ -29598,6 +29598,6 @@ var TempusDominusBootstrap4 = function ($) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return TempusDominusBootstrap4;
|
return TempusDominusBootstrap4;
|
||||||
}(jQuery);
|
}(jQuery);
|
||||||
|
|
||||||
}();
|
}();
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue