branch bow-v2-go updated (73e6fc1 -> 24cee2c)
This is an automated email from the git hooks/post-receive script. New change to branch bow-v2-go in repository bow. See https://gitlab.nuiton.org/chorem/bow.git from 73e6fc1 ajout timing dans les logs modif back pour le renommage de tag new 94d5119 ajout de statistique sur les appels new 8fefd8e ajout de la notion d'admin qui est le seul a pouvoir demander les stats new 24cee2c ajout de l'edition des tags The 3 revisions listed above as "new" are entirely new to this repository and will be described in separate emails. The revisions listed as "adds" were already present in the repository and have only been added to this reference. Detailed log of new commits: commit 24cee2c478783491c6cdb2c176649b04c8615e27 Author: Benjamin <poussin@codelutin.com> Date: Mon May 18 02:13:29 2020 +0200 ajout de l'edition des tags commit 8fefd8e248efb10e053426435678c1ae12329150 Author: Benjamin <poussin@codelutin.com> Date: Mon May 18 00:05:33 2020 +0200 ajout de la notion d'admin qui est le seul a pouvoir demander les stats commit 94d51198a9fb5813df8c9f705f0d7779b7809afe Author: Benjamin <poussin@codelutin.com> Date: Sun May 17 23:15:52 2020 +0200 ajout de statistique sur les appels Summary of changes: migrate/001_init_schema.sql | 3 +- pkg/constant/const.go | 5 + pkg/http/router.go | 65 ++++++++---- pkg/http/systemResource.go | 12 +++ pkg/http/userResource.go | 2 +- pkg/model/user.go | 15 +-- pkg/repository/bookmarkRepository.go | 18 +++- pkg/repository/userRepository.go | 8 +- pkg/utils/stats.go | 87 +++++++++++++++ pkg/utils/utils.go | 2 +- web/package.json | 1 + web/src/components/preferences/TagsEditor.vue | 146 ++++++++++++++++++++++++++ web/src/views/Preferences.vue | 4 +- web/yarn.lock | 9 +- 14 files changed, 341 insertions(+), 36 deletions(-) create mode 100644 pkg/utils/stats.go create mode 100644 web/src/components/preferences/TagsEditor.vue -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.
This is an automated email from the git hooks/post-receive script. New commit to branch bow-v2-go in repository bow. See https://gitlab.nuiton.org/chorem/bow.git commit 94d51198a9fb5813df8c9f705f0d7779b7809afe Author: Benjamin <poussin@codelutin.com> Date: Sun May 17 23:15:52 2020 +0200 ajout de statistique sur les appels --- pkg/http/router.go | 53 +++++++++++++++++++--------- pkg/http/systemResource.go | 12 +++++++ pkg/utils/stats.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 17 deletions(-) diff --git a/pkg/http/router.go b/pkg/http/router.go index 706618a..c7c7c81 100644 --- a/pkg/http/router.go +++ b/pkg/http/router.go @@ -2,6 +2,7 @@ package http import ( "context" + "fmt" "io" "log" "net/http" @@ -20,6 +21,8 @@ import ( var BowPublicURL string +var stats = utils.NewStats() + /* Start web server */ @@ -36,6 +39,7 @@ func Start(bowPublicURL string, addr string) { s := router.PathPrefix("/api/v1").Subrouter() s.HandleFunc("/system/liveness", isAlive).Methods(http.MethodGet, http.MethodOptions) + s.HandleFunc("/system/stats", getStats).Methods(http.MethodGet, http.MethodOptions) s.HandleFunc("/users", createUser).Methods(http.MethodPost, http.MethodOptions) s.HandleFunc("/users/auth", createAuth).Methods(http.MethodPost, http.MethodOptions) @@ -55,7 +59,7 @@ func Start(bowPublicURL string, addr string) { u.HandleFunc("/autofavicon", updateUserAutoFavicon).Methods(http.MethodPut, http.MethodOptions) u.HandleFunc("/maxtagincloud", updateUserMaxTagInCloud).Methods(http.MethodPut, http.MethodOptions) u.HandleFunc("/maxresult", updateUserMaxResult).Methods(http.MethodPut, http.MethodOptions) - + s.HandleFunc("/bookmarks", getBookmarks).Methods(http.MethodGet, http.MethodOptions) s.HandleFunc("/bookmarks", addBookmark).Methods(http.MethodPost, http.MethodOptions) s.HandleFunc("/bookmarks/tags", getTags).Methods(http.MethodGet, http.MethodOptions) @@ -65,7 +69,7 @@ func Start(bowPublicURL string, addr string) { s.HandleFunc("/bookmarks/{id}", updateBookmark).Methods(http.MethodPut, http.MethodOptions) s.HandleFunc("/bookmarks/{id}/visit", addOneVisit).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) s.HandleFunc("/bookmarks/{id}/authenticationinfo", updateBookmarkAuthenticationInfo).Methods(http.MethodPut, http.MethodOptions) - + s.HandleFunc("/opensearch", doActions).Methods(http.MethodGet, http.MethodPost, http.MethodOptions).Queries(constant.Action, "{ask}") s.HandleFunc("/opensearch", doSuggestion).Methods(http.MethodGet, http.MethodPost, http.MethodOptions).Queries(constant.Suggestion, "{ask}") s.HandleFunc("/opensearch", opensearchFile).Methods(http.MethodGet, http.MethodOptions) @@ -93,7 +97,7 @@ func logAll(next http.Handler) http.Handler { query := r.URL.Query() debug := query.Get("bow_debug") - if (os.Getenv("BOW_DEBUG") == "true" || debug == "true") { + if os.Getenv("BOW_DEBUG") == "true" || debug == "true" { log.Println("logAll", r) } next.ServeHTTP(w, r) @@ -120,8 +124,9 @@ func cors(next http.Handler) http.Handler { func withoutAuthenticationEndpoint(r *http.Request) bool { result := !strings.HasPrefix(r.URL.Path, "/api") || // no auth for SPA (html, css, ...) - strings.HasSuffix(r.URL.Path, "/auth") || // no auth to create/delete auth + strings.HasSuffix(r.URL.Path, "/auth") || // no auth to create/delete auth strings.HasSuffix(r.URL.Path, "/system/liveness") || // no auth to test if server is up + strings.HasSuffix(r.URL.Path, "/system/stats") || // no auth to test if server is up strings.HasSuffix(r.URL.Path, "/users") // no auth for creation of user return result } @@ -145,7 +150,8 @@ func authentication(next http.Handler) http.Handler { return } - authTime := time.Now() + authStat := stats.StartStat("authentication") + canBeAppToken := false // 1 as query param @@ -211,15 +217,28 @@ func authentication(next http.Handler) http.Handler { ctx := context.WithValue(r.Context(), constant.User, user) r = r.WithContext(ctx) - utils.Duration("authentication", authTime) + authStat.Stop() - restTime := time.Now() + routeName := getRouteName(r) + restStat := stats.StartStat(fmt.Sprintf("rest(%v)", routeName)) next.ServeHTTP(w, r) - utils.Duration("rest", restTime) + restStat.Stop() }) } +func getRouteName(r *http.Request) string { + currentRoute := mux.CurrentRoute(r) + result := currentRoute.GetName() + if result == "" { + result, _ = currentRoute.GetPathTemplate() + if result == "" { + result, _ = currentRoute.GetPathRegexp() + } + } + return result +} + // spaHandler implements the http.Handler interface, so we can use it // to respond to HTTP requests. The path to the static directory and // path to the index file within that static directory are used to @@ -235,31 +254,31 @@ type spaHandler struct { // is suitable behavior for serving an SPA (single page application). func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Println("serve web content", r.URL.Path) - // get the absolute path to prevent directory traversal + // get the absolute path to prevent directory traversal path, err := filepath.Abs(r.URL.Path) if err != nil { - // if we failed to get the absolute path respond with a 400 bad request - // and stop + // if we failed to get the absolute path respond with a 400 bad request + // and stop http.Error(w, err.Error(), http.StatusBadRequest) return } - // prepend the path with the path to the static directory + // prepend the path with the path to the static directory path = filepath.Join(h.staticPath, path) - // check whether a file exists at the given path + // check whether a file exists at the given path _, err = os.Stat(path) if os.IsNotExist(err) { // file does not exist, serve index.html http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) return } else if err != nil { - // if we got an error (that wasn't that the file doesn't exist) stating the - // file, return a 500 internal server error and stop + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop http.Error(w, err.Error(), http.StatusInternalServerError) return } - // otherwise, use http.FileServer to serve the static dir + // otherwise, use http.FileServer to serve the static dir http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) -} \ No newline at end of file +} diff --git a/pkg/http/systemResource.go b/pkg/http/systemResource.go index 482e97f..2185d85 100644 --- a/pkg/http/systemResource.go +++ b/pkg/http/systemResource.go @@ -2,11 +2,23 @@ package http import ( "encoding/json" + // "io" "log" "net/http" ) func isAlive(w http.ResponseWriter, r *http.Request) { log.Println("http liveness") + + w.Header().Add("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"alive": true}) } + +func getStats(w http.ResponseWriter, r *http.Request) { + log.Println("http stats", stats) + + w.Header().Add("Content-Type", "application/json") + // io.WriteString(w, stats.String()) + // je voulais faire un export json, mais ça reste vide, donc comme le toSting est un json :p + json.NewEncoder(w).Encode(stats) +} diff --git a/pkg/utils/stats.go b/pkg/utils/stats.go new file mode 100644 index 0000000..0d282d5 --- /dev/null +++ b/pkg/utils/stats.go @@ -0,0 +1,88 @@ +package utils + +import ( + "fmt" + "math" + "strings" + "sync" + "time" +) + +type Stat struct { + Nb int64 `json:"call"` + Min time.Duration `json:"min"` + Max time.Duration `json:"max"` + Avg time.Duration `json:"avg"` + StdDeviation time.Duration `json:"stddeviation"` + + variance float64 + + // pour l'ecart type (standard deviation) + delta int64 + M2 int64 + + // pour que les requetes concurrente de corrompe pas les donnees + sync.Mutex +} + +type Stats struct { + Values map[string]*Stat `json:"stats"` + + // pour que les requetes concurrente de corrompe pas les donnees + sync.Mutex +} + +type OneCall struct { + name string + start time.Time + all *Stats +} + +func NewStats() *Stats { + return &Stats{Values: make(map[string]*Stat)} +} + +func (all *Stats) String() string { + var result []string + for name, s := range all.Values { + result = append(result, fmt.Sprintf(`%v: {"call": %v, "min": %q, "max": %q, "avg": %q, "stdderivation": %v}`, name, s.Nb, s.Min, s.Max, s.Avg, s.StdDeviation)) + } + return fmt.Sprintf("{%v}", strings.Join(result, ",")) +} + +func (s *Stats) StartStat(name string) *OneCall { + return &OneCall{name: name, start: time.Now(), all: s} +} + +func (call *OneCall) Stop() { + duration := time.Since(call.start) + s := call.all.Values[call.name] + if s == nil { + call.all.Lock() + s = call.all.Values[call.name] + if s == nil { + s = &Stat{} + call.all.Values[call.name] = s + } + call.all.Unlock() + } + + s.Lock() + defer s.Unlock() + + s.Nb++ + if s.Min == 0 || s.Min > duration { + s.Min = duration + } + + if s.Max < duration { + s.Max = duration + } + + s.delta = duration.Nanoseconds() - s.Avg.Nanoseconds() + s.Avg = time.Duration(s.Avg.Nanoseconds() + s.delta/s.Nb) + s.M2 = s.M2 + s.delta*(duration.Nanoseconds()-s.Avg.Nanoseconds()) + + variance := float64(s.M2) / math.Max(1, float64(s.Nb-1)) + s.StdDeviation = time.Duration(int64(math.Sqrt(variance))) +} -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.
This is an automated email from the git hooks/post-receive script. New commit to branch bow-v2-go in repository bow. See https://gitlab.nuiton.org/chorem/bow.git commit 8fefd8e248efb10e053426435678c1ae12329150 Author: Benjamin <poussin@codelutin.com> Date: Mon May 18 00:05:33 2020 +0200 ajout de la notion d'admin qui est le seul a pouvoir demander les stats --- migrate/001_init_schema.sql | 3 ++- pkg/constant/const.go | 5 +++++ pkg/http/router.go | 17 +++++++++++++---- pkg/http/userResource.go | 2 +- pkg/model/user.go | 15 ++++++++------- pkg/repository/userRepository.go | 8 ++++---- pkg/utils/stats.go | 27 +++++++++++++-------------- pkg/utils/utils.go | 2 +- 8 files changed, 47 insertions(+), 32 deletions(-) diff --git a/migrate/001_init_schema.sql b/migrate/001_init_schema.sql index bdec668..81b09c7 100644 --- a/migrate/001_init_schema.sql +++ b/migrate/001_init_schema.sql @@ -41,6 +41,7 @@ CREATE TABLE bowUser ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), creationDate TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp, updateDate TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp, + admin BOOLEAN DEFAULT false, login TEXT, password TEXT, tokens jsonb, -- [{name: sring, token: string, expire: date}] @@ -166,7 +167,7 @@ CREATE USER nobody WITH NOINHERIT CREATEROLE LOGIN PASSWORD '{{.nobody_password} CREATE ROLE person; -- l'utilisateur nobody a le droit d'inserer des users (creation de compte) et c'est lui qui visite les pages -GRANT INSERT ON bowUser TO nobody; +GRANT SELECT, INSERT ON bowUser TO nobody; GRANT INSERT ON pageHistory TO nobody; -- l'utilisateur nobody a le droit de lire les alias public et l'uri pour permettre la recheche et donc l'utilisation des alias public GRANT SELECT(uri, publicAlias) ON bookmark TO nobody; diff --git a/pkg/constant/const.go b/pkg/constant/const.go index b55f6ba..2f870f9 100644 --- a/pkg/constant/const.go +++ b/pkg/constant/const.go @@ -21,6 +21,11 @@ Token le nom utiliser pour le token dans les cookies et les parameters de query */ const Token = "bow-token" +/* +AuthFields la liste des champs necessaire pour le token d'authentification +*/ +var AuthFields = []string{"id", "admin", "login", "creationdate", "maxtagincloud", "maxresult"} + /* TokenHeader le nom utiliser pour mettre dans le header de requete http (fallback Authorization) */ diff --git a/pkg/http/router.go b/pkg/http/router.go index c7c7c81..33d0fea 100644 --- a/pkg/http/router.go +++ b/pkg/http/router.go @@ -126,11 +126,15 @@ func withoutAuthenticationEndpoint(r *http.Request) bool { result := !strings.HasPrefix(r.URL.Path, "/api") || // no auth for SPA (html, css, ...) strings.HasSuffix(r.URL.Path, "/auth") || // no auth to create/delete auth strings.HasSuffix(r.URL.Path, "/system/liveness") || // no auth to test if server is up - strings.HasSuffix(r.URL.Path, "/system/stats") || // no auth to test if server is up strings.HasSuffix(r.URL.Path, "/users") // no auth for creation of user return result } +func needAdminEndpoint(r *http.Request) bool { + result := strings.HasSuffix(r.URL.Path, "/system/stats") + return result +} + func convertCurrentToId(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] @@ -150,7 +154,7 @@ func authentication(next http.Handler) http.Handler { return } - authStat := stats.StartStat("authentication") + authStat := stats.StartStat("authentication", r.URL.String()) canBeAppToken := false @@ -196,7 +200,7 @@ func authentication(next http.Handler) http.Handler { user, err = utils.JwtVerify(token) if err != nil && canBeAppToken { // try as application token - user, err = repository.UserFromToken(token, "id", "login", "creationdate", "maxtagincloud", "maxresult", "actions") + user, err = repository.UserFromToken(token, constant.AuthFields...) if err != nil { utils.Throw(w, utils.NewHTTPError500(err, constant.Nobody)) return @@ -219,8 +223,13 @@ func authentication(next http.Handler) http.Handler { r = r.WithContext(ctx) authStat.Stop() + if needAdminEndpoint(r) && !user.Admin { + utils.Throw(w, utils.NewHTTPError("Need admin user", user, 403)) + return + } + routeName := getRouteName(r) - restStat := stats.StartStat(fmt.Sprintf("rest(%v)", routeName)) + restStat := stats.StartStat(fmt.Sprintf("rest(%v)", routeName), r.URL.String()) next.ServeHTTP(w, r) restStat.Stop() }) diff --git a/pkg/http/userResource.go b/pkg/http/userResource.go index 9c5b75a..ee5b317 100644 --- a/pkg/http/userResource.go +++ b/pkg/http/userResource.go @@ -42,7 +42,7 @@ func createAuth(w http.ResponseWriter, r *http.Request) { pseudoUser := model.BowUser{ID: id} // on ne met plus les actions dans le cookie car il fini par etre trop gros et plus accepte par les navigateur (>4ko) - userJSON, err := repository.UserJSON(pseudoUser, id, "id", "login", "creationdate", "maxtagincloud", "maxresult") + userJSON, err := repository.UserJSON(pseudoUser, id, constant.AuthFields...) if err != nil { utils.Throw(w, utils.NewHTTPError500(err, pseudoUser)) return diff --git a/pkg/model/user.go b/pkg/model/user.go index 235fda6..41e913b 100644 --- a/pkg/model/user.go +++ b/pkg/model/user.go @@ -14,12 +14,13 @@ type BowUser struct { ID string `json:"id,omitempty"` CreationDate time.Time `json:"creationdate,omitempty"` UpdateDate time.Time `json:"updatedate,omitempty"` + Admin bool `json:"admin,omitempty"` Login string `json:"login,omitempty"` Password string `json:"password,omitempty"` Tokens []Token `json:"tokens,omitempty"` Emails []string `json:"emails,omitempty"` UnconfirmedEmails []UnconfirmedEmails `json:"unconfirmedemails,omitempty"` - AuthenticationInfo AuthenticationInfo `json:"authenticationinfo,omitempty"` + AuthenticationInfo *AuthenticationInfo `json:"authenticationinfo,omitempty"` AutoScreenshot bool `json:"autoscreenshot"` AutoFavicon bool `json:"autofavicon"` MaxTagInCloud int16 `json:"maxtagincloud,omitempty"` @@ -28,15 +29,15 @@ type BowUser struct { } type Token struct { - Name string - Token string - Expiration time.Time + Name string `json:"name,omitempty"` + Token string `json:"token,omitempty"` + Expiration *time.Time `json:"expiration,omitempty"` } type UnconfirmedEmails struct { - Email string - Token string - CreationDate time.Time + Email string `json:"email,omitempty"` + Token string `json:"token,omitempty"` + CreationDate time.Time `json:"creationdate,omitempty"` } /* diff --git a/pkg/repository/userRepository.go b/pkg/repository/userRepository.go index 24d4acb..cf5a448 100644 --- a/pkg/repository/userRepository.go +++ b/pkg/repository/userRepository.go @@ -23,7 +23,7 @@ all field are send except: func UserJSON(currentUser model.BowUser, id string, fields ...string) (string, error) { var askedFields string if len(fields) == 0 { - askedFields = "id, creationdate, updatedate, login, tokens, emails, unconfirmedemails, authenticationinfo, autoscreenshot, autofavicon, maxtagincloud, maxresult, actions" + askedFields = "id, creationdate, updatedate, admin, login, tokens, emails, unconfirmedemails, authenticationinfo, autoscreenshot, autofavicon, maxtagincloud, maxresult, actions" } else { askedFields = strings.Join(fields, ", ") } @@ -77,7 +77,7 @@ func UserFromToken(token string, fields ...string) (model.BowUser, error) { var allFields string if len(fields) == 0 { - allFields = "id, creationdate, updatedate, login, tokens, emails, authenticationinfo, autoscreenshot, autofavicon, maxtagincloud, maxresult, actions" + allFields = "id, creationdate, updatedate, admin, login, tokens, emails, authenticationinfo, autoscreenshot, autofavicon, maxtagincloud, maxresult, actions" } else { allFields = strings.Join(fields, ",") } @@ -157,7 +157,7 @@ func CreateUser(login string, password string) (string, error) { } currentUser := model.BowUser{ - ID: uuid, Login: login, Password: hashPassword, MaxTagInCloud: 20, MaxResult: 20, AutoFavicon: false, AutoScreenshot: false, AuthenticationInfo: model.AuthenticationInfo{DomainComponent: 2, MaxLength: 15}} + ID: uuid, Login: login, Password: hashPassword, MaxTagInCloud: 20, MaxResult: 20, AutoFavicon: false, AutoScreenshot: false, AuthenticationInfo: &model.AuthenticationInfo{DomainComponent: 2, MaxLength: 15}} userAsJSON, err := json.Marshal(currentUser) if err != nil { return "", utils.NewHTTPError500(err, currentUser) @@ -203,7 +203,7 @@ func UpdateUserPassword(currentUser model.BowUser, id string, password string, o /* AddUserToken ajout un tocken d'authentification pour l'utilisateur */ -func AddUserToken(currentUser model.BowUser, id string, name string, expiration time.Time) (string, error) { +func AddUserToken(currentUser model.BowUser, id string, name string, expiration *time.Time) (string, error) { token, err := utils.GenUUID() if err != nil { return "", err diff --git a/pkg/utils/stats.go b/pkg/utils/stats.go index 0d282d5..0d37308 100644 --- a/pkg/utils/stats.go +++ b/pkg/utils/stats.go @@ -1,9 +1,8 @@ package utils import ( - "fmt" + "encoding/json" "math" - "strings" "sync" "time" ) @@ -11,15 +10,15 @@ import ( type Stat struct { Nb int64 `json:"call"` Min time.Duration `json:"min"` + MinInfo string Max time.Duration `json:"max"` + MaxInfo string Avg time.Duration `json:"avg"` StdDeviation time.Duration `json:"stddeviation"` - variance float64 - // pour l'ecart type (standard deviation) delta int64 - M2 int64 + m2 int64 // pour que les requetes concurrente de corrompe pas les donnees sync.Mutex @@ -34,6 +33,7 @@ type Stats struct { type OneCall struct { name string + info string start time.Time all *Stats } @@ -43,15 +43,12 @@ func NewStats() *Stats { } func (all *Stats) String() string { - var result []string - for name, s := range all.Values { - result = append(result, fmt.Sprintf(`%v: {"call": %v, "min": %q, "max": %q, "avg": %q, "stdderivation": %v}`, name, s.Nb, s.Min, s.Max, s.Avg, s.StdDeviation)) - } - return fmt.Sprintf("{%v}", strings.Join(result, ",")) + result, _ := json.Marshal(all) + return string(result) } -func (s *Stats) StartStat(name string) *OneCall { - return &OneCall{name: name, start: time.Now(), all: s} +func (s *Stats) StartStat(name string, info string) *OneCall { + return &OneCall{name: name, info: info, start: time.Now(), all: s} } func (call *OneCall) Stop() { @@ -73,16 +70,18 @@ func (call *OneCall) Stop() { s.Nb++ if s.Min == 0 || s.Min > duration { s.Min = duration + s.MinInfo = call.info } if s.Max < duration { s.Max = duration + s.MaxInfo = call.info } s.delta = duration.Nanoseconds() - s.Avg.Nanoseconds() s.Avg = time.Duration(s.Avg.Nanoseconds() + s.delta/s.Nb) - s.M2 = s.M2 + s.delta*(duration.Nanoseconds()-s.Avg.Nanoseconds()) + s.m2 = s.m2 + s.delta*(duration.Nanoseconds()-s.Avg.Nanoseconds()) - variance := float64(s.M2) / math.Max(1, float64(s.Nb-1)) + variance := float64(s.m2) / math.Max(1, float64(s.Nb-1)) s.StdDeviation = time.Duration(int64(math.Sqrt(variance))) } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 4cffc0d..19338ef 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -12,7 +12,7 @@ RemoveTagReturn remove all \n and \t in the string func RemoveTagReturn(s string) string { return strings.Map(func(c rune) rune { if c == '\n' || c == '\t' { - return -1 + return ' ' } return c }, s) -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.
This is an automated email from the git hooks/post-receive script. New commit to branch bow-v2-go in repository bow. See https://gitlab.nuiton.org/chorem/bow.git commit 24cee2c478783491c6cdb2c176649b04c8615e27 Author: Benjamin <poussin@codelutin.com> Date: Mon May 18 02:13:29 2020 +0200 ajout de l'edition des tags --- pkg/http/router.go | 1 + pkg/repository/bookmarkRepository.go | 18 +++- web/package.json | 1 + web/src/components/preferences/TagsEditor.vue | 146 ++++++++++++++++++++++++++ web/src/views/Preferences.vue | 4 +- web/yarn.lock | 9 +- 6 files changed, 175 insertions(+), 4 deletions(-) diff --git a/pkg/http/router.go b/pkg/http/router.go index 33d0fea..a97d2e1 100644 --- a/pkg/http/router.go +++ b/pkg/http/router.go @@ -64,6 +64,7 @@ func Start(bowPublicURL string, addr string) { s.HandleFunc("/bookmarks", addBookmark).Methods(http.MethodPost, http.MethodOptions) s.HandleFunc("/bookmarks/tags", getTags).Methods(http.MethodGet, http.MethodOptions) s.HandleFunc("/bookmarks/tags/{oldName}/{newName}", renameTag).Methods(http.MethodPut, http.MethodOptions) + s.HandleFunc("/bookmarks/tags/{oldName}", renameTag).Methods(http.MethodDelete, http.MethodOptions) s.HandleFunc("/bookmarks/{id}", getBookmark).Methods(http.MethodGet, http.MethodOptions) s.HandleFunc("/bookmarks/{id}", deleteBookmark).Methods(http.MethodDelete, http.MethodOptions) s.HandleFunc("/bookmarks/{id}", updateBookmark).Methods(http.MethodPut, http.MethodOptions) diff --git a/pkg/repository/bookmarkRepository.go b/pkg/repository/bookmarkRepository.go index 750a741..64eac14 100644 --- a/pkg/repository/bookmarkRepository.go +++ b/pkg/repository/bookmarkRepository.go @@ -58,10 +58,22 @@ func BookmarkJSON(currentUser model.BowUser, id string, uri string, tags string, return result, nil } +/* +RenameTag renomme ou supprime un tag si newName est vide +*/ func RenameTag(currentUser model.BowUser, oldName string, newName string) (int64, error) { tagArray := "{" + oldName + "}" - q := &query{sql: `UPDATE bookmark SET tags=array_replace(tags, '$2, $3) where tags @> $1::text[]`} - count, err := q.execOnNRow(currentUser, tagArray, oldName, newName) + + var q *query + var count int64 + var err error + if newName == "" { + q = &query{sql: `UPDATE bookmark SET tags=array_remove(tags, $2) where tags @> $1::text[]`} + count, err = q.execOnNRow(currentUser, tagArray, oldName) + } else { + q = &query{sql: `UPDATE bookmark SET tags=array_replace(tags, $2, $3) where tags @> $1::text[]`} + count, err = q.execOnNRow(currentUser, tagArray, oldName, newName) + } if err != nil { return 0, utils.NewHTTPError500(err, currentUser) } @@ -72,6 +84,8 @@ func RenameTag(currentUser model.BowUser, oldName string, newName string) (int64 /* TagsJSON retourne la liste des tags qui match le filtre le format de retour est celui d'opensearch +si on demande avec le count, on a un tableau de tag, suivi du tableau du nombre d'occurence de ce tag: +[["tag1", "tag2", ...], [12, 34, ...]] */ func TagsJSON(currentUser model.BowUser, filter string, withCount bool) (string, error) { var q *query diff --git a/web/package.json b/web/package.json index e265de3..2bb54f5 100644 --- a/web/package.json +++ b/web/package.json @@ -8,6 +8,7 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "@johmun/vue-tags-input": "^2.1.0", "core-js": "^3.6.4", "vue": "^2.6.11", "vue-property-decorator": "^8.4.1", diff --git a/web/src/components/preferences/TagsEditor.vue b/web/src/components/preferences/TagsEditor.vue new file mode 100644 index 0000000..3199a4f --- /dev/null +++ b/web/src/components/preferences/TagsEditor.vue @@ -0,0 +1,146 @@ +<template> + <div class="tags-editor"> + <label for="oldTags">Replace</label + ><vue-tags-input + id="oldTags" + v-model="oldName" + :tags="oldTags" + @tags-changed="(t) => (oldTags = t)" + :add-only-from-autocomplete="true" + :max-tags="1" + :autocomplete-items="filteredOldTags" + :add-on-key="key" + :save-on-key="key" + > + <template #tag-right="{ tag }"> + ({{ tag.count }}) + </template> + </vue-tags-input> + <label for="newTags">with</label + ><vue-tags-input + id="newTags" + v-model="newName" + :tags="newTags" + @tags-changed="(t) => (newTags = t)" + :max-tags="1" + :autocomplete-items="filteredNewTags" + :add-on-key="key" + :save-on-key="key" + /> + <button class="replace" @click.prevent="doAction">replace</button> + <span class="errorMsg">{{ errorMsg }}</span> + </div> +</template> + +<script> +import { Component, Vue } from 'vue-property-decorator' +import VueTagsInput from '@johmun/vue-tags-input' + +@Component({ + name: 'TagsEditor', + components: { VueTagsInput } +}) +class TagsEditor extends Vue { + errorMsg = '' + + key = [13, ',', ';', ' ', '\t'] + + tags = [] + + oldName = '' + oldTags = [] + newName = '' + newTags = [] + + get filteredOldTags() { + return this.filteredTags(this.oldName) + } + + get filteredNewTags() { + return this.filteredTags(this.newName) + } + + filteredTags(tag) { + let lowerCased = tag.toLowerCase() + console.log('XXXXX', tag, lowerCased) + return this.tags.filter((a) => { + return a.text.toLowerCase().indexOf(lowerCased) !== -1 + }) + } + + doAction() { + if (this.newTags[0]) { + this.doReplace() + } else { + this.doDelete() + } + } + + doReplace() { + let oldName = this.oldTags[0].text + let newName = this.newTags[0].text + this.$fetch.put(`/bookmarks/tags/${oldName}/${newName}`).then( + (data) => { + this.errorMsg = `renamed ${oldName}(${data.count}) -> ${newName}` + this.oldName = '' + this.newName = '' + this.oldTags = [] + this.newTags = [] + // on recharge pour avoir le nouveau tag et le bon count + this.load() + }, + (err) => { + console.log('ko', err) + this.errorMsg = err.cause + } + ) + } + + doDelete() { + let oldName = this.oldTags[0].text + this.$fetch.delete(`/bookmarks/tags/${oldName}`).then( + (data) => { + this.errorMsg = `deleted ${oldName}(${data.count})` + this.oldName = '' + this.newName = '' + this.oldTags = [] + this.newTags = [] + // on recharge pour avoir le bon count + this.load() + }, + (err) => { + console.log('ko', err) + this.errorMsg = err.cause + } + ) + } + + load() { + this.$fetch.get('/bookmarks/tags?with-count=true').then( + (tags) => { + console.log('ok', tags) + this.tags = tags[0].map((text, i) => { + return {text, count: tags[1][i]} + }) + }, + (err) => { + console.log('ko', err) + this.errorMsg = err.cause + } + ) + } + + beforeMount() { + this.load() + } +} + +export default TagsEditor +</script> + +<style scoped lang="less"> +.tags-editor { + display: flex; + flex-direction: row; +} +</style> diff --git a/web/src/views/Preferences.vue b/web/src/views/Preferences.vue index 249c824..b957322 100644 --- a/web/src/views/Preferences.vue +++ b/web/src/views/Preferences.vue @@ -4,6 +4,7 @@ <a :href="bookmarkletAdd">bookmarklet add</a> <MaxResultEditor :user="user"></MaxResultEditor> <Actions :actions="user.actions"></Actions> + <TagsEditor></TagsEditor> </div> </template> @@ -12,10 +13,11 @@ import { Component, Vue } from 'vue-property-decorator' import MaxResultEditor from '@/components/preferences/MaxResultEditor' import Actions from '@/components/preferences/Actions' +import TagsEditor from '@/components/preferences/TagsEditor' @Component({ name: 'Preferences', - components: {MaxResultEditor, Actions} + components: {MaxResultEditor, Actions, TagsEditor} }) class Preferences extends Vue { errorMsg = '' diff --git a/web/yarn.lock b/web/yarn.lock index eff5d5d..f9561a4 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -837,6 +837,13 @@ cssnano-preset-default "^4.0.0" postcss "^7.0.0" +"@johmun/vue-tags-input@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@johmun/vue-tags-input/-/vue-tags-input-2.1.0.t..." + integrity sha512-Fdwfss/TqCqMJbGAkmlzKbcG/ia1MstYjhqPBj+zG7h/166tIcE1TIftUxhT9LZ+RWjRSG0EFA1UyaHQSr3k3Q== + dependencies: + vue "^2.6.10" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2..." @@ -8017,7 +8024,7 @@ vue-template-es2015-compiler@^1.9.0: resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2..." integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== -vue@^2.6.11: +vue@^2.6.10, vue@^2.6.11: version "2.6.11" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35..." integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.
participants (1)
-
chorem.org scm