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 = "*"
|
pyyaml = "*"
|
||||||
uritemplate = "*"
|
uritemplate = "*"
|
||||||
whitenoise = "*"
|
whitenoise = "*"
|
||||||
|
django-taggit = "==2.1.0"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
|
@ -30,3 +31,4 @@ mkdocs = "*"
|
||||||
mkdocs-material = "*"
|
mkdocs-material = "*"
|
||||||
tblib = "*"
|
tblib = "*"
|
||||||
black = "*"
|
black = "*"
|
||||||
|
pydevd = "*"
|
||||||
|
|
|
@ -6,6 +6,10 @@ 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 +129,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()
|
||||||
|
|
||||||
|
|
||||||
class SleepSerializer(CoreModelWithDurationSerializer):
|
class SleepSerializer(CoreModelWithDurationSerializer):
|
||||||
|
|
|
@ -8,7 +8,6 @@ from core import models
|
||||||
from . import serializers, filters
|
from . import serializers, filters
|
||||||
from .mixins import TimerFieldSupportMixin
|
from .mixins import TimerFieldSupportMixin
|
||||||
|
|
||||||
|
|
||||||
class ChildViewSet(viewsets.ModelViewSet):
|
class ChildViewSet(viewsets.ModelViewSet):
|
||||||
queryset = models.Child.objects.all()
|
queryset = models.Child.objects.all()
|
||||||
serializer_class = serializers.ChildSerializer
|
serializer_class = serializers.ChildSerializer
|
||||||
|
|
|
@ -26,6 +26,7 @@ INSTALLED_APPS = [
|
||||||
"api",
|
"api",
|
||||||
"babybuddy",
|
"babybuddy",
|
||||||
"core",
|
"core",
|
||||||
|
"taggit",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"reports",
|
"reports",
|
||||||
"axes",
|
"axes",
|
||||||
|
|
|
@ -177,3 +177,14 @@ class WeightAdmin(ImportExportMixin, ExportActionMixin, admin.ModelAdmin):
|
||||||
"weight",
|
"weight",
|
||||||
)
|
)
|
||||||
resource_class = WeightImportExportResource
|
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 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):
|
||||||
"""
|
"""
|
||||||
|
@ -161,7 +161,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 +169,7 @@ class NoteForm(CoreModelForm):
|
||||||
"data-target": "#datetimepicker_time",
|
"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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -9,7 +10,10 @@ 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.utils.timezone import now
|
||||||
|
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
|
from taggit.models import TagBase, GenericTaggedItemBase, TaggedItemBase
|
||||||
|
|
||||||
def validate_date(date, field_name):
|
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"
|
{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):
|
class Child(models.Model):
|
||||||
model_name = "child"
|
model_name = "child"
|
||||||
|
@ -241,6 +280,14 @@ class Feeding(models.Model):
|
||||||
validate_duration(self)
|
validate_duration(self)
|
||||||
validate_unique_period(Feeding.objects.filter(child=self.child), 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):
|
class Note(models.Model):
|
||||||
model_name = "note"
|
model_name = "note"
|
||||||
|
@ -251,6 +298,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()
|
||||||
|
|
||||||
objects = models.Manager()
|
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