-
Notifications
You must be signed in to change notification settings - Fork 398
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(examples): add {p,r}/agherasie/forms (#3524)
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
1 parent
b1685f0
commit 42019f2
Showing
14 changed files
with
989 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.