From f5b8881d35aff1e94246c88d76ffeaedbd82ac40 Mon Sep 17 00:00:00 2001 From: Thomas Miceli <27960254+thomiceli@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:39:42 +0100 Subject: [PATCH] Add topics for Gists (#413) --- internal/db/db.go | 6 +- internal/db/gist.go | 72 +++++++++++++++++++---- internal/db/gist_tag.go | 6 ++ internal/i18n/locales/en-US.yml | 7 +++ internal/index/bleve.go | 1 + internal/index/gist.go | 2 + internal/validator/validator.go | 27 +++++++++ internal/web/handlers/gist/all.go | 40 ++++--------- internal/web/handlers/gist/create.go | 4 +- internal/web/handlers/gist/fork.go | 1 + internal/web/handlers/gist/gist.go | 6 ++ internal/web/server/middlewares.go | 8 +++ internal/web/server/renderer.go | 10 ++++ internal/web/server/router.go | 12 ++-- internal/web/test/admin_test.go | 2 + internal/web/test/auth_test.go | 4 ++ internal/web/test/gist_test.go | 85 ++++++++++++++++++++++++++- public/tailwind-embed.config.js | 6 +- public/tailwind.config.js | 6 +- templates/base/base_header.html | 1 + templates/base/gist_header.html | 7 +++ templates/pages/all.html | 4 +- templates/pages/create.html | 4 ++ templates/pages/edit.html | 3 + templates/partials/_gist_preview.html | 13 +++- 25 files changed, 278 insertions(+), 59 deletions(-) create mode 100644 internal/db/gist_tag.go diff --git a/internal/db/db.go b/internal/db/db.go index 6c0cafa..742592c 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -144,7 +144,7 @@ func Setup(dbUri string) error { return err } - if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}); err != nil { + if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}); err != nil { return err } @@ -208,7 +208,7 @@ func setupSQLite(dbInfo databaseInfo) error { u.Scheme = "file" q := u.Query() - q.Set("_fk", "true") + q.Set("_pragma", "foreign_keys(1)") q.Set("_journal_mode", journalMode) u.RawQuery = q.Encode() dsn = u.String() @@ -258,5 +258,5 @@ func DeprecationDBFilename() { } func TruncateDatabase() error { - return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}) + return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}) } diff --git a/internal/db/gist.go b/internal/db/gist.go index 0cc8c79..8d8b09f 100644 --- a/internal/db/gist.go +++ b/internal/db/gist.go @@ -81,6 +81,8 @@ type Gist struct { Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` ForkedID uint + + Topics []GistTopic `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` } type Like struct { @@ -100,7 +102,7 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error { func GetGist(user string, gistUuid string) (*Gist, error) { gist := new(Gist) - err := db.Preload("User").Preload("Forked.User"). + err := db.Preload("User").Preload("Forked.User").Preload("Topics"). Where("(gists.uuid like ? OR gists.url = ?) AND users.username like ?", gistUuid+"%", gistUuid, user). Joins("join users on gists.user_id = users.id"). First(&gist).Error @@ -110,7 +112,7 @@ func GetGist(user string, gistUuid string) (*Gist, error) { func GetGistByID(gistId string) (*Gist, error) { gist := new(Gist) - err := db.Preload("User").Preload("Forked.User"). + err := db.Preload("User").Preload("Forked.User").Preload("Topics"). Where("gists.id = ?", gistId). First(&gist).Error @@ -119,7 +121,9 @@ func GetGistByID(gistId string) (*Gist, error) { func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) { var gists []*Gist - err := db.Preload("User").Preload("Forked.User"). + err := db.Preload("User"). + Preload("Forked.User"). + Preload("Topics"). Where("gists.private = 0 or gists.user_id = ?", currentUserId). Limit(11). Offset(offset * 10). @@ -140,12 +144,18 @@ func GetAllGists(offset int) ([]*Gist, error) { return gists, err } -func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) { +func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string, topic string) ([]*Gist, error) { var gists []*Gist - err := db.Preload("User").Preload("Forked.User"). + tx := db.Preload("User").Preload("Forked.User").Preload("Topics"). Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). - Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%"). - Limit(11). + Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%") + + if topic != "" { + tx = tx.Joins("join gist_topics on gists.id = gist_topics.gist_id"). + Where("gist_topics.topic = ?", topic) + } + + err := tx.Limit(11). Offset(offset * 10). Order("gists." + sort + "_at " + order). Find(&gists).Error @@ -154,7 +164,7 @@ func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort st } func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB { - return db.Preload("User").Preload("Forked.User"). + return db.Preload("User").Preload("Forked.User").Preload("Topics"). Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("users.id = ?", fromUserId). Joins("join users on gists.user_id = users.id") @@ -177,7 +187,7 @@ func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) { } func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB { - return db.Preload("User").Preload("Forked.User"). + return db.Preload("User").Preload("Forked.User").Preload("Topics"). Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("likes.user_id = ?", fromUserId). Joins("join likes on gists.id = likes.gist_id"). @@ -200,7 +210,7 @@ func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error } func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB { - return db.Preload("User").Preload("Forked.User"). + return db.Preload("User").Preload("Forked.User").Preload("Topics"). Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("gists.user_id = ?", fromUserId). Joins("join users on gists.user_id = users.id") @@ -242,7 +252,7 @@ func GetAllGistsVisibleByUser(userId uint) ([]uint, error) { func GetAllGistsByIds(ids []uint) ([]*Gist, error) { var gists []*Gist - err := db.Preload("User").Preload("Forked.User"). + err := db.Preload("User").Preload("Forked.User").Preload("Topics"). Where("id in ?", ids). Find(&gists).Error @@ -259,6 +269,12 @@ func (gist *Gist) CreateForked() error { } func (gist *Gist) Update() error { + // reset the topics + err := db.Model(&GistTopic{}).Where("gist_id = ?", gist.ID).Delete(&GistTopic{}).Error + if err != nil { + return err + } + return db.Omit("forked_id").Save(&gist).Error } @@ -535,6 +551,22 @@ func (gist *Gist) GetLanguagesFromFiles() ([]string, error) { return languages, nil } +func (gist *Gist) GetTopics() ([]string, error) { + var topics []string + err := db.Model(&GistTopic{}). + Where("gist_id = ?", gist.ID). + Pluck("topic", &topics).Error + return topics, err +} + +func (gist *Gist) TopicsSlice() []string { + topics := make([]string, 0, len(gist.Topics)) + for _, topic := range gist.Topics { + topics = append(topics, topic.Topic) + } + return topics +} + // -- DTO -- // type GistDTO struct { @@ -544,6 +576,7 @@ type GistDTO struct { Files []FileDTO `validate:"min=1,dive"` Name []string `form:"name"` Content []string `form:"content"` + Topics string `validate:"gisttopics" form:"topics"` VisibilityDTO } @@ -562,6 +595,7 @@ func (dto *GistDTO) ToGist() *Gist { Description: dto.Description, Private: dto.Private, URL: dto.URL, + Topics: dto.TopicStrToSlice(), } } @@ -569,9 +603,19 @@ func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist { gist.Title = dto.Title gist.Description = dto.Description gist.URL = dto.URL + gist.Topics = dto.TopicStrToSlice() return gist } +func (dto *GistDTO) TopicStrToSlice() []GistTopic { + topics := strings.Fields(dto.Topics) + gistTopics := make([]GistTopic, 0, len(topics)) + for _, topic := range topics { + gistTopics = append(gistTopics, GistTopic{Topic: topic}) + } + return gistTopics +} + // -- Index -- // func (gist *Gist) ToIndexedGist() (*index.Gist, error) { @@ -597,6 +641,11 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) { return nil, err } + topics, err := gist.GetTopics() + if err != nil { + return nil, err + } + indexedGist := &index.Gist{ GistID: gist.ID, Username: gist.User.Username, @@ -605,6 +654,7 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) { Filenames: fileNames, Extensions: exts, Languages: langs, + Topics: topics, CreatedAt: gist.CreatedAt, UpdatedAt: gist.UpdatedAt, } diff --git a/internal/db/gist_tag.go b/internal/db/gist_tag.go new file mode 100644 index 0000000..cf52a9e --- /dev/null +++ b/internal/db/gist_tag.go @@ -0,0 +1,6 @@ +package db + +type GistTopic struct { + GistID uint `gorm:"primaryKey"` + Topic string `gorm:"primaryKey;size:50"` +} diff --git a/internal/i18n/locales/en-US.yml b/internal/i18n/locales/en-US.yml index 144cc7d..3b15e3a 100644 --- a/internal/i18n/locales/en-US.yml +++ b/internal/i18n/locales/en-US.yml @@ -45,6 +45,7 @@ gist.new.create-unlisted-button: Create unlisted gist gist.new.create-private-button: Create private gist gist.new.preview: Preview gist.new.create-a-new-gist: Create a new gist +gist.new.topics: Topics (separate with spaces) gist.edit.editing: Editing gist.edit.edit-gist: Edit %s @@ -74,6 +75,9 @@ gist.list.no-gists: No gists gist.list.all-liked-by: All gists liked by %s gist.list.all-forked-by: All gists forked by %s gist.list.all-from: All gists from %s +gist.list.topic-results-topic: All gists matching topic %s +gist.list.topic-results: All gists matching topic + gist.search.found: gists found gist.search.no-results: No gists found @@ -82,6 +86,8 @@ gist.search.help.title: gists with given title gist.search.help.filename: gists having files with given name gist.search.help.extension: gists having files with given extension gist.search.help.language: gists having files with given language +gist.search.help.topic: gists with given topic + gist.forks: Forks gist.forks.view: View fork @@ -303,5 +309,6 @@ validation.should-only-contain-alphanumeric-characters: Field %s should only con validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s should only contain alphanumeric characters and dashes validation.not-enough: Not enough %s validation.invalid: Invalid %s +validation.invalid-gist-topics: Invalid gist topics, they must start with a letter or number, consist of 50 characters or less, and can include hyphens html.title.admin-panel: Admin panel \ No newline at end of file diff --git a/internal/index/bleve.go b/internal/index/bleve.go index 6747dd2..f823c16 100644 --- a/internal/index/bleve.go +++ b/internal/index/bleve.go @@ -170,6 +170,7 @@ func SearchGists(queryStr string, queryMetadata SearchGistMetadata, gistsIds []u addQuery("Extensions", "."+queryMetadata.Extension) addQuery("Filenames", queryMetadata.Filename) addQuery("Languages", queryMetadata.Language) + addQuery("Topics", queryMetadata.Topic) languageFacet := bleve.NewFacetRequest("Languages", 10) diff --git a/internal/index/gist.go b/internal/index/gist.go index 88b62e8..b9aa834 100644 --- a/internal/index/gist.go +++ b/internal/index/gist.go @@ -8,6 +8,7 @@ type Gist struct { Filenames []string Extensions []string Languages []string + Topics []string CreatedAt int64 UpdatedAt int64 } @@ -18,4 +19,5 @@ type SearchGistMetadata struct { Filename string Extension string Language string + Topic string } diff --git a/internal/validator/validator.go b/internal/validator/validator.go index b4d174b..4f86354 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -16,6 +16,7 @@ func NewValidator() *OpengistValidator { _ = v.RegisterValidation("notreserved", validateReservedKeywords) _ = v.RegisterValidation("alphanumdash", validateAlphaNumDash) _ = v.RegisterValidation("alphanumdashorempty", validateAlphaNumDashOrEmpty) + _ = v.RegisterValidation("gisttopics", validateGistTopics) return &OpengistValidator{v} } @@ -46,6 +47,8 @@ func ValidationMessages(err *error, locale *i18n.Locale) string { messages[i] = locale.String("validation.not-enough", e.Field()) case "notreserved": messages[i] = locale.String("validation.invalid", e.Field()) + case "gisttopics": + messages[i] = locale.String("validation.invalid-gist-topics") } } @@ -72,3 +75,27 @@ func validateAlphaNumDash(fl validator.FieldLevel) bool { func validateAlphaNumDashOrEmpty(fl validator.FieldLevel) bool { return regexp.MustCompile(`^$|^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String()) } + +func validateGistTopics(fl validator.FieldLevel) bool { + topicsInput := fl.Field().String() + if topicsInput == "" { + return true + } + + topics := strings.Fields(topicsInput) + + if len(topics) > 10 { + return false + } + + for _, tag := range topics { + if len(tag) > 50 { + return false + } + if !regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(tag) { + return false + } + } + + return true +} diff --git a/internal/web/handlers/gist/all.go b/internal/web/handlers/gist/all.go index 2b45511..489660b 100644 --- a/internal/web/handlers/gist/all.go +++ b/internal/web/handlers/gist/all.go @@ -10,8 +10,6 @@ import ( "github.com/thomiceli/opengist/internal/web/handlers" "gorm.io/gorm" "html/template" - "regexp" - "strings" ) func AllGists(ctx *context.Context) error { @@ -48,35 +46,25 @@ func AllGists(ctx *context.Context) error { currentUserId = 0 } + mode := ctx.GetData("mode") if fromUserStr == "" { - urlctx := ctx.Request().URL.Path - if strings.HasSuffix(urlctx, "search") { + if mode == "search" { ctx.SetData("htmlTitle", ctx.TrH("gist.list.search-results")) - ctx.SetData("mode", "search") ctx.SetData("searchQuery", ctx.QueryParam("q")) ctx.SetData("searchQueryUrl", template.URL("&q="+ctx.QueryParam("q"))) urlPage = "search" - gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order) - } else if strings.HasSuffix(urlctx, "all") { + gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order, "") + } else if mode == "topics" { + ctx.SetData("htmlTitle", ctx.TrH("gist.list.topic-results-topic", ctx.Param("topic"))) + ctx.SetData("topic", ctx.Param("topic")) + urlPage = "topics/" + ctx.Param("topic") + gists, err = db.GetAllGistsFromSearch(currentUserId, "", pageInt-1, sort, order, ctx.Param("topic")) + } else if mode == "all" { ctx.SetData("htmlTitle", ctx.TrH("gist.list.all")) - ctx.SetData("mode", "all") urlPage = "all" gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order) } } else { - liked := false - forked := false - - liked, err = regexp.MatchString(`/[^/]*/liked`, ctx.Request().URL.Path) - if err != nil { - return ctx.ErrorRes(500, "Error matching regexp", err) - } - - forked, err = regexp.MatchString(`/[^/]*/forked`, ctx.Request().URL.Path) - if err != nil { - return ctx.ErrorRes(500, "Error matching regexp", err) - } - var fromUser *db.User fromUser, err = db.GetUserByUsername(fromUserStr) @@ -106,20 +94,17 @@ func AllGists(ctx *context.Context) error { ctx.SetData("countForked", countForked) } - if liked { + if mode == "liked" { urlPage = fromUserStr + "/liked" ctx.SetData("htmlTitle", ctx.TrH("gist.list.all-liked-by", fromUserStr)) - ctx.SetData("mode", "liked") gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order) - } else if forked { + } else if mode == "forked" { urlPage = fromUserStr + "/forked" ctx.SetData("htmlTitle", ctx.TrH("gist.list.all-forked-by", fromUserStr)) - ctx.SetData("mode", "forked") gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order) - } else { + } else if mode == "fromUser" { urlPage = fromUserStr ctx.SetData("htmlTitle", ctx.TrH("gist.list.all-from", fromUserStr)) - ctx.SetData("mode", "fromUser") gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order) } } @@ -171,6 +156,7 @@ func Search(ctx *context.Context) error { Filename: meta["filename"], Extension: meta["extension"], Language: meta["language"], + Topic: meta["topic"], }, visibleGistsIds, pageInt) if err != nil { return ctx.ErrorRes(500, "Error searching gists", err) diff --git a/internal/web/handlers/gist/create.go b/internal/web/handlers/gist/create.go index 33d3132..993f4a5 100644 --- a/internal/web/handlers/gist/create.go +++ b/internal/web/handlers/gist/create.go @@ -67,14 +67,14 @@ func ProcessCreate(ctx *context.Context) error { if err != nil { ctx.AddFlash(validator.ValidationMessages(&err, ctx.GetData("locale").(*i18n.Locale)), "error") if isCreate { - return ctx.Html("create.html") + return ctx.HtmlWithCode(400, "create.html") } else { files, err := gist.Files("HEAD", false) if err != nil { return ctx.ErrorRes(500, "Error fetching files", err) } ctx.SetData("files", files) - return ctx.Html("edit.html") + return ctx.HtmlWithCode(400, "edit.html") } } diff --git a/internal/web/handlers/gist/fork.go b/internal/web/handlers/gist/fork.go index 1dac4d4..96f2fb3 100644 --- a/internal/web/handlers/gist/fork.go +++ b/internal/web/handlers/gist/fork.go @@ -43,6 +43,7 @@ func Fork(ctx *context.Context) error { UserID: currentUser.ID, ForkedID: gist.ID, NbFiles: gist.NbFiles, + Topics: gist.Topics, } if err = newGist.CreateForked(); err != nil { diff --git a/internal/web/handlers/gist/gist.go b/internal/web/handlers/gist/gist.go index fc01c1a..878040d 100644 --- a/internal/web/handlers/gist/gist.go +++ b/internal/web/handlers/gist/gist.go @@ -54,6 +54,11 @@ func GistJson(ctx *context.Context) error { renderedFiles := render.HighlightFiles(files) ctx.SetData("files", renderedFiles) + topics, err := gist.GetTopics() + if err != nil { + return ctx.ErrorRes(500, "Error fetching topics for gist", err) + } + htmlbuf := bytes.Buffer{} w := bufio.NewWriter(&htmlbuf) if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", ctx.DataMap(), ctx); err != nil { @@ -80,6 +85,7 @@ func GistJson(ctx *context.Context) error { "created_at": time.Unix(gist.CreatedAt, 0).Format(time.RFC3339), "visibility": gist.VisibilityStr(), "files": renderedFiles, + "topics": topics, "embed": map[string]string{ "html": htmlbuf.String(), "css": cssUrl, diff --git a/internal/web/server/middlewares.go b/internal/web/server/middlewares.go index 52ba9a6..e139e4c 100644 --- a/internal/web/server/middlewares.go +++ b/internal/web/server/middlewares.go @@ -405,3 +405,11 @@ func gistNewPushSoftInit(next Handler) Handler { return next(ctx) } } +func setAllGistsMode(mode string) Middleware { + return func(next Handler) Handler { + return func(ctx *context.Context) error { + ctx.SetData("mode", mode) + return next(ctx) + } + } +} diff --git a/internal/web/server/renderer.go b/internal/web/server/renderer.go index ef34890..27d3fe2 100644 --- a/internal/web/server/renderer.go +++ b/internal/web/server/renderer.go @@ -179,6 +179,16 @@ func (s *Server) setFuncMap() { _, err := url.ParseRequestURI(s) return err == nil }, + "topicsToStr": func(topics []db.GistTopic) string { + str := "" + for i, topic := range topics { + if i > 0 { + str += " " + } + str += topic.Topic + } + return str + }, } t := template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html")) diff --git a/internal/web/server/router.go b/internal/web/server/router.go index 974516a..4ddeaab 100644 --- a/internal/web/server/router.go +++ b/internal/web/server/router.go @@ -90,17 +90,19 @@ func (s *Server) registerRoutes() { r.Any("/init/*", git.GitHttp, gistNewPushSoftInit) } - r.GET("/all", gist.AllGists, checkRequireLogin) + r.GET("/all", gist.AllGists, checkRequireLogin, setAllGistsMode("all")) if index.Enabled() { r.GET("/search", gist.Search, checkRequireLogin) } else { - r.GET("/search", gist.AllGists, checkRequireLogin) + r.GET("/search", gist.AllGists, checkRequireLogin, setAllGistsMode("search")) } - r.GET("/:user", gist.AllGists, checkRequireLogin) - r.GET("/:user/liked", gist.AllGists, checkRequireLogin) - r.GET("/:user/forked", gist.AllGists, checkRequireLogin) + r.GET("/:user", gist.AllGists, checkRequireLogin, setAllGistsMode("fromUser")) + r.GET("/:user/liked", gist.AllGists, checkRequireLogin, setAllGistsMode("liked")) + r.GET("/:user/forked", gist.AllGists, checkRequireLogin, setAllGistsMode("forked")) + + r.GET("/topics/:topic", gist.AllGists, checkRequireLogin, setAllGistsMode("topics")) sC := r.SubGroup("/:user/:gistname") { diff --git a/internal/web/test/admin_test.go b/internal/web/test/admin_test.go index a18ebb4..02a10d3 100644 --- a/internal/web/test/admin_test.go +++ b/internal/web/test/admin_test.go @@ -131,6 +131,7 @@ func TestAdminUser(t *testing.T) { }, Name: []string{"gist1.txt"}, Content: []string{"yeah"}, + Topics: "", } err := s.Request("POST", "/", gist1, 302) require.NoError(t, err) @@ -170,6 +171,7 @@ func TestAdminGist(t *testing.T) { }, Name: []string{"gist1.txt"}, Content: []string{"yeah"}, + Topics: "", } err := s.Request("POST", "/", gist1, 302) require.NoError(t, err) diff --git a/internal/web/test/auth_test.go b/internal/web/test/auth_test.go index f7fcacf..b62fc83 100644 --- a/internal/web/test/auth_test.go +++ b/internal/web/test/auth_test.go @@ -110,6 +110,7 @@ func TestAnonymous(t *testing.T) { }, Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, + Topics: "", } err = s.Request("POST", "/", gist1, 302) require.NoError(t, err) @@ -165,6 +166,7 @@ func TestGitOperations(t *testing.T) { Content: []string{ "yeah", }, + Topics: "", } err := s.Request("POST", "/", gist1, 302) require.NoError(t, err) @@ -180,6 +182,7 @@ func TestGitOperations(t *testing.T) { Content: []string{ "cool", }, + Topics: "", } err = s.Request("POST", "/", gist2, 302) require.NoError(t, err) @@ -195,6 +198,7 @@ func TestGitOperations(t *testing.T) { Content: []string{ "super", }, + Topics: "", } err = s.Request("POST", "/", gist3, 302) require.NoError(t, err) diff --git a/internal/web/test/gist_test.go b/internal/web/test/gist_test.go index b46621c..d878955 100644 --- a/internal/web/test/gist_test.go +++ b/internal/web/test/gist_test.go @@ -21,7 +21,7 @@ func TestGists(t *testing.T) { err = s.Request("GET", "/all", nil, 200) require.NoError(t, err) - err = s.Request("POST", "/", nil, 200) + err = s.Request("POST", "/", nil, 400) require.NoError(t, err) gist1 := db.GistDTO{ @@ -32,6 +32,7 @@ func TestGists(t *testing.T) { }, Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, + Topics: "", } err = s.Request("POST", "/", gist1, 302) require.NoError(t, err) @@ -63,8 +64,9 @@ func TestGists(t *testing.T) { }, Name: []string{"", "gist2.txt", "gist3.txt"}, Content: []string{"", "yeah\ncool", "yeah\ncool gist actually"}, + Topics: "", } - err = s.Request("POST", "/", gist2, 200) + err = s.Request("POST", "/", gist2, 400) require.NoError(t, err) gist3 := db.GistDTO{ @@ -75,6 +77,7 @@ func TestGists(t *testing.T) { }, Name: []string{""}, Content: []string{"yeah"}, + Topics: "", } err = s.Request("POST", "/", gist3, 302) require.NoError(t, err) @@ -86,7 +89,7 @@ func TestGists(t *testing.T) { require.NoError(t, err) require.Equal(t, "gistfile1.txt", gist3files[0]) - err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", nil, 200) + err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", nil, 400) require.NoError(t, err) gist1.Name = []string{"gist1.txt"} @@ -118,6 +121,7 @@ func TestVisibility(t *testing.T) { }, Name: []string{""}, Content: []string{"yeah"}, + Topics: "", } err := s.Request("POST", "/", gist1, 302) require.NoError(t, err) @@ -160,6 +164,7 @@ func TestLikeFork(t *testing.T) { }, Name: []string{""}, Content: []string{"yeah"}, + Topics: "", } err := s.Request("POST", "/", gist1, 302) require.NoError(t, err) @@ -220,6 +225,7 @@ func TestCustomUrl(t *testing.T) { }, Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, + Topics: "", } err := s.Request("POST", "/", gist1, 302) require.NoError(t, err) @@ -251,6 +257,7 @@ func TestCustomUrl(t *testing.T) { }, Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, + Topics: "", } err = s.Request("POST", "/", gist2, 302) require.NoError(t, err) @@ -261,3 +268,75 @@ func TestCustomUrl(t *testing.T) { require.Equal(t, gist2db.Uuid, gist2db.Identifier()) require.NotEqual(t, gist2db.URL, gist2db.Identifier()) } + +func TestTopics(t *testing.T) { + s := Setup(t) + defer Teardown(t, s) + + user1 := db.UserDTO{Username: "thomas", Password: "thomas"} + register(t, s, user1) + + gist1 := db.GistDTO{ + Title: "gist1", + URL: "my-gist", + Description: "my first gist", + VisibilityDTO: db.VisibilityDTO{ + Private: 0, + }, + Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, + Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, + Topics: "topic1 topic2 topic3", + } + err := s.Request("POST", "/", gist1, 302) + require.NoError(t, err) + + gist1db, err := db.GetGistByID("1") + require.NoError(t, err) + + require.Equal(t, []db.GistTopic{ + {GistID: 1, Topic: "topic1"}, + {GistID: 1, Topic: "topic2"}, + {GistID: 1, Topic: "topic3"}, + }, gist1db.Topics) + + gist2 := db.GistDTO{ + Title: "gist2", + URL: "my-gist", + Description: "my second gist", + VisibilityDTO: db.VisibilityDTO{ + Private: 0, + }, + Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, + Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, + Topics: "topic1 topic2 topic3 topic2 topic4 topic1", + } + err = s.Request("POST", "/", gist2, 302) + require.NoError(t, err) + + gist2db, err := db.GetGistByID("2") + require.NoError(t, err) + require.Equal(t, []db.GistTopic{ + {GistID: 2, Topic: "topic1"}, + {GistID: 2, Topic: "topic2"}, + {GistID: 2, Topic: "topic3"}, + {GistID: 2, Topic: "topic4"}, + }, gist2db.Topics) + + gist3 := db.GistDTO{ + Title: "gist3", + URL: "my-gist", + Description: "my third gist", + VisibilityDTO: db.VisibilityDTO{ + Private: 0, + }, + Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, + Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, + Topics: "topic1 topic2 topic3 topic4 topic5 topic6 topic7 topic8 topic9 topic10 topic11", + } + err = s.Request("POST", "/", gist3, 400) + require.NoError(t, err) + + gist3.Topics = "topictoolongggggggggggggggggggggggggggggggggggggggg" + err = s.Request("POST", "/", gist3, 400) + require.NoError(t, err) +} diff --git a/public/tailwind-embed.config.js b/public/tailwind-embed.config.js index d1b054d..7eaf911 100644 --- a/public/tailwind-embed.config.js +++ b/public/tailwind-embed.config.js @@ -32,9 +32,9 @@ module.exports = { 500: '#588fee', 600: '#3c79e2', 700: '#356fc0', - 800: '#2d6195', - 900: '#2a5574', - 950: '#173040', + 800: '#2b5da3', + 900: '#1f4b8c', + 950: '#192b57' }, slate: colors.slate diff --git a/public/tailwind.config.js b/public/tailwind.config.js index fc7a346..5217cd5 100644 --- a/public/tailwind.config.js +++ b/public/tailwind.config.js @@ -34,9 +34,9 @@ module.exports = { 500: '#588fee', 600: '#3c79e2', 700: '#356fc0', - 800: '#2d6195', - 900: '#2a5574', - 950: '#173040', + 800: '#2b5da3', + 900: '#1f4b8c', + 950: '#192b57' }, slate: colors.slate diff --git a/templates/base/base_header.html b/templates/base/base_header.html index 1959b82..b36c325 100644 --- a/templates/base/base_header.html +++ b/templates/base/base_header.html @@ -114,6 +114,7 @@
filename:myfile.txt
{{ .locale.Tr "gist.search.help.filename" }}
extension:yml
{{ .locale.Tr "gist.search.help.extension" }}
language:go
{{ .locale.Tr "gist.search.help.language" }}
topic:homelab
{{ .locale.Tr "gist.search.help.topic" }}
{{ .gist.Description }}
+ {{ if .gist.Topics }} +