mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Feature: customizable fields display for documents, saved views & dashboard widgets (#6439)
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# Generated by Django 4.2.11 on 2024-04-16 18:35
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("documents", "1046_workflowaction_remove_all_correspondents_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="savedview",
|
||||
name="display_mode",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("table", "Table"),
|
||||
("smallCards", "Small Cards"),
|
||||
("largeCards", "Large Cards"),
|
||||
],
|
||||
max_length=128,
|
||||
null=True,
|
||||
verbose_name="View display mode",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="savedview",
|
||||
name="page_size",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(1)],
|
||||
verbose_name="View page size",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="savedview",
|
||||
name="display_fields",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Document display fields",
|
||||
),
|
||||
),
|
||||
]
|
@@ -394,6 +394,25 @@ class Log(models.Model):
|
||||
|
||||
|
||||
class SavedView(ModelWithOwner):
|
||||
class DisplayMode(models.TextChoices):
|
||||
TABLE = ("table", _("Table"))
|
||||
SMALL_CARDS = ("smallCards", _("Small Cards"))
|
||||
LARGE_CARDS = ("largeCards", _("Large Cards"))
|
||||
|
||||
class DisplayFields(models.TextChoices):
|
||||
TITLE = ("title", _("Title"))
|
||||
CREATED = ("created", _("Created"))
|
||||
ADDED = ("added", _("Added"))
|
||||
TAGS = ("tag"), _("Tags")
|
||||
CORRESPONDENT = ("correspondent", _("Correspondent"))
|
||||
DOCUMENT_TYPE = ("documenttype", _("Document Type"))
|
||||
STORAGE_PATH = ("storagepath", _("Storage Path"))
|
||||
NOTES = ("note", _("Note"))
|
||||
OWNER = ("owner", _("Owner"))
|
||||
SHARED = ("shared", _("Shared"))
|
||||
ASN = ("asn", _("ASN"))
|
||||
CUSTOM_FIELD = ("custom_field_%d", ("Custom Field"))
|
||||
|
||||
name = models.CharField(_("name"), max_length=128)
|
||||
|
||||
show_on_dashboard = models.BooleanField(
|
||||
@@ -411,6 +430,27 @@ class SavedView(ModelWithOwner):
|
||||
)
|
||||
sort_reverse = models.BooleanField(_("sort reverse"), default=False)
|
||||
|
||||
page_size = models.PositiveIntegerField(
|
||||
_("View page size"),
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
)
|
||||
|
||||
display_mode = models.CharField(
|
||||
max_length=128,
|
||||
verbose_name=_("View display mode"),
|
||||
choices=DisplayMode.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
display_fields = models.JSONField(
|
||||
verbose_name=_("Document display fields"),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
verbose_name = _("saved view")
|
||||
|
@@ -815,12 +815,33 @@ class SavedViewSerializer(OwnedObjectSerializer):
|
||||
"sort_field",
|
||||
"sort_reverse",
|
||||
"filter_rules",
|
||||
"page_size",
|
||||
"display_mode",
|
||||
"display_fields",
|
||||
"owner",
|
||||
"permissions",
|
||||
"user_can_change",
|
||||
"set_permissions",
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
if "display_fields" in attrs and attrs["display_fields"] is not None:
|
||||
for field in attrs["display_fields"]:
|
||||
if (
|
||||
SavedView.DisplayFields.CUSTOM_FIELD[:-2] in field
|
||||
): # i.e. check for 'custom_field_' prefix
|
||||
field_id = int(re.search(r"\d+", field)[0])
|
||||
if not CustomField.objects.filter(id=field_id).exists():
|
||||
raise serializers.ValidationError(
|
||||
f"Invalid field: {field}",
|
||||
)
|
||||
elif field not in SavedView.DisplayFields.values:
|
||||
raise serializers.ValidationError(
|
||||
f"Invalid field: {field}",
|
||||
)
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if "filter_rules" in validated_data:
|
||||
rules_data = validated_data.pop("filter_rules")
|
||||
|
@@ -1614,7 +1614,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
def test_create_update_patch(self):
|
||||
def test_saved_view_create_update_patch(self):
|
||||
User.objects.create_user("user1")
|
||||
|
||||
view = {
|
||||
@@ -1661,6 +1661,155 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
v1 = SavedView.objects.get(id=v1.id)
|
||||
self.assertEqual(v1.filter_rules.count(), 0)
|
||||
|
||||
def test_saved_view_display_options(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Saved view
|
||||
WHEN:
|
||||
- Updating display options
|
||||
THEN:
|
||||
- Display options are updated
|
||||
- Display fields are validated
|
||||
"""
|
||||
User.objects.create_user("user1")
|
||||
|
||||
view = {
|
||||
"name": "test",
|
||||
"show_on_dashboard": True,
|
||||
"show_in_sidebar": True,
|
||||
"sort_field": "created2",
|
||||
"filter_rules": [{"rule_type": 4, "value": "test"}],
|
||||
"page_size": 20,
|
||||
"display_mode": SavedView.DisplayMode.SMALL_CARDS,
|
||||
"display_fields": [
|
||||
SavedView.DisplayFields.TITLE,
|
||||
SavedView.DisplayFields.CREATED,
|
||||
],
|
||||
}
|
||||
|
||||
response = self.client.post("/api/saved_views/", view, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
v1 = SavedView.objects.get(name="test")
|
||||
self.assertEqual(v1.page_size, 20)
|
||||
self.assertEqual(
|
||||
v1.display_mode,
|
||||
SavedView.DisplayMode.SMALL_CARDS,
|
||||
)
|
||||
self.assertEqual(
|
||||
v1.display_fields,
|
||||
[
|
||||
SavedView.DisplayFields.TITLE,
|
||||
SavedView.DisplayFields.CREATED,
|
||||
],
|
||||
)
|
||||
|
||||
response = self.client.patch(
|
||||
f"/api/saved_views/{v1.id}/",
|
||||
{
|
||||
"display_fields": [
|
||||
SavedView.DisplayFields.TAGS,
|
||||
SavedView.DisplayFields.TITLE,
|
||||
SavedView.DisplayFields.CREATED,
|
||||
],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
v1.refresh_from_db()
|
||||
self.assertEqual(
|
||||
v1.display_fields,
|
||||
[
|
||||
SavedView.DisplayFields.TAGS,
|
||||
SavedView.DisplayFields.TITLE,
|
||||
SavedView.DisplayFields.CREATED,
|
||||
],
|
||||
)
|
||||
|
||||
# Invalid display field
|
||||
response = self.client.patch(
|
||||
f"/api/saved_views/{v1.id}/",
|
||||
{
|
||||
"display_fields": [
|
||||
"foobar",
|
||||
],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_saved_view_display_customfields(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Saved view
|
||||
WHEN:
|
||||
- Updating display options with custom fields
|
||||
THEN:
|
||||
- Display filds for custom fields are updated
|
||||
- Display fields for custom fields are validated
|
||||
"""
|
||||
view = {
|
||||
"name": "test",
|
||||
"show_on_dashboard": True,
|
||||
"show_in_sidebar": True,
|
||||
"sort_field": "created2",
|
||||
"filter_rules": [{"rule_type": 4, "value": "test"}],
|
||||
"page_size": 20,
|
||||
"display_mode": SavedView.DisplayMode.SMALL_CARDS,
|
||||
"display_fields": [
|
||||
SavedView.DisplayFields.TITLE,
|
||||
SavedView.DisplayFields.CREATED,
|
||||
],
|
||||
}
|
||||
|
||||
response = self.client.post("/api/saved_views/", view, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
v1 = SavedView.objects.get(name="test")
|
||||
|
||||
custom_field = CustomField.objects.create(
|
||||
name="stringfield",
|
||||
data_type=CustomField.FieldDataType.STRING,
|
||||
)
|
||||
|
||||
response = self.client.patch(
|
||||
f"/api/saved_views/{v1.id}/",
|
||||
{
|
||||
"display_fields": [
|
||||
SavedView.DisplayFields.TITLE,
|
||||
SavedView.DisplayFields.CREATED,
|
||||
SavedView.DisplayFields.CUSTOM_FIELD % custom_field.id,
|
||||
],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
v1.refresh_from_db()
|
||||
self.assertEqual(
|
||||
v1.display_fields,
|
||||
[
|
||||
str(SavedView.DisplayFields.TITLE),
|
||||
str(SavedView.DisplayFields.CREATED),
|
||||
SavedView.DisplayFields.CUSTOM_FIELD % custom_field.id,
|
||||
],
|
||||
)
|
||||
|
||||
# Custom field not found
|
||||
response = self.client.patch(
|
||||
f"/api/saved_views/{v1.id}/",
|
||||
{
|
||||
"display_fields": [
|
||||
SavedView.DisplayFields.TITLE,
|
||||
SavedView.DisplayFields.CREATED,
|
||||
SavedView.DisplayFields.CUSTOM_FIELD % 99,
|
||||
],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_get_logs(self):
|
||||
log_data = "test\ntest2\n"
|
||||
with open(os.path.join(settings.LOGGING_DIR, "mail.log"), "w") as f:
|
||||
|
Reference in New Issue
Block a user