add account preferences model and API endpoints for user settings
This commit is contained in:
parent
a9d5bbb9df
commit
32addb8ed1
11 changed files with 297 additions and 21 deletions
|
|
@ -1,6 +1,7 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from authentication.models import ToolshedUser, KnownIdentity, FriendRequestOutgoing, FriendRequestIncoming
|
||||
from authentication.models import ToolshedUser, KnownIdentity, FriendRequestOutgoing, FriendRequestIncoming, \
|
||||
AccountPreference
|
||||
|
||||
|
||||
class ToolshedUserAdmin(admin.ModelAdmin):
|
||||
|
|
@ -8,6 +9,11 @@ class ToolshedUserAdmin(admin.ModelAdmin):
|
|||
search_fields = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_active', 'date_joined', 'domain')
|
||||
|
||||
|
||||
class AccountPreferenceAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'key', 'value')
|
||||
search_fields = ('user__username', 'key')
|
||||
|
||||
|
||||
class KnownIdentityAdmin(admin.ModelAdmin):
|
||||
list_display = ('username', 'domain', 'public_key')
|
||||
search_fields = ('username', 'domain', 'public_key')
|
||||
|
|
@ -27,3 +33,4 @@ admin.site.register(ToolshedUser, ToolshedUserAdmin)
|
|||
admin.site.register(KnownIdentity, KnownIdentityAdmin)
|
||||
admin.site.register(FriendRequestOutgoing, FriendRequestOutgoingAdmin)
|
||||
admin.site.register(FriendRequestIncoming, FriendRequestIncomingAdmin)
|
||||
admin.site.register(AccountPreference, AccountPreferenceAdmin)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from rest_framework.authtoken.models import Token
|
|||
from rest_framework.authtoken.views import ObtainAuthToken
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentication.models import ToolshedUser
|
||||
from authentication.models import ToolshedUser, AccountPreference
|
||||
from authentication.signature_auth import SignatureAuthenticationLocal
|
||||
from files.models import File
|
||||
from files.serializers import FileSerializer
|
||||
|
|
@ -17,6 +17,48 @@ from hostadmin.models import Domain
|
|||
|
||||
router = routers.SimpleRouter()
|
||||
|
||||
# Schema for the account-level preferences a client may store on the server (see
|
||||
# AccountPreference). Device-level preferences are never sent here - they stay in the
|
||||
# browser's local storage since they describe the device, not the account.
|
||||
PREFERENCE_DEFINITIONS = [
|
||||
{
|
||||
'key': 'ui.compact_mode',
|
||||
'type': 'boolean',
|
||||
'default': False,
|
||||
'label': 'Compact mode',
|
||||
'description': 'Show denser item rows and reduce spacing in lists.',
|
||||
},
|
||||
{
|
||||
'key': 'ui.default_search_scope',
|
||||
'type': 'enum',
|
||||
'options': ['inventory', 'friends', 'all'],
|
||||
'default': 'inventory',
|
||||
'label': 'Default search scope',
|
||||
'description': 'Choose where the global search starts.',
|
||||
},
|
||||
{
|
||||
'key': 'notifications.desktop_enabled',
|
||||
'type': 'boolean',
|
||||
'default': True,
|
||||
'label': 'Desktop notifications',
|
||||
'description': 'Enable in-browser notifications for important updates.',
|
||||
},
|
||||
{
|
||||
'key': 'files.max_upload_mb',
|
||||
'type': 'integer',
|
||||
'default': 25,
|
||||
'label': 'Default upload size limit (MB)',
|
||||
'description': 'Used as a prefill hint in upload dialogs.',
|
||||
},
|
||||
{
|
||||
'key': 'ui.experimental_flags',
|
||||
'type': 'json',
|
||||
'default': {},
|
||||
'label': 'Experimental flags',
|
||||
'description': 'Optional JSON toggles for feature previews.',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class UserAuthToken(ObtainAuthToken):
|
||||
|
||||
|
|
@ -129,6 +171,42 @@ def registerUser(request):
|
|||
return Response({'errors': {'domain': 'Domain does not exist or is not open for registration'}}, status=400)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([])
|
||||
@authentication_classes([])
|
||||
def preference_definitions(request):
|
||||
"""Return the schema (types, defaults, labels) for the account preferences clients may set."""
|
||||
return Response(PREFERENCE_DEFINITIONS)
|
||||
|
||||
|
||||
@api_view(['GET', 'PUT'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
@authentication_classes([SignatureAuthenticationLocal])
|
||||
def account_preferences(request):
|
||||
"""Get or bulk-upsert the authenticated user's account-level preferences.
|
||||
|
||||
GET returns the current preferences as a {key: value} dict. PUT accepts a {key: value}
|
||||
dict of one or more preferences to set/overwrite; unspecified keys are left untouched.
|
||||
"""
|
||||
if request.method == 'PUT':
|
||||
if not isinstance(request.data, dict):
|
||||
return Response({'detail': 'Expected an object of key/value pairs.'}, status=400)
|
||||
for key, value in request.data.items():
|
||||
AccountPreference.objects.update_or_create(user=request.user, key=key, defaults={'value': value})
|
||||
|
||||
preferences = {pref.key: pref.value for pref in request.user.preferences.all()}
|
||||
return Response(preferences)
|
||||
|
||||
|
||||
@api_view(['DELETE'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
@authentication_classes([SignatureAuthenticationLocal])
|
||||
def account_preference_detail(request, key):
|
||||
"""Reset a single account-level preference back to its default by deleting it."""
|
||||
AccountPreference.objects.filter(user=request.user, key=key).delete()
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
router.register(r'users', UserViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -136,4 +214,7 @@ urlpatterns = [
|
|||
path('user/', getUserInfo),
|
||||
path('register/', registerUser),
|
||||
path('token/', UserAuthToken.as_view()),
|
||||
path('preferences/', preference_definitions),
|
||||
path('self/preferences/', account_preferences),
|
||||
path('self/preferences/<str:key>/', account_preference_detail),
|
||||
]
|
||||
|
|
|
|||
26
backend/authentication/migrations/0003_accountpreference.py
Normal file
26
backend/authentication/migrations/0003_accountpreference.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0002_toolsheduser_profile_picture'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AccountPreference',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(max_length=255)),
|
||||
('value', models.JSONField()),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='preferences',
|
||||
to='authentication.toolsheduser')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'key')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
|
@ -113,6 +113,23 @@ class ToolshedUser(AbstractUser):
|
|||
return self.public_identity.public_key
|
||||
|
||||
|
||||
class AccountPreference(models.Model):
|
||||
"""A single account-level (server-synced, cross-device) user preference, stored as a key/value pair.
|
||||
|
||||
Device-level preferences are intentionally *not* stored here - they stay in the browser's
|
||||
local storage since they describe the device, not the account.
|
||||
"""
|
||||
user = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='preferences')
|
||||
key = models.CharField(max_length=255)
|
||||
value = models.JSONField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'key')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user}: {self.key}"
|
||||
|
||||
|
||||
class FriendRequestOutgoing(models.Model):
|
||||
secret = models.CharField(max_length=255)
|
||||
befriender_user = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='friend_requests_outgoing')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue