Adopt SummerCMS v0.1.1 admin features
Datepickers on native time.Time / lagoon.Date fields, file uploads on materials, readings and class sessions managed from the course form (course_id made nullable).
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
_ "time/tzdata" // Europe/Warsaw must resolve on hosts without zoneinfo.
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
)
|
||||
|
||||
// Location is the time zone the programme runs in. Timestamps are stored in
|
||||
@@ -21,9 +22,6 @@ func mustLoadLocation(name string) *time.Location {
|
||||
return loc
|
||||
}
|
||||
|
||||
// DateLayout is the textual form of a Date.
|
||||
const DateLayout = "2006-01-02"
|
||||
|
||||
// dateTimeLayouts are the forms ParseDateTime accepts. RFC 3339 carries its
|
||||
// own offset; the others are wall-clock times in Location.
|
||||
var dateTimeLayouts = []string{
|
||||
@@ -34,7 +32,7 @@ var dateTimeLayouts = []string{
|
||||
}
|
||||
|
||||
// ParseDateTime reads an RFC 3339 timestamp, or a wall-clock time
|
||||
// ("2026-10-03 08:00") in Location.
|
||||
// ("2026-10-03 08:00") in Location. The seed file uses these forms.
|
||||
func ParseDateTime(s string) (time.Time, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
@@ -49,138 +47,14 @@ func ParseDateTime(s string) (time.Time, error) {
|
||||
}
|
||||
|
||||
// ParseDate reads a YYYY-MM-DD calendar date.
|
||||
func ParseDate(s string) (time.Time, error) {
|
||||
t, err := time.ParseInLocation(DateLayout, strings.TrimSpace(s), Location)
|
||||
func ParseDate(s string) (lagoon.Date, error) {
|
||||
d, err := lagoon.ParseDate(strings.TrimSpace(s))
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("jz.bm: %q is not a YYYY-MM-DD date", s)
|
||||
return lagoon.Date{}, fmt.Errorf("jz.bm: %q is not a YYYY-MM-DD date", s)
|
||||
}
|
||||
return t, nil
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// DateTime is a TIMESTAMPTZ column held as RFC 3339 text in Location
|
||||
// FormatDateTime is the API form of a timestamp: RFC 3339 in Location
|
||||
// ("2026-10-03T08:00:00+02:00").
|
||||
//
|
||||
// It is a string type rather than time.Time because the admin form has no
|
||||
// date picker and lagoon.Fill cannot write text into a time.Time field, while
|
||||
// cabana treats any struct-typed field as a relation. A string type is filled
|
||||
// from the form's text input, and Value checks and normalises it.
|
||||
// TODO: requires SummerCMS change (a datetime form field type and Fill
|
||||
// support for time.Time) to use time.Time here.
|
||||
type DateTime string
|
||||
|
||||
// NewDateTime returns t as a DateTime in Location.
|
||||
func NewDateTime(t time.Time) DateTime {
|
||||
return DateTime(t.In(Location).Format(time.RFC3339))
|
||||
}
|
||||
|
||||
// Time parses the value.
|
||||
func (d DateTime) Time() (time.Time, error) { return ParseDateTime(string(d)) }
|
||||
|
||||
// MustTime parses the value and returns the zero time when it does not parse.
|
||||
// Values read from the database always parse.
|
||||
func (d DateTime) MustTime() time.Time {
|
||||
t, _ := d.Time()
|
||||
return t
|
||||
}
|
||||
|
||||
// Valid reports whether the value parses.
|
||||
func (d DateTime) Valid() bool {
|
||||
_, err := d.Time()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Value stores the instant in UTC. An empty value is SQL NULL.
|
||||
func (d DateTime) Value() (driver.Value, error) {
|
||||
if strings.TrimSpace(string(d)) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
t, err := d.Time()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.UTC(), nil
|
||||
}
|
||||
|
||||
// Scan reads a TIMESTAMPTZ (or its text) and normalises it to Location.
|
||||
func (d *DateTime) Scan(src any) error {
|
||||
switch v := src.(type) {
|
||||
case nil:
|
||||
*d = ""
|
||||
case time.Time:
|
||||
*d = NewDateTime(v)
|
||||
case string:
|
||||
return d.scanText(v)
|
||||
case []byte:
|
||||
return d.scanText(string(v))
|
||||
default:
|
||||
return fmt.Errorf("jz.bm: cannot scan %T into DateTime", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DateTime) scanText(s string) error {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
*d = ""
|
||||
return nil
|
||||
}
|
||||
t, err := ParseDateTime(s)
|
||||
if err != nil {
|
||||
// Postgres text output: "2026-10-03 06:00:00+00".
|
||||
t, err = time.Parse("2006-01-02 15:04:05-07", s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
*d = NewDateTime(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Date is a DATE column held as YYYY-MM-DD text, for the same reason as
|
||||
// DateTime.
|
||||
type Date string
|
||||
|
||||
// NewDate returns the calendar date of t in Location.
|
||||
func NewDate(t time.Time) Date { return Date(t.In(Location).Format(DateLayout)) }
|
||||
|
||||
// Time returns midnight of the date in Location.
|
||||
func (d Date) Time() (time.Time, error) { return ParseDate(string(d)) }
|
||||
|
||||
// Valid reports whether the value is a YYYY-MM-DD date.
|
||||
func (d Date) Valid() bool {
|
||||
_, err := d.Time()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Value stores the date as text; Postgres casts it to DATE. An empty value is
|
||||
// SQL NULL.
|
||||
func (d Date) Value() (driver.Value, error) {
|
||||
if strings.TrimSpace(string(d)) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
t, err := d.Time()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.Format(DateLayout), nil
|
||||
}
|
||||
|
||||
// Scan reads a DATE. The driver hands it over as midnight UTC, so the
|
||||
// calendar fields are taken as they are, without a zone conversion.
|
||||
func (d *Date) Scan(src any) error {
|
||||
switch v := src.(type) {
|
||||
case nil:
|
||||
*d = ""
|
||||
case time.Time:
|
||||
*d = Date(v.Format(DateLayout))
|
||||
case string:
|
||||
*d = Date(strings.TrimSpace(v))
|
||||
if len(*d) > len(DateLayout) {
|
||||
*d = (*d)[:len(DateLayout)]
|
||||
}
|
||||
case []byte:
|
||||
return d.Scan(string(v))
|
||||
default:
|
||||
return fmt.Errorf("jz.bm: cannot scan %T into Date", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func FormatDateTime(t time.Time) string { return t.In(Location).Format(time.RFC3339) }
|
||||
|
||||
Reference in New Issue
Block a user