From e5a07fd8bc5075f9d73e3652da20ef9ee63cca95 Mon Sep 17 00:00:00 2001
From: Henry
Date: Mon, 22 Feb 2021 17:55:12 +0100
Subject: [PATCH] (Pinned)Notices
Notices are pages that admins can fill with their content to describe
and customize the room.
Pinned notices are common notices that each room has. Like a description
and privacy policy.
* update models
* simple crud test for basic notices
* edit and save notices as admin
---
admindb/interface.go | 24 +
admindb/mockdb/fixed_pages.go | 115 +++
admindb/mockdb/pages.go | 276 +++++++
admindb/sqlite/migrations/3-notices.sql | 44 +
admindb/sqlite/models/boil_table_names.go | 12 +-
admindb/sqlite/models/notices.go | 959 ++++++++++++++++++++++
admindb/sqlite/models/pinned_notices.go | 943 +++++++++++++++++++++
admindb/sqlite/new.go | 15 +-
admindb/sqlite/notices.go | 102 +++
admindb/sqlite/notices_test.go | 75 ++
admindb/types.go | 28 +
cmd/server/main.go | 1 +
go.mod | 2 +-
go.sum | 6 +-
web/handlers/admin/allow_list.go | 2 +-
web/handlers/admin/handler.go | 18 +-
web/handlers/admin/notices.go | 90 ++
web/handlers/http.go | 20 +-
web/handlers/notices.go | 43 +
web/i18n/defaults/active.en.toml | 4 +
web/router/admin.go | 6 +
web/router/complete.go | 6 +-
web/templates/admin/notice-edit.tmpl | 56 ++
web/templates/landing/index.tmpl | 1 -
web/templates/notice.tmpl | 18 +
25 files changed, 2843 insertions(+), 23 deletions(-)
create mode 100644 admindb/mockdb/fixed_pages.go
create mode 100644 admindb/mockdb/pages.go
create mode 100644 admindb/sqlite/migrations/3-notices.sql
create mode 100644 admindb/sqlite/models/notices.go
create mode 100644 admindb/sqlite/models/pinned_notices.go
create mode 100644 admindb/sqlite/notices.go
create mode 100644 admindb/sqlite/notices_test.go
create mode 100644 web/handlers/admin/notices.go
create mode 100644 web/handlers/notices.go
create mode 100644 web/templates/admin/notice-edit.tmpl
create mode 100644 web/templates/notice.tmpl
diff --git a/admindb/interface.go b/admindb/interface.go
index 806291e..c92e7ab 100644
--- a/admindb/interface.go
+++ b/admindb/interface.go
@@ -47,6 +47,26 @@ type AllowListService interface {
// AliasService manages alias handle registration and lookup
type AliasService interface{}
+// PinnedNoticesService allows an admin to assign Notices to specific placeholder pages.
+// TODO: better name then _fixed_
+// like updates, privacy policy, code of conduct
+// TODO: enum these
+type PinnedNoticesService interface {
+ // Set assigns a fixed page name to an page ID and a language to allow for multiple translated versions of the same page.
+ Set(name PinnedNoticeName, id int64, lang string) error
+}
+
+type NoticesService interface {
+ // GetByID returns the page for that ID or an error
+ GetByID(context.Context, int64) (Notice, error)
+
+ // Save updates the passed page or creates it if it's ID is zero
+ Save(context.Context, *Notice) error
+
+ // RemoveID removes the page for that ID.
+ RemoveID(context.Context, int64) error
+}
+
// for tests we use generated mocks from these interfaces created with https://github.com/maxbrunsfeld/counterfeiter
//go:generate counterfeiter -o mockdb/auth.go . AuthWithSSBService
@@ -56,3 +76,7 @@ type AliasService interface{}
//go:generate counterfeiter -o mockdb/allow.go . AllowListService
//go:generate counterfeiter -o mockdb/alias.go . AliasService
+
+//go:generate counterfeiter -o mockdb/fixed_pages.go . PinnedNoticesService
+
+//go:generate counterfeiter -o mockdb/pages.go . NoticesService
diff --git a/admindb/mockdb/fixed_pages.go b/admindb/mockdb/fixed_pages.go
new file mode 100644
index 0000000..db70afd
--- /dev/null
+++ b/admindb/mockdb/fixed_pages.go
@@ -0,0 +1,115 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package mockdb
+
+import (
+ "sync"
+
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+)
+
+type FakePinnedNoticesService struct {
+ SetStub func(admindb.PinnedNoticeName, int64, string) error
+ setMutex sync.RWMutex
+ setArgsForCall []struct {
+ arg1 admindb.PinnedNoticeName
+ arg2 int64
+ arg3 string
+ }
+ setReturns struct {
+ result1 error
+ }
+ setReturnsOnCall map[int]struct {
+ result1 error
+ }
+ invocations map[string][][]interface{}
+ invocationsMutex sync.RWMutex
+}
+
+func (fake *FakePinnedNoticesService) Set(arg1 admindb.PinnedNoticeName, arg2 int64, arg3 string) error {
+ fake.setMutex.Lock()
+ ret, specificReturn := fake.setReturnsOnCall[len(fake.setArgsForCall)]
+ fake.setArgsForCall = append(fake.setArgsForCall, struct {
+ arg1 admindb.PinnedNoticeName
+ arg2 int64
+ arg3 string
+ }{arg1, arg2, arg3})
+ stub := fake.SetStub
+ fakeReturns := fake.setReturns
+ fake.recordInvocation("Set", []interface{}{arg1, arg2, arg3})
+ fake.setMutex.Unlock()
+ if stub != nil {
+ return stub(arg1, arg2, arg3)
+ }
+ if specificReturn {
+ return ret.result1
+ }
+ return fakeReturns.result1
+}
+
+func (fake *FakePinnedNoticesService) SetCallCount() int {
+ fake.setMutex.RLock()
+ defer fake.setMutex.RUnlock()
+ return len(fake.setArgsForCall)
+}
+
+func (fake *FakePinnedNoticesService) SetCalls(stub func(admindb.PinnedNoticeName, int64, string) error) {
+ fake.setMutex.Lock()
+ defer fake.setMutex.Unlock()
+ fake.SetStub = stub
+}
+
+func (fake *FakePinnedNoticesService) SetArgsForCall(i int) (admindb.PinnedNoticeName, int64, string) {
+ fake.setMutex.RLock()
+ defer fake.setMutex.RUnlock()
+ argsForCall := fake.setArgsForCall[i]
+ return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
+}
+
+func (fake *FakePinnedNoticesService) SetReturns(result1 error) {
+ fake.setMutex.Lock()
+ defer fake.setMutex.Unlock()
+ fake.SetStub = nil
+ fake.setReturns = struct {
+ result1 error
+ }{result1}
+}
+
+func (fake *FakePinnedNoticesService) SetReturnsOnCall(i int, result1 error) {
+ fake.setMutex.Lock()
+ defer fake.setMutex.Unlock()
+ fake.SetStub = nil
+ if fake.setReturnsOnCall == nil {
+ fake.setReturnsOnCall = make(map[int]struct {
+ result1 error
+ })
+ }
+ fake.setReturnsOnCall[i] = struct {
+ result1 error
+ }{result1}
+}
+
+func (fake *FakePinnedNoticesService) Invocations() map[string][][]interface{} {
+ fake.invocationsMutex.RLock()
+ defer fake.invocationsMutex.RUnlock()
+ fake.setMutex.RLock()
+ defer fake.setMutex.RUnlock()
+ copiedInvocations := map[string][][]interface{}{}
+ for key, value := range fake.invocations {
+ copiedInvocations[key] = value
+ }
+ return copiedInvocations
+}
+
+func (fake *FakePinnedNoticesService) recordInvocation(key string, args []interface{}) {
+ fake.invocationsMutex.Lock()
+ defer fake.invocationsMutex.Unlock()
+ if fake.invocations == nil {
+ fake.invocations = map[string][][]interface{}{}
+ }
+ if fake.invocations[key] == nil {
+ fake.invocations[key] = [][]interface{}{}
+ }
+ fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ admindb.PinnedNoticesService = new(FakePinnedNoticesService)
diff --git a/admindb/mockdb/pages.go b/admindb/mockdb/pages.go
new file mode 100644
index 0000000..1bbd897
--- /dev/null
+++ b/admindb/mockdb/pages.go
@@ -0,0 +1,276 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package mockdb
+
+import (
+ "context"
+ "sync"
+
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+)
+
+type FakeNoticesService struct {
+ GetByIDStub func(context.Context, int64) (admindb.Notice, error)
+ getByIDMutex sync.RWMutex
+ getByIDArgsForCall []struct {
+ arg1 context.Context
+ arg2 int64
+ }
+ getByIDReturns struct {
+ result1 admindb.Notice
+ result2 error
+ }
+ getByIDReturnsOnCall map[int]struct {
+ result1 admindb.Notice
+ result2 error
+ }
+ RemoveIDStub func(context.Context, int64) error
+ removeIDMutex sync.RWMutex
+ removeIDArgsForCall []struct {
+ arg1 context.Context
+ arg2 int64
+ }
+ removeIDReturns struct {
+ result1 error
+ }
+ removeIDReturnsOnCall map[int]struct {
+ result1 error
+ }
+ SaveStub func(context.Context, admindb.Notice) (int64, error)
+ saveMutex sync.RWMutex
+ saveArgsForCall []struct {
+ arg1 context.Context
+ arg2 admindb.Notice
+ }
+ saveReturns struct {
+ result1 int64
+ result2 error
+ }
+ saveReturnsOnCall map[int]struct {
+ result1 int64
+ result2 error
+ }
+ invocations map[string][][]interface{}
+ invocationsMutex sync.RWMutex
+}
+
+func (fake *FakeNoticesService) GetByID(arg1 context.Context, arg2 int64) (admindb.Notice, error) {
+ fake.getByIDMutex.Lock()
+ ret, specificReturn := fake.getByIDReturnsOnCall[len(fake.getByIDArgsForCall)]
+ fake.getByIDArgsForCall = append(fake.getByIDArgsForCall, struct {
+ arg1 context.Context
+ arg2 int64
+ }{arg1, arg2})
+ stub := fake.GetByIDStub
+ fakeReturns := fake.getByIDReturns
+ fake.recordInvocation("GetByID", []interface{}{arg1, arg2})
+ fake.getByIDMutex.Unlock()
+ if stub != nil {
+ return stub(arg1, arg2)
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeNoticesService) GetByIDCallCount() int {
+ fake.getByIDMutex.RLock()
+ defer fake.getByIDMutex.RUnlock()
+ return len(fake.getByIDArgsForCall)
+}
+
+func (fake *FakeNoticesService) GetByIDCalls(stub func(context.Context, int64) (admindb.Notice, error)) {
+ fake.getByIDMutex.Lock()
+ defer fake.getByIDMutex.Unlock()
+ fake.GetByIDStub = stub
+}
+
+func (fake *FakeNoticesService) GetByIDArgsForCall(i int) (context.Context, int64) {
+ fake.getByIDMutex.RLock()
+ defer fake.getByIDMutex.RUnlock()
+ argsForCall := fake.getByIDArgsForCall[i]
+ return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakeNoticesService) GetByIDReturns(result1 admindb.Notice, result2 error) {
+ fake.getByIDMutex.Lock()
+ defer fake.getByIDMutex.Unlock()
+ fake.GetByIDStub = nil
+ fake.getByIDReturns = struct {
+ result1 admindb.Notice
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeNoticesService) GetByIDReturnsOnCall(i int, result1 admindb.Notice, result2 error) {
+ fake.getByIDMutex.Lock()
+ defer fake.getByIDMutex.Unlock()
+ fake.GetByIDStub = nil
+ if fake.getByIDReturnsOnCall == nil {
+ fake.getByIDReturnsOnCall = make(map[int]struct {
+ result1 admindb.Notice
+ result2 error
+ })
+ }
+ fake.getByIDReturnsOnCall[i] = struct {
+ result1 admindb.Notice
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeNoticesService) RemoveID(arg1 context.Context, arg2 int64) error {
+ fake.removeIDMutex.Lock()
+ ret, specificReturn := fake.removeIDReturnsOnCall[len(fake.removeIDArgsForCall)]
+ fake.removeIDArgsForCall = append(fake.removeIDArgsForCall, struct {
+ arg1 context.Context
+ arg2 int64
+ }{arg1, arg2})
+ stub := fake.RemoveIDStub
+ fakeReturns := fake.removeIDReturns
+ fake.recordInvocation("RemoveID", []interface{}{arg1, arg2})
+ fake.removeIDMutex.Unlock()
+ if stub != nil {
+ return stub(arg1, arg2)
+ }
+ if specificReturn {
+ return ret.result1
+ }
+ return fakeReturns.result1
+}
+
+func (fake *FakeNoticesService) RemoveIDCallCount() int {
+ fake.removeIDMutex.RLock()
+ defer fake.removeIDMutex.RUnlock()
+ return len(fake.removeIDArgsForCall)
+}
+
+func (fake *FakeNoticesService) RemoveIDCalls(stub func(context.Context, int64) error) {
+ fake.removeIDMutex.Lock()
+ defer fake.removeIDMutex.Unlock()
+ fake.RemoveIDStub = stub
+}
+
+func (fake *FakeNoticesService) RemoveIDArgsForCall(i int) (context.Context, int64) {
+ fake.removeIDMutex.RLock()
+ defer fake.removeIDMutex.RUnlock()
+ argsForCall := fake.removeIDArgsForCall[i]
+ return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakeNoticesService) RemoveIDReturns(result1 error) {
+ fake.removeIDMutex.Lock()
+ defer fake.removeIDMutex.Unlock()
+ fake.RemoveIDStub = nil
+ fake.removeIDReturns = struct {
+ result1 error
+ }{result1}
+}
+
+func (fake *FakeNoticesService) RemoveIDReturnsOnCall(i int, result1 error) {
+ fake.removeIDMutex.Lock()
+ defer fake.removeIDMutex.Unlock()
+ fake.RemoveIDStub = nil
+ if fake.removeIDReturnsOnCall == nil {
+ fake.removeIDReturnsOnCall = make(map[int]struct {
+ result1 error
+ })
+ }
+ fake.removeIDReturnsOnCall[i] = struct {
+ result1 error
+ }{result1}
+}
+
+func (fake *FakeNoticesService) Save(arg1 context.Context, arg2 admindb.Notice) (int64, error) {
+ fake.saveMutex.Lock()
+ ret, specificReturn := fake.saveReturnsOnCall[len(fake.saveArgsForCall)]
+ fake.saveArgsForCall = append(fake.saveArgsForCall, struct {
+ arg1 context.Context
+ arg2 admindb.Notice
+ }{arg1, arg2})
+ stub := fake.SaveStub
+ fakeReturns := fake.saveReturns
+ fake.recordInvocation("Save", []interface{}{arg1, arg2})
+ fake.saveMutex.Unlock()
+ if stub != nil {
+ return stub(arg1, arg2)
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeNoticesService) SaveCallCount() int {
+ fake.saveMutex.RLock()
+ defer fake.saveMutex.RUnlock()
+ return len(fake.saveArgsForCall)
+}
+
+func (fake *FakeNoticesService) SaveCalls(stub func(context.Context, admindb.Notice) (int64, error)) {
+ fake.saveMutex.Lock()
+ defer fake.saveMutex.Unlock()
+ fake.SaveStub = stub
+}
+
+func (fake *FakeNoticesService) SaveArgsForCall(i int) (context.Context, admindb.Notice) {
+ fake.saveMutex.RLock()
+ defer fake.saveMutex.RUnlock()
+ argsForCall := fake.saveArgsForCall[i]
+ return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakeNoticesService) SaveReturns(result1 int64, result2 error) {
+ fake.saveMutex.Lock()
+ defer fake.saveMutex.Unlock()
+ fake.SaveStub = nil
+ fake.saveReturns = struct {
+ result1 int64
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeNoticesService) SaveReturnsOnCall(i int, result1 int64, result2 error) {
+ fake.saveMutex.Lock()
+ defer fake.saveMutex.Unlock()
+ fake.SaveStub = nil
+ if fake.saveReturnsOnCall == nil {
+ fake.saveReturnsOnCall = make(map[int]struct {
+ result1 int64
+ result2 error
+ })
+ }
+ fake.saveReturnsOnCall[i] = struct {
+ result1 int64
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeNoticesService) Invocations() map[string][][]interface{} {
+ fake.invocationsMutex.RLock()
+ defer fake.invocationsMutex.RUnlock()
+ fake.getByIDMutex.RLock()
+ defer fake.getByIDMutex.RUnlock()
+ fake.removeIDMutex.RLock()
+ defer fake.removeIDMutex.RUnlock()
+ fake.saveMutex.RLock()
+ defer fake.saveMutex.RUnlock()
+ copiedInvocations := map[string][][]interface{}{}
+ for key, value := range fake.invocations {
+ copiedInvocations[key] = value
+ }
+ return copiedInvocations
+}
+
+func (fake *FakeNoticesService) recordInvocation(key string, args []interface{}) {
+ fake.invocationsMutex.Lock()
+ defer fake.invocationsMutex.Unlock()
+ if fake.invocations == nil {
+ fake.invocations = map[string][][]interface{}{}
+ }
+ if fake.invocations[key] == nil {
+ fake.invocations[key] = [][]interface{}{}
+ }
+ fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ admindb.NoticesService = new(FakeNoticesService)
diff --git a/admindb/sqlite/migrations/3-notices.sql b/admindb/sqlite/migrations/3-notices.sql
new file mode 100644
index 0000000..f977b96
--- /dev/null
+++ b/admindb/sqlite/migrations/3-notices.sql
@@ -0,0 +1,44 @@
+-- +migrate Up
+CREATE TABLE notices (
+ id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ title text NOT NULL,
+ content text NOT NULL,
+ language text NOT NULL
+);
+
+CREATE TABLE pinned_notices (
+ name text NOT NULL,
+ notice_id integer NOT NULL UNIQUE,
+ language text NOT NULL,
+
+ PRIMARY KEY (name, language),
+
+ -- make sure the notices exist
+ FOREIGN KEY ( notice_id ) REFERENCES notices( "id" )
+);
+
+INSERT INTO notices (title, content, language) VALUES
+('Description', 'Basic description of this Room.', 'en-GB'),
+('News', 'Some recent updates...', 'en-GB'),
+('Code of conduct', 'We expect each other to ...
+* be considerate
+* be respectful
+* be responsible
+* be dedicated
+* be empathetic
+', 'en-GB'),
+('Privacy Policy', 'To be updated', 'en-GB'),
+('Datenschutzrichtlinien', 'Bitte aktualisieren', 'de-DE'),
+('Beschreibung', 'Allgemeine beschreibung des Raumes.', 'de-DE');
+
+INSERT INTO pinned_notices (name, notice_id, language) VALUES
+('NoticeDescription', 1, 'en-GB'),
+('NoticeNews', 2, 'en-GB'),
+('NoticeCodeOfConduct', 3, 'en-GB'),
+('NoticePrivacyPolicy', 4, 'en-GB'),
+('NoticePrivacyPolicy', 5, 'de-DE'),
+('NoticeDescription', 6, 'de-DE');
+
+-- +migrate Down
+DROP TABLE notices;
+DROP TABLE pinned_notices;
\ No newline at end of file
diff --git a/admindb/sqlite/models/boil_table_names.go b/admindb/sqlite/models/boil_table_names.go
index 074b9c6..838cb84 100644
--- a/admindb/sqlite/models/boil_table_names.go
+++ b/admindb/sqlite/models/boil_table_names.go
@@ -4,9 +4,13 @@
package models
var TableNames = struct {
- AllowList string
- AuthFallback string
+ AllowList string
+ AuthFallback string
+ Notices string
+ PinnedNotices string
}{
- AllowList: "allow_list",
- AuthFallback: "auth_fallback",
+ AllowList: "allow_list",
+ AuthFallback: "auth_fallback",
+ Notices: "notices",
+ PinnedNotices: "pinned_notices",
}
diff --git a/admindb/sqlite/models/notices.go b/admindb/sqlite/models/notices.go
new file mode 100644
index 0000000..0eb26ba
--- /dev/null
+++ b/admindb/sqlite/models/notices.go
@@ -0,0 +1,959 @@
+// Code generated by SQLBoiler 4.4.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT.
+// This file is meant to be re-generated in place and/or deleted at any time.
+
+package models
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "reflect"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/friendsofgo/errors"
+ "github.com/volatiletech/sqlboiler/v4/boil"
+ "github.com/volatiletech/sqlboiler/v4/queries"
+ "github.com/volatiletech/sqlboiler/v4/queries/qm"
+ "github.com/volatiletech/sqlboiler/v4/queries/qmhelper"
+ "github.com/volatiletech/strmangle"
+)
+
+// Notice is an object representing the database table.
+type Notice struct {
+ ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"`
+ Title string `boil:"title" json:"title" toml:"title" yaml:"title"`
+ Content string `boil:"content" json:"content" toml:"content" yaml:"content"`
+ Language string `boil:"language" json:"language" toml:"language" yaml:"language"`
+
+ R *noticeR `boil:"-" json:"-" toml:"-" yaml:"-"`
+ L noticeL `boil:"-" json:"-" toml:"-" yaml:"-"`
+}
+
+var NoticeColumns = struct {
+ ID string
+ Title string
+ Content string
+ Language string
+}{
+ ID: "id",
+ Title: "title",
+ Content: "content",
+ Language: "language",
+}
+
+// Generated where
+
+var NoticeWhere = struct {
+ ID whereHelperint64
+ Title whereHelperstring
+ Content whereHelperstring
+ Language whereHelperstring
+}{
+ ID: whereHelperint64{field: "\"notices\".\"id\""},
+ Title: whereHelperstring{field: "\"notices\".\"title\""},
+ Content: whereHelperstring{field: "\"notices\".\"content\""},
+ Language: whereHelperstring{field: "\"notices\".\"language\""},
+}
+
+// NoticeRels is where relationship names are stored.
+var NoticeRels = struct {
+ PinnedNotice string
+}{
+ PinnedNotice: "PinnedNotice",
+}
+
+// noticeR is where relationships are stored.
+type noticeR struct {
+ PinnedNotice *PinnedNotice `boil:"PinnedNotice" json:"PinnedNotice" toml:"PinnedNotice" yaml:"PinnedNotice"`
+}
+
+// NewStruct creates a new relationship struct
+func (*noticeR) NewStruct() *noticeR {
+ return ¬iceR{}
+}
+
+// noticeL is where Load methods for each relationship are stored.
+type noticeL struct{}
+
+var (
+ noticeAllColumns = []string{"id", "title", "content", "language"}
+ noticeColumnsWithoutDefault = []string{}
+ noticeColumnsWithDefault = []string{"id", "title", "content", "language"}
+ noticePrimaryKeyColumns = []string{"id"}
+)
+
+type (
+ // NoticeSlice is an alias for a slice of pointers to Notice.
+ // This should generally be used opposed to []Notice.
+ NoticeSlice []*Notice
+ // NoticeHook is the signature for custom Notice hook methods
+ NoticeHook func(context.Context, boil.ContextExecutor, *Notice) error
+
+ noticeQuery struct {
+ *queries.Query
+ }
+)
+
+// Cache for insert, update and upsert
+var (
+ noticeType = reflect.TypeOf(&Notice{})
+ noticeMapping = queries.MakeStructMapping(noticeType)
+ noticePrimaryKeyMapping, _ = queries.BindMapping(noticeType, noticeMapping, noticePrimaryKeyColumns)
+ noticeInsertCacheMut sync.RWMutex
+ noticeInsertCache = make(map[string]insertCache)
+ noticeUpdateCacheMut sync.RWMutex
+ noticeUpdateCache = make(map[string]updateCache)
+ noticeUpsertCacheMut sync.RWMutex
+ noticeUpsertCache = make(map[string]insertCache)
+)
+
+var (
+ // Force time package dependency for automated UpdatedAt/CreatedAt.
+ _ = time.Second
+ // Force qmhelper dependency for where clause generation (which doesn't
+ // always happen)
+ _ = qmhelper.Where
+)
+
+var noticeBeforeInsertHooks []NoticeHook
+var noticeBeforeUpdateHooks []NoticeHook
+var noticeBeforeDeleteHooks []NoticeHook
+var noticeBeforeUpsertHooks []NoticeHook
+
+var noticeAfterInsertHooks []NoticeHook
+var noticeAfterSelectHooks []NoticeHook
+var noticeAfterUpdateHooks []NoticeHook
+var noticeAfterDeleteHooks []NoticeHook
+var noticeAfterUpsertHooks []NoticeHook
+
+// doBeforeInsertHooks executes all "before insert" hooks.
+func (o *Notice) doBeforeInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range noticeBeforeInsertHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doBeforeUpdateHooks executes all "before Update" hooks.
+func (o *Notice) doBeforeUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range noticeBeforeUpdateHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doBeforeDeleteHooks executes all "before Delete" hooks.
+func (o *Notice) doBeforeDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range noticeBeforeDeleteHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doBeforeUpsertHooks executes all "before Upsert" hooks.
+func (o *Notice) doBeforeUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range noticeBeforeUpsertHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doAfterInsertHooks executes all "after Insert" hooks.
+func (o *Notice) doAfterInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range noticeAfterInsertHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doAfterSelectHooks executes all "after Select" hooks.
+func (o *Notice) doAfterSelectHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range noticeAfterSelectHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doAfterUpdateHooks executes all "after Update" hooks.
+func (o *Notice) doAfterUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range noticeAfterUpdateHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doAfterDeleteHooks executes all "after Delete" hooks.
+func (o *Notice) doAfterDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range noticeAfterDeleteHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doAfterUpsertHooks executes all "after Upsert" hooks.
+func (o *Notice) doAfterUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range noticeAfterUpsertHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// AddNoticeHook registers your hook function for all future operations.
+func AddNoticeHook(hookPoint boil.HookPoint, noticeHook NoticeHook) {
+ switch hookPoint {
+ case boil.BeforeInsertHook:
+ noticeBeforeInsertHooks = append(noticeBeforeInsertHooks, noticeHook)
+ case boil.BeforeUpdateHook:
+ noticeBeforeUpdateHooks = append(noticeBeforeUpdateHooks, noticeHook)
+ case boil.BeforeDeleteHook:
+ noticeBeforeDeleteHooks = append(noticeBeforeDeleteHooks, noticeHook)
+ case boil.BeforeUpsertHook:
+ noticeBeforeUpsertHooks = append(noticeBeforeUpsertHooks, noticeHook)
+ case boil.AfterInsertHook:
+ noticeAfterInsertHooks = append(noticeAfterInsertHooks, noticeHook)
+ case boil.AfterSelectHook:
+ noticeAfterSelectHooks = append(noticeAfterSelectHooks, noticeHook)
+ case boil.AfterUpdateHook:
+ noticeAfterUpdateHooks = append(noticeAfterUpdateHooks, noticeHook)
+ case boil.AfterDeleteHook:
+ noticeAfterDeleteHooks = append(noticeAfterDeleteHooks, noticeHook)
+ case boil.AfterUpsertHook:
+ noticeAfterUpsertHooks = append(noticeAfterUpsertHooks, noticeHook)
+ }
+}
+
+// One returns a single notice record from the query.
+func (q noticeQuery) One(ctx context.Context, exec boil.ContextExecutor) (*Notice, error) {
+ o := &Notice{}
+
+ queries.SetLimit(q.Query, 1)
+
+ err := q.Bind(ctx, exec, o)
+ if err != nil {
+ if errors.Cause(err) == sql.ErrNoRows {
+ return nil, sql.ErrNoRows
+ }
+ return nil, errors.Wrap(err, "models: failed to execute a one query for notices")
+ }
+
+ if err := o.doAfterSelectHooks(ctx, exec); err != nil {
+ return o, err
+ }
+
+ return o, nil
+}
+
+// All returns all Notice records from the query.
+func (q noticeQuery) All(ctx context.Context, exec boil.ContextExecutor) (NoticeSlice, error) {
+ var o []*Notice
+
+ err := q.Bind(ctx, exec, &o)
+ if err != nil {
+ return nil, errors.Wrap(err, "models: failed to assign all query results to Notice slice")
+ }
+
+ if len(noticeAfterSelectHooks) != 0 {
+ for _, obj := range o {
+ if err := obj.doAfterSelectHooks(ctx, exec); err != nil {
+ return o, err
+ }
+ }
+ }
+
+ return o, nil
+}
+
+// Count returns the count of all Notice records in the query.
+func (q noticeQuery) Count(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
+ var count int64
+
+ queries.SetSelect(q.Query, nil)
+ queries.SetCount(q.Query)
+
+ err := q.Query.QueryRowContext(ctx, exec).Scan(&count)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: failed to count notices rows")
+ }
+
+ return count, nil
+}
+
+// Exists checks if the row exists in the table.
+func (q noticeQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) {
+ var count int64
+
+ queries.SetSelect(q.Query, nil)
+ queries.SetCount(q.Query)
+ queries.SetLimit(q.Query, 1)
+
+ err := q.Query.QueryRowContext(ctx, exec).Scan(&count)
+ if err != nil {
+ return false, errors.Wrap(err, "models: failed to check if notices exists")
+ }
+
+ return count > 0, nil
+}
+
+// PinnedNotice pointed to by the foreign key.
+func (o *Notice) PinnedNotice(mods ...qm.QueryMod) pinnedNoticeQuery {
+ queryMods := []qm.QueryMod{
+ qm.Where("\"notice_id\" = ?", o.ID),
+ }
+
+ queryMods = append(queryMods, mods...)
+
+ query := PinnedNotices(queryMods...)
+ queries.SetFrom(query.Query, "\"pinned_notices\"")
+
+ return query
+}
+
+// LoadPinnedNotice allows an eager lookup of values, cached into the
+// loaded structs of the objects. This is for a 1-1 relationship.
+func (noticeL) LoadPinnedNotice(ctx context.Context, e boil.ContextExecutor, singular bool, maybeNotice interface{}, mods queries.Applicator) error {
+ var slice []*Notice
+ var object *Notice
+
+ if singular {
+ object = maybeNotice.(*Notice)
+ } else {
+ slice = *maybeNotice.(*[]*Notice)
+ }
+
+ args := make([]interface{}, 0, 1)
+ if singular {
+ if object.R == nil {
+ object.R = ¬iceR{}
+ }
+ args = append(args, object.ID)
+ } else {
+ Outer:
+ for _, obj := range slice {
+ if obj.R == nil {
+ obj.R = ¬iceR{}
+ }
+
+ for _, a := range args {
+ if a == obj.ID {
+ continue Outer
+ }
+ }
+
+ args = append(args, obj.ID)
+ }
+ }
+
+ if len(args) == 0 {
+ return nil
+ }
+
+ query := NewQuery(
+ qm.From(`pinned_notices`),
+ qm.WhereIn(`pinned_notices.notice_id in ?`, args...),
+ )
+ if mods != nil {
+ mods.Apply(query)
+ }
+
+ results, err := query.QueryContext(ctx, e)
+ if err != nil {
+ return errors.Wrap(err, "failed to eager load PinnedNotice")
+ }
+
+ var resultSlice []*PinnedNotice
+ if err = queries.Bind(results, &resultSlice); err != nil {
+ return errors.Wrap(err, "failed to bind eager loaded slice PinnedNotice")
+ }
+
+ if err = results.Close(); err != nil {
+ return errors.Wrap(err, "failed to close results of eager load for pinned_notices")
+ }
+ if err = results.Err(); err != nil {
+ return errors.Wrap(err, "error occurred during iteration of eager loaded relations for pinned_notices")
+ }
+
+ if len(noticeAfterSelectHooks) != 0 {
+ for _, obj := range resultSlice {
+ if err := obj.doAfterSelectHooks(ctx, e); err != nil {
+ return err
+ }
+ }
+ }
+
+ if len(resultSlice) == 0 {
+ return nil
+ }
+
+ if singular {
+ foreign := resultSlice[0]
+ object.R.PinnedNotice = foreign
+ if foreign.R == nil {
+ foreign.R = &pinnedNoticeR{}
+ }
+ foreign.R.Notice = object
+ }
+
+ for _, local := range slice {
+ for _, foreign := range resultSlice {
+ if local.ID == foreign.NoticeID {
+ local.R.PinnedNotice = foreign
+ if foreign.R == nil {
+ foreign.R = &pinnedNoticeR{}
+ }
+ foreign.R.Notice = local
+ break
+ }
+ }
+ }
+
+ return nil
+}
+
+// SetPinnedNotice of the notice to the related item.
+// Sets o.R.PinnedNotice to related.
+// Adds o to related.R.Notice.
+func (o *Notice) SetPinnedNotice(ctx context.Context, exec boil.ContextExecutor, insert bool, related *PinnedNotice) error {
+ var err error
+
+ if insert {
+ related.NoticeID = o.ID
+
+ if err = related.Insert(ctx, exec, boil.Infer()); err != nil {
+ return errors.Wrap(err, "failed to insert into foreign table")
+ }
+ } else {
+ updateQuery := fmt.Sprintf(
+ "UPDATE \"pinned_notices\" SET %s WHERE %s",
+ strmangle.SetParamNames("\"", "\"", 0, []string{"notice_id"}),
+ strmangle.WhereClause("\"", "\"", 0, pinnedNoticePrimaryKeyColumns),
+ )
+ values := []interface{}{o.ID, related.Name, related.Language}
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, updateQuery)
+ fmt.Fprintln(writer, values)
+ }
+ if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil {
+ return errors.Wrap(err, "failed to update foreign table")
+ }
+
+ related.NoticeID = o.ID
+
+ }
+
+ if o.R == nil {
+ o.R = ¬iceR{
+ PinnedNotice: related,
+ }
+ } else {
+ o.R.PinnedNotice = related
+ }
+
+ if related.R == nil {
+ related.R = &pinnedNoticeR{
+ Notice: o,
+ }
+ } else {
+ related.R.Notice = o
+ }
+ return nil
+}
+
+// Notices retrieves all the records using an executor.
+func Notices(mods ...qm.QueryMod) noticeQuery {
+ mods = append(mods, qm.From("\"notices\""))
+ return noticeQuery{NewQuery(mods...)}
+}
+
+// FindNotice retrieves a single record by ID with an executor.
+// If selectCols is empty Find will return all columns.
+func FindNotice(ctx context.Context, exec boil.ContextExecutor, iD int64, selectCols ...string) (*Notice, error) {
+ noticeObj := &Notice{}
+
+ sel := "*"
+ if len(selectCols) > 0 {
+ sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",")
+ }
+ query := fmt.Sprintf(
+ "select %s from \"notices\" where \"id\"=?", sel,
+ )
+
+ q := queries.Raw(query, iD)
+
+ err := q.Bind(ctx, exec, noticeObj)
+ if err != nil {
+ if errors.Cause(err) == sql.ErrNoRows {
+ return nil, sql.ErrNoRows
+ }
+ return nil, errors.Wrap(err, "models: unable to select from notices")
+ }
+
+ return noticeObj, nil
+}
+
+// Insert a single record using an executor.
+// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts.
+func (o *Notice) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error {
+ if o == nil {
+ return errors.New("models: no notices provided for insertion")
+ }
+
+ var err error
+
+ if err := o.doBeforeInsertHooks(ctx, exec); err != nil {
+ return err
+ }
+
+ nzDefaults := queries.NonZeroDefaultSet(noticeColumnsWithDefault, o)
+
+ key := makeCacheKey(columns, nzDefaults)
+ noticeInsertCacheMut.RLock()
+ cache, cached := noticeInsertCache[key]
+ noticeInsertCacheMut.RUnlock()
+
+ if !cached {
+ wl, returnColumns := columns.InsertColumnSet(
+ noticeAllColumns,
+ noticeColumnsWithDefault,
+ noticeColumnsWithoutDefault,
+ nzDefaults,
+ )
+
+ cache.valueMapping, err = queries.BindMapping(noticeType, noticeMapping, wl)
+ if err != nil {
+ return err
+ }
+ cache.retMapping, err = queries.BindMapping(noticeType, noticeMapping, returnColumns)
+ if err != nil {
+ return err
+ }
+ if len(wl) != 0 {
+ cache.query = fmt.Sprintf("INSERT INTO \"notices\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1))
+ } else {
+ cache.query = "INSERT INTO \"notices\" %sDEFAULT VALUES%s"
+ }
+
+ var queryOutput, queryReturning string
+
+ if len(cache.retMapping) != 0 {
+ cache.retQuery = fmt.Sprintf("SELECT \"%s\" FROM \"notices\" WHERE %s", strings.Join(returnColumns, "\",\""), strmangle.WhereClause("\"", "\"", 0, noticePrimaryKeyColumns))
+ }
+
+ cache.query = fmt.Sprintf(cache.query, queryOutput, queryReturning)
+ }
+
+ value := reflect.Indirect(reflect.ValueOf(o))
+ vals := queries.ValuesFromMapping(value, cache.valueMapping)
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, cache.query)
+ fmt.Fprintln(writer, vals)
+ }
+ result, err := exec.ExecContext(ctx, cache.query, vals...)
+
+ if err != nil {
+ return errors.Wrap(err, "models: unable to insert into notices")
+ }
+
+ var lastID int64
+ var identifierCols []interface{}
+
+ if len(cache.retMapping) == 0 {
+ goto CacheNoHooks
+ }
+
+ lastID, err = result.LastInsertId()
+ if err != nil {
+ return ErrSyncFail
+ }
+
+ o.ID = int64(lastID)
+ if lastID != 0 && len(cache.retMapping) == 1 && cache.retMapping[0] == noticeMapping["id"] {
+ goto CacheNoHooks
+ }
+
+ identifierCols = []interface{}{
+ o.ID,
+ }
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, cache.retQuery)
+ fmt.Fprintln(writer, identifierCols...)
+ }
+ err = exec.QueryRowContext(ctx, cache.retQuery, identifierCols...).Scan(queries.PtrsFromMapping(value, cache.retMapping)...)
+ if err != nil {
+ return errors.Wrap(err, "models: unable to populate default values for notices")
+ }
+
+CacheNoHooks:
+ if !cached {
+ noticeInsertCacheMut.Lock()
+ noticeInsertCache[key] = cache
+ noticeInsertCacheMut.Unlock()
+ }
+
+ return o.doAfterInsertHooks(ctx, exec)
+}
+
+// Update uses an executor to update the Notice.
+// See boil.Columns.UpdateColumnSet documentation to understand column list inference for updates.
+// Update does not automatically update the record in case of default values. Use .Reload() to refresh the records.
+func (o *Notice) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) {
+ var err error
+ if err = o.doBeforeUpdateHooks(ctx, exec); err != nil {
+ return 0, err
+ }
+ key := makeCacheKey(columns, nil)
+ noticeUpdateCacheMut.RLock()
+ cache, cached := noticeUpdateCache[key]
+ noticeUpdateCacheMut.RUnlock()
+
+ if !cached {
+ wl := columns.UpdateColumnSet(
+ noticeAllColumns,
+ noticePrimaryKeyColumns,
+ )
+
+ if !columns.IsWhitelist() {
+ wl = strmangle.SetComplement(wl, []string{"created_at"})
+ }
+ if len(wl) == 0 {
+ return 0, errors.New("models: unable to update notices, could not build whitelist")
+ }
+
+ cache.query = fmt.Sprintf("UPDATE \"notices\" SET %s WHERE %s",
+ strmangle.SetParamNames("\"", "\"", 0, wl),
+ strmangle.WhereClause("\"", "\"", 0, noticePrimaryKeyColumns),
+ )
+ cache.valueMapping, err = queries.BindMapping(noticeType, noticeMapping, append(wl, noticePrimaryKeyColumns...))
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ values := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), cache.valueMapping)
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, cache.query)
+ fmt.Fprintln(writer, values)
+ }
+ var result sql.Result
+ result, err = exec.ExecContext(ctx, cache.query, values...)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to update notices row")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: failed to get rows affected by update for notices")
+ }
+
+ if !cached {
+ noticeUpdateCacheMut.Lock()
+ noticeUpdateCache[key] = cache
+ noticeUpdateCacheMut.Unlock()
+ }
+
+ return rowsAff, o.doAfterUpdateHooks(ctx, exec)
+}
+
+// UpdateAll updates all rows with the specified column values.
+func (q noticeQuery) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) {
+ queries.SetUpdate(q.Query, cols)
+
+ result, err := q.Query.ExecContext(ctx, exec)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to update all for notices")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to retrieve rows affected for notices")
+ }
+
+ return rowsAff, nil
+}
+
+// UpdateAll updates all rows with the specified column values, using an executor.
+func (o NoticeSlice) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) {
+ ln := int64(len(o))
+ if ln == 0 {
+ return 0, nil
+ }
+
+ if len(cols) == 0 {
+ return 0, errors.New("models: update all requires at least one column argument")
+ }
+
+ colNames := make([]string, len(cols))
+ args := make([]interface{}, len(cols))
+
+ i := 0
+ for name, value := range cols {
+ colNames[i] = name
+ args[i] = value
+ i++
+ }
+
+ // Append all of the primary key values for each column
+ for _, obj := range o {
+ pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), noticePrimaryKeyMapping)
+ args = append(args, pkeyArgs...)
+ }
+
+ sql := fmt.Sprintf("UPDATE \"notices\" SET %s WHERE %s",
+ strmangle.SetParamNames("\"", "\"", 0, colNames),
+ strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, noticePrimaryKeyColumns, len(o)))
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, sql)
+ fmt.Fprintln(writer, args...)
+ }
+ result, err := exec.ExecContext(ctx, sql, args...)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to update all in notice slice")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to retrieve rows affected all in update all notice")
+ }
+ return rowsAff, nil
+}
+
+// Delete deletes a single Notice record with an executor.
+// Delete will match against the primary key column to find the record to delete.
+func (o *Notice) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
+ if o == nil {
+ return 0, errors.New("models: no Notice provided for delete")
+ }
+
+ if err := o.doBeforeDeleteHooks(ctx, exec); err != nil {
+ return 0, err
+ }
+
+ args := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), noticePrimaryKeyMapping)
+ sql := "DELETE FROM \"notices\" WHERE \"id\"=?"
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, sql)
+ fmt.Fprintln(writer, args...)
+ }
+ result, err := exec.ExecContext(ctx, sql, args...)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to delete from notices")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: failed to get rows affected by delete for notices")
+ }
+
+ if err := o.doAfterDeleteHooks(ctx, exec); err != nil {
+ return 0, err
+ }
+
+ return rowsAff, nil
+}
+
+// DeleteAll deletes all matching rows.
+func (q noticeQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
+ if q.Query == nil {
+ return 0, errors.New("models: no noticeQuery provided for delete all")
+ }
+
+ queries.SetDelete(q.Query)
+
+ result, err := q.Query.ExecContext(ctx, exec)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to delete all from notices")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for notices")
+ }
+
+ return rowsAff, nil
+}
+
+// DeleteAll deletes all rows in the slice, using an executor.
+func (o NoticeSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
+ if len(o) == 0 {
+ return 0, nil
+ }
+
+ if len(noticeBeforeDeleteHooks) != 0 {
+ for _, obj := range o {
+ if err := obj.doBeforeDeleteHooks(ctx, exec); err != nil {
+ return 0, err
+ }
+ }
+ }
+
+ var args []interface{}
+ for _, obj := range o {
+ pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), noticePrimaryKeyMapping)
+ args = append(args, pkeyArgs...)
+ }
+
+ sql := "DELETE FROM \"notices\" WHERE " +
+ strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, noticePrimaryKeyColumns, len(o))
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, sql)
+ fmt.Fprintln(writer, args)
+ }
+ result, err := exec.ExecContext(ctx, sql, args...)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to delete all from notice slice")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for notices")
+ }
+
+ if len(noticeAfterDeleteHooks) != 0 {
+ for _, obj := range o {
+ if err := obj.doAfterDeleteHooks(ctx, exec); err != nil {
+ return 0, err
+ }
+ }
+ }
+
+ return rowsAff, nil
+}
+
+// Reload refetches the object from the database
+// using the primary keys with an executor.
+func (o *Notice) Reload(ctx context.Context, exec boil.ContextExecutor) error {
+ ret, err := FindNotice(ctx, exec, o.ID)
+ if err != nil {
+ return err
+ }
+
+ *o = *ret
+ return nil
+}
+
+// ReloadAll refetches every row with matching primary key column values
+// and overwrites the original object slice with the newly updated slice.
+func (o *NoticeSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error {
+ if o == nil || len(*o) == 0 {
+ return nil
+ }
+
+ slice := NoticeSlice{}
+ var args []interface{}
+ for _, obj := range *o {
+ pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), noticePrimaryKeyMapping)
+ args = append(args, pkeyArgs...)
+ }
+
+ sql := "SELECT \"notices\".* FROM \"notices\" WHERE " +
+ strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, noticePrimaryKeyColumns, len(*o))
+
+ q := queries.Raw(sql, args...)
+
+ err := q.Bind(ctx, exec, &slice)
+ if err != nil {
+ return errors.Wrap(err, "models: unable to reload all in NoticeSlice")
+ }
+
+ *o = slice
+
+ return nil
+}
+
+// NoticeExists checks if the Notice row exists.
+func NoticeExists(ctx context.Context, exec boil.ContextExecutor, iD int64) (bool, error) {
+ var exists bool
+ sql := "select exists(select 1 from \"notices\" where \"id\"=? limit 1)"
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, sql)
+ fmt.Fprintln(writer, iD)
+ }
+ row := exec.QueryRowContext(ctx, sql, iD)
+
+ err := row.Scan(&exists)
+ if err != nil {
+ return false, errors.Wrap(err, "models: unable to check if notices exists")
+ }
+
+ return exists, nil
+}
diff --git a/admindb/sqlite/models/pinned_notices.go b/admindb/sqlite/models/pinned_notices.go
new file mode 100644
index 0000000..38e46d0
--- /dev/null
+++ b/admindb/sqlite/models/pinned_notices.go
@@ -0,0 +1,943 @@
+// Code generated by SQLBoiler 4.4.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT.
+// This file is meant to be re-generated in place and/or deleted at any time.
+
+package models
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "reflect"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/friendsofgo/errors"
+ "github.com/volatiletech/sqlboiler/v4/boil"
+ "github.com/volatiletech/sqlboiler/v4/queries"
+ "github.com/volatiletech/sqlboiler/v4/queries/qm"
+ "github.com/volatiletech/sqlboiler/v4/queries/qmhelper"
+ "github.com/volatiletech/strmangle"
+)
+
+// PinnedNotice is an object representing the database table.
+type PinnedNotice struct {
+ Name string `boil:"name" json:"name" toml:"name" yaml:"name"`
+ NoticeID int64 `boil:"notice_id" json:"notice_id" toml:"notice_id" yaml:"notice_id"`
+ Language string `boil:"language" json:"language" toml:"language" yaml:"language"`
+
+ R *pinnedNoticeR `boil:"-" json:"-" toml:"-" yaml:"-"`
+ L pinnedNoticeL `boil:"-" json:"-" toml:"-" yaml:"-"`
+}
+
+var PinnedNoticeColumns = struct {
+ Name string
+ NoticeID string
+ Language string
+}{
+ Name: "name",
+ NoticeID: "notice_id",
+ Language: "language",
+}
+
+// Generated where
+
+var PinnedNoticeWhere = struct {
+ Name whereHelperstring
+ NoticeID whereHelperint64
+ Language whereHelperstring
+}{
+ Name: whereHelperstring{field: "\"pinned_notices\".\"name\""},
+ NoticeID: whereHelperint64{field: "\"pinned_notices\".\"notice_id\""},
+ Language: whereHelperstring{field: "\"pinned_notices\".\"language\""},
+}
+
+// PinnedNoticeRels is where relationship names are stored.
+var PinnedNoticeRels = struct {
+ Notice string
+}{
+ Notice: "Notice",
+}
+
+// pinnedNoticeR is where relationships are stored.
+type pinnedNoticeR struct {
+ Notice *Notice `boil:"Notice" json:"Notice" toml:"Notice" yaml:"Notice"`
+}
+
+// NewStruct creates a new relationship struct
+func (*pinnedNoticeR) NewStruct() *pinnedNoticeR {
+ return &pinnedNoticeR{}
+}
+
+// pinnedNoticeL is where Load methods for each relationship are stored.
+type pinnedNoticeL struct{}
+
+var (
+ pinnedNoticeAllColumns = []string{"name", "notice_id", "language"}
+ pinnedNoticeColumnsWithoutDefault = []string{"name", "notice_id", "language"}
+ pinnedNoticeColumnsWithDefault = []string{}
+ pinnedNoticePrimaryKeyColumns = []string{"name", "language"}
+)
+
+type (
+ // PinnedNoticeSlice is an alias for a slice of pointers to PinnedNotice.
+ // This should generally be used opposed to []PinnedNotice.
+ PinnedNoticeSlice []*PinnedNotice
+ // PinnedNoticeHook is the signature for custom PinnedNotice hook methods
+ PinnedNoticeHook func(context.Context, boil.ContextExecutor, *PinnedNotice) error
+
+ pinnedNoticeQuery struct {
+ *queries.Query
+ }
+)
+
+// Cache for insert, update and upsert
+var (
+ pinnedNoticeType = reflect.TypeOf(&PinnedNotice{})
+ pinnedNoticeMapping = queries.MakeStructMapping(pinnedNoticeType)
+ pinnedNoticePrimaryKeyMapping, _ = queries.BindMapping(pinnedNoticeType, pinnedNoticeMapping, pinnedNoticePrimaryKeyColumns)
+ pinnedNoticeInsertCacheMut sync.RWMutex
+ pinnedNoticeInsertCache = make(map[string]insertCache)
+ pinnedNoticeUpdateCacheMut sync.RWMutex
+ pinnedNoticeUpdateCache = make(map[string]updateCache)
+ pinnedNoticeUpsertCacheMut sync.RWMutex
+ pinnedNoticeUpsertCache = make(map[string]insertCache)
+)
+
+var (
+ // Force time package dependency for automated UpdatedAt/CreatedAt.
+ _ = time.Second
+ // Force qmhelper dependency for where clause generation (which doesn't
+ // always happen)
+ _ = qmhelper.Where
+)
+
+var pinnedNoticeBeforeInsertHooks []PinnedNoticeHook
+var pinnedNoticeBeforeUpdateHooks []PinnedNoticeHook
+var pinnedNoticeBeforeDeleteHooks []PinnedNoticeHook
+var pinnedNoticeBeforeUpsertHooks []PinnedNoticeHook
+
+var pinnedNoticeAfterInsertHooks []PinnedNoticeHook
+var pinnedNoticeAfterSelectHooks []PinnedNoticeHook
+var pinnedNoticeAfterUpdateHooks []PinnedNoticeHook
+var pinnedNoticeAfterDeleteHooks []PinnedNoticeHook
+var pinnedNoticeAfterUpsertHooks []PinnedNoticeHook
+
+// doBeforeInsertHooks executes all "before insert" hooks.
+func (o *PinnedNotice) doBeforeInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range pinnedNoticeBeforeInsertHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doBeforeUpdateHooks executes all "before Update" hooks.
+func (o *PinnedNotice) doBeforeUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range pinnedNoticeBeforeUpdateHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doBeforeDeleteHooks executes all "before Delete" hooks.
+func (o *PinnedNotice) doBeforeDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range pinnedNoticeBeforeDeleteHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doBeforeUpsertHooks executes all "before Upsert" hooks.
+func (o *PinnedNotice) doBeforeUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range pinnedNoticeBeforeUpsertHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doAfterInsertHooks executes all "after Insert" hooks.
+func (o *PinnedNotice) doAfterInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range pinnedNoticeAfterInsertHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doAfterSelectHooks executes all "after Select" hooks.
+func (o *PinnedNotice) doAfterSelectHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range pinnedNoticeAfterSelectHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doAfterUpdateHooks executes all "after Update" hooks.
+func (o *PinnedNotice) doAfterUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range pinnedNoticeAfterUpdateHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doAfterDeleteHooks executes all "after Delete" hooks.
+func (o *PinnedNotice) doAfterDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range pinnedNoticeAfterDeleteHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// doAfterUpsertHooks executes all "after Upsert" hooks.
+func (o *PinnedNotice) doAfterUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) {
+ if boil.HooksAreSkipped(ctx) {
+ return nil
+ }
+
+ for _, hook := range pinnedNoticeAfterUpsertHooks {
+ if err := hook(ctx, exec, o); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// AddPinnedNoticeHook registers your hook function for all future operations.
+func AddPinnedNoticeHook(hookPoint boil.HookPoint, pinnedNoticeHook PinnedNoticeHook) {
+ switch hookPoint {
+ case boil.BeforeInsertHook:
+ pinnedNoticeBeforeInsertHooks = append(pinnedNoticeBeforeInsertHooks, pinnedNoticeHook)
+ case boil.BeforeUpdateHook:
+ pinnedNoticeBeforeUpdateHooks = append(pinnedNoticeBeforeUpdateHooks, pinnedNoticeHook)
+ case boil.BeforeDeleteHook:
+ pinnedNoticeBeforeDeleteHooks = append(pinnedNoticeBeforeDeleteHooks, pinnedNoticeHook)
+ case boil.BeforeUpsertHook:
+ pinnedNoticeBeforeUpsertHooks = append(pinnedNoticeBeforeUpsertHooks, pinnedNoticeHook)
+ case boil.AfterInsertHook:
+ pinnedNoticeAfterInsertHooks = append(pinnedNoticeAfterInsertHooks, pinnedNoticeHook)
+ case boil.AfterSelectHook:
+ pinnedNoticeAfterSelectHooks = append(pinnedNoticeAfterSelectHooks, pinnedNoticeHook)
+ case boil.AfterUpdateHook:
+ pinnedNoticeAfterUpdateHooks = append(pinnedNoticeAfterUpdateHooks, pinnedNoticeHook)
+ case boil.AfterDeleteHook:
+ pinnedNoticeAfterDeleteHooks = append(pinnedNoticeAfterDeleteHooks, pinnedNoticeHook)
+ case boil.AfterUpsertHook:
+ pinnedNoticeAfterUpsertHooks = append(pinnedNoticeAfterUpsertHooks, pinnedNoticeHook)
+ }
+}
+
+// One returns a single pinnedNotice record from the query.
+func (q pinnedNoticeQuery) One(ctx context.Context, exec boil.ContextExecutor) (*PinnedNotice, error) {
+ o := &PinnedNotice{}
+
+ queries.SetLimit(q.Query, 1)
+
+ err := q.Bind(ctx, exec, o)
+ if err != nil {
+ if errors.Cause(err) == sql.ErrNoRows {
+ return nil, sql.ErrNoRows
+ }
+ return nil, errors.Wrap(err, "models: failed to execute a one query for pinned_notices")
+ }
+
+ if err := o.doAfterSelectHooks(ctx, exec); err != nil {
+ return o, err
+ }
+
+ return o, nil
+}
+
+// All returns all PinnedNotice records from the query.
+func (q pinnedNoticeQuery) All(ctx context.Context, exec boil.ContextExecutor) (PinnedNoticeSlice, error) {
+ var o []*PinnedNotice
+
+ err := q.Bind(ctx, exec, &o)
+ if err != nil {
+ return nil, errors.Wrap(err, "models: failed to assign all query results to PinnedNotice slice")
+ }
+
+ if len(pinnedNoticeAfterSelectHooks) != 0 {
+ for _, obj := range o {
+ if err := obj.doAfterSelectHooks(ctx, exec); err != nil {
+ return o, err
+ }
+ }
+ }
+
+ return o, nil
+}
+
+// Count returns the count of all PinnedNotice records in the query.
+func (q pinnedNoticeQuery) Count(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
+ var count int64
+
+ queries.SetSelect(q.Query, nil)
+ queries.SetCount(q.Query)
+
+ err := q.Query.QueryRowContext(ctx, exec).Scan(&count)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: failed to count pinned_notices rows")
+ }
+
+ return count, nil
+}
+
+// Exists checks if the row exists in the table.
+func (q pinnedNoticeQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) {
+ var count int64
+
+ queries.SetSelect(q.Query, nil)
+ queries.SetCount(q.Query)
+ queries.SetLimit(q.Query, 1)
+
+ err := q.Query.QueryRowContext(ctx, exec).Scan(&count)
+ if err != nil {
+ return false, errors.Wrap(err, "models: failed to check if pinned_notices exists")
+ }
+
+ return count > 0, nil
+}
+
+// Notice pointed to by the foreign key.
+func (o *PinnedNotice) Notice(mods ...qm.QueryMod) noticeQuery {
+ queryMods := []qm.QueryMod{
+ qm.Where("\"id\" = ?", o.NoticeID),
+ }
+
+ queryMods = append(queryMods, mods...)
+
+ query := Notices(queryMods...)
+ queries.SetFrom(query.Query, "\"notices\"")
+
+ return query
+}
+
+// LoadNotice allows an eager lookup of values, cached into the
+// loaded structs of the objects. This is for an N-1 relationship.
+func (pinnedNoticeL) LoadNotice(ctx context.Context, e boil.ContextExecutor, singular bool, maybePinnedNotice interface{}, mods queries.Applicator) error {
+ var slice []*PinnedNotice
+ var object *PinnedNotice
+
+ if singular {
+ object = maybePinnedNotice.(*PinnedNotice)
+ } else {
+ slice = *maybePinnedNotice.(*[]*PinnedNotice)
+ }
+
+ args := make([]interface{}, 0, 1)
+ if singular {
+ if object.R == nil {
+ object.R = &pinnedNoticeR{}
+ }
+ args = append(args, object.NoticeID)
+
+ } else {
+ Outer:
+ for _, obj := range slice {
+ if obj.R == nil {
+ obj.R = &pinnedNoticeR{}
+ }
+
+ for _, a := range args {
+ if a == obj.NoticeID {
+ continue Outer
+ }
+ }
+
+ args = append(args, obj.NoticeID)
+
+ }
+ }
+
+ if len(args) == 0 {
+ return nil
+ }
+
+ query := NewQuery(
+ qm.From(`notices`),
+ qm.WhereIn(`notices.id in ?`, args...),
+ )
+ if mods != nil {
+ mods.Apply(query)
+ }
+
+ results, err := query.QueryContext(ctx, e)
+ if err != nil {
+ return errors.Wrap(err, "failed to eager load Notice")
+ }
+
+ var resultSlice []*Notice
+ if err = queries.Bind(results, &resultSlice); err != nil {
+ return errors.Wrap(err, "failed to bind eager loaded slice Notice")
+ }
+
+ if err = results.Close(); err != nil {
+ return errors.Wrap(err, "failed to close results of eager load for notices")
+ }
+ if err = results.Err(); err != nil {
+ return errors.Wrap(err, "error occurred during iteration of eager loaded relations for notices")
+ }
+
+ if len(pinnedNoticeAfterSelectHooks) != 0 {
+ for _, obj := range resultSlice {
+ if err := obj.doAfterSelectHooks(ctx, e); err != nil {
+ return err
+ }
+ }
+ }
+
+ if len(resultSlice) == 0 {
+ return nil
+ }
+
+ if singular {
+ foreign := resultSlice[0]
+ object.R.Notice = foreign
+ if foreign.R == nil {
+ foreign.R = ¬iceR{}
+ }
+ foreign.R.PinnedNotice = object
+ return nil
+ }
+
+ for _, local := range slice {
+ for _, foreign := range resultSlice {
+ if local.NoticeID == foreign.ID {
+ local.R.Notice = foreign
+ if foreign.R == nil {
+ foreign.R = ¬iceR{}
+ }
+ foreign.R.PinnedNotice = local
+ break
+ }
+ }
+ }
+
+ return nil
+}
+
+// SetNotice of the pinnedNotice to the related item.
+// Sets o.R.Notice to related.
+// Adds o to related.R.PinnedNotice.
+func (o *PinnedNotice) SetNotice(ctx context.Context, exec boil.ContextExecutor, insert bool, related *Notice) error {
+ var err error
+ if insert {
+ if err = related.Insert(ctx, exec, boil.Infer()); err != nil {
+ return errors.Wrap(err, "failed to insert into foreign table")
+ }
+ }
+
+ updateQuery := fmt.Sprintf(
+ "UPDATE \"pinned_notices\" SET %s WHERE %s",
+ strmangle.SetParamNames("\"", "\"", 0, []string{"notice_id"}),
+ strmangle.WhereClause("\"", "\"", 0, pinnedNoticePrimaryKeyColumns),
+ )
+ values := []interface{}{related.ID, o.Name, o.Language}
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, updateQuery)
+ fmt.Fprintln(writer, values)
+ }
+ if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil {
+ return errors.Wrap(err, "failed to update local table")
+ }
+
+ o.NoticeID = related.ID
+ if o.R == nil {
+ o.R = &pinnedNoticeR{
+ Notice: related,
+ }
+ } else {
+ o.R.Notice = related
+ }
+
+ if related.R == nil {
+ related.R = ¬iceR{
+ PinnedNotice: o,
+ }
+ } else {
+ related.R.PinnedNotice = o
+ }
+
+ return nil
+}
+
+// PinnedNotices retrieves all the records using an executor.
+func PinnedNotices(mods ...qm.QueryMod) pinnedNoticeQuery {
+ mods = append(mods, qm.From("\"pinned_notices\""))
+ return pinnedNoticeQuery{NewQuery(mods...)}
+}
+
+// FindPinnedNotice retrieves a single record by ID with an executor.
+// If selectCols is empty Find will return all columns.
+func FindPinnedNotice(ctx context.Context, exec boil.ContextExecutor, name string, language string, selectCols ...string) (*PinnedNotice, error) {
+ pinnedNoticeObj := &PinnedNotice{}
+
+ sel := "*"
+ if len(selectCols) > 0 {
+ sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",")
+ }
+ query := fmt.Sprintf(
+ "select %s from \"pinned_notices\" where \"name\"=? AND \"language\"=?", sel,
+ )
+
+ q := queries.Raw(query, name, language)
+
+ err := q.Bind(ctx, exec, pinnedNoticeObj)
+ if err != nil {
+ if errors.Cause(err) == sql.ErrNoRows {
+ return nil, sql.ErrNoRows
+ }
+ return nil, errors.Wrap(err, "models: unable to select from pinned_notices")
+ }
+
+ return pinnedNoticeObj, nil
+}
+
+// Insert a single record using an executor.
+// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts.
+func (o *PinnedNotice) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error {
+ if o == nil {
+ return errors.New("models: no pinned_notices provided for insertion")
+ }
+
+ var err error
+
+ if err := o.doBeforeInsertHooks(ctx, exec); err != nil {
+ return err
+ }
+
+ nzDefaults := queries.NonZeroDefaultSet(pinnedNoticeColumnsWithDefault, o)
+
+ key := makeCacheKey(columns, nzDefaults)
+ pinnedNoticeInsertCacheMut.RLock()
+ cache, cached := pinnedNoticeInsertCache[key]
+ pinnedNoticeInsertCacheMut.RUnlock()
+
+ if !cached {
+ wl, returnColumns := columns.InsertColumnSet(
+ pinnedNoticeAllColumns,
+ pinnedNoticeColumnsWithDefault,
+ pinnedNoticeColumnsWithoutDefault,
+ nzDefaults,
+ )
+
+ cache.valueMapping, err = queries.BindMapping(pinnedNoticeType, pinnedNoticeMapping, wl)
+ if err != nil {
+ return err
+ }
+ cache.retMapping, err = queries.BindMapping(pinnedNoticeType, pinnedNoticeMapping, returnColumns)
+ if err != nil {
+ return err
+ }
+ if len(wl) != 0 {
+ cache.query = fmt.Sprintf("INSERT INTO \"pinned_notices\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1))
+ } else {
+ cache.query = "INSERT INTO \"pinned_notices\" %sDEFAULT VALUES%s"
+ }
+
+ var queryOutput, queryReturning string
+
+ if len(cache.retMapping) != 0 {
+ cache.retQuery = fmt.Sprintf("SELECT \"%s\" FROM \"pinned_notices\" WHERE %s", strings.Join(returnColumns, "\",\""), strmangle.WhereClause("\"", "\"", 0, pinnedNoticePrimaryKeyColumns))
+ }
+
+ cache.query = fmt.Sprintf(cache.query, queryOutput, queryReturning)
+ }
+
+ value := reflect.Indirect(reflect.ValueOf(o))
+ vals := queries.ValuesFromMapping(value, cache.valueMapping)
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, cache.query)
+ fmt.Fprintln(writer, vals)
+ }
+ _, err = exec.ExecContext(ctx, cache.query, vals...)
+
+ if err != nil {
+ return errors.Wrap(err, "models: unable to insert into pinned_notices")
+ }
+
+ var identifierCols []interface{}
+
+ if len(cache.retMapping) == 0 {
+ goto CacheNoHooks
+ }
+
+ identifierCols = []interface{}{
+ o.Name,
+ o.Language,
+ }
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, cache.retQuery)
+ fmt.Fprintln(writer, identifierCols...)
+ }
+ err = exec.QueryRowContext(ctx, cache.retQuery, identifierCols...).Scan(queries.PtrsFromMapping(value, cache.retMapping)...)
+ if err != nil {
+ return errors.Wrap(err, "models: unable to populate default values for pinned_notices")
+ }
+
+CacheNoHooks:
+ if !cached {
+ pinnedNoticeInsertCacheMut.Lock()
+ pinnedNoticeInsertCache[key] = cache
+ pinnedNoticeInsertCacheMut.Unlock()
+ }
+
+ return o.doAfterInsertHooks(ctx, exec)
+}
+
+// Update uses an executor to update the PinnedNotice.
+// See boil.Columns.UpdateColumnSet documentation to understand column list inference for updates.
+// Update does not automatically update the record in case of default values. Use .Reload() to refresh the records.
+func (o *PinnedNotice) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) {
+ var err error
+ if err = o.doBeforeUpdateHooks(ctx, exec); err != nil {
+ return 0, err
+ }
+ key := makeCacheKey(columns, nil)
+ pinnedNoticeUpdateCacheMut.RLock()
+ cache, cached := pinnedNoticeUpdateCache[key]
+ pinnedNoticeUpdateCacheMut.RUnlock()
+
+ if !cached {
+ wl := columns.UpdateColumnSet(
+ pinnedNoticeAllColumns,
+ pinnedNoticePrimaryKeyColumns,
+ )
+
+ if !columns.IsWhitelist() {
+ wl = strmangle.SetComplement(wl, []string{"created_at"})
+ }
+ if len(wl) == 0 {
+ return 0, errors.New("models: unable to update pinned_notices, could not build whitelist")
+ }
+
+ cache.query = fmt.Sprintf("UPDATE \"pinned_notices\" SET %s WHERE %s",
+ strmangle.SetParamNames("\"", "\"", 0, wl),
+ strmangle.WhereClause("\"", "\"", 0, pinnedNoticePrimaryKeyColumns),
+ )
+ cache.valueMapping, err = queries.BindMapping(pinnedNoticeType, pinnedNoticeMapping, append(wl, pinnedNoticePrimaryKeyColumns...))
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ values := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), cache.valueMapping)
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, cache.query)
+ fmt.Fprintln(writer, values)
+ }
+ var result sql.Result
+ result, err = exec.ExecContext(ctx, cache.query, values...)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to update pinned_notices row")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: failed to get rows affected by update for pinned_notices")
+ }
+
+ if !cached {
+ pinnedNoticeUpdateCacheMut.Lock()
+ pinnedNoticeUpdateCache[key] = cache
+ pinnedNoticeUpdateCacheMut.Unlock()
+ }
+
+ return rowsAff, o.doAfterUpdateHooks(ctx, exec)
+}
+
+// UpdateAll updates all rows with the specified column values.
+func (q pinnedNoticeQuery) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) {
+ queries.SetUpdate(q.Query, cols)
+
+ result, err := q.Query.ExecContext(ctx, exec)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to update all for pinned_notices")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to retrieve rows affected for pinned_notices")
+ }
+
+ return rowsAff, nil
+}
+
+// UpdateAll updates all rows with the specified column values, using an executor.
+func (o PinnedNoticeSlice) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) {
+ ln := int64(len(o))
+ if ln == 0 {
+ return 0, nil
+ }
+
+ if len(cols) == 0 {
+ return 0, errors.New("models: update all requires at least one column argument")
+ }
+
+ colNames := make([]string, len(cols))
+ args := make([]interface{}, len(cols))
+
+ i := 0
+ for name, value := range cols {
+ colNames[i] = name
+ args[i] = value
+ i++
+ }
+
+ // Append all of the primary key values for each column
+ for _, obj := range o {
+ pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), pinnedNoticePrimaryKeyMapping)
+ args = append(args, pkeyArgs...)
+ }
+
+ sql := fmt.Sprintf("UPDATE \"pinned_notices\" SET %s WHERE %s",
+ strmangle.SetParamNames("\"", "\"", 0, colNames),
+ strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, pinnedNoticePrimaryKeyColumns, len(o)))
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, sql)
+ fmt.Fprintln(writer, args...)
+ }
+ result, err := exec.ExecContext(ctx, sql, args...)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to update all in pinnedNotice slice")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to retrieve rows affected all in update all pinnedNotice")
+ }
+ return rowsAff, nil
+}
+
+// Delete deletes a single PinnedNotice record with an executor.
+// Delete will match against the primary key column to find the record to delete.
+func (o *PinnedNotice) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
+ if o == nil {
+ return 0, errors.New("models: no PinnedNotice provided for delete")
+ }
+
+ if err := o.doBeforeDeleteHooks(ctx, exec); err != nil {
+ return 0, err
+ }
+
+ args := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), pinnedNoticePrimaryKeyMapping)
+ sql := "DELETE FROM \"pinned_notices\" WHERE \"name\"=? AND \"language\"=?"
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, sql)
+ fmt.Fprintln(writer, args...)
+ }
+ result, err := exec.ExecContext(ctx, sql, args...)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to delete from pinned_notices")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: failed to get rows affected by delete for pinned_notices")
+ }
+
+ if err := o.doAfterDeleteHooks(ctx, exec); err != nil {
+ return 0, err
+ }
+
+ return rowsAff, nil
+}
+
+// DeleteAll deletes all matching rows.
+func (q pinnedNoticeQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
+ if q.Query == nil {
+ return 0, errors.New("models: no pinnedNoticeQuery provided for delete all")
+ }
+
+ queries.SetDelete(q.Query)
+
+ result, err := q.Query.ExecContext(ctx, exec)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to delete all from pinned_notices")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for pinned_notices")
+ }
+
+ return rowsAff, nil
+}
+
+// DeleteAll deletes all rows in the slice, using an executor.
+func (o PinnedNoticeSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
+ if len(o) == 0 {
+ return 0, nil
+ }
+
+ if len(pinnedNoticeBeforeDeleteHooks) != 0 {
+ for _, obj := range o {
+ if err := obj.doBeforeDeleteHooks(ctx, exec); err != nil {
+ return 0, err
+ }
+ }
+ }
+
+ var args []interface{}
+ for _, obj := range o {
+ pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), pinnedNoticePrimaryKeyMapping)
+ args = append(args, pkeyArgs...)
+ }
+
+ sql := "DELETE FROM \"pinned_notices\" WHERE " +
+ strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, pinnedNoticePrimaryKeyColumns, len(o))
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, sql)
+ fmt.Fprintln(writer, args)
+ }
+ result, err := exec.ExecContext(ctx, sql, args...)
+ if err != nil {
+ return 0, errors.Wrap(err, "models: unable to delete all from pinnedNotice slice")
+ }
+
+ rowsAff, err := result.RowsAffected()
+ if err != nil {
+ return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for pinned_notices")
+ }
+
+ if len(pinnedNoticeAfterDeleteHooks) != 0 {
+ for _, obj := range o {
+ if err := obj.doAfterDeleteHooks(ctx, exec); err != nil {
+ return 0, err
+ }
+ }
+ }
+
+ return rowsAff, nil
+}
+
+// Reload refetches the object from the database
+// using the primary keys with an executor.
+func (o *PinnedNotice) Reload(ctx context.Context, exec boil.ContextExecutor) error {
+ ret, err := FindPinnedNotice(ctx, exec, o.Name, o.Language)
+ if err != nil {
+ return err
+ }
+
+ *o = *ret
+ return nil
+}
+
+// ReloadAll refetches every row with matching primary key column values
+// and overwrites the original object slice with the newly updated slice.
+func (o *PinnedNoticeSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error {
+ if o == nil || len(*o) == 0 {
+ return nil
+ }
+
+ slice := PinnedNoticeSlice{}
+ var args []interface{}
+ for _, obj := range *o {
+ pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), pinnedNoticePrimaryKeyMapping)
+ args = append(args, pkeyArgs...)
+ }
+
+ sql := "SELECT \"pinned_notices\".* FROM \"pinned_notices\" WHERE " +
+ strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, pinnedNoticePrimaryKeyColumns, len(*o))
+
+ q := queries.Raw(sql, args...)
+
+ err := q.Bind(ctx, exec, &slice)
+ if err != nil {
+ return errors.Wrap(err, "models: unable to reload all in PinnedNoticeSlice")
+ }
+
+ *o = slice
+
+ return nil
+}
+
+// PinnedNoticeExists checks if the PinnedNotice row exists.
+func PinnedNoticeExists(ctx context.Context, exec boil.ContextExecutor, name string, language string) (bool, error) {
+ var exists bool
+ sql := "select exists(select 1 from \"pinned_notices\" where \"name\"=? AND \"language\"=? limit 1)"
+
+ if boil.IsDebug(ctx) {
+ writer := boil.DebugWriterFrom(ctx)
+ fmt.Fprintln(writer, sql)
+ fmt.Fprintln(writer, name, language)
+ }
+ row := exec.QueryRowContext(ctx, sql, name, language)
+
+ err := row.Scan(&exists)
+ if err != nil {
+ return false, errors.Wrap(err, "models: unable to check if pinned_notices exists")
+ }
+
+ return exists, nil
+}
diff --git a/admindb/sqlite/new.go b/admindb/sqlite/new.go
index 1737643..1148d2c 100644
--- a/admindb/sqlite/new.go
+++ b/admindb/sqlite/new.go
@@ -23,6 +23,9 @@ type Database struct {
AllowList admindb.AllowListService
Aliases admindb.AliasService
+
+ PinnedNotices admindb.PinnedNoticesService
+ Notices admindb.NoticesService
}
// Open looks for a database file 'fname'
@@ -55,11 +58,13 @@ func Open(r repo.Interface) (*Database, error) {
}
admindb := &Database{
- db: db,
- AuthWithSSB: AuthWithSSB{db},
- AuthFallback: AuthFallback{db},
- AllowList: AllowList{db},
- Aliases: Aliases{db},
+ db: db,
+ AuthWithSSB: AuthWithSSB{db},
+ AuthFallback: AuthFallback{db},
+ AllowList: AllowList{db},
+ Aliases: Aliases{db},
+ PinnedNotices: PinnedNotices{db},
+ Notices: Notices{db},
}
return admindb, nil
diff --git a/admindb/sqlite/notices.go b/admindb/sqlite/notices.go
new file mode 100644
index 0000000..730f08b
--- /dev/null
+++ b/admindb/sqlite/notices.go
@@ -0,0 +1,102 @@
+package sqlite
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+
+ "github.com/volatiletech/sqlboiler/v4/boil"
+
+ "github.com/friendsofgo/errors"
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb/sqlite/models"
+)
+
+// make sure to implement interfaces correctly
+var _ admindb.PinnedNoticesService = (*PinnedNotices)(nil)
+
+type PinnedNotices struct {
+ db *sql.DB
+}
+
+func (pndb PinnedNotices) Set(name admindb.PinnedNoticeName, pageID int64, lang string) error {
+ if !name.Valid() {
+ return fmt.Errorf("fixed pages: invalid page name: %s", name)
+ }
+
+ return fmt.Errorf("TODO: set fixed page %s to %d:%s", name, pageID, lang)
+}
+
+// make sure to implement interfaces correctly
+var _ admindb.NoticesService = (*Notices)(nil)
+
+type Notices struct {
+ db *sql.DB
+}
+
+func (ndb Notices) GetByID(ctx context.Context, id int64) (admindb.Notice, error) {
+ var n admindb.Notice
+
+ dbEntry, err := models.FindNotice(ctx, ndb.db, id)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return n, admindb.ErrNotFound
+ }
+ return n, err
+ }
+
+ // convert models type to admindb type
+ n.ID = dbEntry.ID
+ n.Title = dbEntry.Title
+ n.Language = dbEntry.Language
+ n.Content = dbEntry.Content
+
+ return n, nil
+}
+
+func (ndb Notices) RemoveID(ctx context.Context, id int64) error {
+ dbEntry, err := models.FindNotice(ctx, ndb.db, id)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return admindb.ErrNotFound
+ }
+ return err
+ }
+
+ _, err = dbEntry.Delete(ctx, ndb.db)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (ndb Notices) Save(ctx context.Context, p *admindb.Notice) error {
+ if p.ID == 0 {
+ var newEntry models.Notice
+ newEntry.Title = p.Title
+ newEntry.Content = p.Content
+ newEntry.Language = p.Language
+ err := newEntry.Insert(ctx, ndb.db, boil.Whitelist("title", "content", "language"))
+ if err != nil {
+ return err
+ }
+ p.ID = newEntry.ID
+ return nil
+ }
+
+ var existing models.Notice
+ existing.ID = p.ID
+ existing.Title = p.Title
+ existing.Content = p.Content
+ existing.Language = p.Language
+ _, err := existing.Update(ctx, ndb.db, boil.Whitelist("title", "content", "language"))
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return admindb.ErrNotFound
+ }
+ return err
+ }
+
+ return nil
+}
diff --git a/admindb/sqlite/notices_test.go b/admindb/sqlite/notices_test.go
new file mode 100644
index 0000000..a93c357
--- /dev/null
+++ b/admindb/sqlite/notices_test.go
@@ -0,0 +1,75 @@
+package sqlite
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+ "github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNoticesCRUD(t *testing.T) {
+ r := require.New(t)
+ ctx := context.Background()
+
+ testRepo := filepath.Join("testrun", t.Name())
+ os.RemoveAll(testRepo)
+
+ tr := repo.New(testRepo)
+
+ db, err := Open(tr)
+ r.NoError(err)
+
+ // boil.DebugWriter = os.Stderr
+ // boil.DebugMode = true
+
+ t.Run("not found", func(t *testing.T) {
+ r := require.New(t)
+
+ _, err = db.Notices.GetByID(ctx, 9999)
+ r.Error(err)
+ r.EqualError(err, admindb.ErrNotFound.Error())
+
+ err = db.Notices.RemoveID(ctx, 9999)
+ r.Error(err)
+ r.EqualError(err, admindb.ErrNotFound.Error())
+ })
+
+ t.Run("new and update", func(t *testing.T) {
+ r := require.New(t)
+ var n admindb.Notice
+
+ n.Title = fmt.Sprintf("Test notice %d", rand.Int())
+ n.Content = `# This is **not** a test!`
+ n.Language = "en-GB"
+
+ err := db.Notices.Save(ctx, &n)
+ r.NoError(err, "failed to save")
+ r.NotEqual(0, n.ID, "should have a fresh id")
+
+ got, err := db.Notices.GetByID(ctx, n.ID)
+ r.NoError(err, "failed to get saved entry")
+ r.Equal(n.Title, got.Title)
+ r.Equal(n.ID, got.ID)
+ r.Equal(n.Language, got.Language)
+
+ oldID := n.ID
+ n.Title = fmt.Sprintf("Updated test notice %d", rand.Int())
+ err = db.Notices.Save(ctx, &n)
+ r.NoError(err, "failed to save")
+ r.Equal(oldID, n.ID, "should have the same ID")
+
+ // be gone
+ err = db.Notices.RemoveID(ctx, oldID)
+ r.NoError(err)
+
+ _, err = db.Notices.GetByID(ctx, oldID)
+ r.Error(err)
+ r.EqualError(err, admindb.ErrNotFound.Error())
+ })
+}
diff --git a/admindb/types.go b/admindb/types.go
index 5cd1e24..ef8230d 100644
--- a/admindb/types.go
+++ b/admindb/types.go
@@ -62,3 +62,31 @@ func (r *DBFeedRef) Scan(src interface{}) error {
func (r DBFeedRef) Value() (driver.Value, error) {
return driver.Value(r.Ref()), nil
}
+
+// PinnedNoticeName holds a name of a well known part of the page with a fixed location.
+// These also double as the i18n labels.
+type PinnedNoticeName string
+
+// These are the well known names that the room page will display
+const (
+ NoticeDescription PinnedNoticeName = "NoticeDescription"
+ NoticeNews PinnedNoticeName = "NoticeNews"
+ NoticePrivacyPolicy PinnedNoticeName = "NoticePrivacyPolicy"
+ NoticeCodeOfConduct PinnedNoticeName = "NoticeCodeOfConduct"
+)
+
+// Valid returns true if the page name is well known.
+func (fpn PinnedNoticeName) Valid() bool {
+ return fpn == NoticeNews ||
+ fpn == NoticeDescription ||
+ fpn == NoticePrivacyPolicy ||
+ fpn == NoticeCodeOfConduct
+}
+
+// Notice holds the title and content of a page that is user generated
+type Notice struct {
+ ID int64
+ Title string
+ Content string
+ Language string
+}
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 7b5b860..255f1e4 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -217,6 +217,7 @@ func runroomsrv() error {
db.AuthWithSSB,
db.AuthFallback,
db.AllowList,
+ db.Notices,
)
if err != nil {
return fmt.Errorf("failed to create HTTPdashboard handler: %w", err)
diff --git a/go.mod b/go.mod
index e101288..96b4410 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
github.com/nicksnyder/go-i18n/v2 v2.1.2
github.com/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351
- github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0
github.com/stretchr/testify v1.6.1
github.com/unrolled/secure v1.0.8
github.com/vcraescu/go-paginator/v2 v2.0.0
diff --git a/go.sum b/go.sum
index ffe2575..fc7736c 100644
--- a/go.sum
+++ b/go.sum
@@ -347,8 +347,11 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.4.0 h1:LUa41nrWTQNGhzdsZ5lTnkwbNjj6rXTdazA1cSdjkOY=
github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
@@ -357,9 +360,8 @@ github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxr
github.com/shurcooL/go v0.0.0-20190121191506-3fef8c783dec/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go v0.0.0-20190330031554-6713ea532688/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
+github.com/shurcooL/httpfs v0.0.0-20190527155220-6a4d4a70508b h1:4kg1wyftSKxLtnPAvcRWakIPpokB9w780/KwrNLnfPA=
github.com/shurcooL/httpfs v0.0.0-20190527155220-6a4d4a70508b/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
-github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk=
-github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
diff --git a/web/handlers/admin/allow_list.go b/web/handlers/admin/allow_list.go
index 5b25bfe..dc231e7 100644
--- a/web/handlers/admin/allow_list.go
+++ b/web/handlers/admin/allow_list.go
@@ -151,7 +151,7 @@ func (h allowListH) removeConfirm(rw http.ResponseWriter, req *http.Request) (in
func (h allowListH) remove(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
- err = weberrors.ErrBadRequest{Where: "ID", Details: err}
+ err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
// TODO "flash" errors
http.Redirect(rw, req, redirectTo, http.StatusFound)
return
diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go
index 1d94350..607b527 100644
--- a/web/handlers/admin/handler.go
+++ b/web/handlers/admin/handler.go
@@ -17,14 +17,23 @@ import (
var HTMLTemplates = []string{
"admin/dashboard.tmpl",
"admin/menu.tmpl",
+
"admin/allow-list.tmpl",
"admin/allow-list-remove-confirm.tmpl",
+
+ "admin/notice-edit.tmpl",
}
// Handler supplies the elevated access pages to known users.
// It is not registering on the mux router like other pages to clean up the authorize flow.
-func Handler(r *render.Renderer, roomState *roomstate.Manager, al admindb.AllowListService) http.Handler {
+func Handler(
+ r *render.Renderer,
+ roomState *roomstate.Manager,
+ al admindb.AllowListService,
+ ndb admindb.NoticesService,
+) http.Handler {
mux := &http.ServeMux{}
+ // TODO: configure 404 handler
mux.HandleFunc("/dashboard", r.HTML("admin/dashboard.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
lst := roomState.List()
@@ -47,6 +56,13 @@ func Handler(r *render.Renderer, roomState *roomstate.Manager, al admindb.AllowL
mux.HandleFunc("/members/remove/confirm", r.HTML("admin/allow-list-remove-confirm.tmpl", ah.removeConfirm))
mux.HandleFunc("/members/remove", ah.remove)
+ var nh = noticeHandler{
+ r: r,
+ db: ndb,
+ }
+ mux.HandleFunc("/notice/edit", r.HTML("admin/notice-edit.tmpl", nh.edit))
+ mux.HandleFunc("/notice/save", nh.save)
+
return customStripPrefix("/admin", mux)
}
diff --git a/web/handlers/admin/notices.go b/web/handlers/admin/notices.go
new file mode 100644
index 0000000..40efe33
--- /dev/null
+++ b/web/handlers/admin/notices.go
@@ -0,0 +1,90 @@
+package admin
+
+import (
+ "errors"
+ "html/template"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/gorilla/csrf"
+ "github.com/russross/blackfriday/v2"
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+ weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
+ "go.mindeco.de/http/render"
+)
+
+type noticeHandler struct {
+ r *render.Renderer
+
+ db admindb.NoticesService
+}
+
+func (nh noticeHandler) edit(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
+ id, err := strconv.ParseInt(req.URL.Query().Get("id"), 10, 64)
+ if err != nil {
+ err = weberrors.ErrBadRequest{Where: "ID", Details: err}
+ return nil, err
+ }
+
+ n, err := nh.db.GetByID(req.Context(), id)
+ if err != nil {
+ if errors.Is(err, admindb.ErrNotFound) {
+ http.Redirect(rw, req, redirectTo, http.StatusFound)
+ return nil, ErrRedirected
+ }
+ return nil, err
+ }
+
+ // https://github.com/russross/blackfriday/issues/575
+ fixedContent := strings.Replace(n.Content, "\r\n", "\n", -1)
+
+ contentBytes := []byte(fixedContent)
+ preview := blackfriday.Run(contentBytes)
+
+ return map[string]interface{}{
+ "Notice": n,
+ "ContentPreview": template.HTML(preview),
+ // "Debug": string(preview),
+ // "DebugHex": hex.Dump(contentBytes),
+ csrf.TemplateTag: csrf.TemplateField(req),
+ }, nil
+}
+
+func (nh noticeHandler) save(rw http.ResponseWriter, req *http.Request) {
+ err := req.ParseForm()
+ if err != nil {
+ err = weberrors.ErrBadRequest{Where: "form data", Details: err}
+ nh.r.Error(rw, req, http.StatusInternalServerError, err)
+ return
+ }
+
+ redirect := req.FormValue("redirect")
+ if redirect == "" {
+ redirect = "/"
+ }
+
+ var n admindb.Notice
+ n.ID, err = strconv.ParseInt(req.FormValue("id"), 10, 64)
+ if err != nil {
+ err = weberrors.ErrBadRequest{Where: "id", Details: err}
+ nh.r.Error(rw, req, http.StatusInternalServerError, err)
+ return
+ }
+
+ n.Title = req.FormValue("title")
+
+ n.Content = req.FormValue("content")
+ // https://github.com/russross/blackfriday/issues/575
+ n.Content = strings.Replace(n.Content, "\r\n", "\n", -1)
+
+ err = nh.db.Save(req.Context(), &n)
+ if err != nil {
+ nh.r.Error(rw, req, http.StatusInternalServerError, err)
+ return
+ }
+
+ // TODO: update langauge
+
+ http.Redirect(rw, req, redirect, http.StatusTemporaryRedirect)
+}
diff --git a/web/handlers/http.go b/web/handlers/http.go
index 641aae3..5e55ab7 100644
--- a/web/handlers/http.go
+++ b/web/handlers/http.go
@@ -25,6 +25,13 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
)
+var HTMLTempaltes = []string{
+ "landing/index.tmpl",
+ "landing/about.tmpl",
+ "notice.tmpl",
+ "error.tmpl",
+}
+
// New initializes the whole web stack for rooms, with all the sub-modules and routing.
func New(
logger logging.Interface,
@@ -33,6 +40,7 @@ func New(
as admindb.AuthWithSSBService,
fs admindb.AuthFallbackService,
al admindb.AllowListService,
+ ns admindb.NoticesService,
) (http.Handler, error) {
m := router.CompleteApp()
@@ -47,11 +55,7 @@ func New(
render.SetLogger(logger),
render.BaseTemplates("base.tmpl", "menu.tmpl"),
render.AddTemplates(concatTemplates(
- []string{
- "landing/index.tmpl",
- "landing/about.tmpl",
- "error.tmpl",
- },
+ HTMLTempaltes,
news.HTMLTemplates,
roomsAuth.HTMLTemplates,
admin.HTMLTemplates,
@@ -189,12 +193,16 @@ func New(
news.Handler(m, r)
roomsAuth.Handler(m, r, a)
- adminHandler := a.Authenticate(admin.Handler(r, roomState, al))
+ adminHandler := a.Authenticate(admin.Handler(r, roomState, al, ns))
mainMux.Handle("/admin/", adminHandler)
m.Get(router.CompleteIndex).Handler(r.StaticHTML("landing/index.tmpl"))
m.Get(router.CompleteAbout).Handler(r.StaticHTML("landing/about.tmpl"))
+ var nr noticeRenderer
+ nr.notices = ns
+ m.Get(router.CompleteNotice).Handler(r.HTML("notice.tmpl", nr.render))
+
m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))
m.NotFoundHandler = r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
diff --git a/web/handlers/notices.go b/web/handlers/notices.go
new file mode 100644
index 0000000..85f8264
--- /dev/null
+++ b/web/handlers/notices.go
@@ -0,0 +1,43 @@
+package handlers
+
+import (
+ "html/template"
+ "net/http"
+ "strconv"
+
+ "github.com/russross/blackfriday/v2"
+
+ "github.com/ssb-ngi-pointer/go-ssb-room/admindb"
+ "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
+)
+
+type noticeRenderer struct {
+ notices admindb.NoticesService
+}
+
+type noticeData struct {
+ ID int64
+ Title, Language string
+ Content template.HTML
+}
+
+func (pr noticeRenderer) render(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
+ noticeID, err := strconv.ParseInt(req.URL.Query().Get("id"), 10, 64)
+ if err != nil {
+ return nil, errors.ErrBadRequest{Where: "notice ID", Details: err}
+ }
+
+ notice, err := pr.notices.GetByID(req.Context(), noticeID)
+ if err != nil {
+ return nil, err
+ }
+
+ markdown := blackfriday.Run([]byte(notice.Content), blackfriday.WithNoExtensions())
+
+ return noticeData{
+ ID: noticeID,
+ Title: notice.Title,
+ Content: template.HTML(markdown),
+ Language: notice.Language,
+ }, nil
+}
diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml
index 25a6922..bdad47c 100644
--- a/web/i18n/defaults/active.en.toml
+++ b/web/i18n/defaults/active.en.toml
@@ -1,4 +1,6 @@
Confirm = "Confirm"
+Save = "Save"
+Preview = "Preview"
PageNotFound = "The requested page was not found."
LandingTitle = "ohai my room"
@@ -33,6 +35,8 @@ description = "Number of members"
one = "1 member"
other = "{{.Count}} members"
+NoticeEditTitle = "Edit Notice"
+
[ListCount]
description = "generic list"
one = "There is one item on the List"
diff --git a/web/router/admin.go b/web/router/admin.go
index 8590533..6860fb2 100644
--- a/web/router/admin.go
+++ b/web/router/admin.go
@@ -13,6 +13,9 @@ const (
AdminAllowListAdd = "admin:allow-list:add"
AdminAllowListRemoveConfirm = "admin:allow-list:remove:confirm"
AdminAllowListRemove = "admin:allow-list:remove"
+
+ AdminNoticeEdit = "admin:notice:edit"
+ AdminNoticeSave = "admin:notice:save"
)
// Admin constructs a mux.Router containing the routes for the admin dashboard and settings pages
@@ -29,5 +32,8 @@ func Admin(m *mux.Router) *mux.Router {
m.Path("/members/remove/confirm").Methods("GET").Name(AdminAllowListRemoveConfirm)
m.Path("/members/remove").Methods("POST").Name(AdminAllowListRemove)
+ m.Path("/notice/edit").Methods("GET").Name(AdminNoticeEdit)
+ m.Path("/notice/save").Methods("POST").Name(AdminNoticeSave)
+
return m
}
diff --git a/web/router/complete.go b/web/router/complete.go
index d22b25f..1cc3168 100644
--- a/web/router/complete.go
+++ b/web/router/complete.go
@@ -8,8 +8,9 @@ import (
// constant names for the named routes
const (
- CompleteIndex = "complete:index"
- CompleteAbout = "complete:about"
+ CompleteIndex = "complete:index"
+ CompleteAbout = "complete:about"
+ CompleteNotice = "complete:notice"
)
// CompleteApp constructs a mux.Router containing the routes for batch Complete html frontend
@@ -22,6 +23,7 @@ func CompleteApp() *mux.Router {
m.Path("/").Methods("GET").Name(CompleteIndex)
m.Path("/about").Methods("GET").Name(CompleteAbout)
+ m.Path("/notice").Methods("GET").Name(CompleteNotice)
return m
}
diff --git a/web/templates/admin/notice-edit.tmpl b/web/templates/admin/notice-edit.tmpl
new file mode 100644
index 0000000..a69d780
--- /dev/null
+++ b/web/templates/admin/notice-edit.tmpl
@@ -0,0 +1,56 @@
+{{ define "title" }}{{i18n "NoticeEditTitle"}}{{ end }}
+{{ define "content" }}
+
+
+
+
{{i18n "Preview"}}
+ {{.ContentPreview}}
+
+
+
+
+{{end}}
\ No newline at end of file
diff --git a/web/templates/landing/index.tmpl b/web/templates/landing/index.tmpl
index 6ab10d5..93e3958 100644
--- a/web/templates/landing/index.tmpl
+++ b/web/templates/landing/index.tmpl
@@ -40,5 +40,4 @@
suspendisse in est ante in. Imperdiet nulla malesuada pellentesque elit. Ut faucibus pulvinar elementum integer enim
neque. Consequat semper viverra nam libero justo laoreet.
-
{{end}}
\ No newline at end of file
diff --git a/web/templates/notice.tmpl b/web/templates/notice.tmpl
new file mode 100644
index 0000000..7e11bb7
--- /dev/null
+++ b/web/templates/notice.tmpl
@@ -0,0 +1,18 @@
+{{ define "title" }}{{.Title}}{{ end }}
+{{ define "content" }}
+
+{{end}}
\ No newline at end of file