diff --git a/templates/default/admin-songs.tmpl b/templates/default/admin-songs.tmpl new file mode 100644 index 00000000..db807567 --- /dev/null +++ b/templates/default/admin-songs.tmpl @@ -0,0 +1,17 @@ +{{define "content"}} +
+
+
+ + +
+
+
+ {{template "pagination" .Page}} + {{range .Forms}} + {{template "form_admin_songs" .}} + {{end}} + {{template "pagination" .Page}} +
+
+{{end}} \ No newline at end of file diff --git a/templates/default/partials/admin-navbar.tmpl b/templates/default/partials/admin-navbar.tmpl index 4b8c1938..5bc4587a 100644 --- a/templates/default/partials/admin-navbar.tmpl +++ b/templates/default/partials/admin-navbar.tmpl @@ -15,7 +15,7 @@ Home {{if .User.UserPermissions.Has "news"}}News{{end}} {{if .User.UserPermissions.Has "pending_view"}}Pending{{end}} - {{if .User.UserPermissions.Has "database_view"}}Song Database{{end}} + {{if .User.UserPermissions.Has "database_view"}}Song Database{{end}} {{if .User.UserPermissions.Has "admin"}}Users{{end}} Profile diff --git a/templates/default/partials/form_admin_songs.tmpl b/templates/default/partials/form_admin_songs.tmpl new file mode 100644 index 00000000..117c87f0 --- /dev/null +++ b/templates/default/partials/form_admin_songs.tmpl @@ -0,0 +1,102 @@ +{{define "form_admin_songs"}} +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ +
+
+ {{.Song.TrackID}} +
+
+
+
+
+
+ +
+
+ {{.Song.Acceptor}} ({{.Song.LastEditor}}) +
+
+
+
+
+
+ +
+
+ {{.Song.LastPlayed | Since}} / {{.Song.LastRequested | Since}} +
+
+
+
+
+
+ +
+
+ {{.Song.Priority}} ({{.Song.RequestCount}}) +
+
+
+
+
+ + + {{if .HasEdit}} + + {{if .Song.NeedReplacement}} + + {{else}} + + {{end}} + {{end}} + {{if .HasDelete}}{{end}} +
+
+
+
+
+{{end}} \ No newline at end of file diff --git a/website/admin/router.go b/website/admin/router.go index dfe0594a..824af473 100644 --- a/website/admin/router.go +++ b/website/admin/router.go @@ -20,6 +20,7 @@ func NewState( cfg config.Config, dp *daypass.Daypass, storage radio.StorageService, + search radio.SearchService, siteTmpl *templates.Site, exec templates.Executor, sessionManager *scs.SessionManager, @@ -30,6 +31,7 @@ func NewState( Config: cfg, Daypass: dp, Storage: storage, + Search: search, Templates: siteTmpl, TemplateExecutor: exec, SessionManager: sessionManager, @@ -43,6 +45,7 @@ type State struct { Daypass *daypass.Daypass Storage radio.StorageService + Search radio.SearchService Templates *templates.Site TemplateExecutor templates.Executor SessionManager *scs.SessionManager @@ -52,13 +55,24 @@ type State struct { func Route(ctx context.Context, s State) func(chi.Router) { return func(r chi.Router) { + // the login middleware will require atleast the active permission r = r.With(s.Authentication.LoginMiddleware) r.HandleFunc("/", s.GetHome) r.Get("/profile", s.GetProfile) r.Post("/profile", s.PostProfile) - r.Get("/pending", vmiddleware.RequirePermission(radio.PermPendingView, s.GetPending)) - r.Post("/pending", vmiddleware.RequirePermission(radio.PermPendingEdit, s.PostPending)) - r.Get("/pending-song/{SubmissionID:[0-9]+}", vmiddleware.RequirePermission(radio.PermPendingView, s.GetPendingSong)) + r.Get("/pending", + vmiddleware.RequirePermission(radio.PermPendingView, s.GetPending)) + r.Post("/pending", + vmiddleware.RequirePermission(radio.PermPendingEdit, s.PostPending)) + r.Get("/pending-song/{SubmissionID:[0-9]+}", + vmiddleware.RequirePermission(radio.PermPendingView, s.GetPendingSong)) + r.Get("/songs", + vmiddleware.RequirePermission(radio.PermDatabaseView, s.GetSongs)) + r.Post("/songs", + vmiddleware.RequirePermission(radio.PermDatabaseEdit, s.PostSongs)) + r.Delete("/songs", + vmiddleware.RequirePermission(radio.PermDatabaseDelete, s.DeleteSongs)) + // debug handlers, might not be needed later r.HandleFunc("/streamer/start", vmiddleware.RequirePermission(radio.PermAdmin, s.StartStreamer)) r.HandleFunc("/streamer/stop", vmiddleware.RequirePermission(radio.PermAdmin, s.StopStreamer)) diff --git a/website/admin/songs.go b/website/admin/songs.go new file mode 100644 index 00000000..d411f9fc --- /dev/null +++ b/website/admin/songs.go @@ -0,0 +1,97 @@ +package admin + +import ( + "net/http" + + radio "github.com/R-a-dio/valkyrie" + "github.com/R-a-dio/valkyrie/errors" + "github.com/R-a-dio/valkyrie/website/middleware" + "github.com/R-a-dio/valkyrie/website/shared" + "github.com/rs/zerolog/hlog" +) + +const songsPageSize = 20 + +type SongsInput struct { + middleware.Input + + Forms []SongsForm + Query string + Page *shared.Pagination +} + +func (SongsInput) TemplateBundle() string { + return "admin-songs" +} + +type SongsForm struct { + HasDelete bool + HasEdit bool + Song radio.Song +} + +func (SongsForm) TemplateName() string { + return "form_admin_songs" +} + +func (SongsForm) TemplateBundle() string { + return "admin-songs" +} + +func NewSongsInput(s radio.SearchService, r *http.Request) (*SongsInput, error) { + const op errors.Op = "website/admin.NewSongInput" + ctx := r.Context() + + page, offset, err := shared.PageAndOffset(r, songsPageSize) + if err != nil { + return nil, errors.E(op, err) + } + + query := r.FormValue("q") + searchResult, err := s.Search(ctx, query, songsPageSize, offset) + if err != nil { + return nil, errors.E(op, err) + } + + // generate the input we can so far, since we need some data from it + input := &SongsInput{ + Input: middleware.InputFromContext(ctx), + Query: query, + Page: shared.NewPagination( + page, shared.PageCount(int64(searchResult.TotalHits), songsPageSize), + r.URL, + ), + } + + forms := make([]SongsForm, len(searchResult.Songs)) + for i := range searchResult.Songs { + forms[i].Song = searchResult.Songs[i] + forms[i].HasDelete = input.User.UserPermissions.Has(radio.PermDatabaseDelete) + forms[i].HasEdit = input.User.UserPermissions.Has(radio.PermDatabaseEdit) + } + + input.Forms = forms + return input, nil +} + +func (s *State) GetSongs(w http.ResponseWriter, r *http.Request) { + input, err := NewSongsInput(s.Search, r) + if err != nil { + hlog.FromRequest(r).Error().Err(err).Msg("input creation failure") + return + } + + err = s.TemplateExecutor.Execute(w, r, input) + if err != nil { + hlog.FromRequest(r).Error().Err(err).Msg("template failure") + return + } +} + +func (s *State) PostSongs(w http.ResponseWriter, r *http.Request) { + +} + +func (s *State) DeleteSongs(w http.ResponseWriter, r *http.Request) { + +} diff --git a/website/main.go b/website/main.go index 280aa39d..94d30515 100644 --- a/website/main.go +++ b/website/main.go @@ -137,6 +137,7 @@ func Execute(ctx context.Context, cfg config.Config) error { cfg, dpass, storage, + searchService, siteTemplates, executor, sessionManager, diff --git a/website/public/search.go b/website/public/search.go index 62ebbd40..13e87a4d 100644 --- a/website/public/search.go +++ b/website/public/search.go @@ -34,14 +34,14 @@ func NewSearchInput(s radio.SearchService, rs radio.RequestStorage, r *http.Requ query := r.FormValue("q") searchResult, err := s.Search(ctx, query, searchPageSize, offset) if err != nil { - return nil, err + return nil, errors.E(op, err) } // TODO(wessie): check if this is the right identifier identifier := r.RemoteAddr lastRequest, err := rs.LastRequest(identifier) if err != nil { - return nil, err + return nil, errors.E(op, err) } cd, ok := radio.CalculateCooldown(requestDelay, lastRequest) diff --git a/website/shared/pagination.go b/website/shared/pagination.go index 0c4f3323..6e413716 100644 --- a/website/shared/pagination.go +++ b/website/shared/pagination.go @@ -2,8 +2,11 @@ package shared import ( "html/template" + "net/http" "net/url" "strconv" + + "github.com/R-a-dio/valkyrie/errors" ) func PageCount(total, size int64) int64 { @@ -14,6 +17,26 @@ func PageCount(total, size int64) int64 { return full } +func PageAndOffset(r *http.Request, pageSize int64) (int64, int64, error) { + var page int64 = 1 + { + rawPage := r.FormValue("page") + if rawPage == "" { + return page, 0, nil + } + parsedPage, err := strconv.ParseInt(rawPage, 10, 0) + if err != nil { + return page, 0, errors.E(err, errors.InvalidForm) + } + page = parsedPage + } + var offset = (page - 1) * pageSize + if offset < 0 { + offset = 0 + } + return page, offset, nil +} + func NewPagination(currentPage, totalPages int64, uri *url.URL) *Pagination { return &Pagination{ Nr: currentPage,