mirror of https://github.com/snachodog/mybuddy.git
Initial (bugged) work on tag editor
This commit is contained in:
parent
2037035e6d
commit
bf49cc92ad
2
Pipfile
2
Pipfile
|
@ -22,6 +22,7 @@ python-dotenv = "*"
|
|||
pyyaml = "*"
|
||||
uritemplate = "*"
|
||||
whitenoise = "*"
|
||||
django-taggit = "==2.1.0"
|
||||
|
||||
[dev-packages]
|
||||
coveralls = "*"
|
||||
|
@ -30,3 +31,4 @@ mkdocs = "*"
|
|||
mkdocs-material = "*"
|
||||
tblib = "*"
|
||||
black = "*"
|
||||
pydevd = "*"
|
||||
|
|
|
@ -6,6 +6,10 @@ from rest_framework.exceptions import ValidationError
|
|||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
|
||||
from taggit.serializers import (
|
||||
TagListSerializerField, TaggitSerializer
|
||||
)
|
||||
|
||||
from core import models
|
||||
|
||||
|
||||
|
@ -125,10 +129,12 @@ class FeedingSerializer(CoreModelWithDurationSerializer):
|
|||
)
|
||||
|
||||
|
||||
class NoteSerializer(CoreModelSerializer):
|
||||
class NoteSerializer(TaggitSerializer, CoreModelSerializer):
|
||||
class Meta:
|
||||
model = models.Note
|
||||
fields = ("id", "child", "note", "time")
|
||||
fields = ("id", "child", "note", "time", "tags")
|
||||
|
||||
tags = TagListSerializerField()
|
||||
|
||||
|
||||
class SleepSerializer(CoreModelWithDurationSerializer):
|
||||
|
|
|
@ -8,7 +8,6 @@ from core import models
|
|||
from . import serializers, filters
|
||||
from .mixins import TimerFieldSupportMixin
|
||||
|
||||
|
||||
class ChildViewSet(viewsets.ModelViewSet):
|
||||
queryset = models.Child.objects.all()
|
||||
serializer_class = serializers.ChildSerializer
|
||||
|
|
|
@ -26,6 +26,7 @@ INSTALLED_APPS = [
|
|||
"api",
|
||||
"babybuddy",
|
||||
"core",
|
||||
"taggit",
|
||||
"dashboard",
|
||||
"reports",
|
||||
"axes",
|
||||
|
|
|
@ -177,3 +177,14 @@ class WeightAdmin(ImportExportMixin, ExportActionMixin, admin.ModelAdmin):
|
|||
"weight",
|
||||
)
|
||||
resource_class = WeightImportExportResource
|
||||
|
||||
class BabyBuddyTaggedItemInline(admin.StackedInline):
|
||||
model = models.BabyBuddyTagged
|
||||
|
||||
@admin.register(models.BabyBuddyTag)
|
||||
class BabyBuddyTagAdmin(admin.ModelAdmin):
|
||||
inlines = [BabyBuddyTaggedItemInline]
|
||||
list_display = ["name", "slug", "color", "last_used"]
|
||||
ordering = ["name", "slug"]
|
||||
search_fields = ["name"]
|
||||
prepopulated_fields = {"slug": ["name"]}
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.utils import timezone
|
|||
from django.utils.translation import gettext as _
|
||||
|
||||
from core import models
|
||||
|
||||
from core.widgets import TagsEditor
|
||||
|
||||
def set_initial_values(kwargs, form_type):
|
||||
"""
|
||||
|
@ -161,7 +161,7 @@ class FeedingForm(CoreModelForm):
|
|||
class NoteForm(CoreModelForm):
|
||||
class Meta:
|
||||
model = models.Note
|
||||
fields = ["child", "note", "time"]
|
||||
fields = ["child", "note", "time", "tags"]
|
||||
widgets = {
|
||||
"time": forms.DateTimeInput(
|
||||
attrs={
|
||||
|
@ -169,6 +169,7 @@ class NoteForm(CoreModelForm):
|
|||
"data-target": "#datetimepicker_time",
|
||||
}
|
||||
),
|
||||
#"tags": TagsEditor()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 4.0.2 on 2022-02-11 10:47
|
||||
|
||||
from django.db import migrations
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0004_alter_taggeditem_content_type_alter_taggeditem_tag'),
|
||||
('core', '0018_bmi_headcircumference_height'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='note',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,46 @@
|
|||
# Generated by Django 4.0.2 on 2022-02-11 15:44
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0019_note_tags'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BabyBuddyTag',
|
||||
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(max_length=32, verbose_name='Color')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Tag',
|
||||
'verbose_name_plural': 'Tags',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BabyBuddyTagged',
|
||||
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.babybuddytag')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='note',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='core.BabyBuddyTagged', to='core.BabyBuddyTag', verbose_name='Tags'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.0.2 on 2022-02-11 17:27
|
||||
|
||||
import core.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0020_babybuddytag_babybuddytagged_alter_note_tags'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='babybuddytag',
|
||||
name='color',
|
||||
field=models.CharField(default='#7F7F7F', max_length=32, validators=[core.models.validate_html_color], verbose_name='Color'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.0.2 on 2022-02-13 13:04
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0021_alter_babybuddytag_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='babybuddytag',
|
||||
name='last_used',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.0.2 on 2022-02-14 15:50
|
||||
|
||||
from django.db import migrations
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0022_babybuddytag_last_used'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='note',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='core.BabyBuddyTagged', to='core.BabyBuddyTag', verbose_name='Tags'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 4.0.2 on 2022-02-15 09:11
|
||||
|
||||
from django.db import migrations
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0004_alter_taggeditem_content_type_alter_taggeditem_tag'),
|
||||
('core', '0023_alter_note_tags'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='note',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||
),
|
||||
]
|
|
@ -1,4 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -9,7 +10,10 @@ from django.utils.text import slugify
|
|||
from django.utils import timezone
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.timezone import now
|
||||
|
||||
from taggit.managers import TaggableManager
|
||||
from taggit.models import TagBase, GenericTaggedItemBase, TaggedItemBase
|
||||
|
||||
def validate_date(date, field_name):
|
||||
"""
|
||||
|
@ -70,6 +74,41 @@ def validate_time(time, field_name):
|
|||
{field_name: _("Date/time can not be in the future.")}, code="time_invalid"
|
||||
)
|
||||
|
||||
def validate_html_color(s: str):
|
||||
return re.match(r"^#[0-9A-F]{6}$", s) is not None
|
||||
|
||||
class BabyBuddyTag(TagBase):
|
||||
class Meta:
|
||||
verbose_name = _("Tag")
|
||||
verbose_name_plural = _("Tags")
|
||||
|
||||
color = models.CharField(
|
||||
"Color",
|
||||
max_length=32,
|
||||
default="#7F7F7F",
|
||||
validators=[validate_html_color]
|
||||
)
|
||||
|
||||
last_used = models.DateTimeField(
|
||||
default=now,
|
||||
blank=False,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
print("BBT SAVE")
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class BabyBuddyTagged(GenericTaggedItemBase):
|
||||
tag = models.ForeignKey(
|
||||
BabyBuddyTag,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="%(app_label)s_%(class)s_items",
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
print("BabyBuddyTagged", args, kwargs)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
class Child(models.Model):
|
||||
model_name = "child"
|
||||
|
@ -241,6 +280,14 @@ class Feeding(models.Model):
|
|||
validate_duration(self)
|
||||
validate_unique_period(Feeding.objects.filter(child=self.child), self)
|
||||
|
||||
from taggit.managers import _TaggableManager
|
||||
class TTT(_TaggableManager):
|
||||
def set(self, tags, *, through_defaults=None, **kwargs):
|
||||
return super().set(tags, through_defaults=through_defaults, **kwargs)
|
||||
|
||||
class TT(TaggableManager):
|
||||
def save_form_data(self, instance, value):
|
||||
return super().save_form_data(instance, value)
|
||||
|
||||
class Note(models.Model):
|
||||
model_name = "note"
|
||||
|
@ -251,6 +298,7 @@ class Note(models.Model):
|
|||
time = models.DateTimeField(
|
||||
default=timezone.now, blank=False, verbose_name=_("Time")
|
||||
)
|
||||
tags = TaggableManager()
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
<div>
|
||||
{{ widget }}
|
||||
</div>
|
||||
|
||||
<div class="form_control"
|
||||
{% for k, v in widget.attrs.items %}
|
||||
{{ k }}={{ v }}
|
||||
{% endfor %}>
|
||||
<span class="prototype-tag btn badge badge-pill cursor-pointer" style="display: none;">
|
||||
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" style="background-color: {{ t.color }};">
|
||||
{{ t.name }}
|
||||
<span class="add-remove-icon pl-1 pr-1">-</span>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="new-tags">
|
||||
<span>Suggestions:</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" style="background-color: {{ t.color }};">
|
||||
{% if not forloop.first %},{% endif %}
|
||||
{{ t.name }}
|
||||
<span class="add-remove-icon pl-1 pr-1">+</span>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script>
|
||||
window.addEventListener('load', () => {
|
||||
const widget = document.getElementById('{{ widget.attrs.id }}');
|
||||
|
||||
const prototype = widget.querySelector('.prototype-tag');
|
||||
const currentTags = widget.querySelector('.current_tags');
|
||||
const newTags = widget.querySelector('.new-tags');
|
||||
|
||||
const inputElement = widget.querySelector('input');
|
||||
|
||||
function updateTag(tag, name, color, actionSymbol) {
|
||||
tag.childNodes[0].textContent = name;
|
||||
tag.setAttribute("data-value", name);
|
||||
tag.setAttribute("data-color", color);
|
||||
tag.setAttribute('style', `background-color: ${color};`);
|
||||
tag.querySelector('.add-remove-icon').childNodes[0].textContent = actionSymbol;
|
||||
}
|
||||
|
||||
function createNewTag(name, color, actionSymbol) {
|
||||
const tag = prototype.cloneNode(true);
|
||||
updateTag(tag, name, color, actionSymbol);
|
||||
return tag;
|
||||
}
|
||||
|
||||
function addTagCallback(event) {
|
||||
const tag = event.target;
|
||||
newTags.removeChild(tag);
|
||||
updateTag(
|
||||
tag,
|
||||
tag.getAttribute("data-value"),
|
||||
tag.getAttribute("data-color"),
|
||||
"-"
|
||||
)
|
||||
currentTags.appendChild(tag);
|
||||
|
||||
tag.removeEventListener('click', addTagCallback);
|
||||
tag.addEventListener('click', removeTagCallback);
|
||||
}
|
||||
|
||||
function registerNewCallback(tag, newParent, newSymbol, onClicked) {
|
||||
console.log(tag);
|
||||
function callback(event) {
|
||||
tag.parentNode.removeChild(tag);
|
||||
updateTag(
|
||||
tag,
|
||||
tag.getAttribute("data-value"),
|
||||
tag.getAttribute("data-color"),
|
||||
newSymbol
|
||||
);
|
||||
newParent.appendChild(tag);
|
||||
|
||||
tag.removeEventListener('click', callback);
|
||||
onClicked(tag);
|
||||
}
|
||||
tag.addEventListener('click', callback);
|
||||
}
|
||||
|
||||
function updateInputList() {
|
||||
const names = [];
|
||||
for (const tag of currentTags.querySelectorAll(".tag")) {
|
||||
names.push(tag.getAttribute("data-value"));
|
||||
}
|
||||
inputElement.value = names.join(",");
|
||||
}
|
||||
|
||||
function addTagCallback(tag) {
|
||||
registerNewCallback(tag, currentTags, "-", removeTagCallback);
|
||||
updateInputList();
|
||||
}
|
||||
|
||||
function removeTagCallback(tag) {
|
||||
registerNewCallback(tag, newTags, "+", addTagCallback);
|
||||
updateInputList();
|
||||
}
|
||||
|
||||
for (const tag of newTags.querySelectorAll(".tag")) {
|
||||
addTagCallback(tag);
|
||||
}
|
||||
for (const tag of currentTags.querySelectorAll(".tag")) {
|
||||
removeTagCallback(tag);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<input
|
||||
type="hidden"
|
||||
name="tags"
|
||||
value="{% for t in widget.value %}{{ t.name }}{% endfor %}"
|
||||
>
|
||||
</div>
|
|
@ -0,0 +1,45 @@
|
|||
from typing import Any, Dict, Optional
|
||||
from django.forms import Widget
|
||||
|
||||
class TagsEditor(Widget):
|
||||
input_type = 'hidden'
|
||||
template_name = 'core/widget_tag_editor.html'
|
||||
|
||||
@staticmethod
|
||||
def __unpack_tag(tag):
|
||||
return {'name': tag.name, 'color': tag.color}
|
||||
|
||||
def format_value(self, value: Any) -> Optional[str]:
|
||||
print("FORMAT", value)
|
||||
if value is not None and not isinstance(value, str):
|
||||
value = [self.__unpack_tag(tag) for tag in value]
|
||||
return value
|
||||
|
||||
def get_context(self, name: str, value: Any, attrs) -> Dict[str, Any]:
|
||||
from . import models
|
||||
|
||||
most_tags = models.BabyBuddyTag.objects.order_by(
|
||||
'-last_used'
|
||||
).all()[:256]
|
||||
quick_suggestion_tags = models.BabyBuddyTag.objects.order_by(
|
||||
'-last_used'
|
||||
).all()
|
||||
|
||||
result = super().get_context(name, value, attrs)
|
||||
|
||||
tag_names = set(x['name'] for x in result['widget']['value'])
|
||||
quick_suggestion_tags = [
|
||||
t for t in quick_suggestion_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
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue