Feature: customizable fields display for documents, saved views & dashboard widgets (#6439)

This commit is contained in:
shamoon
2024-04-26 06:41:12 -07:00
committed by GitHub
parent 7a0334f353
commit bd4476d484
50 changed files with 2929 additions and 1018 deletions

View File

@@ -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",
),
),
]

View File

@@ -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")

View File

@@ -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")

View File

@@ -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: