# WebCase Settings

A flexible Django settings framework for defining, validating, and managing **application-wide** and **per-user settings** using **Pydantic DTOs**, **Django ORM**, and **lazy resolvers**.

## Installation

```sh
pip install wc-django-settings
```

In `settings.py`:

```python
INSTALLED_APPS += [
  'wcd_settings',
]

MIDDLEWARE += [
  'wcd_settings.middleware.settings_middleware',
]
```

## Usage

### 1. Defining and Registering Settings

#### 1.1 Define DTOs (Pydantic Models)

Create Pydantic models to define the shape and validation of your settings.

```python
# myapp/dtos.py
from wcd_settings.registries import SettingsDTO


class ThemeSettings(SettingsDTO):
    color_scheme: str = Field(default='light', description='Theme color scheme')
    font_size: int = Field(default=14, ge=8, le=32, description='Font size in px')


class NotificationsSettings(SettingsDTO):
    enabled: bool = Field(default=True, description='Enable notifications')
    frequency: str = Field(default='daily', description='Notification frequency')
```

#### 1.2 Register DTOs

Register these models with app_settings_registry (for global settings) or user_settings_registry (for per-user settings).

```python
# myapp/registry.py
from wcd_settings.registries import (
  AppSettingsDescriptor, UserSettingsDescriptor,
  app_settings_registry, user_settings_registry,
)
from .dtos import ThemeSettings, NotificationsSettings


# Application-wide setting
app_settings_registry.register(
  AppSettingsDescriptor(
    # Unique key
    key='theme',
    # DTO model to validate settings
    dto=ThemeSettings,
    # Optional json schema object that describes DTO
    schema=ThemeSettings.model_json_schema(),
    # Defines whether this object could be edited through HTTP API
    is_editable=False,
    # Defines whether this object could be accessed through HTTP API
    is_readable=True,
  )
)

# User-specific setting
user_settings_registry.register(
  UserSettingsDescriptor(
    key='notifications', dto=NotificationsSettings,
    schema=NotificationsSettings.model_json_schema(),
    is_editable=True, is_readable=True,
  )
)
```

### 2. Fetching Settings with Resolvers

Resolvers provide lazy-loading and caching of settings from the database.

```python
from wcd_settings import shortcuts

# App Settings
# Optional request parameter to use middleware-injected settings and cahe
app_settings = shortcuts.app_settings(request=None)
theme: Optional[ThemeSettings] = app_settings.resolver.get('theme')

print(theme.color_scheme)  # "light" by default or DB value

# User Settings
user_settings = shortcuts.user_settings(request=None)
# Getting settings for user_id=42
notifications: Optional[NotificationsSettings] = user_settings.resolver.get((42, 'notifications'))

if notifications.enabled:
    print('User has notifications enabled')
```

Or without shortcuts:

```python
from wcd_settings.resolvers import (
    make_app_resolver, make_user_resolver,
)
from wcd_settings.setters import (
    make_app_setter, make_user_setter,
)


app_resolver = make_app_resolver()
user_resolver = make_user_resolver()

# You may use resolvers separately:
theme: Optional[ThemeSettings] = app_resolver.get('theme')
notifications: Optional[NotificationsSettings] = user_resolver.get((42, 'notifications'))

# And them create setters based on them:
app_settings = make_app_setter(app_resolver)
user_settings = make_user_setter(user_resolver)
```

### 3. Updating Settings with Setters

Setters update settings in the database with full Pydantic validation.

#### 3.1 Application Settings Setter

```python
from wcd_settings import shortcuts
from myapp.dtos import ThemeSettings


app_settings = shortcuts.app_settings(request=None)

# Update theme settings
theme_dto = ThemeSettings(color_scheme='dark', font_size=16)
app_settings.save('theme', theme_dto)
```

### 3.2 User Settings Setter

```python
from wcd_settings.setters import make_user_setter
from myapp.dtos import NotificationsSettings


user_settings = shortcuts.user_settings(request=None)

# Update user notifications settings
user_id = 42
notif_dto = NotificationsSettings(enabled=False, frequency='weekly')
user_settings.save((user_id, 'notifications'), notif_dto)
```

### 4 Access Settings in Views

Use the request.app_settings and request.user_settings properties injected by middleware.

```python
# views.py
from django.http import JsonResponse

def profile_view(request):
    theme = request.app_settings.resolver.get('theme')
    notifications = request.user_settings.resolver.get((request.user.id, 'notifications'))

    return JsonResponse({
        'theme': theme.color_scheme,
        'notifications_enabled': notifications.enabled,
    })
```

### 5. Advanced Usage

#### 5.1 Partial Updates

You can update nested fields within a settings DTO:

```python
from wcd_settings.setters import AppSettingsSetAction


actions = [
    AppSettingsSetAction(key='theme', path=['font_size'], value=18),
    AppSettingsSetAction(key='theme', path=['color_scheme'], value='dark'),
]

setter = make_app_setter(make_app_resolver())
commit, failures = setter.set(actions)

if not failures:
    commit()  # Save changes
```

You may even inject values into more complex datastructures

```python
from wcd_settings.setters import UserSettingsSetAction
from wcd_settings.registries import (
  SettingsDTO, UserSettingsDescriptor,
  user_settings_registry,
)
from wcd_settings.resolvers import make_user_resolver
from wcd_settings.setters import make_user_setter


class Nested(SettingsDTO):
    value: List[Optional[str]] = Field(default_factory=list)


class Complex(SettingsDTO):
    title: Optional[str] = None
    nested: Nested = Field(default_factory=Nested)


# User-specific setting
user_settings_registry.register(
  UserSettingsDescriptor(
    key='complex', dto=Complex,
    schema=Complex.model_json_schema(),
    is_editable=True, is_readable=True,
  )
)

actions = [
    UserSettingsSetAction((1, 'complex'), path=['title'], value='Title'),
    UserSettingsSetAction(
      (1, 'complex'),
      # Path could also inject values by list indexes:
      path=['nested', 'value', 0],
      value='first',
    ),
    UserSettingsSetAction(
      (1, 'complex'),
      # Even if index is larger than current data length all the previous
      # items in the list will be filled with `None`'s.
      # Be aware, that DTO validation should also support that.
      path=['nested', 'value', 3],
      value='fourth',
    ),
]

setter = make_user_setter(make_user_resolver())
commit, failures = setter.set(actions)

if not failures:
    commit()  # Save changes
```

### 6. Views

```python
# urls.py
from wcd_settings.urls import make_urlpatterns


# This will add views for getting and editing app and user settings:
urlpatterns += make_urlpatterns()
```

After that you may call:

- `/api/v1/settings/app/all/` - Retrieving all the app settings.
- `/api/v1/settings/app/set/` - Setting the values for the app settings.
- `/api/v1/settings/own/all/` - Retrieving all the current user's settings.
- `/api/v1/settings/own/set/` - Setting the values for the current user's settings.

From the example above calling the `/api/v1/settings/own/set/` with:

```json
{
  "actions": [
    {"path": ["complex", "title"], "value": "Title"},
    {"path": ["complex", "nested", "value", 0], "value": "one"},
  ]
}
```

Will update the user's settings to the following:

```python
settings = Complex(
  title='Title',
  nested=Nested(
    value=['one'],
  ),
)
```
