mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-11-07 03:36:09 -06:00
Compare commits
10 Commits
dependabot
...
feature-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30413c7f22 | ||
|
|
74ee57decc | ||
|
|
e90b114a39 | ||
|
|
a976e00174 | ||
|
|
808c074f48 | ||
|
|
7927e5c436 | ||
|
|
cac48c9855 | ||
|
|
3fda648f37 | ||
|
|
95736eebc4 | ||
|
|
85027dbffd |
@@ -32,7 +32,7 @@ RUN set -eux \
|
||||
# Purpose: Installs s6-overlay and rootfs
|
||||
# Comments:
|
||||
# - Don't leave anything extra in here either
|
||||
FROM ghcr.io/astral-sh/uv:0.9.4-python3.12-bookworm-slim AS s6-overlay-base
|
||||
FROM ghcr.io/astral-sh/uv:0.9.7-python3.12-bookworm-slim AS s6-overlay-base
|
||||
|
||||
WORKDIR /usr/src/s6
|
||||
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 2.19.5
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: ensure custom field query propagation, change detection [@shamoon](https://github.com/shamoon) ([#11291](https://github.com/paperless-ngx/paperless-ngx/pull/11291))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- docker(deps): Bump astral-sh/uv from 0.9.4-python3.12-bookworm-slim to 0.9.7-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#11283](https://github.com/paperless-ngx/paperless-ngx/pull/11283))
|
||||
|
||||
### All App Changes
|
||||
|
||||
- Fix: ensure custom field query propagation, change detection [@shamoon](https://github.com/shamoon) ([#11291](https://github.com/paperless-ngx/paperless-ngx/pull/11291))
|
||||
|
||||
## paperless-ngx 2.19.4
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "paperless-ngx"
|
||||
version = "2.19.4"
|
||||
version = "2.19.5"
|
||||
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -41,7 +41,7 @@ dependencies = [
|
||||
"djangorestframework~=3.16",
|
||||
"djangorestframework-guardian~=0.4.0",
|
||||
"drf-spectacular~=0.28",
|
||||
"drf-spectacular-sidecar~=2025.10.1",
|
||||
"drf-spectacular-sidecar~=2025.9.1",
|
||||
"drf-writable-nested~=0.7.1",
|
||||
"filelock~=3.20.0",
|
||||
"flower~=2.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paperless-ngx-ui",
|
||||
"version": "2.19.4",
|
||||
"version": "2.19.5",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"ng": "ng",
|
||||
|
||||
@@ -354,5 +354,13 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
||||
model.removeElement(atom)
|
||||
expect(completeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should subscribe to existing elements when queries are assigned', () => {
|
||||
const expression = new CustomFieldQueryExpression()
|
||||
const nextSpy = jest.spyOn(model.changed, 'next')
|
||||
model.queries = [expression]
|
||||
expression.changed.next(expression)
|
||||
expect(nextSpy).toHaveBeenCalledWith(model)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { first, Subject, takeUntil } from 'rxjs'
|
||||
import { first, Subject, Subscription, takeUntil } from 'rxjs'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import {
|
||||
CUSTOM_FIELD_QUERY_MAX_ATOMS,
|
||||
@@ -41,10 +41,27 @@ import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.comp
|
||||
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
||||
|
||||
export class CustomFieldQueriesModel {
|
||||
public queries: CustomFieldQueryElement[] = []
|
||||
private _queries: CustomFieldQueryElement[] = []
|
||||
private rootSubscriptions: Subscription[] = []
|
||||
|
||||
public readonly changed = new Subject<CustomFieldQueriesModel>()
|
||||
|
||||
public get queries(): CustomFieldQueryElement[] {
|
||||
return this._queries
|
||||
}
|
||||
|
||||
public set queries(value: CustomFieldQueryElement[]) {
|
||||
this.teardownRootSubscriptions()
|
||||
this._queries = value ?? []
|
||||
for (const element of this._queries) {
|
||||
this.rootSubscriptions.push(
|
||||
element.changed.subscribe(() => {
|
||||
this.changed.next(this)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public clear(fireEvent = true) {
|
||||
this.queries = []
|
||||
if (fireEvent) {
|
||||
@@ -107,14 +124,14 @@ export class CustomFieldQueriesModel {
|
||||
public addExpression(
|
||||
expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
|
||||
) {
|
||||
if (this.queries.length > 0) {
|
||||
;(
|
||||
(this.queries[0] as CustomFieldQueryExpression)
|
||||
.value as CustomFieldQueryElement[]
|
||||
).push(expression)
|
||||
} else {
|
||||
this.queries.push(expression)
|
||||
if (this.queries.length === 0) {
|
||||
this.queries = [expression]
|
||||
return
|
||||
}
|
||||
;(
|
||||
(this.queries[0] as CustomFieldQueryExpression)
|
||||
.value as CustomFieldQueryElement[]
|
||||
).push(expression)
|
||||
expression.changed.subscribe(() => {
|
||||
this.changed.next(this)
|
||||
})
|
||||
@@ -166,6 +183,13 @@ export class CustomFieldQueriesModel {
|
||||
this.changed.next(this)
|
||||
}
|
||||
}
|
||||
|
||||
private teardownRootSubscriptions() {
|
||||
for (const subscription of this.rootSubscriptions) {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
this.rootSubscriptions = []
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -183,6 +183,7 @@ export class ProfileEditDialogComponent
|
||||
this.newPassword && this.currentPassword !== this.newPassword
|
||||
const profile = Object.assign({}, this.form.value)
|
||||
delete profile.totp_code
|
||||
this.error = null
|
||||
this.networkActive = true
|
||||
this.profileService
|
||||
.update(profile)
|
||||
@@ -204,6 +205,7 @@ export class ProfileEditDialogComponent
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.showError($localize`Error saving profile`, error)
|
||||
this.error = error?.error
|
||||
this.networkActive = false
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { fakeAsync, tick } from '@angular/core/testing'
|
||||
import {
|
||||
CustomFieldQueryElementType,
|
||||
CustomFieldQueryLogicalOperator,
|
||||
@@ -111,13 +110,38 @@ describe('CustomFieldQueryAtom', () => {
|
||||
expect(atom.serialize()).toEqual([1, 'operator', 'value'])
|
||||
})
|
||||
|
||||
it('should emit changed on value change after debounce', fakeAsync(() => {
|
||||
it('should emit changed on value change immediately', () => {
|
||||
const atom = new CustomFieldQueryAtom()
|
||||
const changeSpy = jest.spyOn(atom.changed, 'next')
|
||||
atom.value = 'new value'
|
||||
tick(1000)
|
||||
expect(changeSpy).toHaveBeenCalled()
|
||||
}))
|
||||
})
|
||||
|
||||
it('should ignore duplicate array emissions', () => {
|
||||
const atom = new CustomFieldQueryAtom()
|
||||
atom.operator = CustomFieldQueryOperator.In
|
||||
const changeSpy = jest.fn()
|
||||
atom.changed.subscribe(changeSpy)
|
||||
|
||||
atom.value = [1, 2]
|
||||
expect(changeSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
changeSpy.mockClear()
|
||||
atom.value = [1, 2]
|
||||
expect(changeSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should emit when array values differ while length matches', () => {
|
||||
const atom = new CustomFieldQueryAtom()
|
||||
atom.operator = CustomFieldQueryOperator.In
|
||||
const changeSpy = jest.fn()
|
||||
atom.changed.subscribe(changeSpy)
|
||||
|
||||
atom.value = [1, 2]
|
||||
changeSpy.mockClear()
|
||||
atom.value = [1, 3]
|
||||
expect(changeSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('CustomFieldQueryExpression', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Subject, debounceTime, distinctUntilChanged } from 'rxjs'
|
||||
import { Subject, distinctUntilChanged } from 'rxjs'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {
|
||||
CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR,
|
||||
@@ -110,7 +110,22 @@ export class CustomFieldQueryAtom extends CustomFieldQueryElement {
|
||||
|
||||
protected override connectValueModelChanged(): void {
|
||||
this.valueModelChanged
|
||||
.pipe(debounceTime(1000), distinctUntilChanged())
|
||||
.pipe(
|
||||
distinctUntilChanged((previous, current) => {
|
||||
if (Array.isArray(previous) && Array.isArray(current)) {
|
||||
if (previous.length !== current.length) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < previous.length; i++) {
|
||||
if (previous[i] !== current[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return previous === current
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.changed.next(this)
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ export const environment = {
|
||||
apiVersion: '9', // match src/paperless/settings.py
|
||||
appTitle: 'Paperless-ngx',
|
||||
tag: 'prod',
|
||||
version: '2.19.4',
|
||||
version: '2.19.5',
|
||||
webSocketHost: window.location.host,
|
||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||
|
||||
@@ -764,7 +764,7 @@
|
||||
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">lines</target>
|
||||
<target state="translated">línies</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8838884664569764142" datatype="html">
|
||||
<source>Auto refresh</source>
|
||||
@@ -8700,7 +8700,7 @@
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||
<context context-type="linenumber">18</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Select:</target>
|
||||
<target state="translated">Selecciona:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6252070156626006029" datatype="html">
|
||||
<source>None</source>
|
||||
|
||||
@@ -764,7 +764,7 @@
|
||||
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">lines</target>
|
||||
<target state="translated">lignes</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8838884664569764142" datatype="html" approved="yes">
|
||||
<source>Auto refresh</source>
|
||||
@@ -8699,7 +8699,7 @@
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||
<context context-type="linenumber">18</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Select:</target>
|
||||
<target state="translated">Sélectionner :</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6252070156626006029" datatype="html" approved="yes">
|
||||
<source>None</source>
|
||||
|
||||
@@ -8700,7 +8700,7 @@
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||
<context context-type="linenumber">18</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Select:</target>
|
||||
<target state="translated">選取:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6252070156626006029" datatype="html">
|
||||
<source>None</source>
|
||||
|
||||
@@ -648,7 +648,7 @@ class TestApiUser(DirectoriesMixin, APITestCase):
|
||||
|
||||
user1 = {
|
||||
"username": "testuser",
|
||||
"password": "test",
|
||||
"password": "areallysupersecretpassword235",
|
||||
"first_name": "Test",
|
||||
"last_name": "User",
|
||||
}
|
||||
@@ -730,7 +730,7 @@ class TestApiUser(DirectoriesMixin, APITestCase):
|
||||
f"{self.ENDPOINT}{user1.pk}/",
|
||||
data={
|
||||
"first_name": "Updated Name 2",
|
||||
"password": "123xyz",
|
||||
"password": "newreallystrongpassword456",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -192,6 +192,65 @@ class TestApiProfile(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(user.first_name, user_data["first_name"])
|
||||
self.assertEqual(user.last_name, user_data["last_name"])
|
||||
|
||||
def test_update_profile_invalid_password_returns_field_error(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Configured user
|
||||
WHEN:
|
||||
- API call is made to update profile with weak password
|
||||
THEN:
|
||||
- Profile update fails with password field error
|
||||
"""
|
||||
|
||||
user_data = {
|
||||
"email": "new@email.com",
|
||||
"password": "short", # shorter than default validator threshold
|
||||
"first_name": "new first name",
|
||||
"last_name": "new last name",
|
||||
}
|
||||
|
||||
response = self.client.patch(self.ENDPOINT, user_data)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("password", response.data)
|
||||
self.assertIsInstance(response.data["password"], list)
|
||||
self.assertTrue(
|
||||
any(
|
||||
"too short" in message.lower() for message in response.data["password"]
|
||||
),
|
||||
)
|
||||
|
||||
def test_update_profile_placeholder_password_skips_validation(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Configured user with existing password
|
||||
WHEN:
|
||||
- API call is made with the obfuscated placeholder password value
|
||||
THEN:
|
||||
- Profile is updated without changing the password or running validators
|
||||
"""
|
||||
|
||||
original_password = "orig-pass-12345"
|
||||
self.user.set_password(original_password)
|
||||
self.user.save()
|
||||
|
||||
user_data = {
|
||||
"email": "new@email.com",
|
||||
"password": "*" * 12, # matches obfuscated value from serializer
|
||||
"first_name": "new first name",
|
||||
"last_name": "new last name",
|
||||
}
|
||||
|
||||
response = self.client.patch(self.ENDPOINT, user_data)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
user = User.objects.get(username=self.user.username)
|
||||
self.assertTrue(user.check_password(original_password))
|
||||
self.assertEqual(user.email, user_data["email"])
|
||||
self.assertEqual(user.first_name, user_data["first_name"])
|
||||
self.assertEqual(user.last_name, user_data["last_name"])
|
||||
|
||||
def test_update_auth_token(self):
|
||||
"""
|
||||
GIVEN:
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
||||
"PO-Revision-Date: 2025-10-28 18:07\n"
|
||||
"PO-Revision-Date: 2025-11-04 12:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Catalan\n"
|
||||
"Language: ca_ES\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
||||
"PO-Revision-Date: 2025-11-03 17:49\n"
|
||||
"PO-Revision-Date: 2025-11-06 00:35\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
||||
"PO-Revision-Date: 2025-10-29 12:14\n"
|
||||
"PO-Revision-Date: 2025-11-06 12:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-28 18:06+0000\n"
|
||||
"PO-Revision-Date: 2025-10-28 18:07\n"
|
||||
"PO-Revision-Date: 2025-11-06 00:35\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Traditional\n"
|
||||
"Language: zh_TW\n"
|
||||
|
||||
@@ -9,6 +9,7 @@ from allauth.socialaccount.models import SocialApp
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from rest_framework import serializers
|
||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||
|
||||
@@ -19,6 +20,23 @@ from paperless_mail.serialisers import ObfuscatedPasswordField
|
||||
logger = logging.getLogger("paperless.settings")
|
||||
|
||||
|
||||
class PasswordValidationMixin:
|
||||
def _has_real_password(self, value: str | None) -> bool:
|
||||
return bool(value) and value.replace("*", "") != ""
|
||||
|
||||
def validate_password(self, value: str) -> str:
|
||||
if not self._has_real_password(value):
|
||||
return value
|
||||
|
||||
request = self.context.get("request") if hasattr(self, "context") else None
|
||||
user = self.instance or (
|
||||
request.user if request and hasattr(request, "user") else None
|
||||
)
|
||||
validate_password(value, user) # raise ValidationError if invalid
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class PaperlessAuthTokenSerializer(AuthTokenSerializer):
|
||||
code = serializers.CharField(
|
||||
label="MFA Code",
|
||||
@@ -49,7 +67,7 @@ class PaperlessAuthTokenSerializer(AuthTokenSerializer):
|
||||
return attrs
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class UserSerializer(PasswordValidationMixin, serializers.ModelSerializer):
|
||||
password = ObfuscatedPasswordField(required=False)
|
||||
user_permissions = serializers.SlugRelatedField(
|
||||
many=True,
|
||||
@@ -87,11 +105,11 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
return obj.get_group_permissions()
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if "password" in validated_data:
|
||||
if len(validated_data.get("password").replace("*", "")) > 0:
|
||||
instance.set_password(validated_data.get("password"))
|
||||
instance.save()
|
||||
validated_data.pop("password")
|
||||
password = validated_data.pop("password", None)
|
||||
if self._has_real_password(password):
|
||||
instance.set_password(password)
|
||||
instance.save()
|
||||
|
||||
super().update(instance, validated_data)
|
||||
return instance
|
||||
|
||||
@@ -102,12 +120,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
user_permissions = None
|
||||
if "user_permissions" in validated_data:
|
||||
user_permissions = validated_data.pop("user_permissions")
|
||||
password = None
|
||||
if (
|
||||
"password" in validated_data
|
||||
and len(validated_data.get("password").replace("*", "")) > 0
|
||||
):
|
||||
password = validated_data.pop("password")
|
||||
password = validated_data.pop("password", None)
|
||||
user = User.objects.create(**validated_data)
|
||||
# set groups
|
||||
if groups:
|
||||
@@ -116,7 +129,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
if user_permissions:
|
||||
user.user_permissions.set(user_permissions)
|
||||
# set password
|
||||
if password:
|
||||
if self._has_real_password(password):
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
@@ -156,7 +169,7 @@ class SocialAccountSerializer(serializers.ModelSerializer):
|
||||
return "Unknown App"
|
||||
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
class ProfileSerializer(PasswordValidationMixin, serializers.ModelSerializer):
|
||||
email = serializers.EmailField(allow_blank=True, required=False)
|
||||
password = ObfuscatedPasswordField(required=False, allow_null=False)
|
||||
auth_token = serializers.SlugRelatedField(read_only=True, slug_field="key")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Final
|
||||
|
||||
__version__: Final[tuple[int, int, int]] = (2, 19, 4)
|
||||
__version__: Final[tuple[int, int, int]] = (2, 19, 5)
|
||||
# Version string like X.Y.Z
|
||||
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
||||
# Version string like X.Y
|
||||
|
||||
@@ -197,10 +197,10 @@ class ProfileView(GenericAPIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = self.request.user if hasattr(self.request, "user") else None
|
||||
|
||||
if len(serializer.validated_data.get("password").replace("*", "")) > 0:
|
||||
user.set_password(serializer.validated_data.get("password"))
|
||||
password = serializer.validated_data.pop("password", None)
|
||||
if password and password.replace("*", ""):
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
serializer.validated_data.pop("password")
|
||||
|
||||
for key, value in serializer.validated_data.items():
|
||||
setattr(user, key, value)
|
||||
|
||||
10
uv.lock
generated
10
uv.lock
generated
@@ -959,14 +959,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "drf-spectacular-sidecar"
|
||||
version = "2025.10.1"
|
||||
version = "2025.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/e4/99cd1b1c8c69788bd6cb6a2459674f8c75728e79df23ac7beddd094bf805/drf_spectacular_sidecar-2025.10.1.tar.gz", hash = "sha256:506a5a21ce1ad7211c28acb4e2112e213f6dc095a2052ee6ed6db1ffe8eb5a7b", size = 2420998, upload-time = "2025-10-01T11:23:27.092Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/e2/85a0b8dbed8631165a6b49b2aee57636da8e4e710c444566636ffd972a7b/drf_spectacular_sidecar-2025.9.1.tar.gz", hash = "sha256:da2aa45da48fff76de7a1e357b84d1eb0b9df40ca89ec19d5fe94ad1037bb3c8", size = 2420902, upload-time = "2025-09-01T11:23:24.156Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/87/70c67391e4ce68715d4dfae8dd33caeda2552af22f436ba55b8867a040fe/drf_spectacular_sidecar-2025.10.1-py3-none-any.whl", hash = "sha256:f1de343184d1a938179ce363d318258fe1e5f02f2f774625272364835f1c42bd", size = 2440241, upload-time = "2025-10-01T11:23:25.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/24/db59146ba89491fe1d44ca8aef239c94bf3c7fd41523976090f099430312/drf_spectacular_sidecar-2025.9.1-py3-none-any.whl", hash = "sha256:8e80625209b8a23ff27616db305b9ab71c2e2d1069dacd99720a9c11e429af50", size = 2440255, upload-time = "2025-09-01T11:23:22.822Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2115,7 +2115,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "paperless-ngx"
|
||||
version = "2.19.4"
|
||||
version = "2.19.5"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -2277,7 +2277,7 @@ requires-dist = [
|
||||
{ name = "djangorestframework", specifier = "~=3.16" },
|
||||
{ name = "djangorestframework-guardian", specifier = "~=0.4.0" },
|
||||
{ name = "drf-spectacular", specifier = "~=0.28" },
|
||||
{ name = "drf-spectacular-sidecar", specifier = "~=2025.10.1" },
|
||||
{ name = "drf-spectacular-sidecar", specifier = "~=2025.9.1" },
|
||||
{ name = "drf-writable-nested", specifier = "~=0.7.1" },
|
||||
{ name = "filelock", specifier = "~=3.20.0" },
|
||||
{ name = "flower", specifier = "~=2.0.1" },
|
||||
|
||||
Reference in New Issue
Block a user