Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
31a1dadd5c Chore(deps-dev): Bump webpack from 5.102.0 to 5.102.1 in /src-ui
Bumps [webpack](https://github.com/webpack/webpack) from 5.102.0 to 5.102.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.102.0...v5.102.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.102.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-04 01:42:32 +00:00
18 changed files with 48 additions and 119 deletions

View File

@@ -32,7 +32,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs # Purpose: Installs s6-overlay and rootfs
# Comments: # Comments:
# - Don't leave anything extra in here either # - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.9.7-python3.12-bookworm-slim AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.9.4-python3.12-bookworm-slim AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "paperless-ngx" name = "paperless-ngx"
version = "2.19.5" version = "2.19.4"
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents" description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"

View File

@@ -1,6 +1,6 @@
{ {
"name": "paperless-ngx-ui", "name": "paperless-ngx-ui",
"version": "2.19.5", "version": "2.19.4",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"ng": "ng", "ng": "ng",

36
src-ui/pnpm-lock.yaml generated
View File

@@ -3504,8 +3504,8 @@ packages:
resolution: {integrity: sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==} resolution: {integrity: sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==}
hasBin: true hasBin: true
baseline-browser-mapping@2.8.25: baseline-browser-mapping@2.8.23:
resolution: {integrity: sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==} resolution: {integrity: sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==}
hasBin: true hasBin: true
batch@0.6.1: batch@0.6.1:
@@ -3619,8 +3619,8 @@ packages:
caniuse-lite@1.0.30001746: caniuse-lite@1.0.30001746:
resolution: {integrity: sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==} resolution: {integrity: sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==}
caniuse-lite@1.0.30001754: caniuse-lite@1.0.30001753:
resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==}
canvas@3.0.0: canvas@3.0.0:
resolution: {integrity: sha512-NtcIBY88FjymQy+g2g5qnuP5IslrbWCQ3A6rSr1PeuYxVRapRZ3BZCrDyAakvI6CuDYidgZaf55ygulFVwROdg==} resolution: {integrity: sha512-NtcIBY88FjymQy+g2g5qnuP5IslrbWCQ3A6rSr1PeuYxVRapRZ3BZCrDyAakvI6CuDYidgZaf55ygulFVwROdg==}
@@ -4000,8 +4000,8 @@ packages:
electron-to-chromium@1.5.228: electron-to-chromium@1.5.228:
resolution: {integrity: sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA==} resolution: {integrity: sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA==}
electron-to-chromium@1.5.246: electron-to-chromium@1.5.244:
resolution: {integrity: sha512-CKp2enkTcw94o8p7P+nb3in3yILO7jAIoERSmkIhGazMuK2eLnPSVUH/dxUveGN8ulJJDjYUv0vV7y2e2AZ0nA==} resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==}
emittery@0.13.1: emittery@0.13.1:
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
@@ -6381,8 +6381,8 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
terser@5.44.1: terser@5.44.0:
resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==}
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
@@ -10456,7 +10456,7 @@ snapshots:
autoprefixer@10.4.21(postcss@8.5.3): autoprefixer@10.4.21(postcss@8.5.3):
dependencies: dependencies:
browserslist: 4.27.0 browserslist: 4.27.0
caniuse-lite: 1.0.30001754 caniuse-lite: 1.0.30001753
fraction.js: 4.3.7 fraction.js: 4.3.7
normalize-range: 0.1.2 normalize-range: 0.1.2
picocolors: 1.1.1 picocolors: 1.1.1
@@ -10554,7 +10554,7 @@ snapshots:
baseline-browser-mapping@2.8.10: {} baseline-browser-mapping@2.8.10: {}
baseline-browser-mapping@2.8.25: {} baseline-browser-mapping@2.8.23: {}
batch@0.6.1: {} batch@0.6.1: {}
@@ -10658,9 +10658,9 @@ snapshots:
browserslist@4.27.0: browserslist@4.27.0:
dependencies: dependencies:
baseline-browser-mapping: 2.8.25 baseline-browser-mapping: 2.8.23
caniuse-lite: 1.0.30001754 caniuse-lite: 1.0.30001753
electron-to-chromium: 1.5.246 electron-to-chromium: 1.5.244
node-releases: 2.0.27 node-releases: 2.0.27
update-browserslist-db: 1.1.4(browserslist@4.27.0) update-browserslist-db: 1.1.4(browserslist@4.27.0)
@@ -10719,7 +10719,7 @@ snapshots:
caniuse-lite@1.0.30001746: {} caniuse-lite@1.0.30001746: {}
caniuse-lite@1.0.30001754: {} caniuse-lite@1.0.30001753: {}
canvas@3.0.0: canvas@3.0.0:
dependencies: dependencies:
@@ -11061,7 +11061,7 @@ snapshots:
electron-to-chromium@1.5.228: {} electron-to-chromium@1.5.228: {}
electron-to-chromium@1.5.246: {} electron-to-chromium@1.5.244: {}
emittery@0.13.1: {} emittery@0.13.1: {}
@@ -14027,7 +14027,7 @@ snapshots:
jest-worker: 27.5.1 jest-worker: 27.5.1
schema-utils: 4.3.3 schema-utils: 4.3.3
serialize-javascript: 6.0.2 serialize-javascript: 6.0.2
terser: 5.44.1 terser: 5.44.0
webpack: 5.99.8(esbuild@0.25.5) webpack: 5.99.8(esbuild@0.25.5)
optionalDependencies: optionalDependencies:
esbuild: 0.25.5 esbuild: 0.25.5
@@ -14038,7 +14038,7 @@ snapshots:
jest-worker: 27.5.1 jest-worker: 27.5.1
schema-utils: 4.3.3 schema-utils: 4.3.3
serialize-javascript: 6.0.2 serialize-javascript: 6.0.2
terser: 5.44.1 terser: 5.44.0
webpack: 5.102.1 webpack: 5.102.1
terser@5.39.1: terser@5.39.1:
@@ -14048,7 +14048,7 @@ snapshots:
commander: 2.20.3 commander: 2.20.3
source-map-support: 0.5.21 source-map-support: 0.5.21
terser@5.44.1: terser@5.44.0:
dependencies: dependencies:
'@jridgewell/source-map': 0.3.11 '@jridgewell/source-map': 0.3.11
acorn: 8.15.0 acorn: 8.15.0

View File

@@ -354,13 +354,5 @@ describe('CustomFieldsQueryDropdownComponent', () => {
model.removeElement(atom) model.removeElement(atom)
expect(completeSpy).toHaveBeenCalled() 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)
})
}) })
}) })

View File

@@ -17,7 +17,7 @@ import {
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select' import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { first, Subject, Subscription, takeUntil } from 'rxjs' import { first, Subject, takeUntil } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { import {
CUSTOM_FIELD_QUERY_MAX_ATOMS, CUSTOM_FIELD_QUERY_MAX_ATOMS,
@@ -41,27 +41,10 @@ import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.comp
import { DocumentLinkComponent } from '../input/document-link/document-link.component' import { DocumentLinkComponent } from '../input/document-link/document-link.component'
export class CustomFieldQueriesModel { export class CustomFieldQueriesModel {
private _queries: CustomFieldQueryElement[] = [] public queries: CustomFieldQueryElement[] = []
private rootSubscriptions: Subscription[] = []
public readonly changed = new Subject<CustomFieldQueriesModel>() 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) { public clear(fireEvent = true) {
this.queries = [] this.queries = []
if (fireEvent) { if (fireEvent) {
@@ -124,14 +107,14 @@ export class CustomFieldQueriesModel {
public addExpression( public addExpression(
expression: CustomFieldQueryExpression = new CustomFieldQueryExpression() expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
) { ) {
if (this.queries.length === 0) { if (this.queries.length > 0) {
this.queries = [expression] ;(
return (this.queries[0] as CustomFieldQueryExpression)
.value as CustomFieldQueryElement[]
).push(expression)
} else {
this.queries.push(expression)
} }
;(
(this.queries[0] as CustomFieldQueryExpression)
.value as CustomFieldQueryElement[]
).push(expression)
expression.changed.subscribe(() => { expression.changed.subscribe(() => {
this.changed.next(this) this.changed.next(this)
}) })
@@ -183,13 +166,6 @@ export class CustomFieldQueriesModel {
this.changed.next(this) this.changed.next(this)
} }
} }
private teardownRootSubscriptions() {
for (const subscription of this.rootSubscriptions) {
subscription.unsubscribe()
}
this.rootSubscriptions = []
}
} }
@Component({ @Component({

View File

@@ -1,3 +1,4 @@
import { fakeAsync, tick } from '@angular/core/testing'
import { import {
CustomFieldQueryElementType, CustomFieldQueryElementType,
CustomFieldQueryLogicalOperator, CustomFieldQueryLogicalOperator,
@@ -110,38 +111,13 @@ describe('CustomFieldQueryAtom', () => {
expect(atom.serialize()).toEqual([1, 'operator', 'value']) expect(atom.serialize()).toEqual([1, 'operator', 'value'])
}) })
it('should emit changed on value change immediately', () => { it('should emit changed on value change after debounce', fakeAsync(() => {
const atom = new CustomFieldQueryAtom() const atom = new CustomFieldQueryAtom()
const changeSpy = jest.spyOn(atom.changed, 'next') const changeSpy = jest.spyOn(atom.changed, 'next')
atom.value = 'new value' atom.value = 'new value'
tick(1000)
expect(changeSpy).toHaveBeenCalled() 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', () => { describe('CustomFieldQueryExpression', () => {

View File

@@ -1,4 +1,4 @@
import { Subject, distinctUntilChanged } from 'rxjs' import { Subject, debounceTime, distinctUntilChanged } from 'rxjs'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { import {
CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR, CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR,
@@ -110,22 +110,7 @@ export class CustomFieldQueryAtom extends CustomFieldQueryElement {
protected override connectValueModelChanged(): void { protected override connectValueModelChanged(): void {
this.valueModelChanged this.valueModelChanged
.pipe( .pipe(debounceTime(1000), distinctUntilChanged())
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(() => { .subscribe(() => {
this.changed.next(this) this.changed.next(this)
}) })

View File

@@ -6,7 +6,7 @@ export const environment = {
apiVersion: '9', // match src/paperless/settings.py apiVersion: '9', // match src/paperless/settings.py
appTitle: 'Paperless-ngx', appTitle: 'Paperless-ngx',
tag: 'prod', tag: 'prod',
version: '2.19.5', version: '2.19.4',
webSocketHost: window.location.host, webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/', webSocketBaseUrl: base_url.pathname + 'ws/',

View File

@@ -764,7 +764,7 @@
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context> <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
<context context-type="linenumber">17</context> <context context-type="linenumber">17</context>
</context-group> </context-group>
<target state="translated">línies</target> <target state="needs-translation">lines</target>
</trans-unit> </trans-unit>
<trans-unit id="8838884664569764142" datatype="html"> <trans-unit id="8838884664569764142" datatype="html">
<source>Auto refresh</source> <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="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">18</context> <context context-type="linenumber">18</context>
</context-group> </context-group>
<target state="translated">Selecciona:</target> <target state="needs-translation">Select:</target>
</trans-unit> </trans-unit>
<trans-unit id="6252070156626006029" datatype="html"> <trans-unit id="6252070156626006029" datatype="html">
<source>None</source> <source>None</source>

View File

@@ -764,7 +764,7 @@
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context> <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
<context context-type="linenumber">17</context> <context context-type="linenumber">17</context>
</context-group> </context-group>
<target state="translated">lignes</target> <target state="needs-translation">lines</target>
</trans-unit> </trans-unit>
<trans-unit id="8838884664569764142" datatype="html" approved="yes"> <trans-unit id="8838884664569764142" datatype="html" approved="yes">
<source>Auto refresh</source> <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="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">18</context> <context context-type="linenumber">18</context>
</context-group> </context-group>
<target state="translated">Sélectionner :</target> <target state="needs-translation">Select:</target>
</trans-unit> </trans-unit>
<trans-unit id="6252070156626006029" datatype="html" approved="yes"> <trans-unit id="6252070156626006029" datatype="html" approved="yes">
<source>None</source> <source>None</source>

View File

@@ -8700,7 +8700,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">18</context> <context context-type="linenumber">18</context>
</context-group> </context-group>
<target state="translated">選取:</target> <target state="needs-translation">Select:</target>
</trans-unit> </trans-unit>
<trans-unit id="6252070156626006029" datatype="html"> <trans-unit id="6252070156626006029" datatype="html">
<source>None</source> <source>None</source>

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-28 18:06+0000\n" "POT-Creation-Date: 2025-10-28 18:06+0000\n"
"PO-Revision-Date: 2025-11-04 12:15\n" "PO-Revision-Date: 2025-10-28 18:07\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Catalan\n" "Language-Team: Catalan\n"
"Language: ca_ES\n" "Language: ca_ES\n"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-28 18:06+0000\n" "POT-Creation-Date: 2025-10-28 18:06+0000\n"
"PO-Revision-Date: 2025-11-06 00:35\n" "PO-Revision-Date: 2025-11-03 17:49\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
"Language: de_DE\n" "Language: de_DE\n"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-28 18:06+0000\n" "POT-Creation-Date: 2025-10-28 18:06+0000\n"
"PO-Revision-Date: 2025-11-06 12:14\n" "PO-Revision-Date: 2025-10-29 12:14\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: French\n" "Language-Team: French\n"
"Language: fr_FR\n" "Language: fr_FR\n"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-28 18:06+0000\n" "POT-Creation-Date: 2025-10-28 18:06+0000\n"
"PO-Revision-Date: 2025-11-06 00:35\n" "PO-Revision-Date: 2025-10-28 18:07\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Chinese Traditional\n" "Language-Team: Chinese Traditional\n"
"Language: zh_TW\n" "Language: zh_TW\n"

View File

@@ -1,6 +1,6 @@
from typing import Final from typing import Final
__version__: Final[tuple[int, int, int]] = (2, 19, 5) __version__: Final[tuple[int, int, int]] = (2, 19, 4)
# Version string like X.Y.Z # Version string like X.Y.Z
__full_version_str__: Final[str] = ".".join(map(str, __version__)) __full_version_str__: Final[str] = ".".join(map(str, __version__))
# Version string like X.Y # Version string like X.Y

2
uv.lock generated
View File

@@ -2115,7 +2115,7 @@ wheels = [
[[package]] [[package]]
name = "paperless-ngx" name = "paperless-ngx"
version = "2.19.5" version = "2.19.4"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },