Cloud Resume Challenge (Azure) [Part 2 of 3]

Introduction

In my previous post, I walked through the first six steps of the Azure Cloud Resume Challenge. I won't bother explaining it here but if you don't know what I'm talking about, go check out part 1 first! In this post, we'll go through steps 7, 8, 9, 10, and 11 - this is where it started getting difficult for me, but that means we're getting into the juicy stuff now. However, I think the best is still yet to come!

Without further ado, let's get started!

Step 7: JavaScript

So, on the recommendation of the CRC guide, I started doing the JavaScript tutorials on Codecademy. As I was going through them, I had a thought - the official goal of this step is to add a JavaScript view counter to the page, but I want to do a little something extra, too. It turns out, using html2pdf.js, you can convert the page to a pdf and then download a copy. Pretty neat! I added a download link at the top, and the html2pdf__page-break class to the bottom of my experience section (I couldn't get the page break to cleanly work without using the 'legacy' mode, maybe I'll fix that later), along with the following:

<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.js" integrity="sha512-YcsIPGdhPK4P/uRW6/sruonlYj+Q7UHWeKfTAkBW+g83NKM+jMJFJ4iAPfSnVp7BKD4dKMHmVSvICUbE/V1sSw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
    document.getElementById('downloadLink').addEventListener('click', function() {
        var element = document.getElementById('resume');
        html2pdf(element, {
            margin: 0.5,
            filename: 'Sean_Young.pdf',
            image: { type: 'jpeg', quality: 0.98 },
            html2canvas: { scale: 1, logging: true },
            jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
        });
        html2pdf().set({
        pagebreak: { mode: 'legacy' }
        });
    });
</script>

With that out of the way, we can proceed to what I was supposed to do here, and figure out how to do a dynamic page view counter - of course, we're going to have to come back to this periodically as we do the next steps in order to get it functional.

To start, I added some text that just says "This page has been viewed 0000 times" with a <span> tag around the "0000" and an id of "view-count" - the id will be used for the JavaScript to identify what needs to be changed. Then, after working on the next few steps, I had to re-write it all about a thousand times. The final version looked like this:

document.addEventListener("DOMContentLoaded", async function () {
  const updateViewCount = async (method) => {
    try {
      const response = await fetch(
        `https://sy4azureresume-visitorcount.azurewebsites.net/api/http_trigger`,
        {
          method: method,
        }
      );
      const data = await response.text();
      return data; // Return the response data
    } catch (error) {
      console.error("Failed to update or retrieve view count", error);
      return null; // Return null in case of an error
    }
  };

  // Update the view count and then update the text on the webpage
  await updateViewCount("POST");
  const count = await updateViewCount("GET");

  // Use the retrieved count to update the text on the webpage
  const visitorCount = count ? count.toString().padStart(4, "0") : "0000";
  const viewCountElement = document.getElementById("view-count");
  if (viewCountElement) {
    viewCountElement.textContent = visitorCount;
  }
});

Step 8: Database

Like most (or, at least some) of those that have come before me, or so it seems, I went a little against the grain for my database - the CRC guide recommends the Table API for CosmosDB and Serverless capacity mode. I chose the NoSQL API and Provisioned throughput capacity mode so I could choose the 'Free Tier Discount'. I don't expect to ever exceed 1000RU/s or 25GB of storage.

After the DB Account was created, I went ahead and made a new container, then a new item to store my lone visitorcount variable. I also went ahead and updated the CORS rules and added my website address to the list - a tip from CRC veterans past to hopefully save some headache (assuming I did it correctly).

Step 9: API

Since I've used both the CLI and Portal at this point, I decided to use the Azure Functions Extension in VS Code to create my Azure Function. Creating the Function was fairly straightforward, but...

Step 10: Python

This was, without a doubt, the hardest part for me so far. My Python (and JavaScript, to be fair) skills aren't very strong, and this just reinforced that. I researched, I looked at documentation, I checked other CRC blogs, I used ChatGPT - I even signed up for a free trial of GitHub Copilot, which is what eventually led me to functional code (and even that took more trial and error than I'd like to admit). I'm incredibly envious of people that can do this off the top of their heads.

Many of the blogs I found were from years past and were using the 'Python Programming Model v1' for Azure Functions. Model v2 is out now and it behaves differently - the triggers are dynamically created based on the contents of the script, and there's no function.json file. The final version of my API was this:

import logging
import azure.functions as func
import os
from azure.cosmos import CosmosClient, PartitionKey

app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)


@app.route(route="http_trigger", methods=["GET", "POST"])
def http_trigger(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("Python HTTP trigger function processed a request.")

    # Initialize Cosmos Client
    url = os.getenv("COSMOS_DB_URL")
    key = os.getenv("COSMOS_DB_KEY")
    client = CosmosClient(url, credential=key)

    # Select database
    database_name = "AzureResume"
    database = client.get_database_client(database_name)

    # Select container
    container_name = "VisitorCount"
    container = database.get_container_client(container_name)

    # Get the request method
    method = req.method

    if method == "GET":
        # Retrieve a value
        item_id = "index"
        item = container.read_item(item=item_id, partition_key=item_id)
        return func.HttpResponse(str(item["count"]))

    elif method == "POST":
        # Increment count
        item_id = "index"
        item = container.read_item(item=item_id, partition_key=item_id)
        item["count"] += 1
        container.replace_item(item=item_id, body=item)
        return func.HttpResponse("View count incremented", status_code=200)

Step 11: Tests

I'm a bit lost on this one, honestly. I understand the purpose of tests, and I understand the importance of them, and I can write them for simple functions, but I have no idea how to actually perform a test on this Python script. I'll get there - I'll fix this at a later date, after I figure out how to perform the appropriate tests. For now, it's functional, and I don't intend to change it.

Conclusion

As stated at the beginning, this was quite a bit more difficult than the first part - I'd only just scratched the surface of JavaScript and Python with CS50 (though I had a bit more Python experience from dabbling), and honestly, I'm still just scratching the surface. I enjoy the challenge, though - if you're not challenging yourself, and failing (repeat ad infinitum), you're not learning anything. I have a sneaking suspicion the next steps are going to be the most challenging yet.

Next ->