DEV Community

Cover image for GoNetNinja: Day 5
YourTech John
YourTech John

Posted on

GoNetNinja: Day 5

Day 5

Today I worked on handlers to add and update activity records. I want to make use of AJAX type calls to submit and replace a row, but to get things going, I went ahead and just made each addition redirect and refresh the net page.

I made a new html template form for new activity.

<tr class="activity-new" id="activity-new">
    <%= form_for( newactivity, {action: createActivityPath(), method: "POST"}) { %>
    <%= f.InputTag("Net", {value: net.ID, hidden: true}) %>

    <td>
        <%= f.SelectTag("Action",
        {
        hide_label: true,
        size: 1,
        options:
        { "Check In": "checkin",
        "Comment": "comment",
        "Check Out": "checkout",
        "Close Net": "close",
        "Open Net": "open",
        "Assume Net Control": "netcontrol",
        },
        value: "checkin"
        }
        ) %>
    </td>
    <td><i>New</i>
    </td>
    <td class="left">
        <%= f.InputTag("Name", {value: newactivity.Name, hide_label: true, size: 8}) %>
    </td>
    <td>
        <%= f.TextAreaTag("Description", {value: newactivity.Description, hide_label: true, rows:1}) %>
    </td>
    <td>
        <%= f.DateTimeTag("TimeAt", {value: net.PlannedStart.Format("2006-01-02T15:04:00")} ) %>

        <button class="btn btn-success" role="submit" title="save"><i class='fa fa-cloud-upload-alt'></i> </button>
    </td>
    <% } %>
</tr>

Enter fullscreen mode Exit fullscreen mode

I also made a copy of this for _activityrow.plus.html with just a few changes between newaction and action.

<tr class="activity-<%= activity.Action %>" id="activity-row-<%= activity.ID %>">
    <%= form_for( activity, {action: updateActivityPath({id: activity.ID}), method: "POST"}) { %>
    <td>
...
    <td>
        <%= f.DateTimeTag("TimeAt", {value: activity.TimeAt.Format("2006-01-02T15:04:00")}) %>
        <button class="btn btn-success" role="submit" title="save"><i class="fa fa-cloud-upload-alt"></i> </button>
    </td>
Enter fullscreen mode Exit fullscreen mode

Then in netedit.plush.html, we include these partials.

          <tbody>
          <%= partial("newactivity.plush.html") %>
          <%= for (activity) in activities { %>
            <%= partial("activityrow.plush.html") %>
          <% } %>
          </tbody>
Enter fullscreen mode Exit fullscreen mode

I added newactivity as a blank struct pointer in _LearnNet function.

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("newactivity", &models.Activity{})
    c.Set("activities", activities)
Enter fullscreen mode Exit fullscreen mode

Create and Update activity are pretty similiar, which is often the case with CRUD like functions.


func CreateActivity(c buffalo.Context) error {
    activity := &models.Activity{}
    if err := c.Bind(activity); err != nil {
        return err
    }

    newId, err := uuid.NewV1()
    if err != nil {
        return err
    }
    activity.ID = newId

    // Validate the data from the html form
    verrs, err := models.DB.ValidateAndCreate(activity)
    if err != nil {
        return errors.WithStack(err)
    }
    if verrs.HasAny() {
        c.Set("activity", activity)
        // Make the errors available inside the html template
        c.Set("errors", verrs)
        return c.Render(422, r.HTML("home/_newactivity.plush.html"))
    }
    c.Flash().Add("success", "Net was created successfully")
    return c.Redirect(302, "/nets/%s?editmode=true", activity.Net)
}

func UpdateActivity(c buffalo.Context) error {

    activity := &models.Activity{}

    if err := models.DB.Find(activity, c.Param("id")); err != nil {
        return c.Error(http.StatusNotFound, err)
    }

    // Bind Widget to the html form elements
    if err := c.Bind(activity); err != nil {
        return err
    }

    logrus.Info("got activity ", activity.ID, " of ", activity.Action, " for net ", activity.ID)

    verrs, err := models.DB.ValidateAndUpdate(activity)
    if err != nil {
        return errors.WithStack(err)
    }

    if verrs.HasAny() {
        c.Set("activity", activity)
        // Make the errors available inside the html template
        c.Set("errors", verrs)
        return c.Render(422, r.HTML("_activityrow.plush.html"))
    }

    logrus.Info("activity updated ok")
    c.Flash().Add("success", "Activity was updated successfully")
    return c.Redirect(302, "/nets/%s?editmode=true", activity.Net)
}
Enter fullscreen mode Exit fullscreen mode

One thing of note is that verrs will render _activityrow or _newrow, which is just a row of the overall table. I don't actually have any validation in activities yet - so someone could create a blank Name, timeAt, and Description. But this does pave the way for a future dynamic page load. In htmx, I can have a post replace the outer div (or table row id) with the returned html. So I should be able to do a GetActivity for a single row of activity and return that. I can also start setting up partials for the main net page which also get refreshed when an activity is updated.

Despite not implementing the dynamic concept, this app is almost feature complete. Remaining short-term features:

  • Fix the timestamp handling so there is a consistent usage of local time zone and UTC between the web browser and database.
  • Ability to edit a net name, planned star, and planned end
  • Ability to delete individual activities
  • Ability to delete a net and all associated activities

Longer term goals (in no particular order):

  • add tags/categories/labels to individual nets
  • Toggling between editable and readonly view
  • Exports
    • Text summary for email
    • Printable PDF
  • Authentication, local and oauth
  • activity reporting such as:
    • how often does someone take net control?
    • average length of a net, possibly by tag
    • most checkins (possibly by net tag)
    • most consecutive checkins (possibly by net tag)
    • people we haven't heard from in a while (checkin aging report)
  • dynamic editing

Day 5 Wrap Up

NetNinja with Editable Activities

Timer is at 18 hours, 40 minutes. I've gotten the bulk of the program implemented, hopefully I'll find time in the next couple of weeks to implement the remaining short time features.

More importantly, I got a good grasp of using go, even if I'm in a safety bubble provided by Buffalo. I didn't implement the Resources concept they document, and that is a better way to organize the various handlers and methods.

I found that Buffalo's documentation, while good, does leave a lot to be figured out. I also didn't find much help for buffalo/pop specific question on the main Q&A sites. In fact, there are only 51 questions tagged "buffalo", with only 2 asked in 2022. Gorilla has 535 questions, Beego has 287 questions (24 this year). Revel has 198 questions. Maybe all the activity is in a forum. Maybe it's so easy, no questions needed, or maybe the askers don't use the buffalo tag.

If I hadn't used buffalo or some other web framework, I'm certain I would have spent a lot more time working through the http routing. The buffalo.Context is incredibly useful. It's nice to use the Context.Bind to tie a form to a model. Their actual form helper (called tags) is helpful, but not necessary. I didn't like it applying some css classes to my input tags, which make the input field fill the container. I will ultimately either override the css, try to disable that feature, or rebuild the forms by hand.

Discussion (0)