Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add topics for Gists #413

Merged
merged 8 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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{})
}
72 changes: 61 additions & 11 deletions internal/db/gist.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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).
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -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").
Expand All @@ -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")
Expand Down Expand Up @@ -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

Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -562,16 +595,27 @@ func (dto *GistDTO) ToGist() *Gist {
Description: dto.Description,
Private: dto.Private,
URL: dto.URL,
Topics: dto.TopicStrToSlice(),
}
}

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) {
Expand All @@ -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,
Expand All @@ -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,
}
Expand Down
6 changes: 6 additions & 0 deletions internal/db/gist_tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package db

type GistTopic struct {
GistID uint `gorm:"primaryKey"`
Topic string `gorm:"primaryKey;size:50"`
}
7 changes: 7 additions & 0 deletions internal/i18n/locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions internal/index/bleve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions internal/index/gist.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type Gist struct {
Filenames []string
Extensions []string
Languages []string
Topics []string
CreatedAt int64
UpdatedAt int64
}
Expand All @@ -18,4 +19,5 @@ type SearchGistMetadata struct {
Filename string
Extension string
Language string
Topic string
}
27 changes: 27 additions & 0 deletions internal/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}

Expand Down Expand Up @@ -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")
}
}

Expand All @@ -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
}
Loading
Loading