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

View file

@ -181,6 +181,20 @@
</v-list-tile-content>
</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-action :title="$gettext('Favorites')">
<v-icon>favorite</v-icon>
@ -209,20 +223,6 @@
</v-list-tile-content>
</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-action :title="$gettext('Calendar')">
<v-icon>date_range</v-icon>

View file

@ -52,9 +52,9 @@
</v-tab>
<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>
<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">
<template #badge>
<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 "
"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
msgid "Bio"
msgstr "Biographie"
@ -365,7 +358,7 @@ msgstr "Brasilianisches Portugiesisch"
msgid "Brown"
msgstr "Braun"
#: src/pages/settings/general.vue:361
#: src/pages/settings/general.vue:386
msgid "Browse and edit image classification labels."
msgstr "Automatische Bild-Kategorisierung sehen und bearbeiten."
@ -435,7 +428,7 @@ msgid "Change"
msgstr "Ändern"
#: 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."
#: src/component/photo/clipboard.vue:146
@ -860,8 +853,8 @@ msgstr "Jeden zweiten Tag"
#: src/pages/settings/general.vue:273
msgid ""
"Exclude content marked as private from search results, shared albums, labels "
"and places."
"Exclude content marked as private from search results, shared albums, "
"labels, and places."
msgstr ""
"Als privat markierte Inhalte werden nicht in Suchergebnissen und geteilten "
"Alben angezeigt."
@ -890,7 +883,7 @@ msgstr "Belichtungszeit"
msgid "F Number"
msgstr "F Nummer"
#: src/model/face.js:129
#: src/model/face.js:125
msgid "Face"
msgstr "Gesicht"
@ -922,7 +915,7 @@ msgstr "Schnell"
msgid "Favorite"
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
msgid "Favorites"
msgstr "Favoriten"
@ -1192,7 +1185,7 @@ msgstr "Name"
#: 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:216 src/pages/settings/general.vue:360
#: src/dialog/photo/edit.vue:216 src/pages/settings/general.vue:385
#: src/routes.js:259
msgid "Labels"
msgstr "Kategorien"
@ -1225,8 +1218,8 @@ msgstr "Lavendel"
msgid "Lens"
msgstr "Objektiv"
#: src/pages/settings/general.vue:339
msgid "Let PhotoPrism create albums from past events."
#: src/pages/settings/general.vue:364
msgid "Let PhotoPrism automatically create albums from past events."
msgstr ""
"PhotoPrism erstellt automatisch Alben mit besonderen Momenten, Reisen und "
"Orten."
@ -1237,7 +1230,7 @@ msgstr "Jetzt Unterstützer werden"
#: src/component/navigation.vue:301 src/component/navigation.vue:311
#: 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
msgid "Library"
msgstr "Dateien"
@ -1308,7 +1301,7 @@ msgstr "Anmelden"
msgid "Logout"
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"
msgstr "Logs"
@ -1352,8 +1345,8 @@ msgstr "Minimieren"
msgid "Missing"
msgstr "Fehlend"
#: src/component/navigation.vue:189 src/component/navigation.vue:741
#: src/pages/settings/general.vue:338 src/routes.js:121 src/routes.js:128
#: src/component/navigation.vue:202 src/component/navigation.vue:786
#: src/pages/settings/general.vue:363 src/routes.js:121 src/routes.js:128
msgid "Moments"
msgstr "Erlebnisse"
@ -1431,7 +1424,7 @@ msgstr "Name zu lang"
msgid "Never"
msgstr "Nie"
#: src/pages/people.vue:42
#: src/pages/people.vue:43
msgid "New"
msgstr "Neu"
@ -1650,9 +1643,9 @@ msgstr "Passwort geändert"
msgid "pay for operating expenses and external services like satellite maps"
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: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
msgid "People"
msgstr "Personen"
@ -1835,7 +1828,13 @@ msgstr "Zuletzt hinzugefügt"
msgid "Recently edited"
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"
msgstr "Erkannt"
@ -2024,7 +2023,7 @@ msgstr "Mit dir geteilt."
msgid "Show less"
msgstr "Weniger zeigen"
#: src/pages/settings/general.vue:383
#: src/pages/settings/general.vue:408
msgid "Show Library in navigation menu."
msgstr "Datei-Verwaltung in der Navigation anzeigen."
@ -2032,7 +2031,7 @@ msgstr "Datei-Verwaltung in der Navigation anzeigen."
msgid "Show more"
msgstr "Mehr zeigen"
#: src/pages/settings/general.vue:405
#: src/pages/settings/general.vue:430
msgid "Show server logs in Library."
msgstr "Server-Ereignisprotokoll anzeigen, um Fehler zu finden."
@ -2155,7 +2154,7 @@ msgstr "Straßen"
msgid "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"
msgstr "Bildinhalt"
@ -2506,8 +2505,19 @@ msgstr "Deine Nachricht wurde gesendet"
msgid "Zoom in/out"
msgstr "Zoom in/out"
#~ msgid "Detect faces and search for people in your pictures."
#~ msgstr "Findet Gesichter und aktiviert die Suche nach Personen."
#~ msgid ""
#~ "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"
#~ 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."
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
msgid "Bio"
msgstr ""
@ -374,7 +370,7 @@ msgstr ""
msgid "Brown"
msgstr ""
#: src/pages/settings/general.vue:361
#: src/pages/settings/general.vue:386
msgid "Browse and edit image classification labels."
msgstr ""
@ -467,7 +463,7 @@ msgid "Change"
msgstr ""
#: src/pages/settings/general.vue:185
msgid "Change photo titles, locations and other metadata."
msgid "Change photo titles, locations, and other metadata."
msgstr ""
#: src/component/photo/clipboard.vue:146
@ -930,7 +926,7 @@ msgid "Every two days"
msgstr ""
#: 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 ""
#: src/component/navigation.vue:248
@ -958,7 +954,7 @@ msgstr ""
msgid "F Number"
msgstr ""
#: src/model/face.js:129
#: src/model/face.js:125
msgid "Face"
msgstr ""
@ -991,8 +987,8 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: src/component/navigation.vue:176
#: src/component/navigation.vue:698
#: src/component/navigation.vue:189
#: src/component/navigation.vue:743
#: src/routes.js:180
msgid "Favorites"
msgstr ""
@ -1269,7 +1265,7 @@ msgstr ""
#: 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/pages/settings/general.vue:385
#: src/routes.js:259
msgid "Labels"
msgstr ""
@ -1303,8 +1299,8 @@ msgstr ""
msgid "Lens"
msgstr ""
#: src/pages/settings/general.vue:339
msgid "Let PhotoPrism create albums from past events."
#: src/pages/settings/general.vue:364
msgid "Let PhotoPrism automatically create albums from past events."
msgstr ""
#: src/dialog/sponsor.vue:7
@ -1316,7 +1312,7 @@ msgstr ""
#: src/component/navigation.vue:4
#: src/component/navigation.vue:1131
#: src/pages/settings.vue:41
#: src/pages/settings/general.vue:382
#: src/pages/settings/general.vue:407
#: src/routes.js:279
#: src/routes.js:286
#: src/routes.js:293
@ -1397,7 +1393,7 @@ msgid "Logout"
msgstr ""
#: src/pages/library.vue:55
#: src/pages/settings/general.vue:404
#: src/pages/settings/general.vue:429
msgid "Logs"
msgstr ""
@ -1444,9 +1440,9 @@ msgstr ""
msgid "Missing"
msgstr ""
#: src/component/navigation.vue:189
#: src/component/navigation.vue:741
#: src/pages/settings/general.vue:338
#: src/component/navigation.vue:202
#: src/component/navigation.vue:786
#: src/pages/settings/general.vue:363
#: src/routes.js:121
#: src/routes.js:128
msgid "Moments"
@ -1544,7 +1540,7 @@ msgstr ""
msgid "Never"
msgstr ""
#: src/pages/people.vue:42
#: src/pages/people.vue:43
msgid "New"
msgstr ""
@ -1760,12 +1756,12 @@ msgstr ""
msgid "pay for operating expenses and external services like satellite maps"
msgstr ""
#: 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:267
#: src/pages/settings/general.vue:428
#: src/pages/settings/general.vue:340
#: src/routes.js:265
#: src/routes.js:272
msgid "People"
@ -1962,7 +1958,11 @@ msgstr ""
msgid "Recently edited"
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"
msgstr ""
@ -2172,7 +2172,7 @@ msgstr ""
msgid "Show less"
msgstr ""
#: src/pages/settings/general.vue:383
#: src/pages/settings/general.vue:408
msgid "Show Library in navigation menu."
msgstr ""
@ -2180,7 +2180,7 @@ msgstr ""
msgid "Show more"
msgstr ""
#: src/pages/settings/general.vue:405
#: src/pages/settings/general.vue:430
msgid "Show server logs in Library."
msgstr ""
@ -2304,7 +2304,7 @@ msgid "Style"
msgstr ""
#: src/dialog/photo/details.vue:482
#: src/model/subject.js:137
#: src/model/subject.js:143
msgid "Subject"
msgstr ""

View file

@ -68,6 +68,15 @@ export class Rest extends Model {
}
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) =>
Promise.resolve(this.setValues(resp.data))
);

View file

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

View file

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

View file

@ -83,7 +83,7 @@
class="ma-0 pa-0 input-edit"
color="secondary-dark"
:label="$gettext('Edit')"
:hint="$gettext('Change photo titles, locations and other metadata.')"
:hint="$gettext('Change photo titles, locations, and other metadata.')"
prepend-icon="edit"
persistent-hint
@change="onChange"
@ -143,7 +143,7 @@
class="ma-0 pa-0 input-private"
color="secondary-dark"
: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"
persistent-hint
@change="onChange"
@ -181,6 +181,21 @@
</v-checkbox>
</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-checkbox
v-model="settings.features.moments"
@ -188,7 +203,7 @@
class="ma-0 pa-0 input-moments"
color="secondary-dark"
: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"
persistent-hint
@change="onChange"
@ -241,21 +256,6 @@
</v-checkbox>
</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-checkbox
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 album = new Album(values);
assert.equal(album.Description, undefined);
album.Name = "Christmas 2020";
await album.update();
assert.equal(album.Description, "Test description");
});
@ -60,6 +61,7 @@ describe("model/abstract", () => {
it("should save album", async () => {
const values = { UID: "abc", Name: "Christmas 2019", Slug: "christmas-2019" };
const album = new Album(values);
album.Name = "Christmas 2020";
assert.equal(album.Description, undefined);
await album.save();
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/sirupsen/logrus v1.8.1
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/tidwall/gjson v1.9.1
github.com/ugorji/go v1.2.6 // indirect
@ -61,7 +61,7 @@ require (
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272
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/text v0.3.7 // indirect
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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
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-20210630100626-7ff61aa87be8/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df h1:C+J/LwTqP8gRPt1MdSzBNZP0OYuDm5wsmDKgwpLjYzo=
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/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90=
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-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-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ=
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-20190226205417-e64efc72b421/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
}
var f form.GeoSearch
var f form.PhotoSearchGeo
err := c.MustBindWith(&f, binding.Form)

View file

@ -99,6 +99,12 @@ func UpdateSubject(router *gin.RouterGroup) {
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 {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
return

View file

@ -161,14 +161,14 @@ func (m *Label) AfterCreate(scope *gorm.Scope) error {
// SetName changes the label name.
func (m *Label) SetName(name string) {
newName := txt.Clip(name, txt.ClipDefault)
name = txt.NormalizeName(name)
if newName == "" {
if name == "" {
return
}
m.LabelName = txt.Title(newName)
m.CustomSlug = slug.Make(txt.Clip(name, txt.ClipSlug))
m.LabelName = name
m.CustomSlug = txt.NameSlug(name)
}
// 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 {
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 {
return changed, err

View file

@ -202,15 +202,17 @@ func FindSubject(s string) *Subject {
}
// FindSubjectByName find an existing subject by name.
func FindSubjectByName(s string) *Subject {
if s == "" {
func FindSubjectByName(name string) *Subject {
name = txt.NormalizeName(name)
if name == "" {
return nil
}
result := Subject{}
// 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 {
return nil
@ -238,14 +240,14 @@ func (m *Subject) Person() *Person {
// SetName changes the subject's name.
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")
}
m.SubjName = txt.Title(newName)
m.SubjSlug = slug.Make(txt.Clip(name, txt.ClipSlug))
m.SubjName = name
m.SubjSlug = txt.NameSlug(name)
return nil
}

View file

@ -2,8 +2,8 @@ package form
import "time"
// GeoSearch represents search form fields for "/api/v1/geo".
type GeoSearch struct {
// PhotoSearchGeo represents search form fields for "/api/v1/geo".
type PhotoSearchGeo struct {
Query string `form:"q"`
Type string `form:"type"`
Path string `form:"path"`
@ -42,17 +42,17 @@ type GeoSearch struct {
}
// GetQuery returns the query parameter as string.
func (f *GeoSearch) GetQuery() string {
func (f *PhotoSearchGeo) GetQuery() string {
return f.Query
}
// SetQuery sets the query parameter.
func (f *GeoSearch) SetQuery(q string) {
func (f *PhotoSearchGeo) SetQuery(q string) {
f.Query = q
}
// ParseQueryString parses the query parameter if possible.
func (f *GeoSearch) ParseQueryString() error {
func (f *PhotoSearchGeo) ParseQueryString() error {
err := ParseQueryString(f)
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.
func (f *GeoSearch) Serialize() string {
func (f *PhotoSearchGeo) Serialize() string {
return Serialize(f, false)
}
// 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)
}
func NewGeoSearch(query string) GeoSearch {
return GeoSearch{Query: query}
func NewGeoSearch(query string) PhotoSearchGeo {
return PhotoSearchGeo{Query: query}
}

View file

@ -9,7 +9,7 @@ import (
func TestGeoSearch(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()
@ -20,7 +20,7 @@ func TestGeoSearch(t *testing.T) {
assert.Equal(t, "Jens Mander", form.Subjects)
})
t.Run("keywords", func(t *testing.T) {
form := &GeoSearch{Query: "keywords:\"Foo Bar\""}
form := &PhotoSearchGeo{Query: "keywords:\"Foo Bar\""}
err := form.ParseQueryString()
@ -31,7 +31,7 @@ func TestGeoSearch(t *testing.T) {
assert.Equal(t, "Foo Bar", form.Keywords)
})
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()
@ -47,7 +47,7 @@ func TestGeoSearch(t *testing.T) {
assert.Equal(t, float32(33.45343), form.Lat)
})
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()
@ -67,18 +67,18 @@ func TestGeoSearch(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())
}
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())
}
func TestNewGeoSearch(t *testing.T) {
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).
func PhotosGeo(f form.GeoSearch) (results GeoResults, err error) {
func PhotosGeo(f form.PhotoSearchGeo) (results GeoResults, err error) {
start := time.Now()
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) {
f := form.GeoSearch{
f := form.PhotoSearchGeo{
Query: "",
Before: 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) {
f := form.GeoSearch{
f := form.PhotoSearchGeo{
Query: "",
Before: time.Time{},
After: time.Time{},
@ -117,7 +117,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result)
})
t.Run("search for s2", func(t *testing.T) {
f := form.GeoSearch{
f := form.PhotoSearchGeo{
Query: "",
Before: time.Time{},
After: time.Time{},
@ -140,7 +140,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result)
})
t.Run("search for Olc", func(t *testing.T) {
f := form.GeoSearch{
f := form.PhotoSearchGeo{
Query: "",
Before: time.Time{},
After: time.Time{},
@ -162,7 +162,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result)
})
t.Run("query for label flower", func(t *testing.T) {
f := form.GeoSearch{
f := form.PhotoSearchGeo{
Query: "flower",
}
@ -174,7 +174,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result)
})
t.Run("query for label landscape", func(t *testing.T) {
f := form.GeoSearch{
f := form.PhotoSearchGeo{
Query: "landscape",
Album: "test",
Camera: 123,
@ -199,7 +199,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result)
})
t.Run("search with multiple parameters", func(t *testing.T) {
f := form.GeoSearch{
f := form.PhotoSearchGeo{
Query: "landscape",
Photo: true,
Path: "/xxx,xxx",
@ -217,7 +217,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result)
})
t.Run("search for archived true", func(t *testing.T) {
f := form.GeoSearch{
f := form.PhotoSearchGeo{
Query: "landscape",
Photo: true,
Path: "/xxx/xxx/",
@ -233,7 +233,7 @@ func TestGeo(t *testing.T) {
assert.IsType(t, GeoResults{}, result)
})
t.Run("faces:true", func(t *testing.T) {
var f form.GeoSearch
var f form.PhotoSearchGeo
f.Query = "faces:true"
photos, err := PhotosGeo(f)
@ -245,7 +245,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 4)
})
t.Run("faces:yes", func(t *testing.T) {
var f form.GeoSearch
var f form.PhotoSearchGeo
f.Faces = "Yes"
photos, err := PhotosGeo(f)
@ -257,7 +257,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 4)
})
t.Run("faces:no", func(t *testing.T) {
var f form.GeoSearch
var f form.PhotoSearchGeo
f.Faces = "No"
photos, err := PhotosGeo(f)
@ -269,7 +269,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 8)
})
t.Run("faces:2", func(t *testing.T) {
var f form.GeoSearch
var f form.PhotoSearchGeo
f.Faces = "2"
photos, err := PhotosGeo(f)
@ -281,7 +281,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 1)
})
t.Run("day", func(t *testing.T) {
var f form.GeoSearch
var f form.PhotoSearchGeo
f.Day = 18
f.Month = 4
@ -294,7 +294,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 1)
})
t.Run("subject uid in query", func(t *testing.T) {
var f form.GeoSearch
var f form.PhotoSearchGeo
f.Query = "Actress"
photos, err := PhotosGeo(f)
@ -306,7 +306,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 1)
})
t.Run("albums", func(t *testing.T) {
var f form.GeoSearch
var f form.PhotoSearchGeo
f.Albums = "2030"
photos, err := PhotosGeo(f)
@ -318,7 +318,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 10)
})
t.Run("path or path", func(t *testing.T) {
var f form.GeoSearch
var f form.PhotoSearchGeo
f.Path = "1990/04" + "|" + "2015/11"
photos, err := PhotosGeo(f)
@ -330,7 +330,7 @@ func TestGeo(t *testing.T) {
assert.GreaterOrEqual(t, len(photos), 3)
})
t.Run("name or name", func(t *testing.T) {
var f form.GeoSearch
var f form.PhotoSearchGeo
f.Name = "20151101_000000_51C501B5" + "|" + "Video"
photos, err := PhotosGeo(f)

View file

@ -3,6 +3,8 @@ package txt
import (
"fmt"
"strings"
"github.com/gosimple/slug"
)
// 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)...))
}
// 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)
})
}
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(" 陈 赵"))
})
}