diff --git a/assets/locales/messages.pot b/assets/locales/messages.pot index 75b7d0e51..8cb55a865 100644 --- a/assets/locales/messages.pot +++ b/assets/locales/messages.pot @@ -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 \n" "Language-Team: LANGUAGE \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 "" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b5426e022..fb68b8a14 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/src/model/subject.js b/frontend/src/model/subject.js index 1cb407756..8536f31b1 100644 --- a/frontend/src/model/subject.js +++ b/frontend/src/model/subject.js @@ -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() { diff --git a/internal/acl/permissions.go b/internal/acl/permissions.go index 6c9bb2cee..6b5dbf4b2 100644 --- a/internal/acl/permissions.go +++ b/internal/acl/permissions.go @@ -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}, diff --git a/internal/acl/resources.go b/internal/acl/resources.go index 16f1a422b..40fe1ea77 100644 --- a/internal/acl/resources.go +++ b/internal/acl/resources.go @@ -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" diff --git a/internal/api/subject.go b/internal/api/subject.go new file mode 100644 index 000000000..f5d38e4a5 --- /dev/null +++ b/internal/api/subject.go @@ -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) + } + + }) +} diff --git a/internal/api/subject_test.go b/internal/api/subject_test.go new file mode 100644 index 000000000..27e765172 --- /dev/null +++ b/internal/api/subject_test.go @@ -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) + }) +} diff --git a/internal/config/client.go b/internal/config/client.go index b1ed786fc..f67174cee 100644 --- a/internal/config/client.go +++ b/internal/config/client.go @@ -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"). diff --git a/internal/entity/subject.go b/internal/entity/subject.go index 5fd6e645b..fc7caf5fa 100644 --- a/internal/entity/subject.go +++ b/internal/entity/subject.go @@ -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:"-"` diff --git a/internal/entity/subject_fixtures.go b/internal/entity/subject_fixtures.go index 5b2af8191..ec37c9a99 100644 --- a/internal/entity/subject_fixtures.go +++ b/internal/entity/subject_fixtures.go @@ -57,6 +57,7 @@ var SubjectFixtures = SubjectMap{ SubjectUID: "jqy1y111h1njaaaa", SubjectSlug: "dangling-subject", SubjectName: "Dangling Subject", + SubjectAlias: "Powell", SubjectType: SubjectPerson, SubjectSrc: SrcMarker, Favorite: false, diff --git a/internal/form/subject_search.go b/internal/form/subject_search.go new file mode 100644 index 000000000..ec291befa --- /dev/null +++ b/internal/form/subject_search.go @@ -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} +} diff --git a/internal/form/subject_search_test.go b/internal/form/subject_search_test.go new file mode 100644 index 000000000..e65568e3c --- /dev/null +++ b/internal/form/subject_search_test.go @@ -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) +} diff --git a/internal/i18n/messages.go b/internal/i18n/messages.go index 8d48a88bd..e664a70c8 100644 --- a/internal/i18n/messages.go +++ b/internal/i18n/messages.go @@ -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"), diff --git a/internal/query/subject_search.go b/internal/query/subject_search.go new file mode 100644 index 000000000..9f0e52912 --- /dev/null +++ b/internal/query/subject_search.go @@ -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 +} diff --git a/internal/query/subject_search_test.go b/internal/query/subject_search_test.go new file mode 100644 index 000000000..a7b322970 --- /dev/null +++ b/internal/query/subject_search_test.go @@ -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)) + }) + +} diff --git a/internal/server/routes.go b/internal/server/routes.go index 69a77a62d..d7daddf8d 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -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)