Skip to content

Commit

Permalink
feat(examples): add {p,r}/agherasie/forms (#3524)
Browse files Browse the repository at this point in the history
Sorry for the long hiatus which caused #2604 to close, I moved to South
Korea this year for a university exchange program and let this PR
collect dust for a while.
I've addressed
#2604 (comment) in
7a6a032,
hoping that the PR might be ready for merge now !
If not, please let me know if any further updates should be made to the
code !

---

As part of the student contributor program, I attempted to create a new
example realm that allows the creation and submission of forms on gno !

## Features
- **Form Creation**: Create new forms with specified titles,
descriptions, and fields.
`CreateForm(...)`
- **Form Submission**: Submit answers to forms.
`SubmitForm(...)`
- **Form Retrieval**: Retrieve existing forms and their submissions.
`GetForms(...), GetFormByID(...), GetAnswer(...)`
- **Form Deadline**: Set a precise time range during which a form can be
interacted with.

## Field Types
The system supports the following field types:

type|example
-|-
string|`{"label": "Name", "fieldType": "string", "required": true}`
number|`{"label": "Age", "fieldType": "number", "required": true}`
boolean|`{"label": "Is Student?", "fieldType": "boolean", "required":
false}`
choice|`{"label": "Favorite Food", "fieldType":
"[Pizza|Schnitzel|Burger]", "required": true}`
multi-choice|`{"label": "Hobbies", "fieldType":
"{Reading|Swimming|Gaming}", "required": false}`

## Demo

The external repo where the initial development took place and where you
can find the frontend is [here](https://github.com/agherasie/gno-forms).

The web app itself is hosted [here](https://gno-forms.netlify.app/)

And the most recent test4 version of the contract is
[forms2](https://test4.gno.land/r/g1w62226g8hykfmtuasvz80rdf0jl6phgxsphh5v/testing/forms2)

---

Screenshots :

<details>
<summary>
<a
href="https://test4.gno.land/r/g1w62226g8hykfmtuasvz80rdf0jl6phgxsphh5v/testing/forms2">gnoweb
Render()</a>
</summary>
<img width="941" alt="image"
src="https://github.com/user-attachments/assets/24b9c17d-b51e-4d0b-ab19-b9bca49c0a89">
</details>

<details>
<summary>
<a
href="https://gno-forms.netlify.app/results/0000002/g1w62226g8hykfmtuasvz80rdf0jl6phgxsphh5v">
a form response in the web interface
</a>
</summary>
<img width="539" alt="image"
src="https://github.com/user-attachments/assets/b3469545-842f-4030-a4da-1060802c6477">
</details>

<details>
<summary>
<a href="https://gno-forms.netlify.app/create">creating a form in the
web interface</a></summary>
<img width="564" alt="image"
src="https://github.com/user-attachments/assets/c6e16bd0-0523-47a7-bf09-b3dc7f5d9314">
</details>

---

<details><summary>Contributors' checklist...</summary>

- [X] Added new tests, or not needed, or not feasible
- [X] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [X] Updated the official documentation or not needed
- [X] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [ ] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>

---------

Co-authored-by: Leon Hudak <[email protected]>
Co-authored-by: Guilhem Fanton <[email protected]>
Co-authored-by: Nathan Toups <[email protected]>
Co-authored-by: Morgan <[email protected]>
  • Loading branch information
5 people authored Mar 10, 2025
1 parent b1685f0 commit 42019f2
Show file tree
Hide file tree
Showing 14 changed files with 989 additions and 0 deletions.
80 changes: 80 additions & 0 deletions examples/gno.land/p/agherasie/forms/create.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package forms

import (
"std"
"time"

"gno.land/p/demo/json"
)

const dateFormat = "2006-01-02T15:04:05Z"

func CreateField(label string, fieldType string, required bool) Field {
return Field{
Label: label,
FieldType: fieldType,
Required: required,
}
}

// CreateForm creates a new form with the given parameters
func (db *FormDB) CreateForm(title string, description string, openAt string, closeAt string, data string) (string, error) {
// Parsing the dates
var parsedOpenTime, parsedCloseTime time.Time

if openAt != "" {
var err error
parsedOpenTime, err = time.Parse(dateFormat, openAt)
if err != nil {
return "", errInvalidDate
}
}

if closeAt != "" {
var err error
parsedCloseTime, err = time.Parse(dateFormat, closeAt)
if err != nil {
return "", errInvalidDate
}
}

// Parsing the json submission
node, err := json.Unmarshal([]byte(data))
if err != nil {
return "", errInvalidJson
}

fieldsCount := node.Size()
fields := make([]Field, fieldsCount)

// Parsing the json submission to create the gno data structures
for i := 0; i < fieldsCount; i++ {
field := node.MustIndex(i)

fields[i] = CreateField(
field.MustKey("label").MustString(),
field.MustKey("fieldType").MustString(),
field.MustKey("required").MustBool(),
)
}

// Generating the form ID
id := db.IDCounter.Next().String()

// Creating the form
form := Form{
ID: id,
Owner: std.PreviousRealm().Address(),
Title: title,
Description: description,
CreatedAt: time.Now(),
openAt: parsedOpenTime,
closeAt: parsedCloseTime,
Fields: fields,
}

// Adding the form to the database
db.Forms = append(db.Forms, &form)

return id, nil
}
70 changes: 70 additions & 0 deletions examples/gno.land/p/agherasie/forms/create_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package forms

import (
"std"
"testing"

"gno.land/p/demo/testutils"
"gno.land/p/demo/urequire"
)

func TestCreateForm(t *testing.T) {
alice := testutils.TestAddress("alice")
std.TestSetOriginCaller(alice)
db := NewDB()
title := "Simple Form"
description := "This is a form"
openAt := "2021-01-01T00:00:00Z"
closeAt := "2021-01-02T00:00:00Z"
data := `[
{
"label": "Name",
"fieldType": "string",
"required": true
},
{
"label": "Age",
"fieldType": "number",
"required": false
},
{
"label": "Is this a test?",
"fieldType": "boolean",
"required": false
},
{
"label": "Favorite Food",
"fieldType": "['Pizza', 'Schnitzel', 'Burger']",
"required": true
},
{
"label": "Favorite Foods",
"fieldType": "{'Pizza', 'Schnitzel', 'Burger'}",
"required": true
}
]`

urequire.NotPanics(t, func() {
id, err := db.CreateForm(title, description, openAt, closeAt, data)
if err != nil {
panic(err)
}
urequire.True(t, id != "", "Form ID is empty")

form, err := db.GetForm(id)
if err != nil {
panic(err)
}

urequire.True(t, form.ID == id, "Form ID is not correct")
urequire.True(t, form.Owner == alice, "Owner is not correct")
urequire.True(t, form.Title == title, "Title is not correct")
urequire.True(t, form.Description == description, "Description is not correct")
urequire.True(t, len(form.Fields) == 5, "Not enough fields were provided")
urequire.True(t, form.Fields[0].Label == "Name", "Field 0 label is not correct")
urequire.True(t, form.Fields[0].FieldType == "string", "Field 0 type is not correct")
urequire.True(t, form.Fields[0].Required == true, "Field 0 required is not correct")
urequire.True(t, form.Fields[1].Label == "Age", "Field 1 label is not correct")
urequire.True(t, form.Fields[1].FieldType == "number", "Field 1 type is not correct")
})
}
25 changes: 25 additions & 0 deletions examples/gno.land/p/agherasie/forms/doc.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// # Gno forms

// gno-forms is a package which demonstrates a form editing and sharing application in gno

// ## Features
// - **Form Creation**: Create new forms with specified titles, descriptions, and fields.
// - **Form Submission**: Submit answers to forms.
// - **Form Retrieval**: Retrieve existing forms and their submissions.
// - **Form Deadline**: Set a precise time range during which a form can be interacted with.

// ## Field Types
// The system supports the following field types:

// | type | example |
// |--------------|-------------------------------------------------------------------------------------------------|
// | string | `{"label": "Name", "fieldType": "string", "required": true}` |
// | number | `{"label": "Age", "fieldType": "number", "required": true}` |
// | boolean | `{"label": "Is Student?", "fieldType": "boolean", "required": false}` |
// | choice | `{"label": "Favorite Food", "fieldType": "['Pizza', 'Schnitzel', 'Burger']", "required": true}` |
// | multi-choice | `{"label": "Hobbies", "fieldType": "{'Reading', 'Swimming', 'Gaming'}", "required": false}` |

// ## Web-app

// The external repo where the initial development took place and where you can find the frontend is [here](https://github.com/agherasie/gno-forms).
package forms
15 changes: 15 additions & 0 deletions examples/gno.land/p/agherasie/forms/errors.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package forms

import "errors"

var (
errNoOpenDate = errors.New("Form has no open date")
errNoCloseDate = errors.New("Form has no close date")
errInvalidJson = errors.New("Invalid JSON")
errInvalidDate = errors.New("Invalid date")
errFormNotFound = errors.New("Form not found")
errAnswerNotFound = errors.New("Answer not found")
errAlreadySubmitted = errors.New("You already submitted this form")
errFormClosed = errors.New("Form is closed")
errInvalidAnswers = errors.New("Invalid answers")
)
132 changes: 132 additions & 0 deletions examples/gno.land/p/agherasie/forms/forms.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package forms

import (
"std"
"time"

"gno.land/p/demo/seqid"
)

// FieldType examples :
// - string: "string";
// - number: "number";
// - boolean: "boolean";
// - choice: "['Pizza', 'Schnitzel', 'Burger']";
// - multi-choice: "{'Pizza', 'Schnitzel', 'Burger'}";
type Field struct {
Label string
FieldType string
Required bool
}

type Form struct {
ID string
Owner std.Address
Title string
Description string
Fields []Field
CreatedAt time.Time
openAt time.Time
closeAt time.Time
}

// Answers example :
// - ["Alex", 21, true, 0, [0, 1]]
type Submission struct {
FormID string
Author std.Address
Answers string // json
SubmittedAt time.Time
}

type FormDB struct {
Forms []*Form
Answers []*Submission
IDCounter seqid.ID
}

func NewDB() *FormDB {
return &FormDB{
Forms: make([]*Form, 0),
Answers: make([]*Submission, 0),
}
}

// This function checks if the form is open by verifying the given dates
// - If a form doesn't have any dates, it's considered open
// - If a form has only an open date, it's considered open if the open date is in the past
// - If a form has only a close date, it's considered open if the close date is in the future
// - If a form has both open and close dates, it's considered open if the current date is between the open and close dates
func (form *Form) IsOpen() bool {
openAt, errOpen := form.OpenAt()
closedAt, errClose := form.CloseAt()

noOpenDate := errOpen != nil
noCloseDate := errClose != nil

if noOpenDate && noCloseDate {
return true
}

if noOpenDate && !noCloseDate {
return time.Now().Before(closedAt)
}

if !noOpenDate && noCloseDate {
return time.Now().After(openAt)
}

now := time.Now()
return now.After(openAt) && now.Before(closedAt)
}

// OpenAt returns the open date of the form if it exists
func (form *Form) OpenAt() (time.Time, error) {
if form.openAt.IsZero() {
return time.Time{}, errNoOpenDate
}

return form.openAt, nil
}

// CloseAt returns the close date of the form if it exists
func (form *Form) CloseAt() (time.Time, error) {
if form.closeAt.IsZero() {
return time.Time{}, errNoCloseDate
}

return form.closeAt, nil
}

// GetForm returns a form by its ID if it exists
func (db *FormDB) GetForm(id string) (*Form, error) {
for _, form := range db.Forms {
if form.ID == id {
return form, nil
}
}
return nil, errFormNotFound
}

// GetAnswer returns an answer by its form - and author ids if it exists
func (db *FormDB) GetAnswer(formID string, author std.Address) (*Submission, error) {
for _, answer := range db.Answers {
if answer.FormID == formID && answer.Author.String() == author.String() {
return answer, nil
}
}
return nil, errAnswerNotFound
}

// GetSubmissionsByFormID returns a list containing the existing form submissions by the form ID
func (db *FormDB) GetSubmissionsByFormID(formID string) []*Submission {
submissions := make([]*Submission, 0)

for _, answer := range db.Answers {
if answer.FormID == formID {
submissions = append(submissions, answer)
}
}

return submissions
}
Loading

0 comments on commit 42019f2

Please sign in to comment.