From d009bd98ac1277faaf72fa47e0806e5a428995eb Mon Sep 17 00:00:00 2001 From: yequari Date: Sat, 7 Jun 2025 21:46:23 -0700 Subject: [PATCH] merge guestbookmodel with websitemodel and fix minor ui bugs --- cmd/web/handlers_guestbook.go | 77 +-------- cmd/web/handlers_website.go | 22 +-- cmd/web/helpers.go | 3 +- cmd/web/main.go | 4 +- internal/models/guestbook.go | 284 --------------------------------- internal/models/website.go | 290 +++++++++++++++++++++++++++++++--- ui/views/websites.templ | 32 ++-- ui/views/websites_hx.templ | 7 + ui/views/websites_hx_templ.go | 42 +++++ ui/views/websites_templ.go | 108 +++++-------- 10 files changed, 379 insertions(+), 490 deletions(-) delete mode 100644 internal/models/guestbook.go create mode 100644 ui/views/websites_hx.templ create mode 100644 ui/views/websites_hx_templ.go diff --git a/cmd/web/handlers_guestbook.go b/cmd/web/handlers_guestbook.go index 00217b1..e137a81 100644 --- a/cmd/web/handlers_guestbook.go +++ b/cmd/web/handlers_guestbook.go @@ -24,15 +24,6 @@ func (app *application) getGuestbook(w http.ResponseWriter, r *http.Request) { } return } - website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId) - if err != nil { - if errors.Is(err, models.ErrNoRecord) { - http.NotFound(w, r) - } else { - app.serverError(w, r, err) - } - return - } if !website.Guestbook.Settings.IsVisible { u := app.getCurrentUser(r) if u == nil { @@ -63,14 +54,6 @@ func (app *application) getGuestbookSettings(w http.ResponseWriter, r *http.Requ app.serverError(w, r, err) } } - website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId) - if err != nil { - if errors.Is(err, models.ErrNoRecord) { - http.NotFound(w, r) - } else { - app.serverError(w, r, err) - } - } data := app.newCommonData(r) views.GuestbookSettingsView(data, website).Render(r.Context(), w) } @@ -121,7 +104,7 @@ func (app *application) putGuestbookSettings(w http.ResponseWriter, r *http.Requ if err != nil { app.serverError(w, r, err) } - err = app.guestbooks.UpdateGuestbookSettings(website.Guestbook.ID, website.Guestbook.Settings) + err = app.websites.UpdateGuestbookSettings(website.Guestbook.ID, website.Guestbook.Settings) if err != nil { app.serverError(w, r, err) return @@ -144,14 +127,6 @@ func (app *application) getGuestbookComments(w http.ResponseWriter, r *http.Requ } return } - website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId) - if err != nil { - if errors.Is(err, models.ErrNoRecord) { - http.NotFound(w, r) - } else { - app.serverError(w, r, err) - } - } comments, err := app.guestbookComments.GetAll(website.Guestbook.ID) if err != nil { app.serverError(w, r, err) @@ -173,15 +148,6 @@ func (app *application) getGuestbookCommentCreate(w http.ResponseWriter, r *http } return } - website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId) - if err != nil { - if errors.Is(err, models.ErrNoRecord) { - http.NotFound(w, r) - } else { - app.serverError(w, r, err) - } - } - data := app.newCommonData(r) form := forms.CommentCreateForm{} views.CreateGuestbookComment("New Comment", data, website, website.Guestbook, form).Render(r.Context(), w) @@ -198,14 +164,6 @@ func (app *application) postGuestbookCommentCreate(w http.ResponseWriter, r *htt } return } - website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId) - if err != nil { - if errors.Is(err, models.ErrNoRecord) { - http.NotFound(w, r) - } else { - app.serverError(w, r, err) - } - } if !website.Guestbook.CanComment() { app.clientError(w, http.StatusForbidden) @@ -228,6 +186,7 @@ func (app *application) postGuestbookCommentCreate(w http.ResponseWriter, r *htt form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank") if !form.Valid() { + // TODO: use htmx to avoid getting comments again comments, err := app.guestbookComments.GetAll(website.Guestbook.ID) if err != nil { app.serverError(w, r, err) @@ -259,14 +218,6 @@ func (app *application) getCommentQueue(w http.ResponseWriter, r *http.Request) } return } - website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId) - if err != nil { - if errors.Is(err, models.ErrNoRecord) { - http.NotFound(w, r) - } else { - app.serverError(w, r, err) - } - } comments, err := app.guestbookComments.GetUnpublished(website.Guestbook.ID) if err != nil { @@ -293,14 +244,6 @@ func (app *application) getCommentTrash(w http.ResponseWriter, r *http.Request) } return } - website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId) - if err != nil { - if errors.Is(err, models.ErrNoRecord) { - http.NotFound(w, r) - } else { - app.serverError(w, r, err) - } - } comments, err := app.guestbookComments.GetDeleted(website.Guestbook.ID) if err != nil { @@ -332,14 +275,6 @@ func (app *application) putHideGuestbookComment(w http.ResponseWriter, r *http.R if user.ID != website.UserId { app.clientError(w, http.StatusUnauthorized) } - website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId) - if err != nil { - if errors.Is(err, models.ErrNoRecord) { - http.NotFound(w, r) - } else { - app.serverError(w, r, err) - } - } cSlug := r.PathValue("commentId") comment, err := app.guestbookComments.Get(slugToShortId(cSlug)) if err != nil { @@ -374,14 +309,6 @@ func (app *application) deleteGuestbookComment(w http.ResponseWriter, r *http.Re if user.ID != website.UserId { app.clientError(w, http.StatusUnauthorized) } - website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId) - if err != nil { - if errors.Is(err, models.ErrNoRecord) { - http.NotFound(w, r) - } else { - app.serverError(w, r, err) - } - } cSlug := r.PathValue("commentId") comment, err := app.guestbookComments.Get(slugToShortId(cSlug)) if err != nil { diff --git a/cmd/web/handlers_website.go b/cmd/web/handlers_website.go index 8c2d1d5..cb94c72 100644 --- a/cmd/web/handlers_website.go +++ b/cmd/web/handlers_website.go @@ -39,28 +39,13 @@ func (app *application) postWebsiteCreate(w http.ResponseWriter, r *http.Request views.WebsiteCreate("Add a Website", data, form).Render(r.Context(), w) } websiteShortID := app.createShortId() - websiteId, err := app.websites.Insert(websiteShortID, userId, form.Name, form.SiteUrl, form.AuthorName) - if err != nil { - app.serverError(w, r, err) - return - } - // TODO: how to handle website creation success but guestbook creation failure? - guestbookShortID := app.createShortId() - guestbookSettings := models.GuestbookSettings{ - IsCommentingEnabled: true, - } - _, err = app.guestbooks.Insert(guestbookShortID, userId, websiteId, guestbookSettings) + _, err = app.websites.Insert(websiteShortID, userId, form.Name, form.SiteUrl, form.AuthorName) if err != nil { app.serverError(w, r, err) return } app.sessionManager.Put(r.Context(), "flash", "Website successfully registered!") - if r.Header.Get("HX-Request") == "true" { - w.Header().Add("HX-Trigger", "newWebsite") - views.WebsiteCreateButton().Render(r.Context(), w) - return - } - http.Redirect(w, r, fmt.Sprintf("/websites/%s", shortIdToSlug(websiteShortID)), http.StatusSeeOther) + http.Redirect(w, r, fmt.Sprintf("/websites/%s/dashboard", shortIdToSlug(websiteShortID)), http.StatusSeeOther) } func (app *application) getWebsiteDashboard(w http.ResponseWriter, r *http.Request) { @@ -91,7 +76,8 @@ func (app *application) getWebsiteList(w http.ResponseWriter, r *http.Request) { return } if r.Header.Get("HX-Request") == "true" { - w.Header().Add("HX-Trigger", "newWebsite") + views.HxWebsiteList(websites) + return } data := app.newCommonData(r) views.WebsiteList("My Websites", data, websites).Render(r.Context(), w) diff --git a/cmd/web/helpers.go b/cmd/web/helpers.go index 85959d6..7d927d1 100644 --- a/cmd/web/helpers.go +++ b/cmd/web/helpers.go @@ -23,8 +23,7 @@ func (app *application) serverError(w http.ResponseWriter, r *http.Request, err app.logger.Error(err.Error(), "method", method, "uri", uri) if app.debug { - http.Error(w, string(debug.Stack()), http.StatusInternalServerError) - app.logger.Error(err.Error(), "method", method, "uri", uri, "stack", string(debug.Stack())) + http.Error(w, err.Error()+"\n"+string(debug.Stack()), http.StatusInternalServerError) } http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } diff --git a/cmd/web/main.go b/cmd/web/main.go index 89e2ff1..ba9115c 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -22,7 +22,6 @@ type application struct { sequence uint16 logger *slog.Logger websites *models.WebsiteModel - guestbooks *models.GuestbookModel users *models.UserModel guestbookComments *models.GuestbookCommentModel sessionManager *scs.SessionManager @@ -58,7 +57,6 @@ func main() { logger: logger, sessionManager: sessionManager, websites: &models.WebsiteModel{DB: db}, - guestbooks: &models.GuestbookModel{DB: db}, users: &models.UserModel{DB: db, Settings: make(map[string]models.Setting)}, guestbookComments: &models.GuestbookCommentModel{DB: db}, formDecoder: formDecoder, @@ -71,7 +69,7 @@ func main() { logger.Error(err.Error()) os.Exit(1) } - err = app.guestbooks.InitializeSettingsMap() + err = app.websites.InitializeSettingsMap() if err != nil { logger.Error(err.Error()) os.Exit(1) diff --git a/internal/models/guestbook.go b/internal/models/guestbook.go deleted file mode 100644 index 42a8a9b..0000000 --- a/internal/models/guestbook.go +++ /dev/null @@ -1,284 +0,0 @@ -package models - -import ( - "database/sql" - "errors" - "strconv" - "time" -) - -type GuestbookSettings struct { - IsCommentingEnabled bool - ReenableCommenting time.Time - IsVisible bool - FilteredWords []string - AllowRemoteHostAccess bool -} - -var ValidDisableDurations = []string{"true", "false", "1h", "4h", "8h", "24h", "72h", "168h"} - -const ( - SettingGbCommentingEnabled = "commenting_enabled" - SettingGbReenableComments = "reenable_comments" - SettingGbVisible = "is_visible" - SettingGbFilteredWords = "filtered_words" - SettingGbAllowRemote = "remote_enabled" -) - -type Guestbook struct { - ID int64 - ShortId uint64 - UserId int64 - WebsiteId int64 - Created time.Time - Deleted time.Time - IsActive bool - Settings GuestbookSettings -} - -func (g Guestbook) CanComment() bool { - now := time.Now().UTC() - return g.Settings.IsCommentingEnabled && g.Settings.ReenableCommenting.Before(now) -} - -type GuestbookModel struct { - DB *sql.DB - Settings map[string]Setting -} - -func (m *GuestbookModel) InitializeSettingsMap() error { - if m.Settings == nil { - m.Settings = make(map[string]Setting) - } - stmt := `SELECT settings.Id, settings.Description, Constrained, d.Id, d.Description, g.Id, g.Description, MinValue, MaxValue - FROM settings - LEFT JOIN setting_data_types d ON settings.DataType = d.Id - LEFT JOIN setting_groups g ON settings.SettingGroup = g.Id - WHERE SettingGroup = (SELECT Id FROM setting_groups WHERE Description = 'guestbook' LIMIT 1)` - result, err := m.DB.Query(stmt) - if err != nil { - return err - } - for result.Next() { - var s Setting - var mn sql.NullString - var mx sql.NullString - err := result.Scan(&s.id, &s.description, &s.constrained, &s.dataType.id, &s.dataType.description, &s.settingGroup.id, &s.settingGroup.description, &mn, &mx) - if mn.Valid { - s.minValue = mn.String - } - if mx.Valid { - s.maxValue = mx.String - } - if err != nil { - return err - } - m.Settings[s.description] = s - } - return nil -} - -func (m *GuestbookModel) Insert(shortId uint64, userId int64, websiteId int64, settings GuestbookSettings) (int64, error) { - stmt := `INSERT INTO guestbooks (ShortId, UserId, WebsiteId, Created, IsActive) - VALUES(?, ?, ?, ?, TRUE)` - result, err := m.DB.Exec(stmt, shortId, userId, websiteId, time.Now().UTC()) - if err != nil { - return -1, err - } - id, err := result.LastInsertId() - if err != nil { - return -1, err - } - err = m.initializeGuestbookSettings(id, settings) - if err != nil { - return id, err - } - return id, nil -} - -func (m *GuestbookModel) Get(shortId uint64) (Guestbook, error) { - stmt := `SELECT Id, ShortId, UserId, WebsiteId, Created, Deleted, IsActive FROM guestbooks - WHERE ShortId = ?` - row := m.DB.QueryRow(stmt, shortId) - var g Guestbook - var t sql.NullTime - err := row.Scan(&g.ID, &g.ShortId, &g.UserId, &g.WebsiteId, &g.Created, &t, &g.IsActive) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return Guestbook{}, ErrNoRecord - } - return Guestbook{}, err - } - if t.Valid { - g.Deleted = t.Time - } - settings, err := m.GetSettings(g.ID) - if err != nil { - return g, err - } - g.Settings = settings - return g, nil -} - -func (m *GuestbookModel) GetAll(userId int64) ([]Guestbook, error) { - stmt := `SELECT Id, ShortId, UserId, WebsiteId, Created, IsActive FROM guestbooks - WHERE UserId = ? AND DELETED IS NULL` - rows, err := m.DB.Query(stmt, userId) - if err != nil { - return nil, err - } - var guestbooks []Guestbook - for rows.Next() { - var g Guestbook - err = rows.Scan(&g.ID, &g.ShortId, &g.UserId, &g.WebsiteId, &g.Created, &g.IsActive) - if err != nil { - return nil, err - } - guestbooks = append(guestbooks, g) - } - if err = rows.Err(); err != nil { - return nil, err - } - return guestbooks, nil -} - -func (m *GuestbookModel) GetSettings(guestbookId int64) (GuestbookSettings, error) { - stmt := `SELECT g.SettingId, a.ItemValue, g.UnconstrainedValue FROM guestbook_settings AS g - LEFT JOIN allowed_setting_values AS a ON g.AllowedSettingValueId = a.Id - WHERE GuestbookId = ?` - var settings GuestbookSettings - rows, err := m.DB.Query(stmt, guestbookId) - if err != nil { - return settings, err - } - for rows.Next() { - var id int - var itemValue sql.NullString - var unconstrainedValue sql.NullString - err = rows.Scan(&id, &itemValue, &unconstrainedValue) - if err != nil { - return settings, err - } - switch id { - case m.Settings[SettingGbCommentingEnabled].id: - settings.IsCommentingEnabled, err = strconv.ParseBool(itemValue.String) - if err != nil { - return settings, err - } - break - case m.Settings[SettingGbReenableComments].id: - settings.ReenableCommenting, err = time.Parse(time.RFC3339, unconstrainedValue.String) - if err != nil { - return settings, err - } - break - case m.Settings[SettingGbVisible].id: - settings.IsVisible, err = strconv.ParseBool(itemValue.String) - if err != nil { - return settings, err - } - break - case m.Settings[SettingGbAllowRemote].id: - settings.AllowRemoteHostAccess, err = strconv.ParseBool(itemValue.String) - if err != nil { - return settings, err - } - break - } - } - // if comment disable setting has expired, enable commenting - if time.Now().UTC().After(settings.ReenableCommenting) { - settings.IsCommentingEnabled = true - m.UpdateSetting(guestbookId, m.Settings[SettingGbCommentingEnabled], "true") - } - return settings, nil -} - -func (m *GuestbookModel) initializeGuestbookSettings(guestbookId int64, settings GuestbookSettings) error { - stmt := `INSERT INTO guestbook_settings (GuestbookId, SettingId, AllowedSettingValueId, UnconstrainedValue) VALUES - (?, ?, ?, ?), - (?, ?, ?, ?), - (?, ?, ?, ?), - (?, ?, ?, ?), - (?, ?, ?, ?)` - _, err := m.DB.Exec(stmt, - guestbookId, m.Settings[SettingGbCommentingEnabled].id, settings.IsCommentingEnabled, nil, - guestbookId, m.Settings[SettingGbReenableComments].id, nil, settings.ReenableCommenting.String(), - guestbookId, m.Settings[SettingGbVisible].id, settings.IsVisible, nil, - guestbookId, m.Settings[SettingGbAllowRemote].id, settings.AllowRemoteHostAccess, nil) - if err != nil { - return err - } - return nil -} - -func (m *GuestbookModel) UpdateGuestbookSettings(guestbookId int64, settings GuestbookSettings) error { - err := m.UpdateSetting(guestbookId, m.Settings[SettingGbVisible], strconv.FormatBool(settings.IsVisible)) - if err != nil { - return err - } - err = m.UpdateSetting(guestbookId, m.Settings[SettingGbAllowRemote], strconv.FormatBool(settings.AllowRemoteHostAccess)) - if err != nil { - return err - } - err = m.UpdateSetting(guestbookId, m.Settings[SettingGbCommentingEnabled], strconv.FormatBool(settings.IsCommentingEnabled)) - if err != nil { - return err - } - err = m.UpdateSetting(guestbookId, m.Settings[SettingGbReenableComments], settings.ReenableCommenting.Format(time.RFC3339)) - if err != nil { - return err - } - return nil -} - -func (m *GuestbookModel) InsertSetting(guestbookId int64, setting Setting, value string) error { - stmt := ` -INSERT INTO guestbook_settings (GuestbookId, SettingId, AllowedSettingValueId, UnconstrainedValue) -SELECT ?, - settings.Id, - (SELECT Id FROM allowed_setting_values WHERE SettingId = settings.id AND ItemValue = ?), - CASE WHEN NOT EXISTS (SELECT 1 FROM settings AS s where s.Id = settings.Id AND s.Constrained = 1) THEN ? ELSE NULL END -FROM settings -WHERE settings.id = ? - ` - result, err := m.DB.Exec(stmt, guestbookId, value, value, setting.id) - if err != nil { - return err - } - _, err = result.LastInsertId() - if err != nil { - return err - } - return nil -} - -func (m *GuestbookModel) UpdateSetting(guestbookId int64, setting Setting, value string) error { - stmt := `UPDATE guestbook_settings SET - AllowedSettingValueId=IFNULL( - (SELECT Id FROM allowed_setting_values WHERE SettingId = guestbook_settings.SettingId AND ItemValue = ?), AllowedSettingValueId - ), - UnconstrainedValue=(SELECT ? FROM settings WHERE settings.Id = guestbook_settings.SettingId AND settings.Constrained=0) - WHERE GuestbookId = ? - AND SettingId = (SELECT Id from Settings WHERE Description=?);` - result, err := m.DB.Exec(stmt, value, value, guestbookId, setting.description) - if err != nil { - return err - } - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows != 1 { - return ErrInvalidSettingValue - } - return nil -} - -func (m *GuestbookModel) AddFilteredWord(guestbookId int64, word string) error { - return nil -} - -func (m *GuestbookModel) RemoveFilteredWord(guestbookId int64, word string) error { - return nil -} diff --git a/internal/models/website.go b/internal/models/website.go index b753613..456c92d 100644 --- a/internal/models/website.go +++ b/internal/models/website.go @@ -2,6 +2,8 @@ package models import ( "database/sql" + "errors" + "strconv" "time" ) @@ -17,51 +19,206 @@ type Website struct { Guestbook Guestbook } +type GuestbookSettings struct { + IsCommentingEnabled bool + ReenableCommenting time.Time + IsVisible bool + FilteredWords []string + AllowRemoteHostAccess bool +} + +var ValidDisableDurations = []string{"true", "false", "1h", "4h", "8h", "24h", "72h", "168h"} + +const ( + SettingGbCommentingEnabled = "commenting_enabled" + SettingGbReenableComments = "reenable_comments" + SettingGbVisible = "is_visible" + SettingGbFilteredWords = "filtered_words" + SettingGbAllowRemote = "remote_enabled" +) + +type Guestbook struct { + ID int64 + ShortId uint64 + UserId int64 + WebsiteId int64 + Created time.Time + Deleted time.Time + IsActive bool + Settings GuestbookSettings +} + +func (g Guestbook) CanComment() bool { + now := time.Now().UTC() + return g.Settings.IsCommentingEnabled && g.Settings.ReenableCommenting.Before(now) +} + type WebsiteModel struct { - DB *sql.DB + DB *sql.DB + Settings map[string]Setting +} + +func (m *WebsiteModel) InitializeSettingsMap() error { + if m.Settings == nil { + m.Settings = make(map[string]Setting) + } + stmt := `SELECT settings.Id, settings.Description, Constrained, d.Id, d.Description, g.Id, g.Description, MinValue, MaxValue + FROM settings + LEFT JOIN setting_data_types d ON settings.DataType = d.Id + LEFT JOIN setting_groups g ON settings.SettingGroup = g.Id + WHERE SettingGroup = (SELECT Id FROM setting_groups WHERE Description = 'guestbook' LIMIT 1)` + result, err := m.DB.Query(stmt) + if err != nil { + return err + } + for result.Next() { + var s Setting + var mn sql.NullString + var mx sql.NullString + err := result.Scan(&s.id, &s.description, &s.constrained, &s.dataType.id, &s.dataType.description, &s.settingGroup.id, &s.settingGroup.description, &mn, &mx) + if mn.Valid { + s.minValue = mn.String + } + if mx.Valid { + s.maxValue = mx.String + } + if err != nil { + return err + } + m.Settings[s.description] = s + } + return nil } func (m *WebsiteModel) Insert(shortId uint64, userId int64, siteName, siteUrl, authorName string) (int64, error) { stmt := `INSERT INTO websites (ShortId, Name, SiteUrl, AuthorName, UserId, Created) VALUES (?, ?, ?, ?, ?, ?)` - result, err := m.DB.Exec(stmt, shortId, siteName, siteUrl, authorName, userId, time.Now().UTC()) + tx, err := m.DB.Begin() if err != nil { return -1, err } - id, err := result.LastInsertId() + + result, err := tx.Exec(stmt, shortId, siteName, siteUrl, authorName, userId, time.Now().UTC()) + // result, err := m.DB.Exec(stmt, shortId, siteName, siteUrl, authorName, userId, time.Now().UTC()) if err != nil { + if rollbackError := tx.Rollback(); rollbackError != nil { + return -1, err + } return -1, err } - return id, nil + websiteId, err := result.LastInsertId() + if err != nil { + if rollbackError := tx.Rollback(); rollbackError != nil { + return -1, err + } + return -1, err + } + + stmt = `INSERT INTO guestbooks (ShortId, UserId, WebsiteId, Created, IsActive) + VALUES(?, ?, ?, ?, TRUE)` + result, err = tx.Exec(stmt, shortId, userId, websiteId, time.Now().UTC()) + if err != nil { + if rollbackError := tx.Rollback(); rollbackError != nil { + return -1, err + } + return -1, err + } + guestbookId, err := result.LastInsertId() + if err != nil { + if rollbackError := tx.Rollback(); rollbackError != nil { + return -1, err + } + return -1, err + } + + settings := GuestbookSettings{ + IsCommentingEnabled: true, + IsVisible: true, + AllowRemoteHostAccess: true, + } + stmt = `INSERT INTO guestbook_settings (GuestbookId, SettingId, AllowedSettingValueId, UnconstrainedValue) VALUES + (?, ?, ?, ?), + (?, ?, ?, ?), + (?, ?, ?, ?), + (?, ?, ?, ?)` + _, err = tx.Exec(stmt, + guestbookId, m.Settings[SettingGbCommentingEnabled].id, settings.IsCommentingEnabled, nil, + guestbookId, m.Settings[SettingGbReenableComments].id, nil, settings.ReenableCommenting.Format(time.RFC3339), + guestbookId, m.Settings[SettingGbVisible].id, settings.IsVisible, nil, + guestbookId, m.Settings[SettingGbAllowRemote].id, settings.AllowRemoteHostAccess, nil) + if err != nil { + if rollbackError := tx.Rollback(); rollbackError != nil { + return -1, err + } + return -1, err + } + + if err := tx.Commit(); err != nil { + return -1, err + } + return websiteId, nil } func (m *WebsiteModel) Get(shortId uint64) (Website, error) { - stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created, - g.Id, g.ShortId - FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId + stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created + FROM websites AS w WHERE w.ShortId = ? AND w.DELETED IS NULL` - row := m.DB.QueryRow(stmt, shortId) - var w Website - err := row.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created, - &w.Guestbook.ID, &w.Guestbook.ShortId) + tx, err := m.DB.Begin() if err != nil { + return Website{}, nil + } + row := tx.QueryRow(stmt, shortId) + var w Website + err = row.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + err = ErrNoRecord + } + if rollbackErr := tx.Rollback(); rollbackErr != nil { + return Website{}, err + } return Website{}, err } - return w, nil -} -func (m *WebsiteModel) GetById(id int64) (Website, error) { - stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created, - g.Id, g.ShortId, g.Created, g.IsActive - FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId - WHERE w.Id = ? AND w.DELETED IS NULL` - row := m.DB.QueryRow(stmt, id) - var w Website - err := row.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created, - &w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsActive) + stmt = `SELECT Id, ShortId, UserId, WebsiteId, Created, IsActive FROM guestbooks + WHERE WebsiteId = ? AND Deleted IS NULL` + row = tx.QueryRow(stmt, w.ID) + var g Guestbook + err = row.Scan(&g.ID, &g.ShortId, &g.UserId, &g.WebsiteId, &g.Created, &g.IsActive) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + err = ErrNoRecord + } + if rollbackErr := tx.Rollback(); rollbackErr != nil { + return Website{}, err + } return Website{}, err } + + gbSettings, err := m.getGuestbookSettings(tx, g.ID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + err = ErrNoRecord + } + if rollbackErr := tx.Rollback(); rollbackErr != nil { + return Website{}, err + } + return Website{}, err + } + + err = tx.Commit() + if err != nil { + return Website{}, nil + } + + // if comment disable setting has expired, enable commenting + commentingReenabled := time.Now().UTC().After(gbSettings.ReenableCommenting) + if commentingReenabled { + gbSettings.IsCommentingEnabled = true + } + + g.Settings = gbSettings + w.Guestbook = g return w, nil } @@ -113,3 +270,92 @@ func (m *WebsiteModel) GetAll() ([]Website, error) { } return websites, nil } + +func (m *WebsiteModel) getGuestbookSettings(tx *sql.Tx, guestbookId int64) (GuestbookSettings, error) { + stmt := `SELECT g.SettingId, a.ItemValue, g.UnconstrainedValue FROM guestbook_settings AS g + LEFT JOIN allowed_setting_values AS a ON g.AllowedSettingValueId = a.Id + WHERE GuestbookId = ?` + var settings GuestbookSettings + rows, err := tx.Query(stmt, guestbookId) + if err != nil { + return settings, err + } + for rows.Next() { + var id int + var itemValue sql.NullString + var unconstrainedValue sql.NullString + err = rows.Scan(&id, &itemValue, &unconstrainedValue) + if err != nil { + return settings, err + } + switch id { + case m.Settings[SettingGbCommentingEnabled].id: + settings.IsCommentingEnabled, err = strconv.ParseBool(itemValue.String) + if err != nil { + return settings, err + } + break + case m.Settings[SettingGbReenableComments].id: + settings.ReenableCommenting, err = time.Parse(time.RFC3339, unconstrainedValue.String) + if err != nil { + return settings, err + } + break + case m.Settings[SettingGbVisible].id: + settings.IsVisible, err = strconv.ParseBool(itemValue.String) + if err != nil { + return settings, err + } + break + case m.Settings[SettingGbAllowRemote].id: + settings.AllowRemoteHostAccess, err = strconv.ParseBool(itemValue.String) + if err != nil { + return settings, err + } + break + } + } + return settings, nil +} + +func (m *WebsiteModel) UpdateGuestbookSettings(guestbookId int64, settings GuestbookSettings) error { + err := m.UpdateSetting(guestbookId, m.Settings[SettingGbVisible], strconv.FormatBool(settings.IsVisible)) + if err != nil { + return err + } + err = m.UpdateSetting(guestbookId, m.Settings[SettingGbAllowRemote], strconv.FormatBool(settings.AllowRemoteHostAccess)) + if err != nil { + return err + } + err = m.UpdateSetting(guestbookId, m.Settings[SettingGbCommentingEnabled], strconv.FormatBool(settings.IsCommentingEnabled)) + if err != nil { + return err + } + err = m.UpdateSetting(guestbookId, m.Settings[SettingGbReenableComments], settings.ReenableCommenting.Format(time.RFC3339)) + if err != nil { + return err + } + return nil +} + +func (m *WebsiteModel) UpdateSetting(guestbookId int64, setting Setting, value string) error { + stmt := `UPDATE guestbook_settings SET + AllowedSettingValueId=IFNULL( + (SELECT Id FROM allowed_setting_values WHERE SettingId = guestbook_settings.SettingId AND ItemValue = ?), AllowedSettingValueId + ), + UnconstrainedValue=(SELECT ? FROM settings WHERE settings.Id = guestbook_settings.SettingId AND settings.Constrained=0) + WHERE GuestbookId = ? + AND SettingId = (SELECT Id from Settings WHERE Description=?);` + result, err := m.DB.Exec(stmt, value, value, guestbookId, setting.description) + if err != nil { + return err + } + rows, err := result.RowsAffected() + if err != nil { + return err + } + if rows != 1 { + return ErrInvalidSettingValue + } + return nil +} diff --git a/ui/views/websites.templ b/ui/views/websites.templ index 8ef61a6..d778676 100644 --- a/ui/views/websites.templ +++ b/ui/views/websites.templ @@ -98,18 +98,14 @@ templ WebsiteCreateButton() { } templ WebsiteList(title string, data CommonData, websites []models.Website) { - if data.IsHtmx { - @displayWebsites(websites) - } else { - @base(title, data) { -

My Websites

-

- @WebsiteCreateButton() -

-
- @displayWebsites(websites) -
- } + @base(title, data) { +

My Websites

+
+ @WebsiteCreateButton() +
+
+ @displayWebsites(websites) +
} } @@ -142,13 +138,7 @@ templ WebsiteDashboardComingSoon(title string, data CommonData, website models.W } templ WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm) { - if data.IsHtmx { -
- @websiteCreateForm(data.CSRFToken, form) -
- } else { -
- @websiteCreateForm(data.CSRFToken, form) -
- } +
+ @websiteCreateForm(data.CSRFToken, form) +
} diff --git a/ui/views/websites_hx.templ b/ui/views/websites_hx.templ new file mode 100644 index 0000000..28ba2eb --- /dev/null +++ b/ui/views/websites_hx.templ @@ -0,0 +1,7 @@ +package views + +import "git.32bit.cafe/32bitcafe/guestbook/internal/models" + +templ HxWebsiteList(websites []models.Website) { + @displayWebsites(websites) +} diff --git a/ui/views/websites_hx_templ.go b/ui/views/websites_hx_templ.go new file mode 100644 index 0000000..cb3cbf2 --- /dev/null +++ b/ui/views/websites_hx_templ.go @@ -0,0 +1,42 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.833 +package views + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "git.32bit.cafe/32bitcafe/guestbook/internal/models" + +func HxWebsiteList(websites []models.Website) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = displayWebsites(websites).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/ui/views/websites_templ.go b/ui/views/websites_templ.go index 6935168..2d6d6a6 100644 --- a/ui/views/websites_templ.go +++ b/ui/views/websites_templ.go @@ -385,50 +385,43 @@ func WebsiteList(title string, data CommonData, websites []models.Website) templ templ_7745c5c3_Var21 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - if data.IsHtmx { + templ_7745c5c3_Var22 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "

My Websites

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = WebsiteCreateButton().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } templ_7745c5c3_Err = displayWebsites(websites).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } else { - templ_7745c5c3_Var22 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "

My Websites

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = WebsiteCreateButton().Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = displayWebsites(websites).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) - templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var22), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + return nil + }) + templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var22), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err } return nil }) @@ -482,7 +475,7 @@ func WebsiteDashboard(title string, data CommonData, website models.Website) tem var templ_7745c5c3_Var25 string templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 121, Col: 22} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 117, Col: 22} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { @@ -550,7 +543,7 @@ func WebsiteDashboardComingSoon(title string, data CommonData, website models.We var templ_7745c5c3_Var28 string templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 135, Col: 22} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 131, Col: 22} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { @@ -591,32 +584,17 @@ func WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm) templ_7745c5c3_Var29 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - if data.IsHtmx { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = websiteCreateForm(data.CSRFToken, form).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = websiteCreateForm(data.CSRFToken, form).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = websiteCreateForm(data.CSRFToken, form).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err } return nil })