Help us shape the future of Go! Take the 2026 Go Developer Survey

submissions

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 29 Imported by: 0

README

Submissions

Lets visitors send in news with a title, a text and an optional image. Editors review the proposals and accept one as a draft post or reject it.

Each instance feeds one Publishing instance: an accepted proposal becomes an unpublished post there, which editors finish and publish as usual. Use one instance per stream that takes tips from visitors.

Add it to the site

app.Use(
    publishing.Module("news", "News"),
    submissions.Module("tips", "Tips", "news"),
)

The key (tips) is used in the admin, the form action and the template functions, and must be unique on the site. The title (Tips) is what editors see in the admin and the page editor. The third argument is the key of the Publishing instance that accepted proposals go to; add that instance as well. It cannot be empty or the instance's own key.

Place it on a page

In the admin, open a page and choose the instance under Shows on this page. The page shows the proposal form from tips/form.gohtml, or submissions/form.gohtml for every instance without a design of its own. Other addresses below the page get the site's 404 page. Generate a working starting point, with labels and every result message:

task templates -- tips

A minimal form:

{{/* @dot github.com/donseba/go-webthings/modules/submissions.FormView */}}
<h1>{{.Title}}</h1>

<section id="{{.Key}}">
    {{if eq .Result "sent"}}
        <p>Thank you! The editors will read your proposal.</p>
    {{else}}
        {{if eq .Result "invalid"}}<p>Please fill in every field. Images are JPEG, PNG or GIF up to {{.MaxImageMB}} MB.</p>{{end}}
        {{if eq .Result "throttled"}}<p>You sent several proposals already. Please try again later.</p>{{end}}

        {{form_open .Form}}
            {{form_field .Form "name" nil}}
            {{form_field .Form "email" nil}}
            {{form_field .Form "title" nil}}
            {{form_field .Form "text" nil}}
            {{form_field .Form "image" nil}}
            <button>Send</button>
        {{form_close}}
    {{end}}
</section>

.Form is the form: form_open writes the form tag, posting to .Action as multipart/form-data because of the image, with the spam trap and the page to return to, the element whose id is .Key, the instance key. form_field renders one of its fields by its key, with its label and limits, such as the image's accept, translated with the site's translations and in the site's form theme; the ids start with the key, such as tips-title. See Template functions.

Show the form anywhere

The same form works in any template, for example in a sidebar, with three template functions: submitInput is the form, submitURL the address it posts to and submitResult the result of the visitor's last post. The form returns the visitor to the element whose id is the instance key:

<aside id="tips">
    {{if eq (submitResult "tips") "sent"}}
        <p>Thank you for your tip!</p>
    {{else}}
        {{with submitInput "tips"}}
            {{form_open .}}
                {{form_field . "name" nil}}
                {{form_field . "email" nil}}
                {{form_field . "title" nil}}
                {{form_field . "text" nil}}
                {{form_field . "image" nil}}
                <button>Send</button>
            {{form_close}}
        {{end}}
    {{end}}
</aside>

Inputs written by hand with the names of the fields below work too.

Visitors

The form posts the fields of submissions.ProposalInput to /_/tips/submit as multipart/form-data, because of the image; a form without an image field may be sent url-encoded.

Field
name one line, up to 200 characters
email a valid e-mail address
title one line, up to 200 characters
text plain text or Markdown, up to 20000 characters
image optional: JPEG, PNG or GIF up to 5 MB, sent as one of those types or with a .jpg, .jpeg, .png or .gif name, and recognised by its content

The visitor returns to the form's page with ?tips=sent, ?tips=invalid when a field is missing or wrong, or ?tips=throttled after more than five proposals in an hour from the same IP address (list a reverse proxy in Config.TrustedProxies). Posts that fill in the spam trap that form_open adds are answered with sent and dropped. Only a proposal that was stored makes the site's output cache render the pages again; see Modules and the output cache.

With invalid, the form shows the visitor's name, address, title and text again, with a message in the page's language at each field to correct; the image is not kept, so the visitor chooses it again. form_field does this without a change to the template; inputs written by hand do not. See A post with mistakes. A proposal that cannot be saved, such as when the database cannot be reached, gets the site's error page, 500.gohtml, also with HTMX.

A proposal keeps the language of the page the form was on. Its image goes into the media library right away and cannot be deleted there until the proposal is deleted for good.

In the admin

Each instance has its own section (/admin/tips; /admin is the default admin path) with a badge that counts the new proposals. The list shows the new proposals, newest first, with dates in the site's timezone; filters switch to the accepted, the rejected or all proposals.

A proposal shows its text, image, sender, e-mail address and language. Editors with the right to publish in the instance can:

  • Accept as draft post: this creates an unpublished post in news, in the proposal's language, with its title, its text as Markdown and its image as the post's image, dated today. The post's history names the sender. Open draft post leads to it. A rejected proposal can still be accepted.
  • Reject a new proposal.

Move to trash moves a proposal to the trash, where it can be restored or deleted for good; it needs the delete right. Deleting it for good moves its image to the media library's trash too, unless the proposal was accepted or other content uses the image.

Under Settings, editors who may edit the instance set the address that receives new proposals, and whether senders hear from the site. Other editors do not see the settings.

E-mail

Mail Sent to When
notify the notification address, in the admin's language for every new proposal
received the sender, in the proposal's language after sending, with Confirm receipt to submitters on
accepted, rejected the sender, in the proposal's language when an editor decides, with Tell submitters when their proposal is accepted or rejected on

The notification holds the text, a link to the image if there is one, and a link to the proposal in the admin; replying to it answers the sender. Without a notification address no one is notified, and both options for senders are off until editors switch them on. A theme replaces the texts with mail/tips/<name>.gohtml or mail/submissions/<name>.gohtml.

From Go

submissions.Of(app, "tips") returns the instance once the app has started:

tips := submissions.Of(app, "tips")

waiting, err := tips.Count(ctx, submissions.StatusNew)

Create stores a proposal (an image must already be in the media library), List and Get read proposals, Accept and Reject decide as the admin does, and Delete, Undelete and Purge move a proposal to the trash, bring it back or remove it for good. These calls send no e-mail.

  • Publishing holds the posts that accepted proposals become.
  • Forms collects messages that do not become posts, such as a contact form.

Documentation

Overview

Package submissions lets visitors propose a news item: a title, a text and an optional image, with their name and e-mail address. Editors review the proposals in the admin and accept one as a draft post in a Publishing instance, where it is edited and published as usual, or reject it. Add an instance with the key of the Publishing instance it feeds:

app.Use(publishing.Module("news", "News"), submissions.Module("tips", "News tips", "news"))

Form

The form posts to the module's action, multipart because of the image; submitInput gives its model (ProposalInput) for go-form's functions:

<div id="tips">
    {{with submitInput "tips"}}
        {{form_open .}}
            {{form_field . "name" nil}} {{form_field . "email" nil}}
            {{form_field . "title" nil}} {{form_field . "text" nil}}
            {{form_field . "image" nil}}
            <button>Send</button>
        {{form_close}}
    {{end}}
</div>

Fields: name (200 characters), email, title (200 characters), text (plain text or Markdown, 20000 characters) and the optional image (JPEG, PNG or GIF up to 5 MB, stored in the media library). form_open adds the multipart encoding, the spam trap and the page to return to: the element with the instance key as its id. The visitor returns to the form's page with ?<key>=sent, ?<key>=invalid when a field is missing or wrong, or ?<key>=throttled after too many proposals (five per hour); spam is answered with sent and dropped. The proposal keeps the language of that page.

Templates

Placed on a page, the module renders submissions/form.gohtml with a FormView (App.WriteTemplates writes a working starting point). The form can also be placed in any other template with the functions:

{{submitURL "tips"}}     the address the form posts to
{{submitInput "tips"}}   the form's model, as above
{{submitResult "tips"}}  "sent", "invalid", "throttled" or "" after the visitor's post

for example {{if eq (submitResult "tips") "sent"}}<p>Thank you!</p>{{end}}.

Mail

In the admin settings of the instance editors set the address that receives every new proposal, and whether submitters get a confirmation after sending and a message when their proposal is accepted or rejected. The messages are mail templates (see webthings.Module.Mail) with MailView as data: received, accepted and rejected go to the submitter in the proposal's language, notify to the editors in the admin's language. The defaults are English text in tl, so the site's translations translate them; a theme replaces them with mail/<key>/<name>.gohtml or mail/submissions/<name>.gohtml.

Index

Constants

View Source
const (
	// ResultSent is a proposal that was received and waits for an editor.
	ResultSent = "sent"
	// ResultInvalid is a proposal with a missing or wrong field, or an
	// image of another type; nothing was saved.
	ResultInvalid = "invalid"
	// ResultThrottled is the answer to a visitor who sent too many proposals.
	ResultThrottled = frontend.ResultThrottled
)

Results are the values of ?<key>= after a visitor's post.

View Source
const (
	// StatusNew is a proposal no editor has decided on yet.
	StatusNew = "new"
	// StatusAccepted is a proposal that became a draft post.
	StatusAccepted = "accepted"
	// StatusRejected is a proposal an editor turned down.
	StatusRejected = "rejected"
)

Statuses of a submission. New ones wait for an editor's decision.

Variables

View Source
var (
	// ErrInvalid is returned for a proposal with a missing or wrong field.
	ErrInvalid = errors.New("submissions_invalid")
	// ErrDecided is returned when accepting an accepted submission or
	// rejecting one that is no longer new.
	ErrDecided = errors.New("submissions_decided")
	// ErrNoTarget is returned when accepting a submission while the target
	// key names no Publishing instance.
	ErrNoTarget = errors.New("submissions_no_target")
)

Functions

func Module

func Module(key, title, target string) webthings.Module

Module adds a Submissions instance. key is used in URLs of the admin and the form action and must be unique on the site; title is shown in the admin and the page editor; target is the key of the Publishing instance accepted proposals become draft posts in, for example "news". Add that instance to the app as well.

Types

type DetailView

type DetailView struct {
	Title   string
	BackURL string
	Name    string
	Email   string
	Subject string
	Body    string
	// ImageURL is empty without an image.
	ImageURL string
	Locale   string
	Date     string
	Status   string
	// Decided is when and by whom the submission was accepted or rejected.
	Decided   string
	DecidedBy string
	// PostURL opens the draft post of an accepted submission.
	PostURL string
	Accept  *form.RenderModel
	Reject  *form.RenderModel
	Delete  *form.RenderModel
}

DetailView is the data of templates/admin/detail.gohtml: one proposal with the buttons to accept or reject it.

type FormView

type FormView struct {
	// Key is the instance key; results arrive as ?<key>=<result>, and the
	// form returns the visitor to the element with the id Key.
	Key string
	// Title is the title of the page the module is placed on.
	Title string
	// Action is the address the form posts to.
	Action string
	// Result is "sent" after a proposal was received, "invalid" when it was
	// refused, "throttled" after too many proposals, empty otherwise.
	Result string
	// MaxImageMB is the largest image visitors can add, in megabytes.
	MaxImageMB int
	// Form is the form's model (ProposalInput) for go-form's template
	// functions, such as {{form_open .Form}} and {{form_field .Form "title"
	// nil}}.
	Form form.RenderModel
}

FormView is the data of submissions/form.gohtml.

type ListView

type ListView struct {
	Title string
	// SettingsURL is set for editors who may change the settings.
	SettingsURL string
	// TrashURL shows the instance's deleted proposals.
	TrashURL string
	// Filters switch between the statuses; new ones are counted.
	Filters admin.Tabs
	Rows    []Row
	Pager   admin.Pager
}

ListView is the data of templates/admin/list.gohtml: the proposals, filtered by status.

type MailView

type MailView struct {
	Submission
	// ImageURL is the absolute address of the image; empty without one, and
	// in mail to the submitter.
	ImageURL string
	// AdminURL opens the submission in the admin; empty in mail to the
	// submitter.
	AdminURL string
}

MailView is the data of the mail templates: the submission, and for the editors' notification the links to its image and to the admin.

type Proposal

type Proposal struct {
	Name  string
	Email string
	Title string
	// Body is plain text or Markdown.
	Body string
	// MediaID is the uploaded image in the media library, or empty.
	MediaID string
	Locale  string
}

Proposal is what a visitor sends.

type ProposalInput

type ProposalInput struct {
	Name  string                `form:"input,text"  name:"name"  label:"Name"           required:"true" maxLength:"200"   line:"true" autocomplete:"name"`
	Email string                `` /* 137-byte string literal not displayed */
	Title string                `form:"input,text"  name:"title" label:"Title"          required:"true" maxLength:"200"   line:"true"`
	Text  string                `form:"textarea"    name:"text"  label:"Text"           required:"true" maxLength:"20000"`
	Image *multipart.FileHeader `` /* 212-byte string literal not displayed */
}

ProposalInput is the form a visitor posts to /_/<key>/submit: a name, an e-mail address, a title, the text and optionally an image. Themes render its fields with form_field, with their limits (FormView.Form), or write inputs with these names; a form with the image is sent as multipart/form-data.

type Row

type Row struct {
	URL    string
	Date   string
	Title  string
	Name   string
	Locale string
	// Status is the translated status label.
	Status string
}

Row is one proposal in the list.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is one Submissions instance, for the website's own code.

func Of

func Of(app *webthings.App, key string) *Service

Of returns the Submissions instance with key from a started app, or nil.

func (*Service) Accept

func (s *Service) Accept(ctx context.Context, id, user string) (Submission, error)

Accept turns a submission into a draft post in the target Publishing instance, in the submission's language, and returns it with the post's ID. user is the deciding editor's ID. An accepted submission returns ErrDecided, a target that is no Publishing instance ErrNoTarget.

func (*Service) Count

func (s *Service) Count(ctx context.Context, status string) (int, error)

Count returns the number of submissions with status; all when empty.

func (*Service) Create

func (s *Service) Create(ctx context.Context, proposal Proposal) (Submission, error)

Create stores a proposal as a new submission. It returns ErrInvalid when a field is missing or too long; the image must already be in the media library.

func (*Service) Delete

func (s *Service) Delete(ctx context.Context, id string) error

Delete moves a submission to the trash: it leaves the admin's lists and counts, and Undelete brings it back. Its image stays in the media library meanwhile, and the post of an accepted submission is not touched.

func (*Service) Get

func (s *Service) Get(ctx context.Context, id string) (Submission, error)

Get returns one submission that is not in the trash, or db.ErrNotFound.

func (*Service) List

func (s *Service) List(ctx context.Context, status string, offset, limit int) ([]Submission, bool, error)

List returns submissions with status (all when empty) newest first, skipping offset, and whether older ones exist.

func (*Service) Purge

func (s *Service) Purge(ctx context.Context, id string) error

Purge removes a submission in the trash for good; db.ErrNotFound means it is not in the trash. The image of a submission that was not accepted goes to the media library's trash too, unless other content uses it; the post of an accepted one keeps its image and is not touched.

func (*Service) Reject

func (s *Service) Reject(ctx context.Context, id, user string) (Submission, error)

Reject marks a new submission as rejected; user is the deciding editor's ID. A submission that is no longer new returns ErrDecided.

func (*Service) SaveSettings

func (s *Service) SaveSettings(ctx context.Context, settings Settings) error

SaveSettings stores the notification address and which mails submitters get. An invalid address returns ErrInvalid.

func (*Service) Settings

func (s *Service) Settings(ctx context.Context) (Settings, error)

Settings returns the instance's mail settings; empty when never saved.

func (*Service) Undelete

func (s *Service) Undelete(ctx context.Context, id string) error

Undelete brings a submission back from the trash; db.ErrNotFound means it is not in the trash.

type Settings

type Settings struct {
	// Notify receives every new proposal; empty sends nothing.
	Notify string `db:"notify"`
	// ConfirmMail sends submitters a confirmation of their proposal.
	ConfirmMail bool `db:"confirm_mail"`
	// DecisionMail tells submitters when their proposal is accepted or rejected.
	DecisionMail bool `db:"decision_mail"`
}

Settings are the instance's mail settings.

type Submission

type Submission struct {
	ID      string
	Name    string
	Email   string
	Title   string
	Body    string
	MediaID string
	Locale  string
	Status  string
	// PostID is the draft post of an accepted submission.
	PostID    string
	CreatedAt time.Time
	// DecidedAt and DecidedBy (a user ID) are set once accepted or rejected.
	DecidedAt time.Time
	DecidedBy string
}

Submission is a stored proposal with the editors' decision.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL