DEV Community

Cover image for GoNetNinja: Day 4
YourTech John
YourTech John

Posted on

GoNetNinja: Day 4

Day 4

Today I'm working html and javascript. Very little Go. I'm adding an editmode toggle link and a query param to set edit mode.

I'll have elements with the "editelement" class, and a toggle to set a class called "edithidden"

HTML:

    <a id="toggleeditor" title="toggle edit mode">
      <i class="fas fa-user-edit" tooltip="toggle edit mode"></i>Toggle Edit Mode
    </a>
    <div class="editelement"><p>I show up in edit mode</p></div>
Enter fullscreen mode Exit fullscreen mode

CSS

".edithidden {
display: none;
}"
Enter fullscreen mode Exit fullscreen mode

JS:

// editments is my shorthand for edit mode elements
let editments = document.getElementsByClassName("editelement")
let queryParams = new URLSearchParams(window.location.search);

function toggleeditmode() {
    for (let i = 0; i < editments.length; i++) {
        editments[i].classList.toggle("edithidden")
    }
}

function hideeditments() {
    console.log("hide edit mode")
    for (let i = 0; i < editments.length; i++) {
        editments[i].classList.add("edithidden")
    }
}

function showeditments() {
    console.log("edit mode enabled");
    for (let i = 0; i < editments.length; i++) {
        editments[i].classList.remove("edithidden")
    }
}


function initialeditmode() {
    editmode = queryParams.get("editmode",);
    // This is the way I know to turn ?editmode=something
    // into a boolean. If there is a better way, let me
    // know.
    if (editmode === null) { editmode = "false"; }
    if (JSON.parse(editmode.toLowerCase())) {
        showeditments();
    } else {
        hideeditments();
    }
}

document.addEventListener("load", initialeditmode());
document.getElementById('toggleeditor').addEventListener('click', toggleeditmode);
Enter fullscreen mode Exit fullscreen mode

Making a quick net form and model.

I'm creating a form by hand for a quick net entry. This will have fields of netcontrol, opened, closed, early_checkins, and regular_checkins. This form is only going to show if there are not activity records for the net in question.

    <%= if (len(activities) < 1) { %>
    <%= partial("quicknetform.plush.html") %>
    <% } %>
Enter fullscreen mode Exit fullscreen mode

Now I'm going to try and use a pop generated model to setup validation and parsing. The main idea here is that this form data will be used to generate a number of individual activity records.

I'll use buffalo to generate the model, skipping migrations.

buffalo pop generate model -s quicknet
Enter fullscreen mode Exit fullscreen mode

Now we'll review the Binding Docs to work on the handler.

I edit the model to only have json/form fields that are in my form. I also make a new Validate without the tx/pop requirement. From my netcreate experience, I don't really think I need the form variation, but I'll add it for completeness.

// Quicknet is not tied to any database table
type Quicknet struct {
    Opened          time.Time `json:"opened" form:"opened"`
    Closed          time.Time `json:"closed" form:"closed"`
    NetControl      string    `json:"netcontrol" form:"netcontrol"`
    EarlyCheckins   string    `json:"early_checkins" form:"early_checkins"`
    RegularCheckins string    `json:"regular_checkins" form:"regular_checkins"`
}


func (q *Quicknet) Validate() (*validate.Errors, error) {
    return validate.NewErrors(), nil
}
Enter fullscreen mode Exit fullscreen mode

Now in my handler, I should be able to bind the form and log the netcontrol value.

func QuickNetHandler(c buffalo.Context) error {

    qn := &models.Quicknet{}
    if err := c.Bind(qn); err != nil {
        return err
    }

    logrus.Info("netcontrol", qn.NetControl)
    net := models.Net{}
    if err := models.DB.Find(&net, c.Param("id")); err != nil {
        return errors.WithStack(err)
    }

    return c.Redirect(302, "/nets/%s?editmode=true", net.ID)
Enter fullscreen mode Exit fullscreen mode

And indeed that works

time="2022-06-23T13:35:49-04:00" level=info msg=netcontrolsdfas
Enter fullscreen mode Exit fullscreen mode

Now I'll go back to my Validate and add two checks.

func (q *Quicknet) Validate() (*validate.Errors, error) {

    verrs := validate.NewErrors()
    if q.NetControl == "" {
        verrs.Add("netcontrol", "Netcontrol must not be blank")
    }
    if q.Opened.IsZero() {
        verrs.Add("opened", "Opened must not be empty")
    }
    //verrs.Add(&validators.StringIsPresent{Field: n.Name, Name: "Name", Message: "Name can not be blank"})
    return verrs, nil
}
Enter fullscreen mode Exit fullscreen mode

Ok, I ended up doing a lot of work, going to visit family, then coming back. So let's see if I can capture the highlights. The first is that I redid my form to make use of Buffalo's built in tags and form helpers. I also have it rendering the form directly instead of redirecting. This caused me to have near identical code between my NetHandler and my QuickNetHandler. I can see very soon combining the two together, and having some check to tell if the quicknet form was POSTed or not.

In the meantime, I moved all the context set from NetHandler and QuickNetHandler into a common function called _LearnNet.

func _LearnNet(c buffalo.Context, net models.Net) buffalo.Context {
    activities := models.Activities{}
    query := models.DB.Where("net = (?)", net.ID)
    query.Order("time_at desc").All(&activities)
    c.Set("activities", activities)
    c.Set("opened", GetOpen(net.ID))
    c.Set("closed", GetClose(net.ID))
    c.Set("netcontrols", NetControls(net.ID))
    c.Set("participants", NetParticipants(net.ID))
    return c
}
Enter fullscreen mode Exit fullscreen mode

My QuickNetHandler has grown quite large now, so I'm going to take it in sections.

First, we query the net from the database and set that to the context. Then we set and Bind quicknet to the context, before running it through Validate. Notice this is the validate that does not have a Pop Tx database connection. If validation errors occur, we render the template again.

func QuickNetHandler(c buffalo.Context) error {
    net := models.Net{}
    if err := models.DB.Find(&net, c.Param("id")); err != nil {
        return errors.WithStack(err)
    }
    c.Set("net", net)
    quicknet := &models.Quicknet{}
    c.Set("quicknet", quicknet)
    if err := c.Bind(quicknet); err != nil {
        return err
    }

    // Validate the data from the html form
    verrs, err := quicknet.Validate()
    if err != nil {
        return errors.WithStack(err)
    }
    if verrs.HasAny() {
        c = _LearnNet(c, net)
        logrus.Info("quicknet has verrs")

        // Make the errors available inside the html template
        c.Set("errors", verrs)
        //c.Flash().Add("alert", "verrs")
        return c.Render(422, r.HTML("home/netedit.plush.html"))
    }
    logrus.Info("quicknet  has no verrs, continuing")

Enter fullscreen mode Exit fullscreen mode

My html template is using the form_for. I've got text areas for both checkins, followed by net control, open and close. I can set the default values with an iso format based on the scheduled start and close. I haven't figured it out, but the form is not modifying the form group with validation errors like it does in my netnew.plush.html template. I can see the errors in the context, and I've played with the input ids and validation ids. I really want to get that working, but for now I'm just dumping the raw errors.

    <%= form_for( quicknet, {action: quicknetPath({id: net.ID}), method: "POST"}) { %>
    <%= if (errors) { %>
        <%= errors %>
    <% } %>
    <div class="row ">
        <div class="col-md-3">
            <%= f.TextAreaTag("EarlyCheckins", {rows:7}) %>
        </div>
        <div class="col-md-3">
            <%= f.TextAreaTag("RegularCheckins", {rows:7    }) %>
        </div>
        <div class="col-md-6">
            <%= f.InputTag("NetControl") %>

            <div class="form-group">
                <label for="quicknet-opened">Open Net At</label>
                <%= f.DateTimeTag("Opened", {value: net.PlannedStart.Format("2006-01-02T15:04")}) %>

            </div>
            <div class="form-group">
                <label for="quicknet-closed">Close Net At</label>
                <%= f.DateTimeTag("Closed", {value: net.PlannedEnd.Format("2006-01-02T15:04")}) %>
            </div>
            <div class="form-group">
                <button class="btn btn-success" role="submit">Update</button>
            </div>
        </div>

    </div>
    <% } %>
Enter fullscreen mode Exit fullscreen mode

Now, going back into my QuickNetHandler, if we pass all the validation, we start creating activity records. We have already validated that NetControl is not empty, and that Opened is a NotZero value.

I want to assume net control at 5 minutes before the net open. GoLang's time.Time allows you to .Add durations quite easily. There is a .Sub method, but it's not the opposite of Add. To subtract, you .Add a negative duration. I'm skipping all sort of error validation here because I'm trusting my code to be correct, I already know the net.ID exists. Obviously, some race conditions can occur, but since I haven't even written the ability to delete a net yet, I'm fine with this. Also, I hope to move these operations out of the QuickNetHandler and into their own service layer at some point.

    // Assume Net Control
    _ = models.DB.Create(&models.Activity{
        //ID:          ncr_id,
        Net:         net.ID,
        Action:      "netcontrol",
        Name:        quicknet.NetControl,
        TimeAt:      quicknet.Opened.Add(-time.Minute * 5),
        Description: "Assumed Net Control",
    })
Enter fullscreen mode Exit fullscreen mode

Next, we open the net.

    // Open the net
    _ = models.DB.Create(&models.Activity{
        Net:         net.ID,
        Action:      "open",
        Name:        quicknet.NetControl,
        TimeAt:      quicknet.Opened,
        Description: "Opened the net",
    })
Enter fullscreen mode Exit fullscreen mode

Now, I want to close the net, if they specified a close time. For this quick form, I am doing a strong opinion that a net is at least 5 minutes long. If quicknet.Closed is non-zero, but less than 5 minutes after quicknet.Opened, I'll set it to be 5 minutes after quicknet.Opened. I'll add this to the model's Validate function.

    // force Closed (if set) to be at least 5 minutes after Open
    if !q.Closed.IsZero() && !q.Opened.IsZero() {
        if q.Closed.Before(q.Opened.Add(time.Minute * 5)) {
            q.Closed = q.Opened.Add(time.Minute * 5)
        }
    }
Enter fullscreen mode Exit fullscreen mode

Then back in the handler, we can close the net.

    // Close the net if set
    if !quicknet.Closed.IsZero() {
        // In validate, we forced quicknet.Closed to be at least 5
        // minutes after the quicknet.Open
        _ = models.DB.Create(&models.Activity{
            Net:         net.ID,
            Action:      "close",
            Name:        quicknet.NetControl,
            TimeAt:      quicknet.Closed,
            Description: "Opened the net",
        })
    }
Enter fullscreen mode Exit fullscreen mode

I tested these with all sorts of input values and so far everything is working great. If I blank opened, or leave netcontrol blank, I get errors. If I blank out "closed", no Closed activity is created. If I set closed to an earlier time, or the same time as opened, then the time is automatically adjusted.

Side note - I still haven't touched timezones. These activities are being created in the database with a string like "2022-06-23 15:40:29.099776-04:00", which shows the local -4 offset. Basically, I'm ignoring this for now, planning to come back in the future and fix all my timestamps all at once.

Bulk checkins

I still need to handle doing bulk checkins. I'm going to create a function in my model to parse the checkins field and return an array of checkin names.

Awesome builting strings.FieldsFunc

I anticipate users pasting in from various editors. Mostly space separated, but possibly CSV or others. I was searching on ways to split on either a comma or a whitespace and I discover the strings.FieldFunc and the main documentation does exactly what I want.

package main

import (
    "fmt"
    "strings"
    "unicode"
)

func main() {
    f := func(c rune) bool {
        return !unicode.IsLetter(c) && !unicode.IsNumber(c)
    }
    fmt.Printf("Fields are: %q", strings.FieldsFunc(" name foo1;bar2,baz3..., bag,base", f))
}
// Output: Fields are: ["name" "foo1" "bar2" "baz3" "bag" "base"]
Enter fullscreen mode Exit fullscreen mode

I can make a ParseNames function pretty easily that eliminates most garbage and creates list of unique names.

func ParseNames(raw string) []string {

    f := func(c rune) bool {
        return !unicode.IsLetter(c) && !unicode.IsNumber(c)
    }
    fields := strings.FieldsFunc(raw, f)
    set := make(map[string]struct{})
    for _, f := range fields {
        _, isPresent := set[f]
        if !isPresent {
            // If the name is just a number, skip
            // ex: "1. John" => ["1", "John"]
            _, err := strconv.Atoi(f)
            if err != nil {
                set[f] = struct{}{}
            }
        }
    }
    keys := make([]string, 0, len(set))
    for k := range set {
        keys = append(keys, k)
    }
    return keys
}
Enter fullscreen mode Exit fullscreen mode

Then to finish this up, I make activity records the assorted _checkins.

    // now do the early checkins
    if quicknet.EarlyCheckins != "" {
        early_names := ParseNames(quicknet.EarlyCheckins)
        early_time := quicknet.Opened.Add(-time.Second * 60)
        for _, n := range early_names {
            _ = models.DB.Create(&models.Activity{
                Net:         net.ID,
                Action:      "checkin",
                Name:        n,
                TimeAt:      early_time,
                Description: "Early checkin",
            })
        }
    }

    if quicknet.RegularCheckins != "" {
        regular_names := ParseNames(quicknet.RegularCheckins)
        regular_time := quicknet.Opened.Add(time.Second * 30)
        for _, n := range regular_names {
            _ = models.DB.Create(&models.Activity{
                Net:         net.ID,
                Action:      "checkin",
                Name:        n,
                TimeAt:      regular_time,
                Description: "Regular checkin",
            })
        }
    }
Enter fullscreen mode Exit fullscreen mode

Day 4 Wrap Up

My timer is reading 16 hours, 16 minutes. Since I finished yesterday around 10 hours, it seems I put in almost a full "workday" today. Playing with html, javascript, and others is so iterative I really got into a flow. I had lots of breaks during the day, but overall that's more time than I planned to put in today. I was also hoping to get into a more dynamic ajax (htmx) process of editing the net metadata and individual activity records by now.

My QuickNetHandler has bloated in size. If I saw this on one of our python projects, I'd be cringing. I am also certain a number of my "for .. range" loops can be done in a much more sophisticated manner, and if I asked on stackoverflow, I'd probably get comments like "why don't you just the map.unique.splitfield builtin function and save yourself 100 lines of repetitive code?"

Despite all that, I am really happy with my progress. I think I'm starting to get a "feel" for golang now. With a rather quick form fill out, I can quickly populate a net's activity with reasonable approximations. Once I add in-line activity editing, it gets even better.

These two screen shots show filling out the form with a series of "humanisms" in how I enter the list of call signs. I also "accidentally" leave the close time before the start time. The second screenshot shows how that form populated the activity records.

Form Fill

Populated

Populated

Discussion (0)