Skip to content
dryfly.ca
dryfly.ca
where the fishing is always good.
  • Gallery
  • DJ Mixes
  • Education Blog

Simplify Your Django App with HTMX

March 22, 2022

Last year I created a gradebook project using Django for both the frontend and backend. I had studied React for a few weeks but it simply seemed like it would be too much for me to work on, when combined with what I anticipated to be a reasonably large project. I decided to stick with the Django templating knowing that I would have to write a bit of javascript and maybe use a bit of jquery.

This post isn’t so much of a tutorial on using HTMX, as there are many very good tutorials and videos that do a better job than I can. This post is more of an example showing how a bit of htmx can greatly simplify the code needed to create, display and edit objects in Django.

Perhaps the most difficult frontend page for me to code in my project was where a user would enter in the assessment grades for a classroom of students. A standard form would not suffice because the number of learning objectives (grades) in each assessment would vary, as would the number of students. It’s possible that a formset inside a formset could have worked but I thought I could do it easier with some javascript. My view would pass an array to the template which contained all the grades information. My js script would then go through and build an n x m grid for the students and grades.

First was the code for my js script gradebook.js that creates a dynamic form:

var down = document.getElementById("GFG_DOWN");
var scale_mode = nameArray.shift();
var numberObj = nameArray.shift();

var obj = []
// get the names of each learning objective
for (var g = 0; g < numberObj; g++) {
    obj[g] = nameArray.shift();
}
var numberStudents = nameArray.length;

// set attributes for the form
var form = document.createElement("form");
form.setAttribute("method", "post");
form.setAttribute("action", "submit.php");

// grades will be put into a table
let table = document.createElement('table');
table.className = "table table-bordered";
let thead = document.createElement('thead');
let tbody = document.createElement('tbody');

table.appendChild(thead);
table.appendChild(tbody);

//add the table to the body tag
document.getElementById('grade-form').appendChild(table);

//fill out first row of table
// first column will be the student name
let row_1 = document.createElement('tr');
let heading_1 = document.createElement('th');
heading_1.scope = "col";
heading_1.innerHTML = "Student";

// next columns will be names of the learning objective(s)
var headingName = [];
for (var i = 0; i < numberObj; i++) {
    headingName[i] = "heading" + (i + 2);
    headingName[i] = document.createElement('th');
    headingName[i].scope = "col";
    headingName[i].innerHTML = obj[i]
}

headingName[numberObj] = "heading" + (numberObj + 2);
headingName[numberObj] = document.createElement('th');
headingName[numberObj].scope = "col";
headingName[numberObj].innerHTML = "Comments";

row_1.appendChild(heading_1);
for (var i = 0; i < numberObj; i++) {
    row_1.appendChild(headingName[i]);
}
row_1.appendChild(headingName[numberObj]);
thead.appendChild(row_1);

// each row will print the student name in 1st column
// next column will be the objective grades
var m = 0;
var rowName = [];
var studentname = [];

for (var n = 0; n < numberStudents; n++) {
    rowName[n] = "rowname" + (n + 2);
    rowName[n] = document.createElement('tr');
    studentname[n] = document.createElement('td');
    studentname[n].innerHTML = nameArray[n];
    rowName[n].appendChild(studentname[n]);
    var objective = [];
    // two different scale modes to choose from
    switch (scale_mode) {
    
        case 'MOE':
            console.log("3");
            var option_array = {
                '---': '---',
                'I': 'I',
                'EMG': 'EMG',
                'DEV': 'DEV',
                'PRF': 'PRF',
                'EXT': 'EXT'
            };
            break;
        case 'MOEPLUS':
            console.log("4");
            var option_array = {
                '---': '---',
                'I': 'I',
                'EMG': 'EMG',
                'DEV': 'DEV',
                'PRF': 'PRF',
                'PRF+': 'PRF+',
                'EXT': 'EXT'
            };
            break;
    };

    // create the table sell for each grade
    for (k = 0; k < numberObj; k++) {
        objective = document.createElement('td');
        GR = document.createElement("select");
        for (index in option_array) {
            GR.options[GR.options.length] = new Option(option_array[index], index);
        }
        GRName = "row-" + n + "-col-" + k;
        GR.setAttribute("name", GRName);
        rowName[n].appendChild(objective);
        objective.appendChild(GR);
    }

    commentCell = document.createElement('td');
    comment = "row-" + n + "-comment";
    commentText = document.createElement("textarea");
    commentText.setAttribute("name", comment);
    rowName[n].appendChild(commentCell);
    commentCell.appendChild(commentText);
    tbody.appendChild(rowName[n]);
}

The table from the javascript above is placed into an element with id “grade-form”. This shows up in the django template:

{% block content %}
<div class="container">
  <div class="row">
    <div class="col">
      <p></p>
    </div>
  </div>
  <div class="row">
    <div class="col-md-8">
      <div class="form-group">
        <form action="" method="post">{% csrf_token %}
          <div id="grade-form"></div>
          <div class="form-group">
            <button type="submit" class="btn btn-primary">Submit</button>
          </div>
        </form>
      </div>
    </div>

  </div>
{% endblock content %}
<div>
  {% block extra_js %}

    <script>
      var nameArray = {{ data|safe }};
    </script>
    <script type="text/javascript" src="{% static 'gradebook/js/gradeform.js' %}"></script>
  {% endblock extra_js %}
 
</div> 

So this worked but I felt it was a bit messy. The assessment UI wasn’t just this page to enter new grades though. I also needed a separate page to show existing grades, and then I had an UpdateView where the user could edit any one grade. All of this required three separate templates and 3 different looking interfaces. I managed how these three pages would be shown to the user by some logic: if the grades existed, the user would be shown the page for existing grades; if there weren’t any grades yet, a user would be taken to the page to enter new grades. This system also carried some risk with the user possibly entering in grades twice.

HTMX to the rescue

What I really wanted was just one page that showed the assessment info and table to the user. If no grades were entered yet, they would just click in each cell to enter a grade. If there was an existing grade, the user could click on the cell to change the grade. That’s it. One page, one url. Prior to using HTMX, the grades would be shown in the template with:

{% for g in grade_list %}
	{% if g.student.id == student.id %}
		<td><a  href="{% url 'gradebook:updatesinglegrade' g.assessment.pk g.cblock.pk g.pk %}"></a>
		  <input type="text" class="form-control score" title={{ g.score }} name="score" id="input-{{ forloop.counter0 }}" placeholder={{ g.score }} required>
		</td>
	{% endif %}
{% endfor %}

I replaced the <input> element with this:

<input type="text" hx-post="{% url 'gradebook:grade-change' g.pk %}" hx-swap="outerHTML" hx-trigger="keyup delay:1200ms" class="form-control score" title={{ g.score }} name="score" id="input-{{ forloop.counter0 }}" placeholder={{ g.score }} required>

And that was it. No javascript form needed, no separate page for grade entry or grade edit. The hx-post tells Django which view to use, hx-swap tells Django what to replace this element with and hx-trigger tells Django what causes a change. The <id> is used when a bit of js to colour each cell when the template is first loaded. The view for the HTMX is :

class GradeChange(SingleObjectMixin, View):
    """ view to handle htmx grade change"""
    model = Grade

    def post(self, request, *args, **kwargs):
        grade = self.get_object()
        user = grade.user
        sm = GradeBookSetup.objects.get(user=user).scale_mode
        ns = request.POST.get('score')
		new_score = ns.upper()
        if new_score == "RRR":
            new_score = "PRF"
        if new_score == "RRF":
            new_score = "PRF+"

        def get_color(grade):  # map background color to the score
            if grade == "EXT":
                convert_code = "rgba(153,102,255,0.4)"
            elif grade == "PRF+":
                convert_code = "rgba(75, 192, 192, 0.7)"
            elif grade == "PRF":
                convert_code = "rgba(75, 192, 192, 0.3)"
            elif grade == "DEV":
                convert_code = "rgba(255, 205, 86, 0.4)"
            elif grade == "EMG":
                convert_code = "rgba(225, 99, 132, 0.4)"
            else:
                convert_code = "rgba(0, 0, 0, 0.1)"
            return (convert_code)

        elif sm == 'MOEPLUS':
            score_list = ["EXT", "PRF+", "PRF", "DEV", "EMG", "I", "---"]
        elif sm == 'MOE':
            score_list = ["EXT", "PRF", "DEV", "EMG", "I", "---"]

        if new_score in score_list:
            grade.score = new_score
            grade.save()
            grade_score = str(grade.score)
            bgcode = get_color(grade.score)
            input_string = f'<input type="text" hx-post="{reverse("gradebook:grade-change", args=[grade.pk])}" hx-swap="outerHTML" hx-trigger="keyup delay:1200ms" class="form-control score" style="background-color:{ bgcode }" title="{ grade_score }" name="score" placeholder="{ grade.score }" required>'
        else:
            bgcode = get_color(grade.score)
            input_string = f'<input type="text" hx-post="{reverse("gradebook:grade-change", args=[grade.pk])}" hx-swap="outerHTML" hx-trigger="keyup delay:1200ms" class="form-control score" style="background-color:{ bgcode }; border:solid rgb(255, 0, 0,.5);" title="xxx" name="score" placeholder="{ new_score }" required>'

        return HttpResponse(input_string)

This view does a couple of more things than just post a new grade. I have a few “shortcuts” for the user. Instead of typing “PRF” or “PRF+” (which are the grades I’m using), they can type “RRR” and “RRF” for a shortcut. This allows all grades to be entered using only a left hand. The user can also enter in lowercase letters and they’re changed to uppercase. As well, I’m colour coding each cell based on the score entered. This view was very easy to write, is easy to read and understand, and provides a very clean way of entering in assessment grades.

Nepal photos uploaded to a new site

March 21, 2019

I’ve uploaded many of my Nepal photos to a new website. The old site where they used to, zooomr, shut down many years ago. I finally got organized enough to update them.


Before Chanje by Doug Smith on 500px.com

John and Scott Besisahar by Doug Smith on 500px.com

Manang by Doug Smith on 500px.com

Muktinath Valley by Doug Smith on 500px.com

Annapurna South from Deorali Pass by Doug Smith on 500px.com

Machhapuchhre by Doug Smith on 500px.com

Lake Pokhara by Doug Smith on 500px.com

Gerbil Palace For Sale

December 20, 2017

I have a gerbil “cage” for sale. I call it a “cage” because really, if you were a gerbil, you would think that it is a palace. Or at least a really, really nice home with two bedrooms and a bathroom.

It’s quite possibly the best gerbil palace out there. I know lots of people that want it, like my friend Phil. His real name isn’t Phil (it’s Dave) but I don’t want to use his real name in case he gets upset. Anyways, Phil says to me he says, “Doug, can I have your gerbil cage?” My first response was to point out that it wasn’t a cage but a palace. I then reminded Phil that he doesn’t even own a gerbil and he has no intention of getting one. Phil agreed but said that it was the best damn gerbil palase (sp) that he’s ever seen. Phil also isn’t a very good speller but I don’t hold that against him.

Phil isn’t the only person that wants it. Like most people, I know several gerbil breeders and gerbil experts. They all want the palace. My fear is that they’ll turn it into an Air BnB for gerbils. While I can appreciate the upscale gerbil business, I’m more of a grass roots kind of fella.

The palace is made out of glass so you can fill it with wood shavings or other filler material and the gerbils will build tunnels that you can observe. It also has a ceramic den with a removable shelf. There’s no plastic for the gerbils to chew on.

RIP Chedder 2013-2016

Bleeding Avid Elixir 3 Brakes

March 14, 2017

Avid Elixir 3 Brake Bleed

Over the past couple of years I’ve tried to bleed the brakes of my Hayes Elixir 3 hydraulic brakes several times. I’ve had some OK success but rarely was I able to do a really good job. I watched lots of videos on YouTube, read the instructions, searched for posts on internet bike forums, and talked to wrenches at bike shops. The videos and instructions make the task seem easy, the forums and bike mechanics make the task seem difficult. It turns out that what is really needed is patience and perseverance, which I’ll give a bit more detail on at the bottom of this post. These were two things that I’ve never seen discussed on instruction videos or manuals.

To bleed Elixir brakes you need only a few tools, most of which come with the Avid bleed kit. You use two syringes, a brake block for keeping the brake pistons separated, a torq wrench for the bleed port and a few other odds and ends like cloths and some kind of strap for holding your brake lever closed.

Here are a couple of videos to check out:
https://youtu.be/IoaPUw5DliA
https://youtu.be/bZylrZvICrY
https://youtu.be/pxTZ2QTPc2w

Here is the manual: https://www.sram.com/sites/default/files/techdocs/e1_e3_service_manual_rev_b.pdf

And finally, here is the advice that you need to be successful. When you go to bleed the brake lever, you have to keep working at this for a long time. The Hayes design is such that a large amount of air will get trapped in the lever and unless you’re very patient and careful, you’ll think you’ve finished the bleed job prematurely. This is where I got caught over 1/2 dozen times. I would bleed the lever until virtually no air bubbles were being produced and stop. This always gave me a soft/bad result. The last two times I did it though, I kept pulling on the lever syringe with a lot of force, even though it looked like I had gotten all the air out. Sure enough, a few more huge air bubbles would eventually pop out.

Prior to the above, I was being super careful with following the instructions: making sure the lever was 75mm from the handlebar, etc. However, it all came down to keeping pulling on the lever syringe pretty much as hard as I thought I could without pulling the plunger out. So if you’re having problems bleeding Elixir brakes, maybe it’s time to try again and really be patient with the lever bleed. Good luck!

Street Food

August 19, 2016

One of our favorite things to do in Thailand is to visit food markets. The food is varied, delicious and cheap. For example, a dish such as a crab curry with rice can be 50B at a market, but 150B at restaurants and guesthouses.

With Thai street food, each little stall serves one type of food. The ones I’m most familiar with are soups, noodles, curries, Isan salads and fish, grilled meat, and seafood stir fries.

Below are a few photos from the market in Surat Thani.

Food market
Food market

Curries
Curries

Seafood
Seafood

And here a few photos from street food in Bangkok’s Chinatown:

Fruit Stall
Fruit Stall

Eating at Lek & Rut
Eating at Lek & Rut

Chinatown
Chinatown

Yarowarat Road, Chinatown
Yarowat Road, Chinatown

Koh Phangan

August 16, 2016

Leaving Ko Samui, we made our way to Ko Phangan. We took the high speed cataram ferry, Lompreyah, from Mae Nam on Samui to Thongsala on Phangan. The ferry was very different from others I’ve been on: it’s a small passenger ferry and it goes really fast. It took maybe 2 minutes to reach its planing speed and was fairly bouncy in the water. We chose this ferry not for its speed but because its departure pier was closest to Bo Phut. The ferry cost a bit more than others, but the cost for the taxi from Bo Phut to the pier was cheaper than other piers used by other ferry companies.
I’d never been to Ko Phagnan before and we weren’t entirely sure which beaches to visit. We eventually settled on a plan by process of elimination. On of the biggest and most popular beaches is Had Rin, on the southern tip. It’s where they have the full moon parties and I think it’s probably where many of the younger backpackers go. I hadn’t read anything really great about the beach itself, so we gave it a pass. There are foir more main sections of beaches to choose from: north of Had Rin, the wast, the north and the northeast. We decided to visit the last three of these areas.

First up was the west. We chose this first because it is closest to where the ferry lands at Thongsala. We chose Had Salad because of reviews we had read, and stayed at Double Duke Bungalows. It turned out that Had Salad and Bo Phut have some similarities. Both beaches seem to have only one less expensive bungalow operation, and both beaches are predominately visited by people from France. Had Salad was very pretty with beautiful sand and clear blue water. The area is like a small cove with a breakwater, and the water is very shallow. This wasn’t the greatest for full out swimming but it was really nice for snorkeling. We saw lots of different fish, some corals, sea urchins and hundreds of sea cucumbers (sometimes referred to as “sea turds”).

Boats and beach, Had Salad
Boats and beach, Had Salad

To check out the area around Salad, Grady and I walked to the beach just south, called Had Yao, one day. It was nice, but clearly we made the right choice with Salad.

I’d definitely recommend Had Salad to visitors, but I wonder how long Double Duke will be there. Most of the other hotels fall into the “resort” category, and while they’d be great to stay at, I think they cost close to $200/night. It didn’t seem like Had Salad was on the backpackers/travelers radar, we mostly saw older couples and families. Grady met some French boys and they hung out a bit playing Pokémon Go. For such a small beach, there was a lot of Pokemon action.

From Salad we made our way to Bottle Beach. It was described as being secluded and quiet. I was a bit nervous about it being “too quiet”, and we booked ahead for only two nights. Our hotel was a bit more expensive than previous ones and was pretty booked up, so we could only have two nights there. Most people arrive to Bottle Beach by boat and that’s what we did. First we took a taxi to Chaluk Lum, a small fishing village on the north of Phangan, and then we took a longtail boat to Bottle Beach. I was originally going to hike from Chaluk Lum to BB. The trail goes up over a steep hill with a apparently some nice views along the way, and takes about an hour. However, our taxi driver didn’t want to drive me to the end of the road where the trail starts. I didn’t feel like walking for half an hour down an open dirt road, so I jumped on the longtail boat with everyone else.

Pulling into Bottle Beach was beautiful. The beach is a small crescent lined with white sand. Our resort, Bottle Beach 1 (yes, there’s a Bottle Beach 2), was right on the beach. Two of the resorts are in the beach, and the two at either end are set back a bit. Our rooms were “beach view” and in fact they were right on the front of the beach. Normally I don’t care about that kind of thing too much, but in this setting it was perfect.

Looking at the ocean from our deck
Looking at the ocean from our deck

 

Bottle Beach
Bottle Beach

The water was clear and warm and we spent two days lounging around, swimming in the ocean, reading on our deck, and even swimming in the pool a bit.

I think we were all a bit bummed to have to leave BB after only two nights, but that’s how it goes. The one down side to Bottle Beach is that the whole beach gets filled with longtail boats from about noon to 4pm. You can swim around them because they’re about 8 m apart, but it does take away from the beach a bit. The mornings and evenings are lovely though.

Looking back on Bottle Beach as we leave
Looking back on Bottle Beach as we leave

We then took another longtail from Bottle Beach to the next beach down the coast, Thong Nai Pan Noi. There were three beaches in a row that all sounded pretty good: Thong Nai Pan Noi, Thong Nai Pan Yai, and Had Salat. Salat looked the coolest but some of the accommodation is on a hill which could make it hard for Emma’s foot and the beach was described as not having much shade. Noi was described as more developed than Yai, but that it maybe had the best beach on Phangan.

Thong Nai Pan Noi
Thong Nai Pan Noi

Thong Nai Pan Noi’s beach did not disappoint. I imagine that 10 years ago it would be THE best beach to visit. However, we liked the feeling of the quieter Bottle Beach a bit more. The development on TNP Noi was tasteful and the beach itself during the day was actually quieter than Bottle Beach (less boats). I had read about noisy jet skis and our hotel had a note in it asking that visitors do not use jet skis because it threatens the environment. In fact we didn’t see a single jet ski in the area so I assume that somehow they’ve been banned.

From our hotel, Grady and I walked to TNP Yai one day. This beach was quieter than Noi but not as pretty. I think it offers a few good places for backpackers to stay at the south end. It would be a good beach to stay at as well.

TNP Noi had one thing very unique from what we had seen so far: less French amd more Italians!

As I write this we’re on a ferry back to Surat Thani. We left TNP this morning and took a taxi to Thongsala. At the pier we purchased tickets on the Raja ferry. Raja are larger car ferries and there’s no need to book ahead. At the pier there is the “Phangan Tour” ticket booth which sells ferry/bus ticket combos that will take us to the center of Surat Thani, about 200 m from My Place, where we’re spending the night. Raja is said to be a slower ferry, but in reality it won’t make much difference. It’s 4 hours start to finish compared to Lompreyah’s 3.5 hours. Lompreyah stops in Samui along the way, while this ferry does not (we’re on the 11 am ferry, maybe the later ferries also stop at Samui?). The schedule for the Raja is better too, we showed up at the pier at 10:30 and didn’t have to wait around at all. The price is also a lot cheaper, 310B compared to 550B. Finally, the ride is a lot smoother in rougher seas because the boat is so much bigger.

So it’s one night in Surat Thani and then we fly to Bangkok tomorrow. We originally were going to travel overland from the south to Bangkok but we decided that the family would prefer to go from relaxed beach straight to Bangkok and avoid the small challenges that overland brings. In light of the recent bombings in Hua Hin and Surat Thani, I guess flying is also better than stopping in Prachuap Kiri Khan. Unfortunately it’s one more area that I didn’t get to visit in 1991 that I will also miss this time.

Older Posts
Search for:

Categories

  • Education
  • Freedom 15 Canoe
  • Interlude SOF kayak
  • Just Stuff
  • Nepal
  • Photos
  • Politics
  • Sports
  • Technology
  • Tern 14 kayak
  • Thailand
  • Vietnam

Tags

2006 2008 astronomy beer bikes boat build camera Canada canoe Conservatives cycling election Emma-May environment epoxy fishing Freedom FreeNas funny grady Harper hockey humour kayak kids Liberals NAS nepal nikon Photography Photos pygmy social-values Sports strip Technology Thailand theft travel trekking video Vietnam world-cup zooomr

Spam Blocked

90,124 spam blocked by Akismet
Copyright © 2022 Doug Smith. All rights reserved.