mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Merge branch 'feature-autocolor' into dev
This commit is contained in:
@@ -19,12 +19,12 @@ class TagAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"colour",
|
||||
"color",
|
||||
"match",
|
||||
"matching_algorithm"
|
||||
)
|
||||
list_filter = ("colour", "matching_algorithm")
|
||||
list_editable = ("colour", "match", "matching_algorithm")
|
||||
list_filter = ("color", "matching_algorithm")
|
||||
list_editable = ("color", "match", "matching_algorithm")
|
||||
|
||||
|
||||
class DocumentTypeAdmin(admin.ModelAdmin):
|
||||
|
70
src/documents/migrations/1013_migrate_tag_colour.py
Normal file
70
src/documents/migrations/1013_migrate_tag_colour.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# Generated by Django 3.1.4 on 2020-12-02 21:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
COLOURS_OLD = {
|
||||
1: "#a6cee3",
|
||||
2: "#1f78b4",
|
||||
3: "#b2df8a",
|
||||
4: "#33a02c",
|
||||
5: "#fb9a99",
|
||||
6: "#e31a1c",
|
||||
7: "#fdbf6f",
|
||||
8: "#ff7f00",
|
||||
9: "#cab2d6",
|
||||
10: "#6a3d9a",
|
||||
11: "#b15928",
|
||||
12: "#000000",
|
||||
13: "#cccccc",
|
||||
}
|
||||
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
Tag = apps.get_model('documents', 'Tag')
|
||||
|
||||
for tag in Tag.objects.all():
|
||||
colour_old_id = tag.colour_old
|
||||
rgb = COLOURS_OLD[colour_old_id]
|
||||
tag.color = rgb
|
||||
tag.save()
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
Tag = apps.get_model('documents', 'Tag')
|
||||
|
||||
def _get_colour_id(rdb):
|
||||
for idx, rdbx in COLOURS_OLD.items():
|
||||
if rdbx == rdb:
|
||||
return idx
|
||||
# Return colour 1 if we can't match anything
|
||||
return 1
|
||||
|
||||
for tag in Tag.objects.all():
|
||||
colour_id = _get_colour_id(tag.color)
|
||||
tag.colour_old = colour_id
|
||||
tag.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('documents', '1012_fix_archive_files'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='tag',
|
||||
old_name='colour',
|
||||
new_name='colour_old',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='color',
|
||||
field=models.CharField(default='#a6cee3', max_length=7, verbose_name='color'),
|
||||
),
|
||||
migrations.RunPython(forward, reverse),
|
||||
migrations.RemoveField(
|
||||
model_name='tag',
|
||||
name='colour_old',
|
||||
)
|
||||
]
|
@@ -77,25 +77,11 @@ class Correspondent(MatchingModel):
|
||||
|
||||
class Tag(MatchingModel):
|
||||
|
||||
COLOURS = (
|
||||
(1, "#a6cee3"),
|
||||
(2, "#1f78b4"),
|
||||
(3, "#b2df8a"),
|
||||
(4, "#33a02c"),
|
||||
(5, "#fb9a99"),
|
||||
(6, "#e31a1c"),
|
||||
(7, "#fdbf6f"),
|
||||
(8, "#ff7f00"),
|
||||
(9, "#cab2d6"),
|
||||
(10, "#6a3d9a"),
|
||||
(11, "#b15928"),
|
||||
(12, "#000000"),
|
||||
(13, "#cccccc")
|
||||
)
|
||||
|
||||
colour = models.PositiveIntegerField(
|
||||
color = models.CharField(
|
||||
_("color"),
|
||||
choices=COLOURS, default=1)
|
||||
max_length=7,
|
||||
default="#a6cee3"
|
||||
)
|
||||
|
||||
is_inbox_tag = models.BooleanField(
|
||||
_("is inbox tag"),
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
|
||||
import magic
|
||||
import math
|
||||
from django.utils.text import slugify
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
@@ -88,7 +89,40 @@ class DocumentTypeSerializer(MatchingModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
class TagSerializer(MatchingModelSerializer):
|
||||
class ColorField(serializers.Field):
|
||||
|
||||
COLOURS = (
|
||||
(1, "#a6cee3"),
|
||||
(2, "#1f78b4"),
|
||||
(3, "#b2df8a"),
|
||||
(4, "#33a02c"),
|
||||
(5, "#fb9a99"),
|
||||
(6, "#e31a1c"),
|
||||
(7, "#fdbf6f"),
|
||||
(8, "#ff7f00"),
|
||||
(9, "#cab2d6"),
|
||||
(10, "#6a3d9a"),
|
||||
(11, "#b15928"),
|
||||
(12, "#000000"),
|
||||
(13, "#cccccc")
|
||||
)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
for id, color in self.COLOURS:
|
||||
if id == data:
|
||||
return color
|
||||
raise serializers.ValidationError()
|
||||
|
||||
def to_representation(self, value):
|
||||
for id, color in self.COLOURS:
|
||||
if color == value:
|
||||
return id
|
||||
return 1
|
||||
|
||||
|
||||
class TagSerializerVersion1(MatchingModelSerializer):
|
||||
|
||||
colour = ColorField(source='color', default="#a6cee3")
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
@@ -105,6 +139,45 @@ class TagSerializer(MatchingModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
class TagSerializer(MatchingModelSerializer):
|
||||
|
||||
def get_text_color(self, obj):
|
||||
try:
|
||||
h = obj.color.lstrip('#')
|
||||
rgb = tuple(int(h[i:i + 2], 16)/256 for i in (0, 2, 4))
|
||||
luminance = math.sqrt(
|
||||
0.299 * math.pow(rgb[0], 2) +
|
||||
0.587 * math.pow(rgb[1], 2) +
|
||||
0.114 * math.pow(rgb[2], 2)
|
||||
)
|
||||
return "#ffffff" if luminance < 0.53 else "#000000"
|
||||
except ValueError:
|
||||
return "#000000"
|
||||
|
||||
text_color = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = (
|
||||
"id",
|
||||
"slug",
|
||||
"name",
|
||||
"color",
|
||||
"text_color",
|
||||
"match",
|
||||
"matching_algorithm",
|
||||
"is_insensitive",
|
||||
"is_inbox_tag",
|
||||
"document_count"
|
||||
)
|
||||
|
||||
def validate_color(self, color):
|
||||
regex = r"#[0-9a-fA-F]{6}"
|
||||
if not re.match(regex, color):
|
||||
raise serializers.ValidationError(_("Invalid color."))
|
||||
return color
|
||||
|
||||
|
||||
class CorrespondentField(serializers.PrimaryKeyRelatedField):
|
||||
def get_queryset(self):
|
||||
return Correspondent.objects.all()
|
||||
|
@@ -807,6 +807,69 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
||||
}, format='json')
|
||||
self.assertEqual(response.status_code, 201, endpoint)
|
||||
|
||||
def test_tag_color_default(self):
|
||||
response = self.client.post("/api/tags/", {
|
||||
"name": "tag"
|
||||
}, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(Tag.objects.get(id=response.data['id']).color, "#a6cee3")
|
||||
self.assertEqual(self.client.get(f"/api/tags/{response.data['id']}/", format="json").data['colour'], 1)
|
||||
|
||||
def test_tag_color(self):
|
||||
response = self.client.post("/api/tags/", {
|
||||
"name": "tag",
|
||||
"colour": 3
|
||||
}, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(Tag.objects.get(id=response.data['id']).color, "#b2df8a")
|
||||
self.assertEqual(self.client.get(f"/api/tags/{response.data['id']}/", format="json").data['colour'], 3)
|
||||
|
||||
def test_tag_color_invalid(self):
|
||||
response = self.client.post("/api/tags/", {
|
||||
"name": "tag",
|
||||
"colour": 34
|
||||
}, format="json")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_tag_color_custom(self):
|
||||
tag = Tag.objects.create(name="test", color="#abcdef")
|
||||
self.assertEqual(self.client.get(f"/api/tags/{tag.id}/", format="json").data['colour'], 1)
|
||||
|
||||
|
||||
class TestDocumentApiV2(DirectoriesMixin, APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDocumentApiV2, self).setUp()
|
||||
|
||||
self.user = User.objects.create_superuser(username="temp_admin")
|
||||
|
||||
self.client.force_login(user=self.user)
|
||||
self.client.defaults['HTTP_ACCEPT'] = 'application/json; version=2'
|
||||
|
||||
def test_tag_validate_color(self):
|
||||
self.assertEqual(self.client.post("/api/tags/", {"name": "test", "color": "#12fFaA"}, format="json").status_code, 201)
|
||||
|
||||
self.assertEqual(self.client.post("/api/tags/", {"name": "test1", "color": "abcdef"}, format="json").status_code, 400)
|
||||
self.assertEqual(self.client.post("/api/tags/", {"name": "test2", "color": "#abcdfg"}, format="json").status_code, 400)
|
||||
self.assertEqual(self.client.post("/api/tags/", {"name": "test3", "color": "#asd"}, format="json").status_code, 400)
|
||||
self.assertEqual(self.client.post("/api/tags/", {"name": "test4", "color": "#12121212"}, format="json").status_code, 400)
|
||||
|
||||
def test_tag_text_color(self):
|
||||
t = Tag.objects.create(name="tag1", color="#000000")
|
||||
self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#ffffff")
|
||||
|
||||
t.color = "#ffffff"
|
||||
t.save()
|
||||
self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#000000")
|
||||
|
||||
t.color = "asdf"
|
||||
t.save()
|
||||
self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#000000")
|
||||
|
||||
t.color = "123"
|
||||
t.save()
|
||||
self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#000000")
|
||||
|
||||
|
||||
class TestBulkEdit(DirectoriesMixin, APITestCase):
|
||||
|
||||
|
@@ -50,6 +50,7 @@ from .parsers import get_parser_class_for_mime_type
|
||||
from .serialisers import (
|
||||
CorrespondentSerializer,
|
||||
DocumentSerializer,
|
||||
TagSerializerVersion1,
|
||||
TagSerializer,
|
||||
DocumentTypeSerializer,
|
||||
PostDocumentSerializer,
|
||||
@@ -119,7 +120,12 @@ class TagViewSet(ModelViewSet):
|
||||
queryset = Tag.objects.annotate(
|
||||
document_count=Count('documents')).order_by(Lower('name'))
|
||||
|
||||
serializer_class = TagSerializer
|
||||
def get_serializer_class(self):
|
||||
if int(self.request.version) == 1:
|
||||
return TagSerializerVersion1
|
||||
else:
|
||||
return TagSerializer
|
||||
|
||||
pagination_class = StandardPagination
|
||||
permission_classes = (IsAuthenticated,)
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
|
Reference in New Issue
Block a user