mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-05-01 11:19:32 -05:00
Merge branch 'dev' into feature-notification-wf-action
This commit is contained in:
commit
5d3966bd84
@ -48,7 +48,7 @@ repos:
|
|||||||
exclude: "(^Pipfile\\.lock$)"
|
exclude: "(^Pipfile\\.lock$)"
|
||||||
# Python hooks
|
# Python hooks
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: 'v0.7.3'
|
rev: 'v0.8.0'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
135
Pipfile.lock
generated
135
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "a194c6834fba6a14712ba36eb0b896f18d7ef4393523e5d55ccb103104e99ddb"
|
"sha256": "e4cb2328c49829f56793ef25780dcc73ea8e4838e6e9bc25d1b6feb74eb3befe"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {},
|
"requires": {},
|
||||||
@ -3242,11 +3242,11 @@
|
|||||||
},
|
},
|
||||||
"httpcore": {
|
"httpcore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f",
|
"sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c",
|
||||||
"sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"
|
"sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.0.6"
|
"version": "==1.0.7"
|
||||||
},
|
},
|
||||||
"httpx": {
|
"httpx": {
|
||||||
"extras": [
|
"extras": [
|
||||||
@ -3311,7 +3311,6 @@
|
|||||||
"sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369",
|
"sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369",
|
||||||
"sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"
|
"sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==3.1.4"
|
"version": "==3.1.4"
|
||||||
},
|
},
|
||||||
@ -3424,12 +3423,12 @@
|
|||||||
},
|
},
|
||||||
"mkdocs-material": {
|
"mkdocs-material": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca",
|
"sha256:98f0a2039c62e551a68aad0791a8d41324ff90c03a6e6cea381a384b84908b83",
|
||||||
"sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0"
|
"sha256:ae2043f4238e572f9a40e0b577f50400d6fc31e2fef8ea141800aebf3bd273d7"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==9.5.44"
|
"version": "==9.5.46"
|
||||||
},
|
},
|
||||||
"mkdocs-material-extensions": {
|
"mkdocs-material-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -3723,12 +3722,12 @@
|
|||||||
},
|
},
|
||||||
"pytest-httpx": {
|
"pytest-httpx": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4af9ab0dae5e9c14cb1e27d18af3db1f627b2cf3b11c02b34ddf26aff6b0a24c",
|
"sha256:3ca4b0975c0f93b985f17df19e76430c1086b5b0cce32b1af082d8901296a735",
|
||||||
"sha256:bdd1b00a846cfe857194e4d3ba72dc08ba0d163154a4404269c9b971f357c05d"
|
"sha256:42cf0a66f7b71b9111db2897e8b38a903abd33a27b11c48aff4a3c7650313af2"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.9'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==0.33.0"
|
"version": "==0.34.0"
|
||||||
},
|
},
|
||||||
"pytest-mock": {
|
"pytest-mock": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -3741,12 +3740,12 @@
|
|||||||
},
|
},
|
||||||
"pytest-rerunfailures": {
|
"pytest-rerunfailures": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4197bdd2eaeffdbf50b5ea6e7236f47ff0e44d1def8dae08e409f536d84e7b32",
|
"sha256:2d9ac7baf59f4c13ac730b47f6fa80e755d1ba0581da45ce30b72fb3542b4474",
|
||||||
"sha256:4a400bcbcd3c7a4ad151ab8afac123d90eca3abe27f98725dc4d9702887d2e92"
|
"sha256:dd150c4795c229ef44320adc9a0c0532c51b78bb7a6843a8c53556b9a611df1a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==14.0"
|
"version": "==15.0"
|
||||||
},
|
},
|
||||||
"pytest-sugar": {
|
"pytest-sugar": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -3770,8 +3769,7 @@
|
|||||||
"sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
|
"sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
|
||||||
"sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
|
"sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
|
||||||
"version": "==2.9.0.post0"
|
"version": "==2.9.0.post0"
|
||||||
},
|
},
|
||||||
"pywavelets": {
|
"pywavelets": {
|
||||||
@ -3993,28 +3991,28 @@
|
|||||||
},
|
},
|
||||||
"ruff": {
|
"ruff": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2",
|
"sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c",
|
||||||
"sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c",
|
"sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b",
|
||||||
"sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344",
|
"sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df",
|
||||||
"sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9",
|
"sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9",
|
||||||
"sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2",
|
"sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f",
|
||||||
"sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299",
|
"sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468",
|
||||||
"sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc",
|
"sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426",
|
||||||
"sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088",
|
"sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a",
|
||||||
"sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16",
|
"sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd",
|
||||||
"sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5",
|
"sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70",
|
||||||
"sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d",
|
"sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2",
|
||||||
"sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29",
|
"sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c",
|
||||||
"sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e",
|
"sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44",
|
||||||
"sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67",
|
"sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6",
|
||||||
"sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2",
|
"sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362",
|
||||||
"sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5",
|
"sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99",
|
||||||
"sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313",
|
"sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3",
|
||||||
"sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0"
|
"sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==0.7.3"
|
"version": "==0.8.0"
|
||||||
},
|
},
|
||||||
"scipy": {
|
"scipy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -4076,7 +4074,7 @@
|
|||||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.16.0"
|
"version": "==1.16.0"
|
||||||
},
|
},
|
||||||
"sniffio": {
|
"sniffio": {
|
||||||
@ -4148,40 +4146,39 @@
|
|||||||
},
|
},
|
||||||
"watchdog": {
|
"watchdog": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7",
|
"sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a",
|
||||||
"sha256:1021223c08ba8d2d38d71ec1704496471ffd7be42cfb26b87cd5059323a389a1",
|
"sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2",
|
||||||
"sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176",
|
"sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f",
|
||||||
"sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c",
|
"sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c",
|
||||||
"sha256:223160bb359281bb8e31c8f1068bf71a6b16a8ad3d9524ca6f523ac666bb6a1e",
|
"sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c",
|
||||||
"sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97",
|
"sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c",
|
||||||
"sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05",
|
"sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0",
|
||||||
"sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926",
|
"sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13",
|
||||||
"sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45",
|
"sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134",
|
||||||
"sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e",
|
"sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa",
|
||||||
"sha256:53adf73dcdc0ef04f7735066b4a57a4cd3e49ef135daae41d77395f0b5b692cb",
|
"sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e",
|
||||||
"sha256:560135542c91eaa74247a2e8430cf83c4342b29e8ad4f520ae14f0c8a19cfb5b",
|
"sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379",
|
||||||
"sha256:720ef9d3a4f9ca575a780af283c8fd3a0674b307651c1976714745090da5a9e8",
|
"sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a",
|
||||||
"sha256:752fb40efc7cc8d88ebc332b8f4bcbe2b5cc7e881bccfeb8e25054c00c994ee3",
|
"sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11",
|
||||||
"sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c",
|
"sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282",
|
||||||
"sha256:85527b882f3facda0579bce9d743ff7f10c3e1e0db0a0d0e28170a7d0e5ce2ea",
|
"sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b",
|
||||||
"sha256:90a67d7857adb1d985aca232cc9905dd5bc4803ed85cfcdcfcf707e52049eda7",
|
"sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f",
|
||||||
"sha256:91b522adc25614cdeaf91f7897800b82c13b4b8ac68a42ca959f992f6990c490",
|
"sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c",
|
||||||
"sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221",
|
"sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112",
|
||||||
"sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8",
|
"sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948",
|
||||||
"sha256:950f531ec6e03696a2414b6308f5c6ff9dab7821a768c9d5788b1314e9a46ca7",
|
"sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881",
|
||||||
"sha256:a2e8f3f955d68471fa37b0e3add18500790d129cc7efe89971b8a4cc6fdeb0b2",
|
"sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860",
|
||||||
"sha256:ae6deb336cba5d71476caa029ceb6e88047fc1dc74b62b7c4012639c0b563906",
|
"sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3",
|
||||||
"sha256:b8ca4d854adcf480bdfd80f46fdd6fb49f91dd020ae11c89b3a79e19454ec627",
|
"sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680",
|
||||||
"sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49",
|
"sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26",
|
||||||
"sha256:d52db5beb5e476e6853da2e2d24dbbbed6797b449c8bf7ea118a4ee0d2c9040e",
|
"sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26",
|
||||||
"sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91",
|
"sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e",
|
||||||
"sha256:e25adddab85f674acac303cf1f5835951345a56c5f7f582987d266679979c75b",
|
"sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8",
|
||||||
"sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9",
|
"sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c",
|
||||||
"sha256:f01f4a3565a387080dc49bdd1fefe4ecc77f894991b88ef927edbfa45eb10818"
|
"sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
|
||||||
"markers": "python_version >= '3.9'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==5.0.3"
|
"version": "==6.0.0"
|
||||||
},
|
},
|
||||||
"zope-interface": {
|
"zope-interface": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -17,7 +17,11 @@ const customFields: CustomField[] = [
|
|||||||
name: 'Field 4',
|
name: 'Field 4',
|
||||||
data_type: CustomFieldDataType.Select,
|
data_type: CustomFieldDataType.Select,
|
||||||
extra_data: {
|
extra_data: {
|
||||||
select_options: ['Option 1', 'Option 2', 'Option 3'],
|
select_options: [
|
||||||
|
{ label: 'Option 1', id: 'abc-123' },
|
||||||
|
{ label: 'Option 2', id: 'def-456' },
|
||||||
|
{ label: 'Option 3', id: 'ghi-789' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -131,6 +135,8 @@ describe('CustomFieldDisplayComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show select value', () => {
|
it('should show select value', () => {
|
||||||
expect(component.getSelectValue(customFields[3], 2)).toEqual('Option 3')
|
expect(component.getSelectValue(customFields[3], 'ghi-789')).toEqual(
|
||||||
|
'Option 3'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -117,8 +117,8 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
|||||||
return this.docLinkDocuments?.find((d) => d.id === docId)?.title
|
return this.docLinkDocuments?.find((d) => d.id === docId)?.title
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSelectValue(field: CustomField, index: number): string {
|
public getSelectValue(field: CustomField, id: string): string {
|
||||||
return field.extra_data.select_options[index]
|
return field.extra_data.select_options?.find((o) => o.id === id)?.label
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
@ -44,6 +44,8 @@
|
|||||||
<ng-select #fieldSelects
|
<ng-select #fieldSelects
|
||||||
class="paperless-input-select rounded-end"
|
class="paperless-input-select rounded-end"
|
||||||
[items]="getSelectOptionsForField(atom.field)"
|
[items]="getSelectOptionsForField(atom.field)"
|
||||||
|
bindLabel="label"
|
||||||
|
bindValue="id"
|
||||||
[(ngModel)]="atom.value"
|
[(ngModel)]="atom.value"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
(mousedown)="$event.stopImmediatePropagation()"
|
(mousedown)="$event.stopImmediatePropagation()"
|
||||||
@ -99,6 +101,8 @@
|
|||||||
<ng-select
|
<ng-select
|
||||||
class="paperless-input-select rounded-end"
|
class="paperless-input-select rounded-end"
|
||||||
[items]="getSelectOptionsForField(atom.field)"
|
[items]="getSelectOptionsForField(atom.field)"
|
||||||
|
bindLabel="label"
|
||||||
|
bindValue="id"
|
||||||
[(ngModel)]="atom.value"
|
[(ngModel)]="atom.value"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
[multiple]="true"
|
[multiple]="true"
|
||||||
|
@ -39,7 +39,12 @@ const customFields = [
|
|||||||
id: 2,
|
id: 2,
|
||||||
name: 'Test Select Field',
|
name: 'Test Select Field',
|
||||||
data_type: CustomFieldDataType.Select,
|
data_type: CustomFieldDataType.Select,
|
||||||
extra_data: { select_options: ['Option 1', 'Option 2'] },
|
extra_data: {
|
||||||
|
select_options: [
|
||||||
|
{ label: 'Option 1', id: 'abc-123' },
|
||||||
|
{ label: 'Option 2', id: 'def-456' },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -128,11 +133,19 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Field',
|
name: 'Test Field',
|
||||||
data_type: CustomFieldDataType.Select,
|
data_type: CustomFieldDataType.Select,
|
||||||
extra_data: { select_options: ['Option 1', 'Option 2'] },
|
extra_data: {
|
||||||
|
select_options: [
|
||||||
|
{ label: 'Option 1', id: 'abc-123' },
|
||||||
|
{ label: 'Option 2', id: 'def-456' },
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
component.customFields = [field]
|
component.customFields = [field]
|
||||||
const options = component.getSelectOptionsForField(1)
|
const options = component.getSelectOptionsForField(1)
|
||||||
expect(options).toEqual(['Option 1', 'Option 2'])
|
expect(options).toEqual([
|
||||||
|
{ label: 'Option 1', id: 'abc-123' },
|
||||||
|
{ label: 'Option 2', id: 'def-456' },
|
||||||
|
])
|
||||||
|
|
||||||
// Fallback to empty array if field is not found
|
// Fallback to empty array if field is not found
|
||||||
const options2 = component.getSelectOptionsForField(2)
|
const options2 = component.getSelectOptionsForField(2)
|
||||||
|
@ -311,7 +311,9 @@ export class CustomFieldsQueryDropdownComponent implements OnDestroy {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectOptionsForField(fieldID: number): string[] {
|
getSelectOptionsForField(
|
||||||
|
fieldID: number
|
||||||
|
): Array<{ label: string; id: string }> {
|
||||||
const field = this.customFields.find((field) => field.id === fieldID)
|
const field = this.customFields.find((field) => field.id === fieldID)
|
||||||
if (field) {
|
if (field) {
|
||||||
return field.extra_data['select_options']
|
return field.extra_data['select_options']
|
||||||
|
@ -21,8 +21,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<div formArrayName="select_options">
|
<div formArrayName="select_options">
|
||||||
@for (option of objectForm.controls.extra_data.controls.select_options.controls; track option; let i = $index) {
|
@for (option of objectForm.controls.extra_data.controls.select_options.controls; track option; let i = $index) {
|
||||||
<div class="input-group input-group-sm my-2">
|
<div class="input-group input-group-sm my-2" [formGroup]="objectForm.controls.extra_data.controls.select_options.controls[i]">
|
||||||
<input #selectOption type="text" class="form-control" [formControl]="option" autocomplete="off">
|
<input #selectOption type="text" class="form-control" formControlName="label" autocomplete="off">
|
||||||
|
<input type="hidden" formControlName="id">
|
||||||
<button type="button" class="btn btn-outline-danger" (click)="removeSelectOption(i)" i18n>Delete</button>
|
<button type="button" class="btn btn-outline-danger" (click)="removeSelectOption(i)" i18n>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,11 @@ describe('CustomFieldEditDialogComponent', () => {
|
|||||||
name: 'Field 1',
|
name: 'Field 1',
|
||||||
data_type: CustomFieldDataType.Select,
|
data_type: CustomFieldDataType.Select,
|
||||||
extra_data: {
|
extra_data: {
|
||||||
select_options: ['Option 1', 'Option 2', 'Option 3'],
|
select_options: [
|
||||||
|
{ label: 'Option 1', id: '123-xyz' },
|
||||||
|
{ label: 'Option 2', id: '456-abc' },
|
||||||
|
{ label: 'Option 3', id: '789-123' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
@ -94,6 +98,10 @@ describe('CustomFieldEditDialogComponent', () => {
|
|||||||
component.dialogMode = EditDialogMode.CREATE
|
component.dialogMode = EditDialogMode.CREATE
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
|
expect(
|
||||||
|
component.objectForm.get('extra_data').get('select_options').value.length
|
||||||
|
).toBe(0)
|
||||||
|
component.addSelectOption()
|
||||||
expect(
|
expect(
|
||||||
component.objectForm.get('extra_data').get('select_options').value.length
|
component.objectForm.get('extra_data').get('select_options').value.length
|
||||||
).toBe(1)
|
).toBe(1)
|
||||||
@ -101,14 +109,10 @@ describe('CustomFieldEditDialogComponent', () => {
|
|||||||
expect(
|
expect(
|
||||||
component.objectForm.get('extra_data').get('select_options').value.length
|
component.objectForm.get('extra_data').get('select_options').value.length
|
||||||
).toBe(2)
|
).toBe(2)
|
||||||
component.addSelectOption()
|
|
||||||
expect(
|
|
||||||
component.objectForm.get('extra_data').get('select_options').value.length
|
|
||||||
).toBe(3)
|
|
||||||
component.removeSelectOption(0)
|
component.removeSelectOption(0)
|
||||||
expect(
|
expect(
|
||||||
component.objectForm.get('extra_data').get('select_options').value.length
|
component.objectForm.get('extra_data').get('select_options').value.length
|
||||||
).toBe(2)
|
).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should focus on last select option input', () => {
|
it('should focus on last select option input', () => {
|
||||||
|
@ -57,9 +57,16 @@ export class CustomFieldEditDialogComponent
|
|||||||
}
|
}
|
||||||
if (this.object?.data_type === CustomFieldDataType.Select) {
|
if (this.object?.data_type === CustomFieldDataType.Select) {
|
||||||
this.selectOptions.clear()
|
this.selectOptions.clear()
|
||||||
this.object.extra_data.select_options.forEach((option) =>
|
this.object.extra_data.select_options
|
||||||
this.selectOptions.push(new FormControl(option))
|
.filter((option) => option)
|
||||||
)
|
.forEach((option) =>
|
||||||
|
this.selectOptions.push(
|
||||||
|
new FormGroup({
|
||||||
|
label: new FormControl(option.label),
|
||||||
|
id: new FormControl(option.id),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +96,7 @@ export class CustomFieldEditDialogComponent
|
|||||||
name: new FormControl(null),
|
name: new FormControl(null),
|
||||||
data_type: new FormControl(null),
|
data_type: new FormControl(null),
|
||||||
extra_data: new FormGroup({
|
extra_data: new FormGroup({
|
||||||
select_options: new FormArray([new FormControl(null)]),
|
select_options: new FormArray([]),
|
||||||
default_currency: new FormControl(null),
|
default_currency: new FormControl(null),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@ -104,7 +111,9 @@ export class CustomFieldEditDialogComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addSelectOption() {
|
public addSelectOption() {
|
||||||
this.selectOptions.push(new FormControl(''))
|
this.selectOptions.push(
|
||||||
|
new FormGroup({ label: new FormControl(null), id: new FormControl(null) })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeSelectOption(index: number) {
|
public removeSelectOption(index: number) {
|
||||||
|
@ -132,12 +132,4 @@ describe('SelectComponent', () => {
|
|||||||
const expectedTitle = `Filter documents with this ${component.title}`
|
const expectedTitle = `Filter documents with this ${component.title}`
|
||||||
expect(component.filterButtonTitle).toEqual(expectedTitle)
|
expect(component.filterButtonTitle).toEqual(expectedTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support setting items as a plain array', () => {
|
|
||||||
component.itemsArray = ['foo', 'bar']
|
|
||||||
expect(component.items).toEqual([
|
|
||||||
{ id: 0, name: 'foo' },
|
|
||||||
{ id: 1, name: 'bar' },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -34,11 +34,6 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
|||||||
if (items && this.value) this.checkForPrivateItems(this.value)
|
if (items && this.value) this.checkForPrivateItems(this.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input()
|
|
||||||
set itemsArray(items: any[]) {
|
|
||||||
this._items = items.map((item, index) => ({ id: index, name: item }))
|
|
||||||
}
|
|
||||||
|
|
||||||
writeValue(newValue: any): void {
|
writeValue(newValue: any): void {
|
||||||
if (newValue && this._items) {
|
if (newValue && this._items) {
|
||||||
this.checkForPrivateItems(newValue)
|
this.checkForPrivateItems(newValue)
|
||||||
|
@ -190,7 +190,8 @@
|
|||||||
@case (CustomFieldDataType.Select) {
|
@case (CustomFieldDataType.Select) {
|
||||||
<pngx-input-select formControlName="value"
|
<pngx-input-select formControlName="value"
|
||||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||||
[itemsArray]="getCustomFieldFromInstance(fieldInstance)?.extra_data.select_options"
|
[items]="getCustomFieldFromInstance(fieldInstance)?.extra_data.select_options"
|
||||||
|
bindLabel="label"
|
||||||
[allowNull]="true"
|
[allowNull]="true"
|
||||||
[horizontal]="true"
|
[horizontal]="true"
|
||||||
[removable]="userIsOwner"
|
[removable]="userIsOwner"
|
||||||
|
@ -56,7 +56,7 @@ export interface CustomField extends ObjectWithId {
|
|||||||
name: string
|
name: string
|
||||||
created?: Date
|
created?: Date
|
||||||
extra_data?: {
|
extra_data?: {
|
||||||
select_options?: string[]
|
select_options?: Array<{ label: string; id: string }>
|
||||||
default_currency?: string
|
default_currency?: string
|
||||||
}
|
}
|
||||||
document_count?: number
|
document_count?: number
|
||||||
|
@ -176,9 +176,9 @@ class CustomFieldsFilter(Filter):
|
|||||||
if fields_with_matching_selects.count() > 0:
|
if fields_with_matching_selects.count() > 0:
|
||||||
for field in fields_with_matching_selects:
|
for field in fields_with_matching_selects:
|
||||||
options = field.extra_data.get("select_options", [])
|
options = field.extra_data.get("select_options", [])
|
||||||
for index, option in enumerate(options):
|
for _, option in enumerate(options):
|
||||||
if option.lower().find(value.lower()) != -1:
|
if option.get("label").lower().find(value.lower()) != -1:
|
||||||
option_ids.extend([index])
|
option_ids.extend([option.get("id")])
|
||||||
return (
|
return (
|
||||||
qs.filter(custom_fields__field__name__icontains=value)
|
qs.filter(custom_fields__field__name__icontains=value)
|
||||||
| qs.filter(custom_fields__value_text__icontains=value)
|
| qs.filter(custom_fields__value_text__icontains=value)
|
||||||
@ -195,19 +195,21 @@ class CustomFieldsFilter(Filter):
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class SelectField(serializers.IntegerField):
|
class SelectField(serializers.CharField):
|
||||||
def __init__(self, custom_field: CustomField):
|
def __init__(self, custom_field: CustomField):
|
||||||
self._options = custom_field.extra_data["select_options"]
|
self._options = custom_field.extra_data["select_options"]
|
||||||
super().__init__(min_value=0, max_value=len(self._options))
|
super().__init__(max_length=16)
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if not isinstance(data, int):
|
# If the supplied value is the option label instead of the ID
|
||||||
# If the supplied value is not an integer,
|
try:
|
||||||
# we will try to map it to an option index.
|
data = next(
|
||||||
try:
|
option.get("id")
|
||||||
data = self._options.index(data)
|
for option in self._options
|
||||||
except ValueError:
|
if option.get("label") == data
|
||||||
pass
|
)
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
return super().to_internal_value(data)
|
return super().to_internal_value(data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -317,10 +317,8 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
# Check the files against the timeout
|
# Check the files against the timeout
|
||||||
still_waiting = {}
|
still_waiting = {}
|
||||||
for filepath in notified_files:
|
# last_event_time is time of the last inotify event for this file
|
||||||
# Time of the last inotify event for this file
|
for filepath, last_event_time in notified_files.items():
|
||||||
last_event_time = notified_files[filepath]
|
|
||||||
|
|
||||||
# Current time - last time over the configured timeout
|
# Current time - last time over the configured timeout
|
||||||
waited_long_enough = (
|
waited_long_enough = (
|
||||||
monotonic() - last_event_time
|
monotonic() - last_event_time
|
||||||
|
@ -294,9 +294,9 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
manifest_dict = {}
|
manifest_dict = {}
|
||||||
|
|
||||||
# Build an overall manifest
|
# Build an overall manifest
|
||||||
for key in manifest_key_to_object_query:
|
for key, object_query in manifest_key_to_object_query.items():
|
||||||
manifest_dict[key] = json.loads(
|
manifest_dict[key] = json.loads(
|
||||||
serializers.serialize("json", manifest_key_to_object_query[key]),
|
serializers.serialize("json", object_query),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.encrypt_secret_fields(manifest_dict)
|
self.encrypt_secret_fields(manifest_dict)
|
||||||
@ -370,8 +370,8 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
|
|
||||||
# 4.1 write primary manifest to target folder
|
# 4.1 write primary manifest to target folder
|
||||||
manifest = []
|
manifest = []
|
||||||
for key in manifest_dict:
|
for key, item in manifest_dict.items():
|
||||||
manifest.extend(manifest_dict[key])
|
manifest.extend(item)
|
||||||
manifest_path = (self.target / "manifest.json").resolve()
|
manifest_path = (self.target / "manifest.json").resolve()
|
||||||
self.check_and_write_json(
|
self.check_and_write_json(
|
||||||
manifest,
|
manifest,
|
||||||
|
@ -34,7 +34,7 @@ from documents.settings import EXPORTER_ARCHIVE_NAME
|
|||||||
from documents.settings import EXPORTER_CRYPTO_SETTINGS_NAME
|
from documents.settings import EXPORTER_CRYPTO_SETTINGS_NAME
|
||||||
from documents.settings import EXPORTER_FILE_NAME
|
from documents.settings import EXPORTER_FILE_NAME
|
||||||
from documents.settings import EXPORTER_THUMBNAIL_NAME
|
from documents.settings import EXPORTER_THUMBNAIL_NAME
|
||||||
from documents.signals.handlers import update_cf_instance_documents
|
from documents.signals.handlers import check_paths_and_prune_custom_fields
|
||||||
from documents.signals.handlers import update_filename_and_move_files
|
from documents.signals.handlers import update_filename_and_move_files
|
||||||
from documents.utils import copy_file_with_basic_stats
|
from documents.utils import copy_file_with_basic_stats
|
||||||
from paperless import version
|
from paperless import version
|
||||||
@ -262,7 +262,7 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
),
|
),
|
||||||
disable_signal(
|
disable_signal(
|
||||||
post_save,
|
post_save,
|
||||||
receiver=update_cf_instance_documents,
|
receiver=check_paths_and_prune_custom_fields,
|
||||||
sender=CustomField,
|
sender=CustomField,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
# Generated by Django 5.1.1 on 2024-11-13 05:14
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_customfield_selects(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Migrate the custom field selects from a simple list of strings to a list of dictionaries with
|
||||||
|
label and id. Then update all instances of the custom field to use the new format.
|
||||||
|
"""
|
||||||
|
CustomFieldInstance = apps.get_model("documents", "CustomFieldInstance")
|
||||||
|
CustomField = apps.get_model("documents", "CustomField")
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for custom_field in CustomField.objects.filter(
|
||||||
|
data_type="select",
|
||||||
|
): # CustomField.FieldDataType.SELECT
|
||||||
|
old_select_options = custom_field.extra_data["select_options"]
|
||||||
|
custom_field.extra_data["select_options"] = [
|
||||||
|
{"id": get_random_string(16), "label": value}
|
||||||
|
for value in old_select_options
|
||||||
|
]
|
||||||
|
custom_field.save()
|
||||||
|
|
||||||
|
for instance in CustomFieldInstance.objects.filter(field=custom_field):
|
||||||
|
if instance.value_select:
|
||||||
|
instance.value_select = custom_field.extra_data["select_options"][
|
||||||
|
int(instance.value_select)
|
||||||
|
]["id"]
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_migrate_customfield_selects(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Reverse the migration of the custom field selects from a list of dictionaries with label and id
|
||||||
|
to a simple list of strings. Then update all instances of the custom field to use the old format,
|
||||||
|
which is just the index of the selected option.
|
||||||
|
"""
|
||||||
|
CustomFieldInstance = apps.get_model("documents", "CustomFieldInstance")
|
||||||
|
CustomField = apps.get_model("documents", "CustomField")
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for custom_field in CustomField.objects.all():
|
||||||
|
if custom_field.data_type == "select": # CustomField.FieldDataType.SELECT
|
||||||
|
old_select_options = custom_field.extra_data["select_options"]
|
||||||
|
custom_field.extra_data["select_options"] = [
|
||||||
|
option["label"]
|
||||||
|
for option in custom_field.extra_data["select_options"]
|
||||||
|
]
|
||||||
|
custom_field.save()
|
||||||
|
|
||||||
|
for instance in CustomFieldInstance.objects.filter(field=custom_field):
|
||||||
|
instance.value_select = next(
|
||||||
|
index
|
||||||
|
for index, option in enumerate(old_select_options)
|
||||||
|
if option.get("id") == instance.value_select
|
||||||
|
)
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("documents", "1058_workflowtrigger_schedule_date_custom_field_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="customfieldinstance",
|
||||||
|
name="value_select",
|
||||||
|
field=models.CharField(max_length=16, null=True),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_customfield_selects,
|
||||||
|
reverse_migrate_customfield_selects,
|
||||||
|
),
|
||||||
|
]
|
@ -947,7 +947,7 @@ class CustomFieldInstance(SoftDeleteModel):
|
|||||||
|
|
||||||
value_document_ids = models.JSONField(null=True)
|
value_document_ids = models.JSONField(null=True)
|
||||||
|
|
||||||
value_select = models.PositiveSmallIntegerField(null=True)
|
value_select = models.CharField(null=True, max_length=16)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("created",)
|
ordering = ("created",)
|
||||||
@ -962,7 +962,11 @@ class CustomFieldInstance(SoftDeleteModel):
|
|||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
value = (
|
value = (
|
||||||
self.field.extra_data["select_options"][self.value_select]
|
next(
|
||||||
|
option.get("label")
|
||||||
|
for option in self.field.extra_data["select_options"]
|
||||||
|
if option.get("id") == self.value_select
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
self.field.data_type == CustomField.FieldDataType.SELECT
|
self.field.data_type == CustomField.FieldDataType.SELECT
|
||||||
and self.value_select is not None
|
and self.value_select is not None
|
||||||
|
@ -162,7 +162,7 @@ class SetPermissionsMixin:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
if set_permissions is not None:
|
if set_permissions is not None:
|
||||||
for action in permissions_dict:
|
for action, _ in permissions_dict.items():
|
||||||
if action in set_permissions:
|
if action in set_permissions:
|
||||||
users = set_permissions[action]["users"]
|
users = set_permissions[action]["users"]
|
||||||
permissions_dict[action]["users"] = self._validate_user_ids(users)
|
permissions_dict[action]["users"] = self._validate_user_ids(users)
|
||||||
@ -535,20 +535,27 @@ class CustomFieldSerializer(serializers.ModelSerializer):
|
|||||||
if (
|
if (
|
||||||
"data_type" in attrs
|
"data_type" in attrs
|
||||||
and attrs["data_type"] == CustomField.FieldDataType.SELECT
|
and attrs["data_type"] == CustomField.FieldDataType.SELECT
|
||||||
and (
|
) or (
|
||||||
|
self.instance
|
||||||
|
and self.instance.data_type == CustomField.FieldDataType.SELECT
|
||||||
|
):
|
||||||
|
if (
|
||||||
"extra_data" not in attrs
|
"extra_data" not in attrs
|
||||||
or "select_options" not in attrs["extra_data"]
|
or "select_options" not in attrs["extra_data"]
|
||||||
or not isinstance(attrs["extra_data"]["select_options"], list)
|
or not isinstance(attrs["extra_data"]["select_options"], list)
|
||||||
or len(attrs["extra_data"]["select_options"]) == 0
|
or len(attrs["extra_data"]["select_options"]) == 0
|
||||||
or not all(
|
or not all(
|
||||||
isinstance(option, str) and len(option) > 0
|
len(option.get("label", "")) > 0
|
||||||
for option in attrs["extra_data"]["select_options"]
|
for option in attrs["extra_data"]["select_options"]
|
||||||
)
|
)
|
||||||
)
|
):
|
||||||
):
|
raise serializers.ValidationError(
|
||||||
raise serializers.ValidationError(
|
{"error": "extra_data.select_options must be a valid list"},
|
||||||
{"error": "extra_data.select_options must be a valid list"},
|
)
|
||||||
)
|
# labels are valid, generate ids if not present
|
||||||
|
for option in attrs["extra_data"]["select_options"]:
|
||||||
|
if option.get("id") is None:
|
||||||
|
option["id"] = get_random_string(length=16)
|
||||||
elif (
|
elif (
|
||||||
"data_type" in attrs
|
"data_type" in attrs
|
||||||
and attrs["data_type"] == CustomField.FieldDataType.MONETARY
|
and attrs["data_type"] == CustomField.FieldDataType.MONETARY
|
||||||
@ -648,10 +655,14 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
|
|||||||
elif field.data_type == CustomField.FieldDataType.SELECT:
|
elif field.data_type == CustomField.FieldDataType.SELECT:
|
||||||
select_options = field.extra_data["select_options"]
|
select_options = field.extra_data["select_options"]
|
||||||
try:
|
try:
|
||||||
select_options[data["value"]]
|
next(
|
||||||
|
option
|
||||||
|
for option in select_options
|
||||||
|
if option["id"] == data["value"]
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
f"Value must be index of an element in {select_options}",
|
f"Value must be an id of an element in {select_options}",
|
||||||
)
|
)
|
||||||
elif field.data_type == CustomField.FieldDataType.DOCUMENTLINK:
|
elif field.data_type == CustomField.FieldDataType.DOCUMENTLINK:
|
||||||
doc_ids = data["value"]
|
doc_ids = data["value"]
|
||||||
|
@ -372,21 +372,6 @@ class CannotMoveFilesException(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# should be disabled in /src/documents/management/commands/document_importer.py handle
|
|
||||||
@receiver(models.signals.post_save, sender=CustomField)
|
|
||||||
def update_cf_instance_documents(sender, instance: CustomField, **kwargs):
|
|
||||||
"""
|
|
||||||
'Select' custom field instances get their end-user value (e.g. in file names) from the select_options in extra_data,
|
|
||||||
which is contained in the custom field itself. So when the field is changed, we (may) need to update the file names
|
|
||||||
of all documents that have this custom field.
|
|
||||||
"""
|
|
||||||
if (
|
|
||||||
instance.data_type == CustomField.FieldDataType.SELECT
|
|
||||||
): # Only select fields, for now
|
|
||||||
for cf_instance in instance.fields.all():
|
|
||||||
update_filename_and_move_files(sender, cf_instance)
|
|
||||||
|
|
||||||
|
|
||||||
# should be disabled in /src/documents/management/commands/document_importer.py handle
|
# should be disabled in /src/documents/management/commands/document_importer.py handle
|
||||||
@receiver(models.signals.post_save, sender=CustomFieldInstance)
|
@receiver(models.signals.post_save, sender=CustomFieldInstance)
|
||||||
@receiver(models.signals.m2m_changed, sender=Document.tags.through)
|
@receiver(models.signals.m2m_changed, sender=Document.tags.through)
|
||||||
@ -525,6 +510,34 @@ def update_filename_and_move_files(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# should be disabled in /src/documents/management/commands/document_importer.py handle
|
||||||
|
@receiver(models.signals.post_save, sender=CustomField)
|
||||||
|
def check_paths_and_prune_custom_fields(sender, instance: CustomField, **kwargs):
|
||||||
|
"""
|
||||||
|
When a custom field is updated:
|
||||||
|
1. 'Select' custom field instances get their end-user value (e.g. in file names) from the select_options in extra_data,
|
||||||
|
which is contained in the custom field itself. So when the field is changed, we (may) need to update the file names
|
||||||
|
of all documents that have this custom field.
|
||||||
|
2. If a 'Select' field option was removed, we need to nullify the custom field instances that have the option.
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
instance.data_type == CustomField.FieldDataType.SELECT
|
||||||
|
): # Only select fields, for now
|
||||||
|
for cf_instance in instance.fields.all():
|
||||||
|
options = instance.extra_data.get("select_options", [])
|
||||||
|
try:
|
||||||
|
next(
|
||||||
|
option["label"]
|
||||||
|
for option in options
|
||||||
|
if option["id"] == cf_instance.value
|
||||||
|
)
|
||||||
|
except StopIteration:
|
||||||
|
# The value of this custom field instance is not in the select options anymore
|
||||||
|
cf_instance.value_select = None
|
||||||
|
cf_instance.save()
|
||||||
|
update_filename_and_move_files(sender, cf_instance)
|
||||||
|
|
||||||
|
|
||||||
def set_log_entry(sender, document: Document, logging_group=None, **kwargs):
|
def set_log_entry(sender, document: Document, logging_group=None, **kwargs):
|
||||||
ct = ContentType.objects.get(model="document")
|
ct = ContentType.objects.get(model="document")
|
||||||
user = User.objects.get(username="consumer")
|
user = User.objects.get(username="consumer")
|
||||||
|
@ -253,7 +253,11 @@ def get_custom_fields_context(
|
|||||||
):
|
):
|
||||||
options = field_instance.field.extra_data["select_options"]
|
options = field_instance.field.extra_data["select_options"]
|
||||||
value = pathvalidate.sanitize_filename(
|
value = pathvalidate.sanitize_filename(
|
||||||
options[int(field_instance.value)],
|
next(
|
||||||
|
option["label"]
|
||||||
|
for option in options
|
||||||
|
if option["id"] == field_instance.value
|
||||||
|
),
|
||||||
replacement_text="-",
|
replacement_text="-",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
from unittest.mock import ANY
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -61,7 +62,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
"data_type": "select",
|
"data_type": "select",
|
||||||
"name": "Select Field",
|
"name": "Select Field",
|
||||||
"extra_data": {
|
"extra_data": {
|
||||||
"select_options": ["Option 1", "Option 2"],
|
"select_options": [
|
||||||
|
{"label": "Option 1", "id": "abc-123"},
|
||||||
|
{"label": "Option 2", "id": "def-456"},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -73,7 +77,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
self.assertCountEqual(
|
self.assertCountEqual(
|
||||||
data["extra_data"]["select_options"],
|
data["extra_data"]["select_options"],
|
||||||
["Option 1", "Option 2"],
|
[
|
||||||
|
{"label": "Option 1", "id": "abc-123"},
|
||||||
|
{"label": "Option 2", "id": "def-456"},
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_custom_field_nonunique_name(self):
|
def test_create_custom_field_nonunique_name(self):
|
||||||
@ -138,6 +145,133 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_custom_field_select_unique_ids(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Nothing
|
||||||
|
- Existing custom field
|
||||||
|
WHEN:
|
||||||
|
- API request to create custom field with select options without id
|
||||||
|
THEN:
|
||||||
|
- Unique ids are generated for each option
|
||||||
|
"""
|
||||||
|
resp = self.client.post(
|
||||||
|
self.ENDPOINT,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"data_type": "select",
|
||||||
|
"name": "Select Field",
|
||||||
|
"extra_data": {
|
||||||
|
"select_options": [
|
||||||
|
{"label": "Option 1"},
|
||||||
|
{"label": "Option 2"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
data["extra_data"]["select_options"],
|
||||||
|
[
|
||||||
|
{"label": "Option 1", "id": ANY},
|
||||||
|
{"label": "Option 2", "id": ANY},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a new option
|
||||||
|
resp = self.client.patch(
|
||||||
|
f"{self.ENDPOINT}{data['id']}/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"extra_data": {
|
||||||
|
"select_options": data["extra_data"]["select_options"]
|
||||||
|
+ [{"label": "Option 3"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
data["extra_data"]["select_options"],
|
||||||
|
[
|
||||||
|
{"label": "Option 1", "id": ANY},
|
||||||
|
{"label": "Option 2", "id": ANY},
|
||||||
|
{"label": "Option 3", "id": ANY},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_custom_field_select_options_pruned(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Select custom field exists and document instance with one of the options
|
||||||
|
WHEN:
|
||||||
|
- API request to remove an option from the select field
|
||||||
|
THEN:
|
||||||
|
- The option is removed from the field
|
||||||
|
- The option is removed from the document instance
|
||||||
|
"""
|
||||||
|
custom_field_select = CustomField.objects.create(
|
||||||
|
name="Select Field",
|
||||||
|
data_type=CustomField.FieldDataType.SELECT,
|
||||||
|
extra_data={
|
||||||
|
"select_options": [
|
||||||
|
{"label": "Option 1", "id": "abc-123"},
|
||||||
|
{"label": "Option 2", "id": "def-456"},
|
||||||
|
{"label": "Option 3", "id": "ghi-789"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
doc = Document.objects.create(
|
||||||
|
title="WOW",
|
||||||
|
content="the content",
|
||||||
|
checksum="123",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
)
|
||||||
|
CustomFieldInstance.objects.create(
|
||||||
|
document=doc,
|
||||||
|
field=custom_field_select,
|
||||||
|
value_text="abc-123",
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = self.client.patch(
|
||||||
|
f"{self.ENDPOINT}{custom_field_select.id}/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"extra_data": {
|
||||||
|
"select_options": [
|
||||||
|
{"label": "Option 1", "id": "abc-123"},
|
||||||
|
{"label": "Option 3", "id": "ghi-789"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
data["extra_data"]["select_options"],
|
||||||
|
[
|
||||||
|
{"label": "Option 1", "id": "abc-123"},
|
||||||
|
{"label": "Option 3", "id": "ghi-789"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.refresh_from_db()
|
||||||
|
self.assertEqual(doc.custom_fields.first().value, None)
|
||||||
|
|
||||||
def test_create_custom_field_monetary_validation(self):
|
def test_create_custom_field_monetary_validation(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@ -261,7 +395,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
name="Test Custom Field Select",
|
name="Test Custom Field Select",
|
||||||
data_type=CustomField.FieldDataType.SELECT,
|
data_type=CustomField.FieldDataType.SELECT,
|
||||||
extra_data={
|
extra_data={
|
||||||
"select_options": ["Option 1", "Option 2"],
|
"select_options": [
|
||||||
|
{"label": "Option 1", "id": "abc-123"},
|
||||||
|
{"label": "Option 2", "id": "def-456"},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -309,7 +446,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": custom_field_select.id,
|
"field": custom_field_select.id,
|
||||||
"value": 0,
|
"value": "abc-123",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -332,7 +469,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
{"field": custom_field_monetary.id, "value": "EUR11.10"},
|
{"field": custom_field_monetary.id, "value": "EUR11.10"},
|
||||||
{"field": custom_field_monetary2.id, "value": "11.1"},
|
{"field": custom_field_monetary2.id, "value": "11.1"},
|
||||||
{"field": custom_field_documentlink.id, "value": [doc2.id]},
|
{"field": custom_field_documentlink.id, "value": [doc2.id]},
|
||||||
{"field": custom_field_select.id, "value": 0},
|
{"field": custom_field_select.id, "value": "abc-123"},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -722,7 +859,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
name="Test Custom Field SELECT",
|
name="Test Custom Field SELECT",
|
||||||
data_type=CustomField.FieldDataType.SELECT,
|
data_type=CustomField.FieldDataType.SELECT,
|
||||||
extra_data={
|
extra_data={
|
||||||
"select_options": ["Option 1", "Option 2"],
|
"select_options": [
|
||||||
|
{"label": "Option 1", "id": "abc-123"},
|
||||||
|
{"label": "Option 2", "id": "def-456"},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -730,7 +870,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
|||||||
f"/api/documents/{doc.id}/",
|
f"/api/documents/{doc.id}/",
|
||||||
data={
|
data={
|
||||||
"custom_fields": [
|
"custom_fields": [
|
||||||
{"field": custom_field_select.id, "value": 3},
|
{"field": custom_field_select.id, "value": "not an option"},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
format="json",
|
format="json",
|
||||||
|
@ -657,13 +657,16 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
name="Test Custom Field Select",
|
name="Test Custom Field Select",
|
||||||
data_type=CustomField.FieldDataType.SELECT,
|
data_type=CustomField.FieldDataType.SELECT,
|
||||||
extra_data={
|
extra_data={
|
||||||
"select_options": ["Option 1", "Choice 2"],
|
"select_options": [
|
||||||
|
{"label": "Option 1", "id": "abc123"},
|
||||||
|
{"label": "Choice 2", "id": "def456"},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
CustomFieldInstance.objects.create(
|
CustomFieldInstance.objects.create(
|
||||||
document=doc1,
|
document=doc1,
|
||||||
field=custom_field_select,
|
field=custom_field_select,
|
||||||
value_select=1,
|
value_select="def456",
|
||||||
)
|
)
|
||||||
|
|
||||||
r = self.client.get("/api/documents/?custom_fields__icontains=choice")
|
r = self.client.get("/api/documents/?custom_fields__icontains=choice")
|
||||||
|
@ -46,7 +46,13 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
# Add some options to the select_field
|
# Add some options to the select_field
|
||||||
select = self.custom_fields["select_field"]
|
select = self.custom_fields["select_field"]
|
||||||
select.extra_data = {"select_options": ["A", "B", "C"]}
|
select.extra_data = {
|
||||||
|
"select_options": [
|
||||||
|
{"label": "A", "id": "abc-123"},
|
||||||
|
{"label": "B", "id": "def-456"},
|
||||||
|
{"label": "C", "id": "ghi-789"},
|
||||||
|
],
|
||||||
|
}
|
||||||
select.save()
|
select.save()
|
||||||
|
|
||||||
# Now we will create some test documents
|
# Now we will create some test documents
|
||||||
@ -122,9 +128,9 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
# CustomField.FieldDataType.SELECT
|
# CustomField.FieldDataType.SELECT
|
||||||
self._create_document(select_field=None)
|
self._create_document(select_field=None)
|
||||||
self._create_document(select_field=0)
|
self._create_document(select_field="abc-123")
|
||||||
self._create_document(select_field=1)
|
self._create_document(select_field="def-456")
|
||||||
self._create_document(select_field=2)
|
self._create_document(select_field="ghi-789")
|
||||||
|
|
||||||
def _create_document(self, **kwargs):
|
def _create_document(self, **kwargs):
|
||||||
title = str(kwargs)
|
title = str(kwargs)
|
||||||
@ -296,18 +302,18 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_select(self):
|
def test_select(self):
|
||||||
# For select fields, you can either specify the index
|
# For select fields, you can either specify the id of the option
|
||||||
# or the name of the option. They function exactly the same.
|
# or the name of the option. They function exactly the same.
|
||||||
self._assert_query_match_predicate(
|
self._assert_query_match_predicate(
|
||||||
["select_field", "exact", 1],
|
["select_field", "exact", "def-456"],
|
||||||
lambda document: "select_field" in document
|
lambda document: "select_field" in document
|
||||||
and document["select_field"] == 1,
|
and document["select_field"] == "def-456",
|
||||||
)
|
)
|
||||||
# This is the same as:
|
# This is the same as:
|
||||||
self._assert_query_match_predicate(
|
self._assert_query_match_predicate(
|
||||||
["select_field", "exact", "B"],
|
["select_field", "exact", "B"],
|
||||||
lambda document: "select_field" in document
|
lambda document: "select_field" in document
|
||||||
and document["select_field"] == 1,
|
and document["select_field"] == "def-456",
|
||||||
)
|
)
|
||||||
|
|
||||||
# ==========================================================#
|
# ==========================================================#
|
||||||
@ -522,9 +528,9 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
def test_invalid_value(self):
|
def test_invalid_value(self):
|
||||||
self._assert_validation_error(
|
self._assert_validation_error(
|
||||||
json.dumps(["select_field", "exact", "not an option"]),
|
json.dumps(["select_field", "exact", []]),
|
||||||
["custom_field_query", "2"],
|
["custom_field_query", "2"],
|
||||||
"integer",
|
"string",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_invalid_logical_operator(self):
|
def test_invalid_logical_operator(self):
|
||||||
|
@ -544,7 +544,11 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
name="test",
|
name="test",
|
||||||
data_type=CustomField.FieldDataType.SELECT,
|
data_type=CustomField.FieldDataType.SELECT,
|
||||||
extra_data={
|
extra_data={
|
||||||
"select_options": ["apple", "banana", "cherry"],
|
"select_options": [
|
||||||
|
{"label": "apple", "id": "abc123"},
|
||||||
|
{"label": "banana", "id": "def456"},
|
||||||
|
{"label": "cherry", "id": "ghi789"},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@ -555,14 +559,22 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
archive_checksum="B",
|
archive_checksum="B",
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
)
|
)
|
||||||
CustomFieldInstance.objects.create(field=cf, document=doc, value_select=0)
|
CustomFieldInstance.objects.create(
|
||||||
|
field=cf,
|
||||||
|
document=doc,
|
||||||
|
value_select="abc123",
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc), "document_apple.pdf")
|
self.assertEqual(generate_filename(doc), "document_apple.pdf")
|
||||||
|
|
||||||
# handler should not have been called
|
# handler should not have been called
|
||||||
self.assertEqual(m.call_count, 0)
|
self.assertEqual(m.call_count, 0)
|
||||||
cf.extra_data = {
|
cf.extra_data = {
|
||||||
"select_options": ["aubergine", "banana", "cherry"],
|
"select_options": [
|
||||||
|
{"label": "aubergine", "id": "abc123"},
|
||||||
|
{"label": "banana", "id": "def456"},
|
||||||
|
{"label": "cherry", "id": "ghi789"},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
cf.save()
|
cf.save()
|
||||||
self.assertEqual(generate_filename(doc), "document_aubergine.pdf")
|
self.assertEqual(generate_filename(doc), "document_aubergine.pdf")
|
||||||
@ -1373,13 +1385,18 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
cf2 = CustomField.objects.create(
|
cf2 = CustomField.objects.create(
|
||||||
name="Select Field",
|
name="Select Field",
|
||||||
data_type=CustomField.FieldDataType.SELECT,
|
data_type=CustomField.FieldDataType.SELECT,
|
||||||
extra_data={"select_options": ["ChoiceOne", "ChoiceTwo"]},
|
extra_data={
|
||||||
|
"select_options": [
|
||||||
|
{"label": "ChoiceOne", "id": "abc=123"},
|
||||||
|
{"label": "ChoiceTwo", "id": "def-456"},
|
||||||
|
],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
cfi1 = CustomFieldInstance.objects.create(
|
cfi1 = CustomFieldInstance.objects.create(
|
||||||
document=doc_a,
|
document=doc_a,
|
||||||
field=cf2,
|
field=cf2,
|
||||||
value_select=0,
|
value_select="abc=123",
|
||||||
)
|
)
|
||||||
|
|
||||||
cfi = CustomFieldInstance.objects.create(
|
cfi = CustomFieldInstance.objects.create(
|
||||||
|
87
src/documents/tests/test_migration_custom_field_selects.py
Normal file
87
src/documents/tests/test_migration_custom_field_selects.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
from unittest.mock import ANY
|
||||||
|
|
||||||
|
from documents.tests.utils import TestMigrations
|
||||||
|
|
||||||
|
|
||||||
|
class TestMigrateCustomFieldSelects(TestMigrations):
|
||||||
|
migrate_from = "1058_workflowtrigger_schedule_date_custom_field_and_more"
|
||||||
|
migrate_to = "1059_alter_customfieldinstance_value_select"
|
||||||
|
|
||||||
|
def setUpBeforeMigration(self, apps):
|
||||||
|
CustomField = apps.get_model("documents.CustomField")
|
||||||
|
self.old_format = CustomField.objects.create(
|
||||||
|
name="cf1",
|
||||||
|
data_type="select",
|
||||||
|
extra_data={"select_options": ["Option 1", "Option 2", "Option 3"]},
|
||||||
|
)
|
||||||
|
Document = apps.get_model("documents.Document")
|
||||||
|
doc = Document.objects.create(title="doc1")
|
||||||
|
CustomFieldInstance = apps.get_model("documents.CustomFieldInstance")
|
||||||
|
self.old_instance = CustomFieldInstance.objects.create(
|
||||||
|
field=self.old_format,
|
||||||
|
value_select=0,
|
||||||
|
document=doc,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_migrate_old_to_new_select_fields(self):
|
||||||
|
self.old_format.refresh_from_db()
|
||||||
|
self.old_instance.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.old_format.extra_data["select_options"],
|
||||||
|
[
|
||||||
|
{"label": "Option 1", "id": ANY},
|
||||||
|
{"label": "Option 2", "id": ANY},
|
||||||
|
{"label": "Option 3", "id": ANY},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.old_instance.value_select,
|
||||||
|
self.old_format.extra_data["select_options"][0]["id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMigrationCustomFieldSelectsReverse(TestMigrations):
|
||||||
|
migrate_from = "1059_alter_customfieldinstance_value_select"
|
||||||
|
migrate_to = "1058_workflowtrigger_schedule_date_custom_field_and_more"
|
||||||
|
|
||||||
|
def setUpBeforeMigration(self, apps):
|
||||||
|
CustomField = apps.get_model("documents.CustomField")
|
||||||
|
self.new_format = CustomField.objects.create(
|
||||||
|
name="cf1",
|
||||||
|
data_type="select",
|
||||||
|
extra_data={
|
||||||
|
"select_options": [
|
||||||
|
{"label": "Option 1", "id": "id1"},
|
||||||
|
{"label": "Option 2", "id": "id2"},
|
||||||
|
{"label": "Option 3", "id": "id3"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Document = apps.get_model("documents.Document")
|
||||||
|
doc = Document.objects.create(title="doc1")
|
||||||
|
CustomFieldInstance = apps.get_model("documents.CustomFieldInstance")
|
||||||
|
self.new_instance = CustomFieldInstance.objects.create(
|
||||||
|
field=self.new_format,
|
||||||
|
value_select="id1",
|
||||||
|
document=doc,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_migrate_new_to_old_select_fields(self):
|
||||||
|
self.new_format.refresh_from_db()
|
||||||
|
self.new_instance.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.new_format.extra_data["select_options"],
|
||||||
|
[
|
||||||
|
"Option 1",
|
||||||
|
"Option 2",
|
||||||
|
"Option 3",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.new_instance.value_select,
|
||||||
|
0,
|
||||||
|
)
|
@ -5,9 +5,9 @@ from paperless.checks import paths_check
|
|||||||
from paperless.checks import settings_values_check
|
from paperless.checks import settings_values_check
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"celery_app",
|
"audit_log_check",
|
||||||
"binaries_check",
|
"binaries_check",
|
||||||
|
"celery_app",
|
||||||
"paths_check",
|
"paths_check",
|
||||||
"settings_values_check",
|
"settings_values_check",
|
||||||
"audit_log_check",
|
|
||||||
]
|
]
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
from paperless_tesseract.checks import check_default_language_available
|
from paperless_tesseract.checks import check_default_language_available
|
||||||
from paperless_tesseract.checks import get_tesseract_langs
|
from paperless_tesseract.checks import get_tesseract_langs
|
||||||
|
|
||||||
__all__ = ["get_tesseract_langs", "check_default_language_available"]
|
__all__ = ["check_default_language_available", "get_tesseract_langs"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user