People: Add subjects API #22

This commit is contained in:
Michael Mayer 2021-09-02 16:12:31 +02:00
parent 5f07b8bb10
commit 97af133763
16 changed files with 566 additions and 242 deletions

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-23 13:30+0000\n"
"POT-Creation-Date: 2021-09-02 14:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,273 +17,277 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: messages.go:74
#: messages.go:75
msgid "Unexpected error, please try again"
msgstr ""
#: messages.go:75
#: messages.go:76
msgid "Invalid request"
msgstr ""
#: messages.go:76
#: messages.go:77
msgid "Changes could not be saved"
msgstr ""
#: messages.go:77
#: messages.go:78
msgid "Could not be deleted"
msgstr ""
#: messages.go:78
#: messages.go:79
#, c-format
msgid "%s already exists"
msgstr ""
#: messages.go:79 messages.go:82
#: messages.go:80 messages.go:83
msgid "Not found on server, deleted?"
msgstr ""
#: messages.go:80
#: messages.go:81
msgid "File not found"
msgstr ""
#: messages.go:81
#: messages.go:82
msgid "Selection not found"
msgstr ""
#: messages.go:83
#: messages.go:84
msgid "Account not found"
msgstr ""
#: messages.go:84
#: messages.go:85
msgid "User not found"
msgstr ""
#: messages.go:85
#: messages.go:86
msgid "Label not found"
msgstr ""
#: messages.go:86
#: messages.go:87
msgid "Album not found"
msgstr ""
#: messages.go:87
msgid "Not available in public mode"
msgstr ""
#: messages.go:88
msgid "not available in read-only mode"
msgid "Subject not found"
msgstr ""
#: messages.go:89
msgid "Please log in and try again"
msgid "Not available in public mode"
msgstr ""
#: messages.go:90
msgid "Upload might be offensive"
msgid "not available in read-only mode"
msgstr ""
#: messages.go:91
msgid "No items selected"
msgid "Please log in and try again"
msgstr ""
#: messages.go:92
msgid "Failed creating file, please check permissions"
msgid "Upload might be offensive"
msgstr ""
#: messages.go:93
msgid "Failed creating folder, please check permissions"
msgid "No items selected"
msgstr ""
#: messages.go:94
msgid "Could not connect, please try again"
msgid "Failed creating file, please check permissions"
msgstr ""
#: messages.go:95
msgid "Invalid password, please try again"
msgid "Failed creating folder, please check permissions"
msgstr ""
#: messages.go:96
msgid "Feature disabled"
msgid "Could not connect, please try again"
msgstr ""
#: messages.go:97
msgid "No labels selected"
msgid "Invalid password, please try again"
msgstr ""
#: messages.go:98
msgid "No albums selected"
msgid "Feature disabled"
msgstr ""
#: messages.go:99
msgid "No files available for download"
msgid "No labels selected"
msgstr ""
#: messages.go:100
msgid "Failed to create zip file"
msgid "No albums selected"
msgstr ""
#: messages.go:101
msgid "Invalid credentials"
msgid "No files available for download"
msgstr ""
#: messages.go:102
msgid "Failed to create zip file"
msgstr ""
#: messages.go:103
msgid "Invalid credentials"
msgstr ""
#: messages.go:104
msgid "Invalid link"
msgstr ""
#: messages.go:105
#: messages.go:107
msgid "Changes successfully saved"
msgstr ""
#: messages.go:106
#: messages.go:108
msgid "Album created"
msgstr ""
#: messages.go:107
#: messages.go:109
msgid "Album saved"
msgstr ""
#: messages.go:108
#: messages.go:110
#, c-format
msgid "Album %s deleted"
msgstr ""
#: messages.go:109
#: messages.go:111
msgid "Album contents cloned"
msgstr ""
#: messages.go:110
#: messages.go:112
msgid "File removed from stack"
msgstr ""
#: messages.go:111
msgid "File deleted"
msgstr ""
#: messages.go:112
#, c-format
msgid "Selection added to %s"
msgstr ""
#: messages.go:113
#, c-format
msgid "One entry added to %s"
msgid "File deleted"
msgstr ""
#: messages.go:114
#, c-format
msgid "%d entries added to %s"
msgid "Selection added to %s"
msgstr ""
#: messages.go:115
#, c-format
msgid "One entry removed from %s"
msgid "One entry added to %s"
msgstr ""
#: messages.go:116
#, c-format
msgid "%d entries removed from %s"
msgid "%d entries added to %s"
msgstr ""
#: messages.go:117
msgid "Account created"
#, c-format
msgid "One entry removed from %s"
msgstr ""
#: messages.go:118
msgid "Account saved"
#, c-format
msgid "%d entries removed from %s"
msgstr ""
#: messages.go:119
msgid "Account deleted"
msgid "Account created"
msgstr ""
#: messages.go:120
msgid "Settings saved"
msgid "Account saved"
msgstr ""
#: messages.go:121
msgid "Password changed"
msgid "Account deleted"
msgstr ""
#: messages.go:122
#, c-format
msgid "Import completed in %d s"
msgid "Settings saved"
msgstr ""
#: messages.go:123
msgid "Import canceled"
msgid "Password changed"
msgstr ""
#: messages.go:124
#, c-format
msgid "Indexing completed in %d s"
msgid "Import completed in %d s"
msgstr ""
#: messages.go:125
msgid "Indexing originals..."
msgid "Import canceled"
msgstr ""
#: messages.go:126
#, c-format
msgid "Indexing files in %s"
msgid "Indexing completed in %d s"
msgstr ""
#: messages.go:127
msgid "Indexing canceled"
msgid "Indexing originals..."
msgstr ""
#: messages.go:128
#, c-format
msgid "Removed %d files and %d photos"
msgid "Indexing files in %s"
msgstr ""
#: messages.go:129
#, c-format
msgid "Moving files from %s"
msgid "Indexing canceled"
msgstr ""
#: messages.go:130
#, c-format
msgid "Copying files from %s"
msgid "Removed %d files and %d photos"
msgstr ""
#: messages.go:131
msgid "Labels deleted"
#, c-format
msgid "Moving files from %s"
msgstr ""
#: messages.go:132
msgid "Label saved"
#, c-format
msgid "Copying files from %s"
msgstr ""
#: messages.go:133
msgid "Labels deleted"
msgstr ""
#: messages.go:134
msgid "Label saved"
msgstr ""
#: messages.go:135
#, c-format
msgid "%d files uploaded in %d s"
msgstr ""
#: messages.go:134
#: messages.go:136
msgid "Selection approved"
msgstr ""
#: messages.go:135
#: messages.go:137
msgid "Selection archived"
msgstr ""
#: messages.go:136
#: messages.go:138
msgid "Selection restored"
msgstr ""
#: messages.go:137
#: messages.go:139
msgid "Selection marked as private"
msgstr ""
#: messages.go:138
#: messages.go:140
msgid "Albums deleted"
msgstr ""
#: messages.go:139
#: messages.go:141
#, c-format
msgid "Zip created in %d s"
msgstr ""
#: messages.go:140
#: messages.go:142
msgid "Permanently deleted"
msgstr ""

View file

@ -1915,13 +1915,13 @@
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
},
"node_modules/@vue/compiler-core": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.6.tgz",
"integrity": "sha512-vbwnz7+OhtLO5p5i630fTuQCL+MlUpEMTKHuX+RfetQ+3pFCkItt2JUH+9yMaBG2Hkz6av+T9mwN/acvtIwpbw==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.7.tgz",
"integrity": "sha512-OcWy72QNTkcNYtZIb927pRx2cRujrlDWsAx7ejWDnRzwo83gIyF8NeTrMv/7wbnHoeA+Gga9AK4Wo9PlCzhuLg==",
"dependencies": {
"@babel/parser": "^7.15.0",
"@babel/types": "^7.15.0",
"@vue/shared": "3.2.6",
"@vue/shared": "3.2.7",
"estree-walker": "^2.0.2",
"source-map": "^0.6.1"
}
@ -1935,27 +1935,27 @@
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.6.tgz",
"integrity": "sha512-+a/3oBAzFIXhHt8L5IHJOTP4a5egzvpXYyi13jR7CUYOR1S+Zzv7vBWKYBnKyJLwnrxTZnTQVjeHCgJq743XKg==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.7.tgz",
"integrity": "sha512-YZyZNoZlTbTMqyY8QMC8IhwmcDVOiE1DdVwjnXbyihg+XVqpGQkDjNcG5nyMTbtZDKXREsYkcjaZntEfKyWK5g==",
"dependencies": {
"@vue/compiler-core": "3.2.6",
"@vue/shared": "3.2.6"
"@vue/compiler-core": "3.2.7",
"@vue/shared": "3.2.7"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.6.tgz",
"integrity": "sha512-Ariz1eDsf+2fw6oWXVwnBNtfKHav72RjlWXpEgozYBLnfRPzP+7jhJRw4Nq0OjSsLx2HqjF3QX7HutTjYB0/eA==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.7.tgz",
"integrity": "sha512-qjfvRw7/9Q2Qm4cDmrJwTNqnFTXSSI3z/mmS9BSJTYZqh4FC4w+IIYYLWUpS3ef6UTTYY9STC5IUZqfpIod9Uw==",
"dependencies": {
"@babel/parser": "^7.15.0",
"@babel/types": "^7.15.0",
"@types/estree": "^0.0.48",
"@vue/compiler-core": "3.2.6",
"@vue/compiler-dom": "3.2.6",
"@vue/compiler-ssr": "3.2.6",
"@vue/ref-transform": "3.2.6",
"@vue/shared": "3.2.6",
"@vue/compiler-core": "3.2.7",
"@vue/compiler-dom": "3.2.7",
"@vue/compiler-ssr": "3.2.7",
"@vue/ref-transform": "3.2.7",
"@vue/shared": "3.2.7",
"consolidate": "^0.16.0",
"estree-walker": "^2.0.2",
"hash-sum": "^2.0.0",
@ -1977,12 +1977,12 @@
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.6.tgz",
"integrity": "sha512-A7IKRKHSyPnTC4w1FxHkjzoyjXInsXkcs/oX22nBQ+6AWlXj2Tt1le96CWPOXy5vYlsTYkF1IgfBaKIdeN/39g==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.7.tgz",
"integrity": "sha512-4moQTeMujIk+fG8NaMxU5aPhMCnWE+O3xNEK6+kd9GjNoN+n3y3YZ6CkVy+aOP2HpqWenZbS/20TBzOSdon5Cw==",
"dependencies": {
"@vue/compiler-dom": "3.2.6",
"@vue/shared": "3.2.6"
"@vue/compiler-dom": "3.2.7",
"@vue/shared": "3.2.7"
}
},
"node_modules/@vue/component-compiler-utils": {
@ -2082,21 +2082,21 @@
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
"node_modules/@vue/ref-transform": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.6.tgz",
"integrity": "sha512-ie39+Y4nbirDLvH+WEq6Eo/l3n3mFATayqR+kEMSphrtMW6Uh/eEMx1Gk2Jnf82zmj3VLRq7dnmPx72JLcBYkQ==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.7.tgz",
"integrity": "sha512-5I7IeUqoDEhPmkPHBhw7YlsFCFO/ZXHWwgdrokQATyVRkEkqeAd8erthuZ9a4sZAo5JBYmxjYw8WD9Kx9mabmg==",
"dependencies": {
"@babel/parser": "^7.15.0",
"@vue/compiler-core": "3.2.6",
"@vue/shared": "3.2.6",
"@vue/compiler-core": "3.2.7",
"@vue/shared": "3.2.7",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7"
}
},
"node_modules/@vue/shared": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.6.tgz",
"integrity": "sha512-uwX0Qs2e6kdF+WmxwuxJxOnKs/wEkMArtYpHSm7W+VY/23Tl8syMRyjnzEeXrNCAP0/8HZxEGkHJsjPEDNRuHw=="
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.7.tgz",
"integrity": "sha512-YwGOcNZjOY/MmadpzFBXWyHEwZSf0lVU4XF5zpD7tXC9dmqjdo38Jkk06wATu4LYHDPW4emXKMB5YLFPWPkwFA=="
},
"node_modules/@vvo/tzdb": {
"version": "6.10.0",
@ -3846,9 +3846,9 @@
}
},
"node_modules/core-js": {
"version": "3.16.4",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.4.tgz",
"integrity": "sha512-Tq4GVE6XCjE+hcyW6hPy0ofN3hwtLudz5ZRdrlCnsnD/xkm/PWQRudzYHiKgZKUcefV6Q57fhDHjZHJP5dpfSg==",
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.17.1.tgz",
"integrity": "sha512-C8i/FNpVN2Ti89QIJcFn9ZQmnM+HaAQr2OpE+ja3TRM9Q34FigsGlAVuwPGkIgydSVClo/1l1D1grP8LVt9IYA==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
@ -3856,9 +3856,9 @@
}
},
"node_modules/core-js-compat": {
"version": "3.16.4",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.16.4.tgz",
"integrity": "sha512-IzCSomxRdahCYb6G3HiN6pl3JCiM0NMunRcNa1pIeC7g17Vd6Ue3AT9anQiENPIm/svThUVer1pIbLMDERIsFw==",
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.17.1.tgz",
"integrity": "sha512-Oqp6qybMdCFcWSroh/6Q8j7YNOjRD0ThY02cAd6rugr//FCkMYonizLV8AryLU5wNJOweauIBxQYCZoV3emfcw==",
"dependencies": {
"browserslist": "^4.16.8",
"semver": "7.0.0"
@ -4759,9 +4759,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.3.826",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.826.tgz",
"integrity": "sha512-bpLc4QU4B8PYmdO4MSu2ZBTMD8lAaEXRS43C09lB31BvYwuk9UxgBRXbY5OJBw7VuMGcg2MZG5FyTaP9u4PQnw=="
"version": "1.3.827",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.827.tgz",
"integrity": "sha512-ye+4uQOY/jbjRutMcE/EmOcNwUeo1qo9aKL2tPyb09cU3lmxNeyDF4RWiemmkknW+p29h7dyDqy02higTxc9/A=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@ -6539,19 +6539,19 @@
"deprecated": "flatten is deprecated in favor of utility frameworks such as lodash."
},
"node_modules/flow-parser": {
"version": "0.158.0",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.158.0.tgz",
"integrity": "sha512-0hMsPkBTRrkII/0YiG9ehOxFXy4gOWdk8RSRze5WbfeKAQpL5kC2K4BmumyTfU9o5gr7/llgElF3UpSSrjzQAA==",
"version": "0.159.0",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.159.0.tgz",
"integrity": "sha512-/AFSLMSbqictmgPm+vrXBD0rLTsVRrlKfiGRoDjt/WhhUxqy5ZMuLVHbRD/g3C3JRnJgDrKSb3+piQoM1dzVGw==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/flow-remove-types": {
"version": "2.158.0",
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.158.0.tgz",
"integrity": "sha512-mLDU+B2LuiCg0KA9V0A5OdgI5evugWlHKqG+GLlNIrtOPEPO497p3PJAUxGD3SR+gJmtxCDdgL+qLZnw5op9bA==",
"version": "2.159.0",
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.159.0.tgz",
"integrity": "sha512-1S1qkw8HTYW2ypoqnTQyGHQEWh6kxKc7SUdj07lOa9WzEuhdu2+wr+SO5BBxCwoqJ4XrwBgegmWnEFrAz2WF/w==",
"dependencies": {
"flow-parser": "^0.158.0",
"flow-parser": "^0.159.0",
"pirates": "^3.0.2",
"vlq": "^0.2.1"
},
@ -8844,9 +8844,9 @@
}
},
"node_modules/mini-css-extract-plugin": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.2.1.tgz",
"integrity": "sha512-A0GBXpz8WIPgh2HfASJ0EeY8grd2dGxmC4R8uTujFJXZY7zFy0nvYSYW6SKCLKlz7y45BdHONfaxZQMIZpeF/w==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.2.2.tgz",
"integrity": "sha512-eUjQ/q1rQIeHWgIx7ny/DNgXHcMXHdBwgrZQK7Ev8dbR+HxhroFM2Cb6kMiswOYaq05IRJhPuQqXWUABIjjA3g==",
"dependencies": {
"schema-utils": "^3.1.0"
},
@ -13015,9 +13015,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sass": {
"version": "1.38.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.38.2.tgz",
"integrity": "sha512-Bz1fG6qiyF0FX6m/I+VxtdVKz1Dfmg/e9kfDy2PhWOkq3T384q2KxwIfP0fXpeI+EyyETdOauH+cRHQDFASllA==",
"version": "1.39.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.39.0.tgz",
"integrity": "sha512-F4o+RhJkNOIG0b6QudYU8c78ZADKZjKDk5cyrf8XTKWfrgbtyVVXImFstJrc+1pkQDCggyidIOytq6gS4gCCZg==",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0"
},
@ -14354,9 +14354,9 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.0.tgz",
"integrity": "sha512-FpR4Qe0Yt4knSQ5u2bA1wkM0R8VlVsvhyfSHvomXRivS4vPLk0dJV2IhRBIHRABh7AFutdMeElIA5y1dETwMBg==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.1.tgz",
"integrity": "sha512-mUAWsS2RDNL3rEr0ZTr7hm/R1DDxNwrED7Kf59F2rgFTfy+LrnciwA51MNWhGGQcqHnqvbPHgkW9LYr5HGBhfw==",
"dependencies": {
"jest-worker": "^27.0.6",
"p-limit": "^3.1.0",
@ -15329,9 +15329,9 @@
}
},
"node_modules/webpack": {
"version": "5.51.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.51.1.tgz",
"integrity": "sha512-xsn3lwqEKoFvqn4JQggPSRxE4dhsRcysWTqYABAZlmavcoTmwlOb9b1N36Inbt/eIispSkuHa80/FJkDTPos1A==",
"version": "5.51.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.51.2.tgz",
"integrity": "sha512-odydxP4WA3XYYzwSQUivPxywdzMlY42bbfxMwCaEtHb+i/N9uzKSHcLgWkXo/Gsa+4Zlzf3Jg0hEHn1FnZpk2Q==",
"dependencies": {
"@types/eslint-scope": "^3.7.0",
"@types/estree": "^0.0.50",
@ -17217,13 +17217,13 @@
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
},
"@vue/compiler-core": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.6.tgz",
"integrity": "sha512-vbwnz7+OhtLO5p5i630fTuQCL+MlUpEMTKHuX+RfetQ+3pFCkItt2JUH+9yMaBG2Hkz6av+T9mwN/acvtIwpbw==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.7.tgz",
"integrity": "sha512-OcWy72QNTkcNYtZIb927pRx2cRujrlDWsAx7ejWDnRzwo83gIyF8NeTrMv/7wbnHoeA+Gga9AK4Wo9PlCzhuLg==",
"requires": {
"@babel/parser": "^7.15.0",
"@babel/types": "^7.15.0",
"@vue/shared": "3.2.6",
"@vue/shared": "3.2.7",
"estree-walker": "^2.0.2",
"source-map": "^0.6.1"
},
@ -17236,27 +17236,27 @@
}
},
"@vue/compiler-dom": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.6.tgz",
"integrity": "sha512-+a/3oBAzFIXhHt8L5IHJOTP4a5egzvpXYyi13jR7CUYOR1S+Zzv7vBWKYBnKyJLwnrxTZnTQVjeHCgJq743XKg==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.7.tgz",
"integrity": "sha512-YZyZNoZlTbTMqyY8QMC8IhwmcDVOiE1DdVwjnXbyihg+XVqpGQkDjNcG5nyMTbtZDKXREsYkcjaZntEfKyWK5g==",
"requires": {
"@vue/compiler-core": "3.2.6",
"@vue/shared": "3.2.6"
"@vue/compiler-core": "3.2.7",
"@vue/shared": "3.2.7"
}
},
"@vue/compiler-sfc": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.6.tgz",
"integrity": "sha512-Ariz1eDsf+2fw6oWXVwnBNtfKHav72RjlWXpEgozYBLnfRPzP+7jhJRw4Nq0OjSsLx2HqjF3QX7HutTjYB0/eA==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.7.tgz",
"integrity": "sha512-qjfvRw7/9Q2Qm4cDmrJwTNqnFTXSSI3z/mmS9BSJTYZqh4FC4w+IIYYLWUpS3ef6UTTYY9STC5IUZqfpIod9Uw==",
"requires": {
"@babel/parser": "^7.15.0",
"@babel/types": "^7.15.0",
"@types/estree": "^0.0.48",
"@vue/compiler-core": "3.2.6",
"@vue/compiler-dom": "3.2.6",
"@vue/compiler-ssr": "3.2.6",
"@vue/ref-transform": "3.2.6",
"@vue/shared": "3.2.6",
"@vue/compiler-core": "3.2.7",
"@vue/compiler-dom": "3.2.7",
"@vue/compiler-ssr": "3.2.7",
"@vue/ref-transform": "3.2.7",
"@vue/shared": "3.2.7",
"consolidate": "^0.16.0",
"estree-walker": "^2.0.2",
"hash-sum": "^2.0.0",
@ -17277,12 +17277,12 @@
}
},
"@vue/compiler-ssr": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.6.tgz",
"integrity": "sha512-A7IKRKHSyPnTC4w1FxHkjzoyjXInsXkcs/oX22nBQ+6AWlXj2Tt1le96CWPOXy5vYlsTYkF1IgfBaKIdeN/39g==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.7.tgz",
"integrity": "sha512-4moQTeMujIk+fG8NaMxU5aPhMCnWE+O3xNEK6+kd9GjNoN+n3y3YZ6CkVy+aOP2HpqWenZbS/20TBzOSdon5Cw==",
"requires": {
"@vue/compiler-dom": "3.2.6",
"@vue/shared": "3.2.6"
"@vue/compiler-dom": "3.2.7",
"@vue/shared": "3.2.7"
}
},
"@vue/component-compiler-utils": {
@ -17360,21 +17360,21 @@
}
},
"@vue/ref-transform": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.6.tgz",
"integrity": "sha512-ie39+Y4nbirDLvH+WEq6Eo/l3n3mFATayqR+kEMSphrtMW6Uh/eEMx1Gk2Jnf82zmj3VLRq7dnmPx72JLcBYkQ==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.7.tgz",
"integrity": "sha512-5I7IeUqoDEhPmkPHBhw7YlsFCFO/ZXHWwgdrokQATyVRkEkqeAd8erthuZ9a4sZAo5JBYmxjYw8WD9Kx9mabmg==",
"requires": {
"@babel/parser": "^7.15.0",
"@vue/compiler-core": "3.2.6",
"@vue/shared": "3.2.6",
"@vue/compiler-core": "3.2.7",
"@vue/shared": "3.2.7",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7"
}
},
"@vue/shared": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.6.tgz",
"integrity": "sha512-uwX0Qs2e6kdF+WmxwuxJxOnKs/wEkMArtYpHSm7W+VY/23Tl8syMRyjnzEeXrNCAP0/8HZxEGkHJsjPEDNRuHw=="
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.7.tgz",
"integrity": "sha512-YwGOcNZjOY/MmadpzFBXWyHEwZSf0lVU4XF5zpD7tXC9dmqjdo38Jkk06wATu4LYHDPW4emXKMB5YLFPWPkwFA=="
},
"@vvo/tzdb": {
"version": "6.10.0",
@ -18748,14 +18748,14 @@
"optional": true
},
"core-js": {
"version": "3.16.4",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.4.tgz",
"integrity": "sha512-Tq4GVE6XCjE+hcyW6hPy0ofN3hwtLudz5ZRdrlCnsnD/xkm/PWQRudzYHiKgZKUcefV6Q57fhDHjZHJP5dpfSg=="
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.17.1.tgz",
"integrity": "sha512-C8i/FNpVN2Ti89QIJcFn9ZQmnM+HaAQr2OpE+ja3TRM9Q34FigsGlAVuwPGkIgydSVClo/1l1D1grP8LVt9IYA=="
},
"core-js-compat": {
"version": "3.16.4",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.16.4.tgz",
"integrity": "sha512-IzCSomxRdahCYb6G3HiN6pl3JCiM0NMunRcNa1pIeC7g17Vd6Ue3AT9anQiENPIm/svThUVer1pIbLMDERIsFw==",
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.17.1.tgz",
"integrity": "sha512-Oqp6qybMdCFcWSroh/6Q8j7YNOjRD0ThY02cAd6rugr//FCkMYonizLV8AryLU5wNJOweauIBxQYCZoV3emfcw==",
"requires": {
"browserslist": "^4.16.8",
"semver": "7.0.0"
@ -19398,9 +19398,9 @@
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
},
"electron-to-chromium": {
"version": "1.3.826",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.826.tgz",
"integrity": "sha512-bpLc4QU4B8PYmdO4MSu2ZBTMD8lAaEXRS43C09lB31BvYwuk9UxgBRXbY5OJBw7VuMGcg2MZG5FyTaP9u4PQnw=="
"version": "1.3.827",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.827.tgz",
"integrity": "sha512-ye+4uQOY/jbjRutMcE/EmOcNwUeo1qo9aKL2tPyb09cU3lmxNeyDF4RWiemmkknW+p29h7dyDqy02higTxc9/A=="
},
"emoji-regex": {
"version": "8.0.0",
@ -20767,16 +20767,16 @@
"integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg=="
},
"flow-parser": {
"version": "0.158.0",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.158.0.tgz",
"integrity": "sha512-0hMsPkBTRrkII/0YiG9ehOxFXy4gOWdk8RSRze5WbfeKAQpL5kC2K4BmumyTfU9o5gr7/llgElF3UpSSrjzQAA=="
"version": "0.159.0",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.159.0.tgz",
"integrity": "sha512-/AFSLMSbqictmgPm+vrXBD0rLTsVRrlKfiGRoDjt/WhhUxqy5ZMuLVHbRD/g3C3JRnJgDrKSb3+piQoM1dzVGw=="
},
"flow-remove-types": {
"version": "2.158.0",
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.158.0.tgz",
"integrity": "sha512-mLDU+B2LuiCg0KA9V0A5OdgI5evugWlHKqG+GLlNIrtOPEPO497p3PJAUxGD3SR+gJmtxCDdgL+qLZnw5op9bA==",
"version": "2.159.0",
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.159.0.tgz",
"integrity": "sha512-1S1qkw8HTYW2ypoqnTQyGHQEWh6kxKc7SUdj07lOa9WzEuhdu2+wr+SO5BBxCwoqJ4XrwBgegmWnEFrAz2WF/w==",
"requires": {
"flow-parser": "^0.158.0",
"flow-parser": "^0.159.0",
"pirates": "^3.0.2",
"vlq": "^0.2.1"
},
@ -22489,9 +22489,9 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"mini-css-extract-plugin": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.2.1.tgz",
"integrity": "sha512-A0GBXpz8WIPgh2HfASJ0EeY8grd2dGxmC4R8uTujFJXZY7zFy0nvYSYW6SKCLKlz7y45BdHONfaxZQMIZpeF/w==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.2.2.tgz",
"integrity": "sha512-eUjQ/q1rQIeHWgIx7ny/DNgXHcMXHdBwgrZQK7Ev8dbR+HxhroFM2Cb6kMiswOYaq05IRJhPuQqXWUABIjjA3g==",
"requires": {
"schema-utils": "^3.1.0"
},
@ -25525,9 +25525,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sass": {
"version": "1.38.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.38.2.tgz",
"integrity": "sha512-Bz1fG6qiyF0FX6m/I+VxtdVKz1Dfmg/e9kfDy2PhWOkq3T384q2KxwIfP0fXpeI+EyyETdOauH+cRHQDFASllA==",
"version": "1.39.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.39.0.tgz",
"integrity": "sha512-F4o+RhJkNOIG0b6QudYU8c78ZADKZjKDk5cyrf8XTKWfrgbtyVVXImFstJrc+1pkQDCggyidIOytq6gS4gCCZg==",
"requires": {
"chokidar": ">=3.0.0 <4.0.0"
}
@ -26619,9 +26619,9 @@
}
},
"terser-webpack-plugin": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.0.tgz",
"integrity": "sha512-FpR4Qe0Yt4knSQ5u2bA1wkM0R8VlVsvhyfSHvomXRivS4vPLk0dJV2IhRBIHRABh7AFutdMeElIA5y1dETwMBg==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.1.tgz",
"integrity": "sha512-mUAWsS2RDNL3rEr0ZTr7hm/R1DDxNwrED7Kf59F2rgFTfy+LrnciwA51MNWhGGQcqHnqvbPHgkW9LYr5HGBhfw==",
"requires": {
"jest-worker": "^27.0.6",
"p-limit": "^3.1.0",
@ -27333,9 +27333,9 @@
}
},
"webpack": {
"version": "5.51.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.51.1.tgz",
"integrity": "sha512-xsn3lwqEKoFvqn4JQggPSRxE4dhsRcysWTqYABAZlmavcoTmwlOb9b1N36Inbt/eIispSkuHa80/FJkDTPos1A==",
"version": "5.51.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.51.2.tgz",
"integrity": "sha512-odydxP4WA3XYYzwSQUivPxywdzMlY42bbfxMwCaEtHb+i/N9uzKSHcLgWkXo/Gsa+4Zlzf3Jg0hEHn1FnZpk2Q==",
"requires": {
"@types/eslint-scope": "^3.7.0",
"@types/estree": "^0.0.50",

View file

@ -39,17 +39,18 @@ export class Subject extends RestModel {
return {
UID: "",
Thumb: "",
PreviewSrc: "",
ThumbSrc: "",
Type: "",
Src: "",
Slug: "",
Name: "",
Alias: "",
Bio: "",
Notes: "",
Favorite: false,
Private: false,
Excluded: false,
FileCount: 0,
Files: 0,
Metadata: {},
CreatedAt: "",
UpdatedAt: "",
@ -113,7 +114,7 @@ export class Subject extends RestModel {
}
static batchSize() {
return 24;
return 60;
}
static getCollectionResource() {

View file

@ -11,6 +11,9 @@ var Permissions = ACL{
ResourceConfigOptions: Roles{
RoleAdmin: Actions{ActionDefault: true},
},
ResourceSubjects: Roles{
RoleAdmin: Actions{ActionDefault: true},
},
ResourceAlbums: Roles{
RoleAdmin: Actions{ActionDefault: true},
RoleGuest: Actions{ActionSearch: true, ActionRead: true},

View file

@ -9,6 +9,7 @@ const (
ResourceSettings Resource = "settings"
ResourceLogs Resource = "logs"
ResourceAccounts Resource = "accounts"
ResourceSubjects Resource = "subjects"
ResourceAlbums Resource = "albums"
ResourceCameras Resource = "cameras"
ResourceCategories Resource = "categories"

73
internal/api/subject.go Normal file
View file

@ -0,0 +1,73 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
)
// GetSubjects finds and returns subjects as JSON.
//
// GET /api/v1/subjects
func GetSubjects(router *gin.RouterGroup) {
router.GET("/subjects", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionSearch)
if s.Invalid() {
AbortUnauthorized(c)
return
}
var f form.SubjectSearch
err := c.MustBindWith(&f, binding.Form)
if err != nil {
AbortBadRequest(c)
return
}
result, err := query.SubjectSearch(f)
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
return
}
AddCountHeader(c, len(result))
AddLimitHeader(c, f.Count)
AddOffsetHeader(c, f.Offset)
AddTokenHeaders(c)
c.JSON(http.StatusOK, result)
})
}
// GetSubject returns a subject as JSON.
//
// GET /api/v1/subjects/:uid
func GetSubject(router *gin.RouterGroup) {
router.GET("/subjects/:uid", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionRead)
if s.Invalid() {
AbortUnauthorized(c)
return
}
if subj := entity.FindSubject(c.Param("uid")); subj == nil {
Abort(c, http.StatusNotFound, i18n.ErrSubjectNotFound)
return
} else {
c.JSON(http.StatusOK, subj)
}
})
}

View file

@ -0,0 +1,46 @@
package api
import (
"net/http"
"testing"
"github.com/tidwall/gjson"
"github.com/stretchr/testify/assert"
)
func TestGetSubjects(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, _ := NewApiTest()
GetSubjects(router)
r := PerformRequest(app, "GET", "/api/v1/subjects?count=10")
count := gjson.Get(r.Body.String(), "#")
assert.LessOrEqual(t, int64(3), count.Int())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, _ := NewApiTest()
GetSubjects(router)
r := PerformRequest(app, "GET", "/api/v1/subjects?xxx=10")
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}
func TestGetSubject(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, _ := NewApiTest()
GetSubject(router)
r := PerformRequest(app, "GET", "/api/v1/subjects/jqy1y111h1njaaaa")
val := gjson.Get(r.Body.String(), "Slug")
assert.Equal(t, "dangling-subject", val.String())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
app, router, _ := NewApiTest()
GetSubject(router)
r := PerformRequest(app, "GET", "/api/v1/subjects/xxx1y111h1njaaaa")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Subject not found", val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
})
}

View file

@ -4,6 +4,9 @@ import (
"strings"
"time"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/colors"
"github.com/photoprism/photoprism/pkg/fs"
@ -12,52 +15,52 @@ import (
// ClientConfig represents HTTP client / Web UI config options.
type ClientConfig struct {
Mode string `json:"mode"`
Name string `json:"name"`
Version string `json:"version"`
Copyright string `json:"copyright"`
Flags string `json:"flags"`
BaseUri string `json:"baseUri"`
StaticUri string `json:"staticUri"`
ApiUri string `json:"apiUri"`
ContentUri string `json:"contentUri"`
SiteUrl string `json:"siteUrl"`
SitePreview string `json:"sitePreview"`
SiteTitle string `json:"siteTitle"`
SiteCaption string `json:"siteCaption"`
SiteDescription string `json:"siteDescription"`
SiteAuthor string `json:"siteAuthor"`
Debug bool `json:"debug"`
Test bool `json:"test"`
Demo bool `json:"demo"`
Sponsor bool `json:"sponsor"`
ReadOnly bool `json:"readonly"`
UploadNSFW bool `json:"uploadNSFW"`
Public bool `json:"public"`
Experimental bool `json:"experimental"`
AlbumCategories []string `json:"albumCategories"`
Albums entity.Albums `json:"albums"`
Cameras entity.Cameras `json:"cameras"`
Lenses entity.Lenses `json:"lenses"`
Countries entity.Countries `json:"countries"`
Subjects entity.Subjects `json:"subjects"`
Thumbs ThumbTypes `json:"thumbs"`
Status string `json:"status"`
MapKey string `json:"mapKey"`
DownloadToken string `json:"downloadToken"`
PreviewToken string `json:"previewToken"`
JSHash string `json:"jsHash"`
CSSHash string `json:"cssHash"`
ManifestHash string `json:"manifestHash"`
Settings Settings `json:"settings"`
Disable ClientDisable `json:"disable"`
Count ClientCounts `json:"count"`
Pos ClientPosition `json:"pos"`
Years Years `json:"years"`
Colors []map[string]string `json:"colors"`
Categories CategoryLabels `json:"categories"`
Clip int `json:"clip"`
Server RuntimeInfo `json:"server"`
Mode string `json:"mode"`
Name string `json:"name"`
Version string `json:"version"`
Copyright string `json:"copyright"`
Flags string `json:"flags"`
BaseUri string `json:"baseUri"`
StaticUri string `json:"staticUri"`
ApiUri string `json:"apiUri"`
ContentUri string `json:"contentUri"`
SiteUrl string `json:"siteUrl"`
SitePreview string `json:"sitePreview"`
SiteTitle string `json:"siteTitle"`
SiteCaption string `json:"siteCaption"`
SiteDescription string `json:"siteDescription"`
SiteAuthor string `json:"siteAuthor"`
Debug bool `json:"debug"`
Test bool `json:"test"`
Demo bool `json:"demo"`
Sponsor bool `json:"sponsor"`
ReadOnly bool `json:"readonly"`
UploadNSFW bool `json:"uploadNSFW"`
Public bool `json:"public"`
Experimental bool `json:"experimental"`
AlbumCategories []string `json:"albumCategories"`
Albums entity.Albums `json:"albums"`
Cameras entity.Cameras `json:"cameras"`
Lenses entity.Lenses `json:"lenses"`
Countries entity.Countries `json:"countries"`
Subjects query.SubjectResults `json:"subjects"`
Thumbs ThumbTypes `json:"thumbs"`
Status string `json:"status"`
MapKey string `json:"mapKey"`
DownloadToken string `json:"downloadToken"`
PreviewToken string `json:"previewToken"`
JSHash string `json:"jsHash"`
CSSHash string `json:"cssHash"`
ManifestHash string `json:"manifestHash"`
Settings Settings `json:"settings"`
Disable ClientDisable `json:"disable"`
Count ClientCounts `json:"count"`
Pos ClientPosition `json:"pos"`
Years Years `json:"years"`
Colors []map[string]string `json:"colors"`
Categories CategoryLabels `json:"categories"`
Clip int `json:"clip"`
Server RuntimeInfo `json:"server"`
}
// Years represents a list of years.
@ -399,6 +402,8 @@ func (c *Config) UserConfig() ClientConfig {
Order("country_slug").
Find(&result.Countries)
result.Subjects, _ = query.SubjectSearch(form.SubjectSearch{Type: entity.SubjectPerson})
c.Db().
Where("id IN (SELECT photos.camera_id FROM photos WHERE photos.photo_quality >= 0 OR photos.deleted_at IS NULL)").
Where("deleted_at IS NULL").

View file

@ -37,7 +37,7 @@ type Subject struct {
Favorite bool `gorm:"default:false" json:"Favorite" yaml:"Favorite,omitempty"`
Private bool `gorm:"default:false" json:"Private" yaml:"Private,omitempty"`
Excluded bool `gorm:"default:false" json:"Excluded" yaml:"Excluded,omitempty"`
FileCount int `gorm:"default:0" json:"FileCount" yaml:"-"`
FileCount int `gorm:"default:0" json:"Files" yaml:"-"`
MetadataJSON json.RawMessage `gorm:"type:MEDIUMBLOB;" json:"Metadata,omitempty" yaml:"Metadata,omitempty"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`

View file

@ -57,6 +57,7 @@ var SubjectFixtures = SubjectMap{
SubjectUID: "jqy1y111h1njaaaa",
SubjectSlug: "dangling-subject",
SubjectName: "Dangling Subject",
SubjectAlias: "Powell",
SubjectType: SubjectPerson,
SubjectSrc: SrcMarker,
Favorite: false,

View file

@ -0,0 +1,32 @@
package form
// SubjectSearch represents search form fields for "/api/v1/subjects".
type SubjectSearch struct {
Query string `form:"q"`
ID string `form:"id"`
Type string `form:"type"`
Name string `form:"name"`
Hidden bool `form:"hidden"`
Favorite bool `form:"favorite"`
Private bool `form:"private"`
Excluded bool `form:"excluded"`
Count int `form:"count" binding:"required" serialize:"-"`
Offset int `form:"offset" serialize:"-"`
Order string `form:"order" serialize:"-"`
}
func (f *SubjectSearch) GetQuery() string {
return f.Query
}
func (f *SubjectSearch) SetQuery(q string) {
f.Query = q
}
func (f *SubjectSearch) ParseQueryString() error {
return ParseQueryString(f)
}
func NewSubjectSearch(query string) SubjectSearch {
return SubjectSearch{Query: query}
}

View file

@ -0,0 +1,36 @@
package form
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSubjectSearchForm(t *testing.T) {
form := &SubjectSearch{}
assert.IsType(t, new(SubjectSearch), form)
}
func TestParseQueryStringSubject(t *testing.T) {
t.Run("Ok", func(t *testing.T) {
form := &SubjectSearch{Query: "type:person favorite:true count:5"}
err := form.ParseQueryString()
// log.Debugf("%+v\n", form)
if err != nil {
t.Fatal("err should be nil")
}
assert.Equal(t, "person", form.Type)
assert.Equal(t, true, form.Favorite)
assert.Equal(t, 5, form.Count)
})
}
func TestNewSubjectSearch(t *testing.T) {
r := NewSubjectSearch("john")
assert.IsType(t, SubjectSearch{}, r)
}

View file

@ -14,6 +14,7 @@ const (
ErrUserNotFound
ErrLabelNotFound
ErrAlbumNotFound
ErrSubjectNotFound
ErrPublic
ErrReadOnly
ErrUnauthorized
@ -84,6 +85,7 @@ var Messages = MessageMap{
ErrUserNotFound: gettext("User not found"),
ErrLabelNotFound: gettext("Label not found"),
ErrAlbumNotFound: gettext("Album not found"),
ErrSubjectNotFound: gettext("Subject not found"),
ErrPublic: gettext("Not available in public mode"),
ErrReadOnly: gettext("not available in read-only mode"),
ErrUnauthorized: gettext("Please log in and try again"),

View file

@ -0,0 +1,97 @@
package query
import (
"fmt"
"strings"
"time"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/capture"
)
// SubjectResult represents a subject search result.
type SubjectResult struct {
SubjectUID string `json:"UID"`
SubjectType string `json:"Type"`
SubjectSlug string `json:"Slug"`
SubjectName string `json:"Name"`
SubjectAlias string `json:"Alias,omitempty"`
Thumb string `json:"Thumb,omitempty"`
Favorite bool `json:"Favorite,omitempty"`
Private bool `json:"Private,omitempty"`
Excluded bool `json:"Excluded,omitempty"`
FileCount int `json:"Files,omitempty"`
}
// SubjectResults represents subject search results.
type SubjectResults []SubjectResult
// SubjectSearch searches subjects and returns them.
func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
if err := f.ParseQueryString(); err != nil {
return results, err
}
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("subjects: search %s", form.Serialize(f, true))))
// Base query.
s := UnscopedDb().Table(entity.Subject{}.TableName()).
Select("subject_uid, subject_slug, subject_name, subject_alias, subject_type, thumb, favorite, private, excluded, file_count")
// Limit result count.
if f.Count > 0 && f.Count <= MaxResults {
s = s.Limit(f.Count).Offset(f.Offset)
} else {
s = s.Limit(MaxResults).Offset(f.Offset)
}
// Set sort order.
switch f.Order {
case "count":
s = s.Order("file_count DESC")
default:
s = s.Order("subject_name")
}
if f.ID != "" {
s = s.Where("subject_uid IN (?)", strings.Split(f.ID, Or))
if result := s.Scan(&results); result.Error != nil {
return results, result.Error
}
return results, nil
}
if f.Query != "" {
for _, where := range LikeAnyWord("subject_name", f.Query) {
s = s.Where("(?)", gorm.Expr(where))
}
}
if f.Type != "" {
s = s.Where("subject_type IN (?)", strings.Split(f.Type, Or))
}
if f.Favorite {
s = s.Where("favorite = 1")
}
if f.Private {
s = s.Where("private = 1")
}
if f.Excluded {
s = s.Where("excluded = 1")
}
s = s.Where("deleted_at IS NULL")
if result := s.Scan(&results); result.Error != nil {
return results, result.Error
}
return results, nil
}

View file

@ -0,0 +1,20 @@
package query
import (
"testing"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/entity"
"github.com/stretchr/testify/assert"
)
func TestSubjectSearch(t *testing.T) {
t.Run("FindAll", func(t *testing.T) {
results, err := SubjectSearch(form.SubjectSearch{Type: entity.SubjectPerson})
assert.NoError(t, err)
assert.LessOrEqual(t, 3, len(results))
})
}

View file

@ -86,6 +86,9 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
api.PhotoPrimary(v1)
api.PhotoUnstack(v1)
api.GetSubjects(v1)
api.GetSubject(v1)
api.GetLabels(v1)
api.UpdateLabel(v1)
api.GetLabelLinks(v1)