Merge pull request #919 from paperless-ngx/feature-settings-saved-to-db

Feature: frontend settings saved to database
This commit is contained in:
shamoon
2022-05-18 11:33:17 -07:00
committed by GitHub
29 changed files with 639 additions and 260 deletions

View File

@@ -18,6 +18,7 @@ from documents.models import DocumentType
from documents.models import SavedView
from documents.models import SavedViewFilterRule
from documents.models import Tag
from documents.models import UiSettings
from documents.settings import EXPORTER_ARCHIVE_NAME
from documents.settings import EXPORTER_FILE_NAME
from documents.settings import EXPORTER_THUMBNAIL_NAME
@@ -112,8 +113,8 @@ class Command(BaseCommand):
map(lambda f: os.path.abspath(os.path.join(root, f)), files),
)
# 2. Create manifest, containing all correspondents, types, tags and
# documents
# 2. Create manifest, containing all correspondents, types, tags,
# documents and ui_settings
with transaction.atomic():
manifest = json.loads(
serializers.serialize("json", Correspondent.objects.all()),
@@ -150,6 +151,10 @@ class Command(BaseCommand):
manifest += json.loads(serializers.serialize("json", User.objects.all()))
manifest += json.loads(
serializers.serialize("json", UiSettings.objects.all()),
)
# 3. Export files from each document
for index, document_dict in tqdm.tqdm(
enumerate(document_manifest),

View File

@@ -0,0 +1,39 @@
# Generated by Django 4.0.4 on 2022-05-07 05:10
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("documents", "1018_alter_savedviewfilterrule_value"),
]
operations = [
migrations.CreateModel(
name="UiSettings",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("settings", models.JSONField(null=True)),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="ui_settings",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@@ -465,3 +465,17 @@ class FileInfo:
cls._mangle_property(properties, "created")
cls._mangle_property(properties, "title")
return cls(**properties)
# Extending User Model Using a One-To-One Link
class UiSettings(models.Model):
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name="ui_settings",
)
settings = models.JSONField(null=True)
def __str__(self):
return self.user.username

View File

@@ -15,6 +15,7 @@ from .models import MatchingModel
from .models import SavedView
from .models import SavedViewFilterRule
from .models import Tag
from .models import UiSettings
from .parsers import is_mime_type_supported
@@ -505,3 +506,24 @@ class BulkDownloadSerializer(DocumentListSerializer):
"bzip2": zipfile.ZIP_BZIP2,
"lzma": zipfile.ZIP_LZMA,
}[compression]
class UiSettingsViewSerializer(serializers.ModelSerializer):
class Meta:
model = UiSettings
depth = 1
fields = [
"id",
"settings",
]
def update(self, instance, validated_data):
super().update(instance, validated_data)
return instance
def create(self, validated_data):
ui_settings = UiSettings.objects.update_or_create(
user=validated_data.get("user"),
defaults={"settings": validated_data.get("settings", None)},
)
return ui_settings

View File

@@ -9,8 +9,6 @@
<title>Paperless-ngx</title>
<base href="{% url 'base' %}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="username" content="{{username}}">
<meta name="full_name" content="{{full_name}}">
<meta name="cookie_prefix" content="{{cookie_prefix}}">
<meta name="robots" content="noindex,nofollow">
<link rel="icon" type="image/x-icon" href="favicon.ico">

View File

@@ -27,6 +27,7 @@ from documents.models import DocumentType
from documents.models import MatchingModel
from documents.models import SavedView
from documents.models import Tag
from documents.models import UiSettings
from documents.tests.utils import DirectoriesMixin
from paperless import version
from rest_framework.test import APITestCase
@@ -1398,6 +1399,41 @@ class TestDocumentApiV2(DirectoriesMixin, APITestCase):
"#000000",
)
def test_ui_settings(self):
test_user = User.objects.create_superuser(username="test")
self.client.force_login(user=test_user)
response = self.client.get("/api/ui_settings/", format="json")
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
response.data["settings"],
{},
)
settings = {
"settings": {
"dark_mode": {
"enabled": True,
},
},
}
response = self.client.post(
"/api/ui_settings/",
json.dumps(settings),
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
response = self.client.get("/api/ui_settings/", format="json")
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
response.data["settings"],
settings["settings"],
)
class TestBulkEdit(DirectoriesMixin, APITestCase):
def setUp(self):

View File

@@ -11,6 +11,7 @@ from unicodedata import normalize
from urllib.parse import quote
from django.conf import settings
from django.contrib.auth.models import User
from django.db.models import Case
from django.db.models import Count
from django.db.models import IntegerField
@@ -74,6 +75,7 @@ from .serialisers import PostDocumentSerializer
from .serialisers import SavedViewSerializer
from .serialisers import TagSerializer
from .serialisers import TagSerializerVersion1
from .serialisers import UiSettingsViewSerializer
logger = logging.getLogger("paperless.api")
@@ -81,12 +83,18 @@ logger = logging.getLogger("paperless.api")
class IndexView(TemplateView):
template_name = "index.html"
def get_language(self):
def get_frontend_language(self):
if hasattr(
self.request.user,
"ui_settings",
) and self.request.user.ui_settings.settings.get("language"):
lang = self.request.user.ui_settings.settings.get("language")
else:
lang = get_language()
# This is here for the following reason:
# Django identifies languages in the form "en-us"
# However, angular generates locales as "en-US".
# this translates between these two forms.
lang = get_language()
if "-" in lang:
first = lang[: lang.index("-")]
second = lang[lang.index("-") + 1 :]
@@ -99,16 +107,18 @@ class IndexView(TemplateView):
context["cookie_prefix"] = settings.COOKIE_PREFIX
context["username"] = self.request.user.username
context["full_name"] = self.request.user.get_full_name()
context["styles_css"] = f"frontend/{self.get_language()}/styles.css"
context["runtime_js"] = f"frontend/{self.get_language()}/runtime.js"
context["polyfills_js"] = f"frontend/{self.get_language()}/polyfills.js"
context["main_js"] = f"frontend/{self.get_language()}/main.js"
context["styles_css"] = f"frontend/{self.get_frontend_language()}/styles.css"
context["runtime_js"] = f"frontend/{self.get_frontend_language()}/runtime.js"
context[
"polyfills_js"
] = f"frontend/{self.get_frontend_language()}/polyfills.js"
context["main_js"] = f"frontend/{self.get_frontend_language()}/main.js"
context[
"webmanifest"
] = f"frontend/{self.get_language()}/manifest.webmanifest" # noqa: E501
] = f"frontend/{self.get_frontend_language()}/manifest.webmanifest" # noqa: E501
context[
"apple_touch_icon"
] = f"frontend/{self.get_language()}/apple-touch-icon.png" # noqa: E501
] = f"frontend/{self.get_frontend_language()}/apple-touch-icon.png" # noqa: E501
return context
@@ -717,3 +727,41 @@ class RemoteVersionView(GenericAPIView):
"feature_is_set": feature_is_set,
},
)
class UiSettingsView(GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = UiSettingsViewSerializer
def get(self, request, format=None):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = User.objects.get(pk=request.user.id)
displayname = user.username
if user.first_name or user.last_name:
displayname = " ".join([user.first_name, user.last_name])
settings = {}
if hasattr(user, "ui_settings"):
settings = user.ui_settings.settings
return Response(
{
"user_id": user.id,
"username": user.username,
"display_name": displayname,
"settings": settings,
},
)
def post(self, request, format=None):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(user=self.request.user)
return Response(
{
"success": True,
},
)