Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(examples): forms #2604

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
47d5d84
feat(examples): gno-forms
agherasie Jul 16, 2024
a19037a
chore: fmt gno-forms
agherasie Jul 16, 2024
34c23eb
feat(examples): gno-forms readme and comments
agherasie Jul 16, 2024
1ac3eeb
feat(examples): r/forms date restrictions
agherasie Jul 18, 2024
332d856
chore(examples): fmt
agherasie Jul 18, 2024
92797f1
Update examples/gno.land/p/demo/forms/submit_test.gno
agherasie Jul 21, 2024
82dd483
Update examples/gno.land/p/demo/forms/create_test.gno
agherasie Jul 21, 2024
9504f6f
fix(examples): newlines in gno-forms structs
agherasie Jul 21, 2024
254c10f
fix(examples): FormDB rename gno-forms
agherasie Jul 21, 2024
76afc46
fix(examples): gno-forms unexported functions
agherasie Jul 21, 2024
6e35760
fix(examples): gno-forms error constants
agherasie Jul 21, 2024
f0a0e58
fix(examples): gno-forms handling all invalid json errors
agherasie Jul 21, 2024
8d97b6d
feat(examples): gno-forms using urequire
agherasie Jul 21, 2024
c6f0b15
chore(examples): gno-forms fmt
agherasie Jul 21, 2024
361170a
chore(examples): gno-forms godoc
agherasie Jul 21, 2024
2588cb3
chore(examples): gno-forms tidy
agherasie Jul 21, 2024
e9fb754
Merge branch 'master' into feat/gno-forms
agherasie Jul 21, 2024
9ccffcf
Update README.md
agherasie Jul 30, 2024
9d5f76b
Merge branch 'master' into feat/gno-forms
agherasie Jul 30, 2024
f4e7378
Update examples/gno.land/p/demo/forms/forms.gno
agherasie Aug 2, 2024
8da6592
Update examples/gno.land/p/demo/forms/create.gno
agherasie Aug 2, 2024
10e6843
chore(examples): gno forms readme markdown table formatting
agherasie Aug 2, 2024
854d61b
Merge branch 'master' into feat/gno-forms
agherasie Aug 2, 2024
39c8bcb
Merge branch 'master' into feat/gno-forms
leohhhn Aug 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions examples/gno.land/p/demo/forms/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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).
121 changes: 121 additions & 0 deletions examples/gno.land/p/demo/forms/create.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package forms

import (
"std"
"time"

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

func CreateField(label string, fieldType string, required bool) Field {
leohhhn marked this conversation as resolved.
Show resolved Hide resolved
return Field{
Label: label,
FieldType: fieldType,
Required: required,
}
}

type times struct {
openAt *time.Time
closeAt *time.Time
}

func parseDates(openAt string, closeAt string) (times, error) {
var openAtTime, closeAtTime *time.Time

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

// Parse openAt if it's not empty
if openAt != "" {
res, err := time.Parse(dateFormat, openAt)
if err != nil {
return times{}, errInvalidDate
}
openAtTime = &res
}

// Parse closeAt if it's not empty
if closeAt != "" {
res, err := time.Parse(dateFormat, closeAt)
if err != nil {
return times{}, errInvalidDate
}
closeAtTime = &res
}

return times{openAtTime, closeAtTime}, nil
}
Comment on lines +18 to +47
Copy link
Contributor

@ltzmaxwell ltzmaxwell Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. this(times) seems to be a redundant design;
  2. using time.Time directly (without a pointer) is perfectly fine.
    @leohhhn


// 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 dates
times, err := parseDates(openAt, closeAt)
if err != nil {
return "", err
}

// 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, err := node.GetIndex(i)
if err != nil {
return "", errInvalidJson
}

labelNode, err := field.GetKey("label")
if err != nil {
return "", errInvalidJson
}
fieldTypeNode, err := field.GetKey("fieldType")
if err != nil {
return "", errInvalidJson
}
requiredNode, err := field.GetKey("required")
if err != nil {
return "", errInvalidJson
}

label, err := labelNode.GetString()
if err != nil {
return "", errInvalidJson
}
fieldType, err := fieldTypeNode.GetString()
if err != nil {
return "", errInvalidJson
}
required, err := requiredNode.GetBool()
if err != nil {
return "", errInvalidJson
}

fields[i] = CreateField(label, fieldType, required)
}

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

// Creating the form
form := Form{
ID: id,
Owner: std.PrevRealm().Addr(),
Title: title,
Description: description,
CreatedAt: time.Now(),
openAt: times.openAt,
closeAt: times.closeAt,
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/demo/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.TestSetOrigCaller(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")
})
}
15 changes: 15 additions & 0 deletions examples/gno.land/p/demo/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/demo/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 {
leohhhn marked this conversation as resolved.
Show resolved Hide resolved
openAt, errOpen := form.OpenAt()
closedAt, errClose := form.CloseAt()

noOpenDate := errOpen != nil
leohhhn marked this conversation as resolved.
Show resolved Hide resolved
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 == nil {
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 == nil {
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
}
8 changes: 8 additions & 0 deletions examples/gno.land/p/demo/forms/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module gno.land/p/demo/forms

require (
gno.land/p/demo/json v0.0.0-latest
gno.land/p/demo/seqid v0.0.0-latest
gno.land/p/demo/testutils v0.0.0-latest
gno.land/p/demo/urequire v0.0.0-latest
)
Loading
Loading