People: Normalize names #22

This commit is contained in:
Michael Mayer 2021-09-19 13:35:44 +02:00
parent ed962a36da
commit 1f92f294dd
25 changed files with 267 additions and 151 deletions

View file

@ -1887,9 +1887,9 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "16.9.2", "version": "16.9.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.3.tgz",
"integrity": "sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w==" "integrity": "sha512-5UmMznRvrwKqisJ458JbNoq3AyXHxlAKMkGtNe143W1SkZ1BVgvCHYBzn7wD66J+smE+BolqA1mes5BeXlWY6w=="
}, },
"node_modules/@types/parse-json": { "node_modules/@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",
@ -15535,9 +15535,9 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
}, },
"@types/node": { "@types/node": {
"version": "16.9.2", "version": "16.9.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.3.tgz",
"integrity": "sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w==" "integrity": "sha512-5UmMznRvrwKqisJ458JbNoq3AyXHxlAKMkGtNe143W1SkZ1BVgvCHYBzn7wD66J+smE+BolqA1mes5BeXlWY6w=="
}, },
"@types/parse-json": { "@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",

View file

@ -181,6 +181,20 @@
</v-list-tile-content> </v-list-tile-content>
</v-list-tile> </v-list-tile>
<v-list-tile v-show="$config.feature('people')" :to="{ name: 'people' }" class="nav-people" @click.stop="">
<v-list-tile-action :title="$gettext('People')">
<v-icon>person</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>
<translate key="People">People</translate>
<span v-show="config.count.people > 0"
:class="`nav-count ${rtl ? '--rtl' : ''}`">{{ config.count.people }}</span>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/favorites" class="nav-favorites" @click.stop=""> <v-list-tile to="/favorites" class="nav-favorites" @click.stop="">
<v-list-tile-action :title="$gettext('Favorites')"> <v-list-tile-action :title="$gettext('Favorites')">
<v-icon>favorite</v-icon> <v-icon>favorite</v-icon>
@ -209,20 +223,6 @@
</v-list-tile-content> </v-list-tile-content>
</v-list-tile> </v-list-tile>
<v-list-tile v-show="$config.feature('people')" :to="{ name: 'people' }" class="nav-people" @click.stop="">
<v-list-tile-action :title="$gettext('People')">
<v-icon>emoji_people</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>
<translate key="People">People</translate>
<span v-show="config.count.people > 0"
:class="`nav-count ${rtl ? '--rtl' : ''}`">{{ config.count.people }}</span>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile :to="{ name: 'calendar' }" class="nav-calendar" @click.stop=""> <v-list-tile :to="{ name: 'calendar' }" class="nav-calendar" @click.stop="">
<v-list-tile-action :title="$gettext('Calendar')"> <v-list-tile-action :title="$gettext('Calendar')">
<v-icon>date_range</v-icon> <v-icon>date_range</v-icon>

View file

@ -52,9 +52,9 @@
</v-tab> </v-tab>
<v-tab id="tab-people" :disabled="!$config.feature('people')" ripple> <v-tab id="tab-people" :disabled="!$config.feature('people')" ripple>
<v-icon v-if="$vuetify.breakpoint.smAndDown" :title="$gettext('People')">emoji_people</v-icon> <v-icon v-if="$vuetify.breakpoint.smAndDown" :title="$gettext('People')">people_alt</v-icon>
<template v-else> <template v-else>
<v-icon :size="18" :left="!rtl" :right="rtl">emoji_people</v-icon> <v-icon :size="18" :left="!rtl" :right="rtl">people_alt</v-icon>
<v-badge color="secondary-dark" :left="rtl" :right="!rtl"> <v-badge color="secondary-dark" :left="rtl" :right="!rtl">
<template #badge> <template #badge>
<span v-if="model.Faces">{{ model.Faces }}</span> <span v-if="model.Faces">{{ model.Faces }}</span>

Binary file not shown.

View file

@ -334,13 +334,6 @@ msgstr ""
"Videos und andere Bild-Formate nach JPEG konvertieren, damit sie indexiert " "Videos und andere Bild-Formate nach JPEG konvertieren, damit sie indexiert "
"und angezeigt werden können." "und angezeigt werden können."
#: src/pages/settings/general.vue:429
msgid ""
"Automatically detects faces so that you can search your pictures for people."
msgstr ""
"Erkennt automatisch Gesichter, so dass Sie Ihre Bilder nach Personen "
"durchsuchen können."
#: src/pages/people/subjects.vue:339 #: src/pages/people/subjects.vue:339
msgid "Bio" msgid "Bio"
msgstr "Biographie" msgstr "Biographie"
@ -365,7 +358,7 @@ msgstr "Brasilianisches Portugiesisch"
msgid "Brown" msgid "Brown"
msgstr "Braun" msgstr "Braun"
#: src/pages/settings/general.vue:361 #: src/pages/settings/general.vue:386
msgid "Browse and edit image classification labels." msgid "Browse and edit image classification labels."
msgstr "Automatische Bild-Kategorisierung sehen und bearbeiten." msgstr "Automatische Bild-Kategorisierung sehen und bearbeiten."
@ -435,7 +428,7 @@ msgid "Change"
msgstr "Ändern" msgstr "Ändern"
#: src/pages/settings/general.vue:185 #: src/pages/settings/general.vue:185
msgid "Change photo titles, locations and other metadata." msgid "Change photo titles, locations, and other metadata."
msgstr "Titel, Datum, Ort und andere Metadaten können geändert werden." msgstr "Titel, Datum, Ort und andere Metadaten können geändert werden."
#: src/component/photo/clipboard.vue:146 #: src/component/photo/clipboard.vue:146
@ -860,8 +853,8 @@ msgstr "Jeden zweiten Tag"
#: src/pages/settings/general.vue:273 #: src/pages/settings/general.vue:273
msgid "" msgid ""
"Exclude content marked as private from search results, shared albums, labels " "Exclude content marked as private from search results, shared albums, "
"and places." "labels, and places."
msgstr "" msgstr ""
"Als privat markierte Inhalte werden nicht in Suchergebnissen und geteilten " "Als privat markierte Inhalte werden nicht in Suchergebnissen und geteilten "
"Alben angezeigt." "Alben angezeigt."
@ -890,7 +883,7 @@ msgstr "Belichtungszeit"
msgid "F Number" msgid "F Number"
msgstr "F Nummer" msgstr "F Nummer"
#: src/model/face.js:129 #: src/model/face.js:125
msgid "Face" msgid "Face"
msgstr "Gesicht" msgstr "Gesicht"
@ -922,7 +915,7 @@ msgstr "Schnell"
msgid "Favorite" msgid "Favorite"
msgstr "Favorit" msgstr "Favorit"
#: src/component/navigation.vue:176 src/component/navigation.vue:698 #: src/component/navigation.vue:189 src/component/navigation.vue:743
#: src/routes.js:180 #: src/routes.js:180
msgid "Favorites" msgid "Favorites"
msgstr "Favoriten" msgstr "Favoriten"
@ -1192,7 +1185,7 @@ msgstr "Name"
#: src/component/navigation.vue:262 src/component/navigation.vue:994 #: src/component/navigation.vue:262 src/component/navigation.vue:994
#: src/dialog/photo/edit.vue:39 src/dialog/photo/edit.vue:6 #: src/dialog/photo/edit.vue:39 src/dialog/photo/edit.vue:6
#: src/dialog/photo/edit.vue:216 src/pages/settings/general.vue:360 #: src/dialog/photo/edit.vue:216 src/pages/settings/general.vue:385
#: src/routes.js:259 #: src/routes.js:259
msgid "Labels" msgid "Labels"
msgstr "Kategorien" msgstr "Kategorien"
@ -1225,8 +1218,8 @@ msgstr "Lavendel"
msgid "Lens" msgid "Lens"
msgstr "Objektiv" msgstr "Objektiv"
#: src/pages/settings/general.vue:339 #: src/pages/settings/general.vue:364
msgid "Let PhotoPrism create albums from past events." msgid "Let PhotoPrism automatically create albums from past events."
msgstr "" msgstr ""
"PhotoPrism erstellt automatisch Alben mit besonderen Momenten, Reisen und " "PhotoPrism erstellt automatisch Alben mit besonderen Momenten, Reisen und "
"Orten." "Orten."
@ -1237,7 +1230,7 @@ msgstr "Jetzt Unterstützer werden"
#: src/component/navigation.vue:301 src/component/navigation.vue:311 #: src/component/navigation.vue:301 src/component/navigation.vue:311
#: src/component/navigation.vue:4 src/component/navigation.vue:1131 #: src/component/navigation.vue:4 src/component/navigation.vue:1131
#: src/pages/settings.vue:41 src/pages/settings/general.vue:382 #: src/pages/settings.vue:41 src/pages/settings/general.vue:407
#: src/routes.js:279 src/routes.js:286 src/routes.js:293 #: src/routes.js:279 src/routes.js:286 src/routes.js:293
msgid "Library" msgid "Library"
msgstr "Dateien" msgstr "Dateien"
@ -1308,7 +1301,7 @@ msgstr "Anmelden"
msgid "Logout" msgid "Logout"
msgstr "Abmelden" msgstr "Abmelden"
#: src/pages/library.vue:55 src/pages/settings/general.vue:404 #: src/pages/library.vue:55 src/pages/settings/general.vue:429
msgid "Logs" msgid "Logs"
msgstr "Logs" msgstr "Logs"
@ -1352,8 +1345,8 @@ msgstr "Minimieren"
msgid "Missing" msgid "Missing"
msgstr "Fehlend" msgstr "Fehlend"
#: src/component/navigation.vue:189 src/component/navigation.vue:741 #: src/component/navigation.vue:202 src/component/navigation.vue:786
#: src/pages/settings/general.vue:338 src/routes.js:121 src/routes.js:128 #: src/pages/settings/general.vue:363 src/routes.js:121 src/routes.js:128
msgid "Moments" msgid "Moments"
msgstr "Erlebnisse" msgstr "Erlebnisse"
@ -1431,7 +1424,7 @@ msgstr "Name zu lang"
msgid "Never" msgid "Never"
msgstr "Nie" msgstr "Nie"
#: src/pages/people.vue:42 #: src/pages/people.vue:43
msgid "New" msgid "New"
msgstr "Neu" msgstr "Neu"
@ -1650,9 +1643,9 @@ msgstr "Passwort geändert"
msgid "pay for operating expenses and external services like satellite maps" msgid "pay for operating expenses and external services like satellite maps"
msgstr "Betriebskosten und externe Dienste wie Satellitenkarten zu bezahlen" msgstr "Betriebskosten und externe Dienste wie Satellitenkarten zu bezahlen"
#: src/component/navigation.vue:202 src/component/navigation.vue:786 #: src/component/navigation.vue:176 src/component/navigation.vue:698
#: src/dialog/photo/edit.vue:52 src/dialog/photo/edit.vue:6 #: src/dialog/photo/edit.vue:52 src/dialog/photo/edit.vue:6
#: src/dialog/photo/edit.vue:267 src/pages/settings/general.vue:428 #: src/dialog/photo/edit.vue:267 src/pages/settings/general.vue:340
#: src/routes.js:265 src/routes.js:272 #: src/routes.js:265 src/routes.js:272
msgid "People" msgid "People"
msgstr "Personen" msgstr "Personen"
@ -1835,7 +1828,13 @@ msgstr "Zuletzt hinzugefügt"
msgid "Recently edited" msgid "Recently edited"
msgstr "Zuletzt bearbeitet" msgstr "Zuletzt bearbeitet"
#: src/pages/people.vue:31 #: src/pages/settings/general.vue:341
msgid ""
"Recognize faces so that specific people can be found and albums created."
msgstr ""
"Gesichter erkennen, damit Personen gefunden und Alben erstellt werden können."
#: src/pages/people.vue:32
msgid "Recognized" msgid "Recognized"
msgstr "Erkannt" msgstr "Erkannt"
@ -2024,7 +2023,7 @@ msgstr "Mit dir geteilt."
msgid "Show less" msgid "Show less"
msgstr "Weniger zeigen" msgstr "Weniger zeigen"
#: src/pages/settings/general.vue:383 #: src/pages/settings/general.vue:408
msgid "Show Library in navigation menu." msgid "Show Library in navigation menu."
msgstr "Datei-Verwaltung in der Navigation anzeigen." msgstr "Datei-Verwaltung in der Navigation anzeigen."
@ -2032,7 +2031,7 @@ msgstr "Datei-Verwaltung in der Navigation anzeigen."
msgid "Show more" msgid "Show more"
msgstr "Mehr zeigen" msgstr "Mehr zeigen"
#: src/pages/settings/general.vue:405 #: src/pages/settings/general.vue:430
msgid "Show server logs in Library." msgid "Show server logs in Library."
msgstr "Server-Ereignisprotokoll anzeigen, um Fehler zu finden." msgstr "Server-Ereignisprotokoll anzeigen, um Fehler zu finden."
@ -2155,7 +2154,7 @@ msgstr "Straßen"
msgid "Style" msgid "Style"
msgstr "Style" msgstr "Style"
#: src/dialog/photo/details.vue:482 src/model/subject.js:137 #: src/dialog/photo/details.vue:482 src/model/subject.js:143
msgid "Subject" msgid "Subject"
msgstr "Bildinhalt" msgstr "Bildinhalt"
@ -2506,8 +2505,19 @@ msgstr "Deine Nachricht wurde gesendet"
msgid "Zoom in/out" msgid "Zoom in/out"
msgstr "Zoom in/out" msgstr "Zoom in/out"
#~ msgid "Detect faces and search for people in your pictures." #~ msgid ""
#~ msgstr "Findet Gesichter und aktiviert die Suche nach Personen." #~ "Detect faces so that you can search for specific people, and share photos "
#~ "with them."
#~ msgstr ""
#~ "Erkennt Gesichter, damit bestimmte Personen gefunden und Alben erstellt "
#~ "werden können."
#~ msgid ""
#~ "Automatically detects faces so that you can search your pictures for "
#~ "people."
#~ msgstr ""
#~ "Erkennt automatisch Gesichter, so dass Sie Ihre Bilder nach Personen "
#~ "durchsuchen können."
#~ msgid "Not implemented yet" #~ msgid "Not implemented yet"
#~ msgstr "Noch nicht implementiert" #~ msgstr "Noch nicht implementiert"

File diff suppressed because one or more lines are too long

View file

@ -346,10 +346,6 @@ msgstr ""
msgid "Automatically create JPEGs for other file types so that they can be displayed in a browser." msgid "Automatically create JPEGs for other file types so that they can be displayed in a browser."
msgstr "" msgstr ""
#: src/pages/settings/general.vue:429
msgid "Automatically detects faces so that you can search your pictures for people."
msgstr ""
#: src/pages/people/subjects.vue:339 #: src/pages/people/subjects.vue:339
msgid "Bio" msgid "Bio"
msgstr "" msgstr ""
@ -374,7 +370,7 @@ msgstr ""
msgid "Brown" msgid "Brown"
msgstr "" msgstr ""
#: src/pages/settings/general.vue:361 #: src/pages/settings/general.vue:386
msgid "Browse and edit image classification labels." msgid "Browse and edit image classification labels."
msgstr "" msgstr ""
@ -467,7 +463,7 @@ msgid "Change"
msgstr "" msgstr ""
#: src/pages/settings/general.vue:185 #: src/pages/settings/general.vue:185
msgid "Change photo titles, locations and other metadata." msgid "Change photo titles, locations, and other metadata."
msgstr "" msgstr ""
#: src/component/photo/clipboard.vue:146 #: src/component/photo/clipboard.vue:146
@ -930,7 +926,7 @@ msgid "Every two days"
msgstr "" msgstr ""
#: src/pages/settings/general.vue:273 #: src/pages/settings/general.vue:273
msgid "Exclude content marked as private from search results, shared albums, labels and places." msgid "Exclude content marked as private from search results, shared albums, labels, and places."
msgstr "" msgstr ""
#: src/component/navigation.vue:248 #: src/component/navigation.vue:248
@ -958,7 +954,7 @@ msgstr ""
msgid "F Number" msgid "F Number"
msgstr "" msgstr ""
#: src/model/face.js:129 #: src/model/face.js:125
msgid "Face" msgid "Face"
msgstr "" msgstr ""
@ -991,8 +987,8 @@ msgstr ""
msgid "Favorite" msgid "Favorite"
msgstr "" msgstr ""
#: src/component/navigation.vue:176 #: src/component/navigation.vue:189
#: src/component/navigation.vue:698 #: src/component/navigation.vue:743
#: src/routes.js:180 #: src/routes.js:180
msgid "Favorites" msgid "Favorites"
msgstr "" msgstr ""
@ -1269,7 +1265,7 @@ msgstr ""
#: src/dialog/photo/edit.vue:39 #: src/dialog/photo/edit.vue:39
#: src/dialog/photo/edit.vue:6 #: src/dialog/photo/edit.vue:6
#: src/dialog/photo/edit.vue:216 #: src/dialog/photo/edit.vue:216
#: src/pages/settings/general.vue:360 #: src/pages/settings/general.vue:385
#: src/routes.js:259 #: src/routes.js:259
msgid "Labels" msgid "Labels"
msgstr "" msgstr ""
@ -1303,8 +1299,8 @@ msgstr ""
msgid "Lens" msgid "Lens"
msgstr "" msgstr ""
#: src/pages/settings/general.vue:339 #: src/pages/settings/general.vue:364
msgid "Let PhotoPrism create albums from past events." msgid "Let PhotoPrism automatically create albums from past events."
msgstr "" msgstr ""
#: src/dialog/sponsor.vue:7 #: src/dialog/sponsor.vue:7
@ -1316,7 +1312,7 @@ msgstr ""
#: src/component/navigation.vue:4 #: src/component/navigation.vue:4
#: src/component/navigation.vue:1131 #: src/component/navigation.vue:1131
#: src/pages/settings.vue:41 #: src/pages/settings.vue:41
#: src/pages/settings/general.vue:382 #: src/pages/settings/general.vue:407
#: src/routes.js:279 #: src/routes.js:279
#: src/routes.js:286 #: src/routes.js:286
#: src/routes.js:293 #: src/routes.js:293
@ -1397,7 +1393,7 @@ msgid "Logout"
msgstr "" msgstr ""
#: src/pages/library.vue:55 #: src/pages/library.vue:55
#: src/pages/settings/general.vue:404 #: src/pages/settings/general.vue:429
msgid "Logs" msgid "Logs"
msgstr "" msgstr ""
@ -1444,9 +1440,9 @@ msgstr ""
msgid "Missing" msgid "Missing"
msgstr "" msgstr ""
#: src/component/navigation.vue:189 #: src/component/navigation.vue:202
#: src/component/navigation.vue:741 #: src/component/navigation.vue:786
#: src/pages/settings/general.vue:338 #: src/pages/settings/general.vue:363
#: src/routes.js:121 #: src/routes.js:121
#: src/routes.js:128 #: src/routes.js:128
msgid "Moments" msgid "Moments"
@ -1544,7 +1540,7 @@ msgstr ""
msgid "Never" msgid "Never"
msgstr "" msgstr ""
#: src/pages/people.vue:42 #: src/pages/people.vue:43
msgid "New" msgid "New"
msgstr "" msgstr ""
@ -1760,12 +1756,12 @@ msgstr ""
msgid "pay for operating expenses and external services like satellite maps" msgid "pay for operating expenses and external services like satellite maps"
msgstr "" msgstr ""
#: src/component/navigation.vue:202 #: src/component/navigation.vue:176
#: src/component/navigation.vue:786 #: src/component/navigation.vue:698
#: src/dialog/photo/edit.vue:52 #: src/dialog/photo/edit.vue:52
#: src/dialog/photo/edit.vue:6 #: src/dialog/photo/edit.vue:6
#: src/dialog/photo/edit.vue:267 #: src/dialog/photo/edit.vue:267
#: src/pages/settings/general.vue:428 #: src/pages/settings/general.vue:340
#: src/routes.js:265 #: src/routes.js:265
#: src/routes.js:272 #: src/routes.js:272
msgid "People" msgid "People"
@ -1962,7 +1958,11 @@ msgstr ""
msgid "Recently edited" msgid "Recently edited"
msgstr "" msgstr ""
#: src/pages/people.vue:31 #: src/pages/settings/general.vue:341
msgid "Recognize faces so that specific people can be found and albums created."
msgstr ""
#: src/pages/people.vue:32
msgid "Recognized" msgid "Recognized"
msgstr "" msgstr ""
@ -2172,7 +2172,7 @@ msgstr ""
msgid "Show less" msgid "Show less"
msgstr "" msgstr ""
#: src/pages/settings/general.vue:383 #: src/pages/settings/general.vue:408
msgid "Show Library in navigation menu." msgid "Show Library in navigation menu."
msgstr "" msgstr ""
@ -2180,7 +2180,7 @@ msgstr ""
msgid "Show more" msgid "Show more"
msgstr "" msgstr ""
#: src/pages/settings/general.vue:405 #: src/pages/settings/general.vue:430
msgid "Show server logs in Library." msgid "Show server logs in Library."
msgstr "" msgstr ""
@ -2304,7 +2304,7 @@ msgid "Style"
msgstr "" msgstr ""
#: src/dialog/photo/details.vue:482 #: src/dialog/photo/details.vue:482
#: src/model/subject.js:137 #: src/model/subject.js:143
msgid "Subject" msgid "Subject"
msgstr "" msgstr ""

View file

@ -68,6 +68,15 @@ export class Rest extends Model {
} }
update() { update() {
// Get updated values.
const values = this.getValues(true);
// Return if no values were changed.
if (Object.keys(values).length === 0) {
return Promise.resolve(this);
}
// Send PUT request.
return Api.put(this.getEntityResource(), this.getValues(true)).then((resp) => return Api.put(this.getEntityResource(), this.getValues(true)).then((resp) =>
Promise.resolve(this.setValues(resp.data)) Promise.resolve(this.setValues(resp.data))
); );

View file

@ -34,6 +34,8 @@ import { DateTime } from "luxon";
import { config } from "../session"; import { config } from "../session";
import { $gettext } from "common/vm"; import { $gettext } from "common/vm";
const SubjPerson = "person";
export class Subject extends RestModel { export class Subject extends RestModel {
getDefaults() { getDefaults() {
return { return {
@ -61,6 +63,10 @@ export class Subject extends RestModel {
} }
route(view) { route(view) {
if (!this.Type || this.Type === SubjPerson) {
return { name: view, query: { q: `people:"${this.Name}"` } };
}
return { name: view, query: { q: "subject:" + this.UID } }; return { name: view, query: { q: "subject:" + this.UID } };
} }

View file

@ -46,6 +46,7 @@ export default {
tab: String, tab: String,
}, },
data() { data() {
let tabName = this.tab;
const config = this.$config.values; const config = this.$config.values;
const isDemo = this.$config.get("demo"); const isDemo = this.$config.get("demo");
const isPublic = this.$config.get("public"); const isPublic = this.$config.get("public");
@ -76,10 +77,14 @@ export default {
}, },
]; ];
if (config.count.people === 0) {
tabName = "people-faces";
}
let active = 0; let active = 0;
if (typeof this.tab === 'string' && this.tab !== '') { if (typeof tabName === 'string' && tabName !== '') {
active = tabs.findIndex((t) => t.name === this.tab); active = tabs.findIndex((t) => t.name === tabName);
} }
return { return {

View file

@ -83,7 +83,7 @@
class="ma-0 pa-0 input-edit" class="ma-0 pa-0 input-edit"
color="secondary-dark" color="secondary-dark"
:label="$gettext('Edit')" :label="$gettext('Edit')"
:hint="$gettext('Change photo titles, locations and other metadata.')" :hint="$gettext('Change photo titles, locations, and other metadata.')"
prepend-icon="edit" prepend-icon="edit"
persistent-hint persistent-hint
@change="onChange" @change="onChange"
@ -143,7 +143,7 @@
class="ma-0 pa-0 input-private" class="ma-0 pa-0 input-private"
color="secondary-dark" color="secondary-dark"
:label="$gettext('Private')" :label="$gettext('Private')"
:hint="$gettext('Exclude content marked as private from search results, shared albums, labels and places.')" :hint="$gettext('Exclude content marked as private from search results, shared albums, labels, and places.')"
prepend-icon="lock" prepend-icon="lock"
persistent-hint persistent-hint
@change="onChange" @change="onChange"
@ -181,6 +181,21 @@
</v-checkbox> </v-checkbox>
</v-flex> </v-flex>
<v-flex v-if="config.experimental" xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.people"
:disabled="busy"
class="ma-0 pa-0 input-people"
color="secondary-dark"
:label="$gettext('People')"
:hint="$gettext('Recognize faces so that specific people can be found and albums created.')"
prepend-icon="person"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2"> <v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox <v-checkbox
v-model="settings.features.moments" v-model="settings.features.moments"
@ -188,7 +203,7 @@
class="ma-0 pa-0 input-moments" class="ma-0 pa-0 input-moments"
color="secondary-dark" color="secondary-dark"
:label="$gettext('Moments')" :label="$gettext('Moments')"
:hint="$gettext('Let PhotoPrism create albums from past events.')" :hint="$gettext('Let PhotoPrism automatically create albums from past events.')"
prepend-icon="star" prepend-icon="star"
persistent-hint persistent-hint
@change="onChange" @change="onChange"
@ -241,21 +256,6 @@
</v-checkbox> </v-checkbox>
</v-flex> </v-flex>
<v-flex v-if="config.experimental" xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.people"
:disabled="busy"
class="ma-0 pa-0 input-people"
color="secondary-dark"
:label="$gettext('People')"
:hint="$gettext('Automatically detects faces so that you can search your pictures for people.')"
prepend-icon="emoji_people"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex v-if="!config.disable.places" xs12 sm6 lg3 class="px-2 pb-2 pt-2"> <v-flex v-if="!config.disable.places" xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox <v-checkbox
v-model="settings.features.places" v-model="settings.features.places"

View file

@ -53,6 +53,7 @@ describe("model/abstract", () => {
const values = { id: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66 }; const values = { id: 5, Name: "Christmas 2019", Slug: "christmas-2019", UID: 66 };
const album = new Album(values); const album = new Album(values);
assert.equal(album.Description, undefined); assert.equal(album.Description, undefined);
album.Name = "Christmas 2020";
await album.update(); await album.update();
assert.equal(album.Description, "Test description"); assert.equal(album.Description, "Test description");
}); });
@ -60,6 +61,7 @@ describe("model/abstract", () => {
it("should save album", async () => { it("should save album", async () => {
const values = { UID: "abc", Name: "Christmas 2019", Slug: "christmas-2019" }; const values = { UID: "abc", Name: "Christmas 2019", Slug: "christmas-2019" };
const album = new Album(values); const album = new Album(values);
album.Name = "Christmas 2020";
assert.equal(album.Description, undefined); assert.equal(album.Description, undefined);
await album.save(); await album.save();
assert.equal(album.Description, "Test description"); assert.equal(album.Description, "Test description");

4
go.mod
View file

@ -52,7 +52,7 @@ require (
github.com/shopspring/decimal v1.2.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8 github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df
github.com/tensorflow/tensorflow v1.15.2 github.com/tensorflow/tensorflow v1.15.2
github.com/tidwall/gjson v1.9.1 github.com/tidwall/gjson v1.9.1
github.com/ugorji/go v1.2.6 // indirect github.com/ugorji/go v1.2.6 // indirect
@ -61,7 +61,7 @@ require (
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 golang.org/x/crypto v0.0.0-20210915214749-c084706c2272
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 golang.org/x/net v0.0.0-20210917221730-978cfadd31cf
golang.org/x/sys v0.0.0-20210915083310-ed5796bab164 // indirect golang.org/x/sys v0.0.0-20210915083310-ed5796bab164 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
gonum.org/v1/gonum v0.9.3 gonum.org/v1/gonum v0.9.3

8
go.sum
View file

@ -284,8 +284,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8 h1:ipNUBPHSUmHhhcLhvqC2vGZsJPzVuJap8rJx3uGAEco= github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df h1:C+J/LwTqP8gRPt1MdSzBNZP0OYuDm5wsmDKgwpLjYzo=
github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s= github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
github.com/tensorflow/tensorflow v1.15.2 h1:7/f/A664Tml/nRJg04+p3StcrsT53mkcvmxYHXI21Qo= github.com/tensorflow/tensorflow v1.15.2 h1:7/f/A664Tml/nRJg04+p3StcrsT53mkcvmxYHXI21Qo=
github.com/tensorflow/tensorflow v1.15.2/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90= github.com/tensorflow/tensorflow v1.15.2/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90=
github.com/tidwall/gjson v1.9.1 h1:wrrRk7TyL7MmKanNRck/Mcr3VU1sdMvJHvJXzqBIUNo= github.com/tidwall/gjson v1.9.1 h1:wrrRk7TyL7MmKanNRck/Mcr3VU1sdMvJHvJXzqBIUNo=
@ -384,8 +384,8 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

View file

@ -27,7 +27,7 @@ func SearchPhotosGeo(router *gin.RouterGroup) {
return return
} }
var f form.GeoSearch var f form.PhotoSearchGeo
err := c.MustBindWith(&f, binding.Form) err := c.MustBindWith(&f, binding.Form)

View file

@ -99,6 +99,12 @@ func UpdateSubject(router *gin.RouterGroup) {
return return
} }
if txt.NameSlug(f.SubjName) == "" {
// Return unchanged model data if (normalized) name is empty.
c.JSON(http.StatusOK, m)
return
}
if _, err := m.UpdateName(f.SubjName); err != nil { if _, err := m.UpdateName(f.SubjName); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
return return

View file

@ -161,14 +161,14 @@ func (m *Label) AfterCreate(scope *gorm.Scope) error {
// SetName changes the label name. // SetName changes the label name.
func (m *Label) SetName(name string) { func (m *Label) SetName(name string) {
newName := txt.Clip(name, txt.ClipDefault) name = txt.NormalizeName(name)
if newName == "" { if name == "" {
return return
} }
m.LabelName = txt.Title(newName) m.LabelName = name
m.CustomSlug = slug.Make(txt.Clip(name, txt.ClipSlug)) m.CustomSlug = txt.NameSlug(name)
} }
// UpdateClassify updates a label if necessary // UpdateClassify updates a label if necessary

View file

@ -127,7 +127,7 @@ func (m *Marker) SaveForm(f form.Marker) (changed bool, err error) {
if f.SubjSrc == SrcManual && strings.TrimSpace(f.MarkerName) != "" && f.MarkerName != m.MarkerName { if f.SubjSrc == SrcManual && strings.TrimSpace(f.MarkerName) != "" && f.MarkerName != m.MarkerName {
m.SubjSrc = SrcManual m.SubjSrc = SrcManual
m.MarkerName = txt.Title(txt.Clip(f.MarkerName, txt.ClipDefault)) m.MarkerName = txt.NormalizeName(f.MarkerName)
if err := m.SyncSubject(true); err != nil { if err := m.SyncSubject(true); err != nil {
return changed, err return changed, err

View file

@ -202,15 +202,17 @@ func FindSubject(s string) *Subject {
} }
// FindSubjectByName find an existing subject by name. // FindSubjectByName find an existing subject by name.
func FindSubjectByName(s string) *Subject { func FindSubjectByName(name string) *Subject {
if s == "" { name = txt.NormalizeName(name)
if name == "" {
return nil return nil
} }
result := Subject{} result := Subject{}
// Search database. // Search database.
db := UnscopedDb().Where("subj_name LIKE ?", s).First(&result) db := UnscopedDb().Where("subj_name LIKE ?", name).First(&result)
if err := db.First(&result).Error; err != nil { if err := db.First(&result).Error; err != nil {
return nil return nil
@ -238,14 +240,14 @@ func (m *Subject) Person() *Person {
// SetName changes the subject's name. // SetName changes the subject's name.
func (m *Subject) SetName(name string) error { func (m *Subject) SetName(name string) error {
newName := txt.Clip(name, txt.ClipDefault) name = txt.NormalizeName(name)
if newName == "" { if name == "" {
return fmt.Errorf("subject: name must not be empty") return fmt.Errorf("subject: name must not be empty")
} }
m.SubjName = txt.Title(newName) m.SubjName = name
m.SubjSlug = slug.Make(txt.Clip(name, txt.ClipSlug)) m.SubjSlug = txt.NameSlug(name)
return nil return nil
} }

View file

@ -2,8 +2,8 @@ package form
import "time" import "time"
// GeoSearch represents search form fields for "/api/v1/geo". // PhotoSearchGeo represents search form fields for "/api/v1/geo".
type GeoSearch struct { type PhotoSearchGeo struct {
Query string `form:"q"` Query string `form:"q"`
Type string `form:"type"` Type string `form:"type"`
Path string `form:"path"` Path string `form:"path"`
@ -42,17 +42,17 @@ type GeoSearch struct {
} }
// GetQuery returns the query parameter as string. // GetQuery returns the query parameter as string.
func (f *GeoSearch) GetQuery() string { func (f *PhotoSearchGeo) GetQuery() string {
return f.Query return f.Query
} }
// SetQuery sets the query parameter. // SetQuery sets the query parameter.
func (f *GeoSearch) SetQuery(q string) { func (f *PhotoSearchGeo) SetQuery(q string) {
f.Query = q f.Query = q
} }
// ParseQueryString parses the query parameter if possible. // ParseQueryString parses the query parameter if possible.
func (f *GeoSearch) ParseQueryString() error { func (f *PhotoSearchGeo) ParseQueryString() error {
err := ParseQueryString(f) err := ParseQueryString(f)
if f.Path == "" && f.Folder != "" { if f.Path == "" && f.Folder != "" {
@ -67,15 +67,15 @@ func (f *GeoSearch) ParseQueryString() error {
} }
// Serialize returns a string containing non-empty fields and values of a struct. // Serialize returns a string containing non-empty fields and values of a struct.
func (f *GeoSearch) Serialize() string { func (f *PhotoSearchGeo) Serialize() string {
return Serialize(f, false) return Serialize(f, false)
} }
// SerializeAll returns a string containing all non-empty fields and values of a struct. // SerializeAll returns a string containing all non-empty fields and values of a struct.
func (f *GeoSearch) SerializeAll() string { func (f *PhotoSearchGeo) SerializeAll() string {
return Serialize(f, true) return Serialize(f, true)
} }
func NewGeoSearch(query string) GeoSearch { func NewGeoSearch(query string) PhotoSearchGeo {
return GeoSearch{Query: query} return PhotoSearchGeo{Query: query}
} }

View file

@ -9,7 +9,7 @@ import (
func TestGeoSearch(t *testing.T) { func TestGeoSearch(t *testing.T) {
t.Run("subjects", func(t *testing.T) { t.Run("subjects", func(t *testing.T) {
form := &GeoSearch{Query: "subjects:\"Jens Mander\""} form := &PhotoSearchGeo{Query: "subjects:\"Jens Mander\""}
err := form.ParseQueryString() err := form.ParseQueryString()
@ -20,7 +20,7 @@ func TestGeoSearch(t *testing.T) {
assert.Equal(t, "Jens Mander", form.Subjects) assert.Equal(t, "Jens Mander", form.Subjects)
}) })
t.Run("keywords", func(t *testing.T) { t.Run("keywords", func(t *testing.T) {
form := &GeoSearch{Query: "keywords:\"Foo Bar\""} form := &PhotoSearchGeo{Query: "keywords:\"Foo Bar\""}
err := form.ParseQueryString() err := form.ParseQueryString()
@ -31,7 +31,7 @@ func TestGeoSearch(t *testing.T) {
assert.Equal(t, "Foo Bar", form.Keywords) assert.Equal(t, "Foo Bar", form.Keywords)
}) })
t.Run("valid query", func(t *testing.T) { t.Run("valid query", func(t *testing.T) {
form := &GeoSearch{Query: "query:\"fooBar baz\" before:2019-01-15 dist:25000 lat:33.45343166666667"} form := &PhotoSearchGeo{Query: "query:\"fooBar baz\" before:2019-01-15 dist:25000 lat:33.45343166666667"}
err := form.ParseQueryString() err := form.ParseQueryString()
@ -47,7 +47,7 @@ func TestGeoSearch(t *testing.T) {
assert.Equal(t, float32(33.45343), form.Lat) assert.Equal(t, float32(33.45343), form.Lat)
}) })
t.Run("valid query path empty folder not empty", func(t *testing.T) { t.Run("valid query path empty folder not empty", func(t *testing.T) {
form := &GeoSearch{Query: "query:\"fooBar baz\" before:2019-01-15 dist:25000 lat:33.45343166666667 folder:test"} form := &PhotoSearchGeo{Query: "query:\"fooBar baz\" before:2019-01-15 dist:25000 lat:33.45343166666667 folder:test"}
err := form.ParseQueryString() err := form.ParseQueryString()
@ -67,18 +67,18 @@ func TestGeoSearch(t *testing.T) {
} }
func TestGeoSearch_Serialize(t *testing.T) { func TestGeoSearch_Serialize(t *testing.T) {
form := &GeoSearch{Query: "query:\"fooBar baz\"", Favorite: true} form := &PhotoSearchGeo{Query: "query:\"fooBar baz\"", Favorite: true}
assert.Equal(t, "q:\"query:fooBar baz\" favorite:true", form.Serialize()) assert.Equal(t, "q:\"query:fooBar baz\" favorite:true", form.Serialize())
} }
func TestGeoSearch_SerializeAll(t *testing.T) { func TestGeoSearch_SerializeAll(t *testing.T) {
form := &GeoSearch{Query: "query:\"fooBar baz\"", Favorite: true} form := &PhotoSearchGeo{Query: "query:\"fooBar baz\"", Favorite: true}
assert.Equal(t, "q:\"query:fooBar baz\" favorite:true", form.SerializeAll()) assert.Equal(t, "q:\"query:fooBar baz\" favorite:true", form.SerializeAll())
} }
func TestNewGeoSearch(t *testing.T) { func TestNewGeoSearch(t *testing.T) {
r := NewGeoSearch("Berlin") r := NewGeoSearch("Berlin")
assert.IsType(t, GeoSearch{}, r) assert.IsType(t, PhotoSearchGeo{}, r)
} }

View file

@ -16,7 +16,7 @@ import (
) )
// PhotosGeo searches for photos based on Form values and returns GeoResults ([]GeoResult). // PhotosGeo searches for photos based on Form values and returns GeoResults ([]GeoResult).
func PhotosGeo(f form.GeoSearch) (results GeoResults, err error) { func PhotosGeo(f form.PhotoSearchGeo) (results GeoResults, err error) {
start := time.Now() start := time.Now()
if err := f.ParseQueryString(); err != nil { if err := f.ParseQueryString(); err != nil {

View file

@ -66,7 +66,7 @@ func TestGeo(t *testing.T) {
}) })
t.Run("search for review true, quality 0", func(t *testing.T) { t.Run("search for review true, quality 0", func(t *testing.T) {
f := form.GeoSearch{ f := form.PhotoSearchGeo{
Query: "", Query: "",
Before: time.Time{}, Before: time.Time{},
After: time.Time{}, After: time.Time{},
@ -94,7 +94,7 @@ func TestGeo(t *testing.T) {
}) })
t.Run("search for review false, quality > 0", func(t *testing.T) { t.Run("search for review false, quality > 0", func(t *testing.T) {
f := form.GeoSearch{ f := form.PhotoSearchGeo{
Query: "", Query: "",
Before: time.Time{}, Before: time.Time{},
After: time.Time{}, After: time.Time{},
@ -117,7 +117,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result) assert.IsType(t, GeoResults{}, result)
}) })
t.Run("search for s2", func(t *testing.T) { t.Run("search for s2", func(t *testing.T) {
f := form.GeoSearch{ f := form.PhotoSearchGeo{
Query: "", Query: "",
Before: time.Time{}, Before: time.Time{},
After: time.Time{}, After: time.Time{},
@ -140,7 +140,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result) assert.IsType(t, GeoResults{}, result)
}) })
t.Run("search for Olc", func(t *testing.T) { t.Run("search for Olc", func(t *testing.T) {
f := form.GeoSearch{ f := form.PhotoSearchGeo{
Query: "", Query: "",
Before: time.Time{}, Before: time.Time{},
After: time.Time{}, After: time.Time{},
@ -162,7 +162,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result) assert.IsType(t, GeoResults{}, result)
}) })
t.Run("query for label flower", func(t *testing.T) { t.Run("query for label flower", func(t *testing.T) {
f := form.GeoSearch{ f := form.PhotoSearchGeo{
Query: "flower", Query: "flower",
} }
@ -174,7 +174,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result) assert.IsType(t, GeoResults{}, result)
}) })
t.Run("query for label landscape", func(t *testing.T) { t.Run("query for label landscape", func(t *testing.T) {
f := form.GeoSearch{ f := form.PhotoSearchGeo{
Query: "landscape", Query: "landscape",
Album: "test", Album: "test",
Camera: 123, Camera: 123,
@ -199,7 +199,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result) assert.IsType(t, GeoResults{}, result)
}) })
t.Run("search with multiple parameters", func(t *testing.T) { t.Run("search with multiple parameters", func(t *testing.T) {
f := form.GeoSearch{ f := form.PhotoSearchGeo{
Query: "landscape", Query: "landscape",
Photo: true, Photo: true,
Path: "/xxx,xxx", Path: "/xxx,xxx",
@ -217,7 +217,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result) assert.IsType(t, GeoResults{}, result)
}) })
t.Run("search for archived true", func(t *testing.T) { t.Run("search for archived true", func(t *testing.T) {
f := form.GeoSearch{ f := form.PhotoSearchGeo{
Query: "landscape", Query: "landscape",
Photo: true, Photo: true,
Path: "/xxx/xxx/", Path: "/xxx/xxx/",
@ -233,7 +233,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result) assert.IsType(t, GeoResults{}, result)
}) })
t.Run("faces:true", func(t *testing.T) { t.Run("faces:true", func(t *testing.T) {
var f form.GeoSearch var f form.PhotoSearchGeo
f.Query = "faces:true" f.Query = "faces:true"
photos, err := PhotosGeo(f) photos, err := PhotosGeo(f)
@ -245,7 +245,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 4) assert.GreaterOrEqual(t, len(photos), 4)
}) })
t.Run("faces:yes", func(t *testing.T) { t.Run("faces:yes", func(t *testing.T) {
var f form.GeoSearch var f form.PhotoSearchGeo
f.Faces = "Yes" f.Faces = "Yes"
photos, err := PhotosGeo(f) photos, err := PhotosGeo(f)
@ -257,7 +257,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 4) assert.GreaterOrEqual(t, len(photos), 4)
}) })
t.Run("faces:no", func(t *testing.T) { t.Run("faces:no", func(t *testing.T) {
var f form.GeoSearch var f form.PhotoSearchGeo
f.Faces = "No" f.Faces = "No"
photos, err := PhotosGeo(f) photos, err := PhotosGeo(f)
@ -269,7 +269,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 8) assert.GreaterOrEqual(t, len(photos), 8)
}) })
t.Run("faces:2", func(t *testing.T) { t.Run("faces:2", func(t *testing.T) {
var f form.GeoSearch var f form.PhotoSearchGeo
f.Faces = "2" f.Faces = "2"
photos, err := PhotosGeo(f) photos, err := PhotosGeo(f)
@ -281,7 +281,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 1) assert.GreaterOrEqual(t, len(photos), 1)
}) })
t.Run("day", func(t *testing.T) { t.Run("day", func(t *testing.T) {
var f form.GeoSearch var f form.PhotoSearchGeo
f.Day = 18 f.Day = 18
f.Month = 4 f.Month = 4
@ -294,7 +294,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 1) assert.GreaterOrEqual(t, len(photos), 1)
}) })
t.Run("subject uid in query", func(t *testing.T) { t.Run("subject uid in query", func(t *testing.T) {
var f form.GeoSearch var f form.PhotoSearchGeo
f.Query = "Actress" f.Query = "Actress"
photos, err := PhotosGeo(f) photos, err := PhotosGeo(f)
@ -306,7 +306,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 1) assert.GreaterOrEqual(t, len(photos), 1)
}) })
t.Run("albums", func(t *testing.T) { t.Run("albums", func(t *testing.T) {
var f form.GeoSearch var f form.PhotoSearchGeo
f.Albums = "2030" f.Albums = "2030"
photos, err := PhotosGeo(f) photos, err := PhotosGeo(f)
@ -318,7 +318,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 10) assert.GreaterOrEqual(t, len(photos), 10)
}) })
t.Run("path or path", func(t *testing.T) { t.Run("path or path", func(t *testing.T) {
var f form.GeoSearch var f form.PhotoSearchGeo
f.Path = "1990/04" + "|" + "2015/11" f.Path = "1990/04" + "|" + "2015/11"
photos, err := PhotosGeo(f) photos, err := PhotosGeo(f)
@ -330,7 +330,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 3) assert.GreaterOrEqual(t, len(photos), 3)
}) })
t.Run("name or name", func(t *testing.T) { t.Run("name or name", func(t *testing.T) {
var f form.GeoSearch var f form.PhotoSearchGeo
f.Name = "20151101_000000_51C501B5" + "|" + "Video" f.Name = "20151101_000000_51C501B5" + "|" + "Video"
photos, err := PhotosGeo(f) photos, err := PhotosGeo(f)

View file

@ -3,6 +3,8 @@ package txt
import ( import (
"fmt" "fmt"
"strings" "strings"
"github.com/gosimple/slug"
) )
// UniqueNames removes exact duplicates from a list of strings without changing their order. // UniqueNames removes exact duplicates from a list of strings without changing their order.
@ -92,3 +94,38 @@ func NameKeywords(names, aliases string) (results []string) {
return UniqueNames(append(Words(names), Words(aliases)...)) return UniqueNames(append(Words(names), Words(aliases)...))
} }
// NormalizeName sanitizes and capitalizes names.
func NormalizeName(name string) string {
if name == "" {
return ""
}
// Remove double quotes and other special characters.
name = strings.Map(func(r rune) rune {
switch r {
case '"', '`', '~', '\\', '/', '*', '%', '&', '|', '+', '=', '$', '@', '!', '?', ':', ';', '<', '>', '{', '}':
return -1
}
return r
}, name)
// Shorten.
name = Clip(name, ClipDefault)
if name == "" {
return ""
}
// Capitalize.
return Title(name)
}
// NameSlug converts a name to a valid slug.
func NameSlug(name string) string {
if name == "" {
return ""
}
return slug.Make(Clip(name, ClipSlug))
}

View file

@ -105,3 +105,42 @@ func TestNameKeywords(t *testing.T) {
assert.Equal(t, []string{"william", "henry", "gates", "iii", "windows", "guru"}, result) assert.Equal(t, []string{"william", "henry", "gates", "iii", "windows", "guru"}, result)
}) })
} }
func TestNormalizeName(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
assert.Equal(t, "", NormalizeName(""))
})
t.Run("BillGates", func(t *testing.T) {
assert.Equal(t, "William Henry Gates III", NormalizeName("William Henry Gates III"))
})
t.Run("Quotes", func(t *testing.T) {
assert.Equal(t, "William HenRy Gates'", NormalizeName("william \"HenRy\" gates' "))
})
t.Run("Slash", func(t *testing.T) {
assert.Equal(t, "William McCorn Gates'", NormalizeName("william\\ \"McCorn\" / gates' "))
})
t.Run("SpecialCharacters", func(t *testing.T) {
assert.Equal(t,
"'', '', '', '', '', '', '', '', '', '', '', '', Foo '', '', '', '', '', '', '', McBar '', ''",
NormalizeName("'\"', '`', '~', '\\\\', '/', '*', '%', '&', '|', '+', '=', '$', Foo '@', '!', '?', ':', ';', '<', '>', McBar '{', '}'"),
)
})
t.Run("Chinese", func(t *testing.T) {
assert.Equal(t, "陈 赵", NormalizeName(" 陈 赵"))
})
}
func TestNameSlug(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
assert.Equal(t, "", NameSlug(""))
})
t.Run("BillGates", func(t *testing.T) {
assert.Equal(t, "william-henry-gates-iii", NameSlug("William Henry Gates III"))
})
t.Run("Quotes", func(t *testing.T) {
assert.Equal(t, "william-henry-gates", NameSlug("william \"HenRy\" gates' "))
})
t.Run("Chinese", func(t *testing.T) {
assert.Equal(t, "chen-zhao", NameSlug(" 陈 赵"))
})
}