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 5 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}`
gfanton marked this conversation as resolved.
Show resolved Hide resolved

## 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).
95 changes: 95 additions & 0 deletions examples/gno.land/p/demo/forms/create.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package forms

import (
"std"
"time"

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

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,
}
}

func ParseDates(openAt string, closeAt string) (*time.Time, *time.Time) {
var openAtTime, closeAtTime *time.Time

dateFormat := "2006-01-02T15:04:05Z"
agherasie marked this conversation as resolved.
Show resolved Hide resolved

// Parse openAt if it's not empty
if openAt != "" {
res, err := time.Parse(dateFormat, openAt)
if err != nil {
panic(ufmt.Errorf("invalid date: %v", openAt))
}
openAtTime = &res
}

// Parse closeAt if it's not empty
if closeAt != "" {
res, err := time.Parse(dateFormat, closeAt)
if err != nil {
panic(ufmt.Errorf("invalid date: %v", closeAt))
}
closeAtTime = &res
}

return openAtTime, closeAtTime
}

func (db *FormDatabase) CreateForm(title string, description string, openAt string, closeAt string, data string) string {
// Parsing dates
openAtTime, closeAtTime := ParseDates(openAt, closeAt)

// Parsing the json submission
node, err := json.Unmarshal([]byte(data))
if err != nil {
ufmt.Errorf("invalid json: %v", err)
}

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 {
ufmt.Errorf("error: %v", err)
}

labelNode, _ := field.GetKey("label")
leohhhn marked this conversation as resolved.
Show resolved Hide resolved
fieldTypeNode, _ := field.GetKey("fieldType")
requiredNode, _ := field.GetKey("required")

label, _ := labelNode.GetString()
fieldType, _ := fieldTypeNode.GetString()
required, _ := requiredNode.GetBool()

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: openAtTime,
closeAt: closeAtTime,
Fields: fields,
}

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

return id
}
81 changes: 81 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,81 @@
package forms

import (
"std"
"testing"

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

func TestCreateForm(t *testing.T) {
alice := testutils.TestAddress("alice")
std.TestSetOrigCaller(alice)

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
}
]`
db := NewDatabase()
id := db.CreateForm(title, description, openAt, closeAt, data)
if id == "" {
t.Error("Form ID is empty")
}
form := db.GetForm(id)
if form == nil {
t.Error("Form is nil")
}
if form.Owner != alice {
t.Error("Owner is not correct")
}
if form.Title != title {
t.Error("Title is not correct")
}
if form.Description != description {
t.Error("Description is not correct")
}
if len(form.Fields) != 5 {
t.Error("Fields are not correct")
agherasie marked this conversation as resolved.
Show resolved Hide resolved
}
if form.Fields[0].Label != "Name" {
t.Error("Field 0 label is not correct")
}
if form.Fields[0].FieldType != "string" {
t.Error("Field 0 type is not correct")
}
if form.Fields[0].Required != true {
t.Error("Field 0 required is not correct")
}
if form.Fields[1].Label != "Age" {
t.Error("Field 1 label is not correct")
}
if form.Fields[1].FieldType != "number" {
t.Error("Field 1 type is not correct")
}
}
131 changes: 131 additions & 0 deletions examples/gno.land/p/demo/forms/forms.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package forms

import (
"errors"
"std"
"time"

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

type Field struct {
Label string

/*
leohhhn marked this conversation as resolved.
Show resolved Hide resolved
string: "string";
number: "number";
boolean: "boolean";
choice: "['Pizza', 'Schnitzel', 'Burger']";
multi-choice: "{'Pizza', 'Schnitzel', 'Burger'}";
*/
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
}

type Submission struct {
FormID string

Author std.Address

/* ["Alex", 21, true, 0, [0, 1]] */
leohhhn marked this conversation as resolved.
Show resolved Hide resolved
Answers string // json

SubmittedAt time.Time
}

type FormDatabase struct {
leohhhn marked this conversation as resolved.
Show resolved Hide resolved
Forms []*Form

leohhhn marked this conversation as resolved.
Show resolved Hide resolved
Answers []*Submission

IDCounter seqid.ID
}

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

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)
}

return time.Now().After(openAt) && time.Now().Before(closedAt)
agherasie marked this conversation as resolved.
Show resolved Hide resolved
}

func (form *Form) OpenAt() (time.Time, error) {
if form.openAt == nil {
return time.Time{}, errors.New("Form has no open date")
}

return *form.openAt, nil
}

func (form *Form) CloseAt() (time.Time, error) {
if form.closeAt == nil {
return time.Time{}, errors.New("Form has no close date")
}

return *form.closeAt, nil
}

func (db *FormDatabase) GetForm(id string) *Form {
for _, form := range db.Forms {
if form.ID == id {
return form
}
}
return nil
}

func (db *FormDatabase) GetAnswer(formID string, author std.Address) *Submission {
for _, answer := range db.Answers {
if answer.FormID == formID && answer.Author.String() == author.String() {
return answer
}
}
return nil
}

func (db *FormDatabase) GetSubmissionsByFormID(formID string) []*Submission {
submissions := make([]*Submission, 0)

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

return submissions
}
7 changes: 7 additions & 0 deletions examples/gno.land/p/demo/forms/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module gno.land/p/demo/forms

require (
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/demo/seqid v0.0.0-latest
gno.land/p/demo/json v0.0.0-latest
)
41 changes: 41 additions & 0 deletions examples/gno.land/p/demo/forms/submit.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package forms

import (
"std"
"time"

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

func (db *FormDatabase) SubmitForm(formID string, answers string) {
// Check if form exists
form := db.GetForm(formID)
if form == nil {
panic(ufmt.Errorf("Form not found: %s", formID))
}

// Check if form was already submitted by this user
previousAnswer := db.GetAnswer(formID, std.PrevRealm().Addr())
if previousAnswer != nil {
panic("You already submitted this form")
}

// Check time restrictions
if !form.IsOpen() {
panic("Form is closed")
}

// Check if answers are formatted correctly
if ValidateAnswers(answers, form.Fields) == false {
panic("Invalid answers")
}

// Save answers
answer := Submission{
FormID: formID,
Answers: answers,
Author: std.PrevRealm().Addr(),
SubmittedAt: time.Now(),
}
db.Answers = append(db.Answers, &answer)
}
Loading
Loading