tag:blogger.com,1999:blog-74137051874904734732024-03-13T16:02:59.291-05:00Chad's Tech BlogChadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.comBlogger49125tag:blogger.com,1999:blog-7413705187490473473.post-69802366603577257262023-05-25T08:47:00.002-05:002023-06-08T13:55:42.221-05:00Using VS Code over Samba on Mac<i>Updated 06/08/23 with some minor code edits to make sure dev server is mounted first and to ignore certain file patterns such as git files</i>
<br>
<h2>The Problem</h2>
In my current job I am working on a very old codebase where the development has to be done on a dedicated dev server. For me that means using Samba on a MacBook. And from <a href="https://serverfault.com/a/1000147" target="_blank">what I've read</a> in various places, the Samba implementation on MacOS isn't always the best. For me, it is terribly slow and I have run into a couple of major issues that make development more difficult. The first issue is that I cannot use VS Code's built-in version control tools. I have tried changing various settings but it still doesn't work right. And the bigger issue is that because of the large number of files and the slowness of the connection, I cannot use the global find or easily pull up a file by name.
<br>
<h2>A Solution?</h2>
Microsoft has a solution for this. It's called <a href="https://code.visualstudio.com/docs/remote/remote-overview" target="_blank">VS Code Remote Development</a>. I have not tried it but it sounds promising. The problem is that at my job, it could take weeks (months?) to convince the powers-that-be that it's needed and secure. It's just one of those times where I can write my own solution faster than waiting on management to debate the issue. So here's what I came up with.
<br>
<h2>My Solution</h2>
Based on <a href="https://gist.github.com/tureki/10851084" target="_blank">this gist</a> I found, I wrote my own code to keep my local codebase in sync with the dev server.
<br><br>
The first part was to create a bash script called <i>devcodemon</i> to watch for changes in my local code directory using <a href="https://github.com/emcrisostomo/fswatch" target="_blank">fswatch</a>. Then whenever a file changes, it passes that to a Node script to process it.
(By the way, I should point out that both of these scripts are in my PATH.)
<br><br>
<b>devcodemon</b>
<pre><code>#!/bin/bash
is_mounted=$(mount | grep rsync-test | wc -l)
if [ $is_mounted -eq 0 ]
then
echo "Dev server not mounted"
exit 1
else
dir2watch="$HOME/rsync-test"
echo "Monitoring $dir2watch"
fswatch $dir2watch | (while read line; do sync-file.js "$line"; done)
fi</code></pre>
<br>
The second script, <i>sync-file.js</i> checks to see if the file was updated or deleted. If it was the former, then it copies the file over to the dev server. If it's the latter, it removes the file from the dev server. The <a href="https://github.com/sindresorhus/ora" target="_blank">ora</a> pacakge gives the CLI a nice UI: a spinner while the file is processing and a green check on success or a red X on failure. A Node.js tip here: in order to use this package, you need to set <code>type</code> to <code>module</code> in your <code>package.json</code> file. For more information on CommonJS modules vs ES modules, see <a href="https://blog.logrocket.com/commonjs-vs-es-modules-node-js/" target="_blank">this blog post</a>.
<br><br>
<b>sync-file.js</b>
<pre><code>#!/usr/bin/env node
import { copyFileSync, existsSync, unlinkSync } from 'fs';
import ora from 'ora';
const basePath = `${process.env.HOME}/code/io-application-code`;
const destPath = `${process.env.HOME}/mounts/io-dev1`;
const ignorePatterns = [
/^\/\.git/,
];
const src = process.argv[2];
const dst = src.replace(basePath, destPath);
(async () => {
ignorePatterns.forEach(pattern => {
const relativePath = src.replace(basePath, '');
if (relativePath.match(pattern)) {
process.exit();
}
});
const spinner = ora({
text: `Synching ${src}...`,
color: 'green',
spinner: 'circleHalves',
interval: 200,
}).start();
const fileExists = await existsSync(src);
const relativeSource = src.replace(basePath, '');
const relativeDest = dst.replace(basePath, '');
if (fileExists) {
try {
await copyFileSync(src, dst);
spinner.succeed(`${relativeSource} copied ${new Date()}`);
} catch (e) {
spinner.fail(`${src} failed to copy`);
console.log(e)
}
} else {
try {
await unlinkSync(dst);
spinner.succeed(`${relativeDest} removed`);
} catch (e) {
spinner.fail(`Failed to remove ${relativeDest}`);
console.log(e)
}
}
})();</code></pre>
And here is what that looks like in action.
<br><br>
<b>Saving a file</b>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-O4IjXqJv4iPoUwR47cU2C51BOVUO-R_Hb9f_plSxHNMpEbc7hvilA40_1eAsVtAWkvz-jKfYfP7pYEBf6bvWOkttx_sqi-IdDUrZOsb-Y21knkxfo3XPKsuyVEy8AzKrCuoTRPnJQsCs4QtsJ3p-Ck5zzg-YFVo6R9MIaPPDA_p6xNOnr87lEQL56Q/s1600/sync-save.gif" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="744" data-original-width="1196" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-O4IjXqJv4iPoUwR47cU2C51BOVUO-R_Hb9f_plSxHNMpEbc7hvilA40_1eAsVtAWkvz-jKfYfP7pYEBf6bvWOkttx_sqi-IdDUrZOsb-Y21knkxfo3XPKsuyVEy8AzKrCuoTRPnJQsCs4QtsJ3p-Ck5zzg-YFVo6R9MIaPPDA_p6xNOnr87lEQL56Q/s1600/sync-save.gif" style="width:100%"/></a></div>
<br>
<b>Deleting a file</b>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg9aDBuWqWbUA7E6M0Q5OM3I299m49fuN_lbSqkvQJONGSVGpUkzhQbvF4tLQp9ODugr-xQyfiBq-MINzD0D0NJJyQCWj5Uv0QGA9qeNCvPHmA4Y17kLD89V0R_KEUd1eN37CWhy2bjvNJSbocIORbSCwJi-FF5OP0MZcOnbhWUEaz-tBWGwaOJlE8gSQ/s1600/sync-remove.gif" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="744" data-original-width="1196" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg9aDBuWqWbUA7E6M0Q5OM3I299m49fuN_lbSqkvQJONGSVGpUkzhQbvF4tLQp9ODugr-xQyfiBq-MINzD0D0NJJyQCWj5Uv0QGA9qeNCvPHmA4Y17kLD89V0R_KEUd1eN37CWhy2bjvNJSbocIORbSCwJi-FF5OP0MZcOnbhWUEaz-tBWGwaOJlE8gSQ/s1600/sync-remove.gif" style="width:100%"/></a></div>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-47531237739394058482023-03-09T13:35:00.002-06:002023-03-09T13:36:05.220-06:00Learning ReactI have wanted to learn React for a while but after seeing just how many job postings were looking for React experience, I decided it was time to finally dig in. So far I have just picked up the basics of class components. I know from experience that the real learning comes from putting what you know into pratice. So as a challenge exercise, I created a simple little app to take the most popular images from NASA and display them as cards. Click on each card will then open up the full record on the <a href="https://images.nasa.gov/">NASA Images site</a>.
<br><br>
Here is the <a href="https://github.com/clarmond/nasa-images">source code on Github</a>.
<br><br>
And here is a <a href="https://www.csarmond.com/nasa-images/">demo</a>.
<br><br>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghu-x-_o91B4rPdt8cooKoHnOldjeSO9rXldROlg5wJQ2OHr_Cowv4VaAw3p1DJ6DpfcL2x3sIBIndSJWaj14aPtng50vdhJSoAESWGx1qY5rHPjO4X1_AyF692Y07rbAqS9M9Ds4IJPaDjSQ_FoTEWQHea71iC8hlngd28beq4sUeqH8-WD85xV5-_g/s1600/nasa-images-app-480.gif" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="316" data-original-width="640" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghu-x-_o91B4rPdt8cooKoHnOldjeSO9rXldROlg5wJQ2OHr_Cowv4VaAw3p1DJ6DpfcL2x3sIBIndSJWaj14aPtng50vdhJSoAESWGx1qY5rHPjO4X1_AyF692Y07rbAqS9M9Ds4IJPaDjSQ_FoTEWQHea71iC8hlngd28beq4sUeqH8-WD85xV5-_g/s1600/nasa-images-app-480.gif"/></a></div>
<br><br>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-32420358143880123662023-02-02T18:01:00.000-06:002023-02-02T18:01:25.965-06:00The Magic Fridge Part 2<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEih_VvR2B37J3InJBAVf1b_xxUOpO18pskrYWkhwlTDc2CrvYasLp0rMVAiLAs25nQbjR90GqyKG1A7bwKpVAGpzfinvAKYIRNtu8Ss-SMpJjGtNbm4KZTM6WecjHfKQThJZ74TAoLifOacH4p-wYMIO7mjouxyTqhqRmWWyqNgae6EZ0Y-4ehZQ3uPww/s1024/the-magic-fridge.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="1000" data-original-width="1024" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEih_VvR2B37J3InJBAVf1b_xxUOpO18pskrYWkhwlTDc2CrvYasLp0rMVAiLAs25nQbjR90GqyKG1A7bwKpVAGpzfinvAKYIRNtu8Ss-SMpJjGtNbm4KZTM6WecjHfKQThJZ74TAoLifOacH4p-wYMIO7mjouxyTqhqRmWWyqNgae6EZ0Y-4ehZQ3uPww/s320/the-magic-fridge.png"/></a></div>
In my first post about <a href="http://chads-tech-blog.blogspot.com/2023/02/the-magic-fridge.html" target="_blank">The Magic Fridge</a>, I talked about how I could use a <a href="https://www.raspberrypi.org/" target="_blank">Raspberry Pi</a> to turn my garage fridge into a smart appliance. The first thing I did was set up a door alarm for when the door was open for too long. The reason I wanted this feature is because on this particular fridge if you slam the door shut, it will pop back open. So on many occassions I would walk into the garage to find the door open to the fridge.
<br><br>
The first thing I did was purchase a
<a href="https://www.amazon.com/gp/product/B07PRR33DF/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1" target="_blank">magnetic reed switch from Amazon</a>. Here is an image of what that looks like:
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj4wTPEPswN6eHVw-DQ-8tCbjl6FQ2ZbXDLvnp1sKYu2LsuTol-lTb77v3Tg9IWDxc6x1zStwF9XywDpxZ8r-SL6LRFCEesa0yeRPk93dRJOb0JDy1Y2HxRdPkdYKuv66MN_11XQeEq81VF0Sdga7_lMcyxfaanD55EfQQ091pYDc3igsNDty6O6hF2HA/s541/reed-switch.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="476" data-original-width="541" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj4wTPEPswN6eHVw-DQ-8tCbjl6FQ2ZbXDLvnp1sKYu2LsuTol-lTb77v3Tg9IWDxc6x1zStwF9XywDpxZ8r-SL6LRFCEesa0yeRPk93dRJOb0JDy1Y2HxRdPkdYKuv66MN_11XQeEq81VF0Sdga7_lMcyxfaanD55EfQQ091pYDc3igsNDty6O6hF2HA/s320/reed-switch.jpg"/></a></div>
If you are not very familiar with electronics, a magnetic reed switch is a simple switch for controlling the flow of electricity through the use of a magnet.
You can get a good <a href="https://www.explainthatstuff.com/howreedswitcheswork.html" target="_blank">explanation here</a>. To use this with my fridge, I attached one side to the side of the fridge and one side to the door. When the door is closed, the magnet closes the switch and thus completes the circuit. When the door is open, the magnet pulls away and opens the circuit which then prevents the circuit from being completed.
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5Rz8X79mNknGgWBU-QtLXEVOUQS-p1iBrH_msXW3zNebXD2MY6siyJ8UEGqNj7DGRIm6z3pzCpuot_lGSIZp-4equHh50lLUPNURzx0W0nEAvhJuO5oowHgHsRWw55h2QKFYbzRG_7OhVCwXYWclgXU0mH9gVt3teRT10bCeoVRhg2TwpY5kGpyoMkg/s4032/IMG_5842.JPG" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" height="320" data-original-height="4032" data-original-width="3024" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5Rz8X79mNknGgWBU-QtLXEVOUQS-p1iBrH_msXW3zNebXD2MY6siyJ8UEGqNj7DGRIm6z3pzCpuot_lGSIZp-4equHh50lLUPNURzx0W0nEAvhJuO5oowHgHsRWw55h2QKFYbzRG_7OhVCwXYWclgXU0mH9gVt3teRT10bCeoVRhg2TwpY5kGpyoMkg/s320/IMG_5842.JPG"/></a></div>
I then also wired up an LED bulb to the Pi to turn on when the door is opened. Here is a diagram that gives an overview of everything we have thus far.
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgAFWXHgg6lnERqYWa40Yl81qeHXry9UdpcxyhCjHEo5OiTSXcVML4LPK-eYSeDwLLWKj5xTowYhZKUwTMJEozO56piBXiNDxxySnEFDjstYg4A5p5Nmq2l-DxBeL_8foVyJJOOQdj265AVfHx0pQsbLzpOtyk1pkEEUKDWjbmrPw3XXGaiPhtEaOgqIw/s1176/rpi-door-alarm.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="944" data-original-width="1176" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgAFWXHgg6lnERqYWa40Yl81qeHXry9UdpcxyhCjHEo5OiTSXcVML4LPK-eYSeDwLLWKj5xTowYhZKUwTMJEozO56piBXiNDxxySnEFDjstYg4A5p5Nmq2l-DxBeL_8foVyJJOOQdj265AVfHx0pQsbLzpOtyk1pkEEUKDWjbmrPw3XXGaiPhtEaOgqIw/s320/rpi-door-alarm.png"/></a></div>
Notice that in both circuits I added a small resistor. This is to minimize the current flow and protect the Raspberry Pi from damage. Now that we have our hardware configured, let's take a look at the code.
<br><br>
Step 1 is just to import the Python package that I'll be using:
<pre><code class="python">from config import config
from gpiozero import LED, Button
from message import sendTextMessage
from time import sleep
</code></pre>
Next I am going to set up a few things:
I will use the <code>max_count</code> variable to be my threshold. So in this case, if the door is open more than 60 seconds, I will take action. <code>Button</code> and <code>LED</code> come from the <a href="https://gpiozero.readthedocs.io/en/stable/" target="_blank">gpiozero</a> interface (which greatly simplies working with components on the Raspberry Pi in Python). This is just telling my code which pins my button or switch in this case and LED bulb are connected to respectively. The <code>testing</code> and <code>verbose</code> variables will just be debugging purposes.
<pre><code class="python">max_count = 60
testing = False
verbose = False
switch = Button(17)
light = LED(26)
</code></pre>
Next I set up a couple of functions to tell it what to do when the switch is open or closed. If I am debugging, it will print a message to the console and then either turn the LED bulb on or off as in each case appropriate.
<pre><code class="python">def switch_open():
if testing or verbose:
print("Switch open")
light.on()
def switch_closed():
if testing or verbose:
print("Switch closed")
light.off()
</code></pre>
Next, I set up the event handlers <code>when_released</code> and <code>when_pressed</code> to tell it what to do when the switch is opened or closed.
<pre><code class="python">switch.when_released = switch_open
switch.when_pressed = switch_closed
</code></pre>
And then comes the main code block. It is going to initialize my counter to 0, then perform an infinite loop. Each iteration of the loop will check to see if the switch is "pressed" or closed. If that's the case, we reset our counter and do nothing. But if the switch is open, we start counting. When our count reaches our threshold, we will start blinking the light: on for .2 seconds and then off for .2 seconds. And then it will send me a text message to let me know that the door is open so that I can go close it.
<pre><code class="python">count = 0
while True:
if switch.is_pressed:
count = 0
else:
count = count + 1
if verbose:
print("Switch has been opened for %s seconds" % count)
if (count == max_count):
light.off()
light.blink(.2, .2)
message = 'Fridge door has been open for ' + str(max_count) + ' seconds'
if verbose:
print ('Send message to ' + config["alertPhoneNumber"] + ': ' + message)
if not testing:
sendTextMessage(config["alertPhoneNumber"], message)
sleep(1)
</code></pre>
Here is the content of my <code>message</code> package that I used for sending myself text message:
<pre><code class="python">import os;
import requests;
def sendTextMessage(phone, message):
apiKey = '*my-api-key*'
resp = requests.post('https://textbelt.com/text', {
'phone': phone,
'message': message,
'key': apiKey
})
json = resp.json()
remaining = int(json["quotaRemaining"])
if (remaining < 5):
message = 'Less than 5 text messages left. Go to https://textbelt.com/purchase/ to purchase more.\n\nAPI key: ' + apiKey
os.system('echo "' + message + '" | mail -s "*my-email-address*')
return resp.json()
</code></pre>
Finally, to kick off the script running, I added it to my crontab so that it will automatically run at startup.
<pre><code>@reboot /home/pi/python/fridge/door-check.py</code></pre>
One final note: In my <code>message</code> package above, I have it email me if my quota is getting low and I need to purchse more message on my SMS gateway.
Unfortunately as of May of last year <a href="https://support.google.com/accounts/answer/6010255?hl=en" target="_blank">Google no longer supports this</a>.
So if you have a way of sending emails from your Rasperry Pi, please let me know.
<br><br>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-41200000665921456252023-02-02T16:40:00.002-06:002023-02-02T18:01:57.951-06:00The Magic Fridge<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEih_VvR2B37J3InJBAVf1b_xxUOpO18pskrYWkhwlTDc2CrvYasLp0rMVAiLAs25nQbjR90GqyKG1A7bwKpVAGpzfinvAKYIRNtu8Ss-SMpJjGtNbm4KZTM6WecjHfKQThJZ74TAoLifOacH4p-wYMIO7mjouxyTqhqRmWWyqNgae6EZ0Y-4ehZQ3uPww/s1024/the-magic-fridge.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="1000" data-original-width="1024" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEih_VvR2B37J3InJBAVf1b_xxUOpO18pskrYWkhwlTDc2CrvYasLp0rMVAiLAs25nQbjR90GqyKG1A7bwKpVAGpzfinvAKYIRNtu8Ss-SMpJjGtNbm4KZTM6WecjHfKQThJZ74TAoLifOacH4p-wYMIO7mjouxyTqhqRmWWyqNgae6EZ0Y-4ehZQ3uPww/s320/the-magic-fridge.png"/></a></div>
This is the first in what is hopefully going to be a series about The Magic Fridge. What is The Magic Fridge, you say? Well, I have a beverage fridge that I keep in the garage. I am always trying to find unique sodas and drinks to stock it with. My wife made a comment one day about how she never saw me stocking it yet it always seemed to have something new. She said "it's like some sort of magic fridge." The name stuck and now everyone affectionately refers to it as the magic fridge.
<br><br>
One day I got an idea to make it a bit more magical. I thought, what if I could use a <a href="https://www.raspberrypi.org/" target="_blank">Raspberry Pi</a> to turn my ordinary beverage fridge into a smart fridge. Sure, I could probably just go buy a smart fridge (if I had the space and the money) but I have an engieering mindset and enjoyed the technical challenge of making it happen.
<br>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLq0z8XYZ0DMlMOLWkAFLzndzOCN1yx7CX-cJjWc971ooMVNCsk82J5lPknH-e4gPpuvhug55uFQQJKsKYOir2xmAZ7g66W0-ZAKq_LyxYDTkGKO8WgUrgJ2LKodfSP5M5Za-U7e9Ov2dA1j9ccfKtW0FmIFT_u1UPMeoaL2gpFGi6lusC02AjLEI-TQ/s1000/rpi4.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="589" data-original-width="1000" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLq0z8XYZ0DMlMOLWkAFLzndzOCN1yx7CX-cJjWc971ooMVNCsk82J5lPknH-e4gPpuvhug55uFQQJKsKYOir2xmAZ7g66W0-ZAKq_LyxYDTkGKO8WgUrgJ2LKodfSP5M5Za-U7e9Ov2dA1j9ccfKtW0FmIFT_u1UPMeoaL2gpFGi6lusC02AjLEI-TQ/s320/rpi4.jpg"/></a></div>
<br>
There are 3 main features that I implemented with on the Magic Fridge:
<ul type="1">
<li>1. <a href="https://chads-tech-blog.blogspot.com/2023/02/the-magic-fridge-part-2.html">A door alarm</a></li>
<li>2. A power alarm</li>
<li>3. A temperature sensor</li>
</ul>
My hope is to also write an app to track my inventory. I've done some preliminary work on that but as things go with side projects, it has been stagnant for a while.
<br><br>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-43854954842982091722020-06-16T15:37:00.000-05:002020-06-16T15:37:39.711-05:00isNaN is not the same as Number.isNaNSince JavaScript is an untyped programming language, a variable can contain data of any type at any time. But sometimes you need to know what type the data is. Something I have done in the past, is use the <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN">isNaN</a></code> function for this.
<br><br>
Take this (bad?) example. If the variable <code>myData</code> is a number, then we want to format it to 2 decimal places. Otherwise, we just want to display it as is. One way to do that would be:
<br>
<pre><code class="javascript">
if (isNaN(myData)) {
displayString(myData);
else {
displayDecimalNumber(myData);
}
</pre></code>
But then along comes the linter complaining about <code>isNaN</code>:
<br>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkrb3qizalA0wpwyxzFp0COV-kV2zqZ91n5u6AO6XTEt0nakvEIZNhYgFV8-CKkXnBKdwqrpKYB6bM5tV6TS4mq-rPKv-9CFmg-_eh43c9dd5LYf-6Hx7axfPXy1SKrOQd7oj_6calp_9x/s1600/isNaN-linter-error.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkrb3qizalA0wpwyxzFp0COV-kV2zqZ91n5u6AO6XTEt0nakvEIZNhYgFV8-CKkXnBKdwqrpKYB6bM5tV6TS4mq-rPKv-9CFmg-_eh43c9dd5LYf-6Hx7axfPXy1SKrOQd7oj_6calp_9x/s1600/isNaN-linter-error.png" data-original-width="1600" data-original-height="213" width="880" /></a>
<br>
So I blindly accept its suggestion and change it to <code>Number.isNaN</code>. But now there's a problem because now it's trying to format my string data as a number. What happened?
<br><br>
The problem is that the global <code>isNaN</code> <a href="https://www.w3schools.com/jsref/jsref_isnan_number.asp#:~:text=The%20Number.,Otherwise%20it%20returns%20false.">does not behave the same way</a> as <code>Number.isNaN</code>.
<br><br>
The global <code>isNaN</code> function <i>converts</i> the value to a Number, <i>then</i> tests it.
<br><br>
<code>Number.isNaN</code> does <i>not</i> convert the value and will return <i>true</i> for any value <i>that is not of type Number</i>.
<br><br>
So maybe a better way to write this code would be:
<pre><code class="javascript">
if (typeof myData === 'number') {
displayDecimalNumber(myData);
} else {
displayString(myData);
}
</pre></code>
<br><br>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-58661096394544217362020-04-22T11:22:00.000-05:002020-04-22T11:22:24.949-05:00Implicit Argument to JavaScript PromiseI came across some code at work that I did not understand. After doing a little digging, I learned something I did not know about Promises. If you have a single argument to a function, you can use an implicit argument. What does the mean? Let's start with an example:
<br>
<pre><code class="javascript">test(0)
.then((results) => test(results))
.then((results) => test(results))
.then((results) => { console.log(`Results from first test: ${results}`) });
function test(arg) {
return new Promise((resolve, reject) => {
resolve(arg + 1);
});
}</code></pre>
So what's happening here? I am passing the number zero to the function <code>test</code>. It is taking that number, adding one to it, and returning a new Promise. <code>.then((results) => test(results)</code> then takes that result and passes it to the test function again which again adds one and returns a new Promise. Finally, we are printing the result which is 3. Now here is a different way to write that code with an implicit argument:
<br><pre><code class="javascript">test(0)
.then(test)
.then(test)
.then(results => { console.log(`Results from second test: ${results}`) });
function test(arg) {
return new Promise((resolve, reject) => {
resolve(arg + 1);
});
}</code></pre>
<br>
The result is the same. In this case, <code>.then(test)</code> is the equivalent of <code>.then((results) => test(results))</code>. This shorthand notation only works for a single argument. If you have multiple arguments to your function, you will have to write out the full code.
<br><br>
Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-25976470733096947912020-04-07T10:24:00.000-05:002020-04-07T10:24:05.467-05:00MySQL SubqueriesFor most of my software development career, I used an Oracle database. At my current job, we use a MySQL database. So I am still learning the ins and outs of MySQL. Here is a common thing I would do in Oracle:
<br>
<pre><code class="sql">SELECT
name, total_points
FROM (
SELECT
name,
SUM(points) AS total_points
FROM
scores
GROUP BY
name
)
WHERE
total_points > 10
ORDER BY
total_points DESC, name
</code></pre>
That would give me a list of all players who have scored more than 10 points. When I tried this same format in MySQL, I got this error:
<pre>Error Code: 1248. Every derived table must have its own alias</pre>
After a little searching, I came up with the solution. I had to name my inner query. So with a slight modification it worked:
<pre><code class="sql">SELECT
tp.name, tp.total_points
FROM (
SELECT
name,
SUM(points) AS total_points
FROM
scores
GROUP BY
name
) tp
WHERE
total_points > 10
ORDER BY
total_points DESC, name
</code></pre>
<br><br>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-36270758736193900732020-04-06T11:42:00.002-05:002023-02-02T15:51:50.629-06:00Accessibility and Social Media ManagementThere is a <a href="http://www.randylemmon.com/index.html" target="_blank">local radio program</a> that I used to listen to that gives advice on yards and gardening. The host is very knowledgeable and is my go-to resource when I have questions about planting, fertilizing, and weeding. Here was an exchange this morning between (presumably) the social media manager for his account and one of his followers regarding a video that was posted:
<br><br>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjyIMTRHzszY6vPafBAcj5OMuES2sZUzpYWiL2m6VuHnO5C3K36eh48_ZoBUQJBG5qdEb1E5IKzQtX2WVSltFxK0ahPAXxlB5MY9nH2fkPc8qxatUf3EieAZmnu9JnzryUIVEJoaby323vf/s1600/garden-line-resonse.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjyIMTRHzszY6vPafBAcj5OMuES2sZUzpYWiL2m6VuHnO5C3K36eh48_ZoBUQJBG5qdEb1E5IKzQtX2WVSltFxK0ahPAXxlB5MY9nH2fkPc8qxatUf3EieAZmnu9JnzryUIVEJoaby323vf/s1600/garden-line-resonse.jpg" data-original-width="1188" data-original-height="522" width="700" /></a></div>
<br>
The follower asked a simple question but got back a seemingly harsh response. First, let's look at why the follower may have misunderstood the audio.
<ul>
<li>· Maybe the follower is hard of hearing</li>
<li>· Maybe the follower's native language in not English</li>
<li>· Maybe the follower was in a noisy environment</li>
<li>· Maybe all of the above are true</li>
</ul>
The response from the author did not take any of these things into account. That is why accessibility is important not just for us web developers but for content providers as well. A better response might have been:
<br><br>
<div style="width: 90%; margin: 0 auto; text-align: center; font-style: italic">
Thank you for your question. He was saying "rose soil". Here is an example of a product that we recommend.
</div>
<br>
Here is another thing to consider. Various sites such as <a href="https://biteable.com/blog/tips/video-marketing-statistics/" target="_blank">this one</a> report that <b>85%</b> of Facebook videos are watched <b><i>without sound!</i></b> Additionally the World Health Organization reports that globally <a href="https://www.who.int/news-room/fact-sheets/detail/deafness-and-hearing-loss" target="_blank">over 400 million people suffering from some sort of hearing loss</a>. So if you are relying solely on audio on your site to get your message across, you are alienating a large group of people. This is why it is so important to provide captioning on your social media videos.
<br><br>
I realize that not everyone will have the skills, times, and/or technology to caption their videos. So in that case, a good fallback plan when someone poses a questions about the audio is to just be kind.
<br><br>
Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-85028667238885620782020-02-18T08:38:00.001-06:002020-02-18T08:43:23.297-06:00Git Status on Command Line Does Not Match Git Status in VSCode.A couple of times recently I ran into a situation where VSCode was showing a lot of untracked files in git but the command line was not. Today when I opened up VSCode, it showed over 60 untracked items while the command line only showed 15. It turned out to be a simple misunderstanding on my part.
<br><br>
To demonstrate, I created a new git project with a new subdirectory called <code class="cmd">new-dir</code>. Inside of <code class="cmd">new-dir</code>, I created 3 files.
<br><br>
When I run <code class="cmd">git status</code> on the command line without any parameters, it collapses new directories into single entries.
<br><br>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkKT56ppVHDfUT56ONj5SVEvy-hg52gqEqULrl7fLnMU8lATNXfd6omRGbVB299okF_weZRIUjlO_3dHzlMNKRCe6EXbPbybXtYpcl5GyvMEcpADzygZNo2MayLg51v2Ixx5txm4sqNz2j/s1600/Screen+Shot+2020-02-18+at+8.19.54+AM.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkKT56ppVHDfUT56ONj5SVEvy-hg52gqEqULrl7fLnMU8lATNXfd6omRGbVB299okF_weZRIUjlO_3dHzlMNKRCe6EXbPbybXtYpcl5GyvMEcpADzygZNo2MayLg51v2Ixx5txm4sqNz2j/s1600/Screen+Shot+2020-02-18+at+8.19.54+AM.png" data-original-width="688" data-original-height="172" alt="Command line example" /></a>
<br><br>
By examining the git output in VSCode, I see now that it is running the command <code class="cmd">git status -z -u</code>. The <code class="cmd">-z</code> just terminates each entry in the list with a NUL instead of LF presumably for better parsing. The <code class="cmd">-u</code> lists out each untracked <i>file</i> individually. And this makes sense because in the source control window of VSCode, you want to be able to see the diff of each file individually.
<br><br>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiU3ty2ycrGgdJ6EWYrSgd955XT32-mKfoIstDDck7iy149LqvBskgAuro0G66UI-FKZQAis7_CIhE_qFt8MLLhFDQ6dNy-PtQijq5WbtmiCT4NV8kWp7nujxEUQjc9-Z_7Y8IdtAgHuoG3/s1600/Screen+Shot+2020-02-18+at+8.20.27+AM.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiU3ty2ycrGgdJ6EWYrSgd955XT32-mKfoIstDDck7iy149LqvBskgAuro0G66UI-FKZQAis7_CIhE_qFt8MLLhFDQ6dNy-PtQijq5WbtmiCT4NV8kWp7nujxEUQjc9-Z_7Y8IdtAgHuoG3/s1600/Screen+Shot+2020-02-18+at+8.20.27+AM.png" data-original-width="513" data-original-height="461" alt="View of source control panel in VSCode" /></a>
<br><br>
Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com1tag:blogger.com,1999:blog-7413705187490473473.post-48794227049849460192020-01-29T08:31:00.000-06:002020-01-29T08:31:12.833-06:00What I Learned from the Job I Didn't GetRecently I had the unique experience of talking face-to-face with the person who beat me out for the job we both interviewed for. I want to share what I learned from this situation but first let's back up to the beginning of the story.
<br><br>
In 2001 I accepted a job working as NASA's Johnson Space Center as a software developer. I was equally parts shocked and excited when they offered me the job. Over the next 15 years I would have a great career working there. In May of 2016 when our contract was renewed, the government changed some things up from the previous contracts I had been on. Long story short, I ended up with a significant pay cut. It had already been 3 years since my last pay raise due to budget cuts. And with Congress at an impasse more and more, government shutdowns were becoming a regular threat. So for those reasons and others, I decided to start looking for a new job.
<br><br>
In July of that year, I got a call from a recruiter that had found my resume online. He told me about a frontend developer job just down the road from NASA. He got me an interview and I felt pretty good about it. Afterwards he told me that I was their first choice based on my resume and experience. However, after a few weeks went by and I heard nothing, I contacted the recruiter to get a status. He told me that they had decided to go with someone else. I was shocked, although in retrospect I shouldn't have been. I had done a terrible job of selling myself.
<br><br>
I also had a revelation after that: I had not dedicated enough time to professional development. In a perfect world, an employer will make professional development a priority for their employees. But we don't live in a perfect world and so the onus was on me to keep up. And I had not. Over the next couple of years I would take to Twitter, newsletters, and online training to educate myself on the latest and greatest tech in frontend development. I landed interviews with several companies during that time (sometimes even multiple interviews) but couldn't seem to get an offer. I finally decided to take a break from the job search.
<br><br>
About the time I made the decision to quit looking, I got a call from that company I had first interviewed with in 2016. They wanted to know if I'd be interested in interviewing for a developer job. I interviewed with some of the same people I had interviewed with before. And this time, I got an offer. I accepted the job and with much sadness, I left NASA after 17 years.
<br><br>
One day I realized that my coworker DJ (not his real name) was the person who had beat me out for the frontend developer position two years earlier. So I asked him about it. He told me that when he had interviewed for the job, he built a small prototype to demonstrate his skills. I really think this extra initiative on his part was what won him the job.
<h1>Lessons Learned</h1>
<h2>Professional Development</h2>
The world of web development is changing very rapidly. I had let my success at NASA stagnate me. I did look at new technologies from time to time, but I wasn't making a concerted effort to study and learn. That was a mistake. Take a little time every day out of our schedule for professional development.
<br>
<h2>Interviewing</h2>
Don't expect your resume to do all of the talking for you. I have a really hard time talking about myself. I don't like to draw attention to myself. But when you are interviewing, you have to do this. Build an online portfolio to show your work. Do something to make yourself stand out. For one job I applied for, I incorporated the company's colors and logo into my resume. The interviewer mentioned that it caught his eye.
<br>
<h2>It Will Take Some Time</h2>
When I first started interviewing, I thought it would take a few weeks maybe a couple of months at most to get an offer. I had no idea it would take two years! Be patient. Don't get your hopes up too high. Sometimes an interview will go great and you'll still get passed over for the job. That's ok. That job might have sounded great but maybe it wasn't so great. Maybe the people were difficult to work with. Maybe the management was unreasonable. Maybe the company is about to go under. Just assume that if you didn't get the job, it was for good reason.
<br><br><br>
Oh and for the record, I think hiring DJ over me was definitely the right decision. :)
<br><br>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-3127825278104123492019-12-04T13:27:00.000-06:002019-12-04T13:27:49.686-06:00Destructuring and Spreading in JavaScriptI wanted to get a better understanding of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment">destructuring</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax">spreading</a> for a project I am working on. So I created <a href="https://jsfiddle.net/clarmond/62Lgtnpb/11/">this jsfiddle</a> to understand it better.
<br><br>
What I really wanted to know was would happen if I tried to unpack a value from an object that did not exist. And then what would subsequently happen if I tried to use that value in a spread operation. For example:
<br>
<pre><code class="javascript">const testObj = {
a: [1, 2, 3],
b: [4, 5, 6]
}
const { a } = testObj;
const { b } = testObj;
const { c } = testObj;
const combined = [...a, ...b, ...c];
</code></pre>
<br>
The short answer is this: if you try to destructure a non-existent property, then you get an undefined value. And if you try to use that undefined value in a spread operation, JavaScript will throw an error: <code>TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))</code>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-49951063967822048732019-09-15T20:58:00.000-05:002019-09-15T21:02:38.211-05:00PHP: Failed opening required filepath in Unknown on line 0I ran into this error message while working on a personal project. I had set up the code in my Microsoft OneDrive folder to sync it between my laptop and my desktop. I suspected the error might have something to do with OneDrive, so I copied my code to another folder on my computer. Sure enough, it worked perfectly. Finally, I discovered the problem. It was a setting in OneDrive called "Files On-Demand". What I suspect is that the files were simply just 0-byte stubs on my desktop. So when PHP requested them, it failed. Once I unchecked this box and OneDrive downloaded all of the necessary files, it worked just fine.
<br><br>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhZrR7dxb8fpGklcrqJ0jsRyQ4LdzzDyQae-vmoBnCvuG1aBJ2OIG8EKhuh6E_Uv45hjwHvUWTl74ioyppqK4CUtQsqsDnlOKkH49lHmPJqT7DUvS8hLZzlkZuBxjXD7pUFztIt5WZNhRcL/s1600/one-drive-php-fix.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhZrR7dxb8fpGklcrqJ0jsRyQ4LdzzDyQae-vmoBnCvuG1aBJ2OIG8EKhuh6E_Uv45hjwHvUWTl74ioyppqK4CUtQsqsDnlOKkH49lHmPJqT7DUvS8hLZzlkZuBxjXD7pUFztIt5WZNhRcL/s400/one-drive-php-fix.png" width="358" height="400" data-original-width="421" data-original-height="470" /></a></div>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-14624201973567529472019-04-29T09:37:00.000-05:002019-04-29T09:37:09.339-05:00Make Your Console Statements Stand OutUsing the <a href='https://developers.google.com/web/tools/chrome-devtools/'>Chrome DevTools</a> to debug your JavaScript code is great. Breakpoints are a great way to analyze what is going on with your code. But sometimes you just need an old-fashioned <a href='https://developer.mozilla.org/en-US/docs/Web/API/Console/log'><code>console.log</code></a>. But if you have an app that is already chatty in development mode with a lot of console messages, there is a way to make your console statement stand out. You may not know that you can format your console statements with CSS. For example:
<br>
<pre><code class="javascript">console.log('%cExample', 'background: #a00; color: #fff; font-weight: bold; font-size: 110%');</code></pre>
will look like this in your console:
<p><span style='background: #a00; color: #fff; font-family: monospace; font-weight: bold; font-size: 110%;'>Example</span></p>
A better way to do it, though, is to use a JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals">template literal</a>. For example:
<br>
<pre><code class="javascript">let xyz = 123;
console.log('%c%s', 'background: #a00; color: #fff; font-weight: bold; font-size: 110%',
`The value of xyz is ${xyz}`);</code></pre>
Will produce:
<p><span style='background: #a00; color: #fff; font-family: monospace; font-weight: bold; font-size: 110%;'>The value of xyz is 123</span></p>
Now to make it even easier, we can create a <a href="https://code.visualstudio.com/docs/editor/userdefinedsnippets">VS Code snippet</a>:
<pre><code class="javascript">"color-console": {
"prefix": "color-console",
"body": [
"console.log('%c%s', 'background: #a00; color: #fff; font-weight: bold; font-size: 110%', `$1`);"
],
"description": "Color coded console message"
},</code></pre>
And then <i>voilà</i>
<br><br>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgC1v-ASUPQYMunjqu5Uyp0t5lDx61FY3f3Yrd4OIEUjDGd2RDTSpm1uFGYYhnarRb7XYnboHbn97cQexp5BTyLJ84Abkx_f_MTp08LyLysNckVUdB40Da_O1Q6p5icI4yz5xJQnNy7q4O1/s1600/color-console-example.gif" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgC1v-ASUPQYMunjqu5Uyp0t5lDx61FY3f3Yrd4OIEUjDGd2RDTSpm1uFGYYhnarRb7XYnboHbn97cQexp5BTyLJ84Abkx_f_MTp08LyLysNckVUdB40Da_O1Q6p5icI4yz5xJQnNy7q4O1/s1600/color-console-example.gif" data-original-width="870" data-original-height="136" /></a>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-36925210755937678082019-04-23T14:26:00.002-05:002019-04-23T14:29:59.238-05:00Sequelize Newbie MistakeToday I inherited a piece of code that looked something like this:
<pre><code class="javascript">const records = Records.findOne({
where: {
pk: 'blah',
},
sort: ['dateField', 'DESC']
});</code></pre>
It did not seem to be sorting correctly, so I looked up <a href="http://docs.sequelizejs.com/manual/querying.html#ordering">ordering for Sequelize</a>. Since I am new to Sequelize, I did not know that the correct property is <code>order</code> and not <code>sort</code>. But when I changed it, I got this error message:
<pre>Unknown column \'DESC\' in \'order clause\'</pre>
It took me longer than I care to admit to see what the issue was. If you pass an array of single items to the <code>order</code> property, it will use them all as part of the sort. So the backend SQL query looked something like this:
<pre><code class="sql">SELECT * FROM my_table WHERE pk = 'blah' ORDER BY dateField, DESC</code></pre>
What I really wanted was to pass it an array of arrays to sort on. So the correct code looks like this:
<pre><code class="javascript">const records = Records.findOne({
where: {
pk: myValue,
},
order: [
['dateField', 'DESC']
],
});</code></pre>
Notice that <code>order</code> property now is an array containing an array specifying the field to sort on and the direction with which to sort it.
Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-82363089339486300752019-03-21T08:41:00.001-05:002019-03-21T08:46:55.512-05:00Case-insensitive matching with case-sensitive replacements in JavaScriptThat title is a mouth full. Here is what I was trying to accomplish: I had a search term that I wanted to highlight in a body of text. So my code needed to match all occurrences of the search term and then enclose the matched text within some HTML in order to format it. Seems simple enough, but I ran into a couple of issues.
<br><br>
Let's say I am going to format a phone number from a string of digits. I would do it like this:
<pre><code class="javascript">let phone = '5551234567';
phone = phone.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3');
</code></pre>
But that syntax doesn't work for me here because I need to pass the search pattern as a variable name <i>and</i> I need to specify the "i" and "g" flags. So this code will not work:
<pre><code class="javascript">const searchTerm = 'xyz';
myText = myText.replace(/searchTerm/ig, 'THE'); // Not valid
</code></pre>
First, I use the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp" target="_blank">RegExp constructor</a> to define my pattern.
<pre><code class="javascript">const searchTerm = 'xyz';
const pattern = RegExp(searchTerm, 'ig');
let myText = 'xyz XYZ xYz';
myText = myText.replace(pattern,
`<span class='highlight'>${searchTerm}</span>);
// Results will all be lowercase because searchTerm is lowercase
</code></pre>
Then I had to create an array of matches that I could then loop through and replace each one individually:
<pre><code class="javascript">let sampleText = 'This is my sample text: test Test TEST';
const searchText = 'test';
const pattern = new RegExp(searchText, 'ig');
const matches = sampleText.match(pattern, sampleText);
if (matches !== null) {
matches.forEach((match) => {
sampleText = sampleText.replace(match, `<span class="highlight">${match}</span>`);
});
}
</code></pre>
To see this in action, take a look at
<a href="https://jsfiddle.net/clarmond/1npku42v/29/" target="_blank">this JSFiddle</a>.Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-54172753183510519922019-01-31T09:33:00.001-06:002019-01-31T09:33:43.808-06:00Using WorkspacesI'm a big fan of Visual Studio Code's <a href="https://flaviocopes.com/vscode/#workspaces">workspaces</a>. In the past, I've used them to separate my work into different projects and applications or different parts of an application (like server-side vs client-side). At the moment we are wrapping up a release at my job so I am often jumping between various tickets. It occurred to me this morning that I could create a new workspace for each ticket. The reason this is helpful is because sometimes I may need these 4 files open for one ticket and a different 5 files open for a different ticket. So instead of keeping them all open and jumping around, I can have just the files I need open for that particular ticket to avoid confusion.
<br><br>
So then the thought occurred to me, what if I could do the same with Chrome? Fortunately, there is <a href="https://chrome.google.com/webstore/detail/workspaces/eehocaoejekpmecmfecdkpohdkgkmoee?hl=en">an extension</a> for that. Now I can open my application and all my reference material in one workspace and a different set of tabs in another workspace.
<br><br>
Simple. Efficient. I like that.Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-48527606997289583432019-01-08T13:02:00.003-06:002019-01-08T13:28:46.526-06:00Find This Not That in jQueryToday at work, I came across a jQuery <code><a href="https://api.jquery.com/find/">find</a></code> statement that was selecting a bunch of different elements but I needed to excluded a single class from the selection. Of course, I hit Google and Stack Overflow for an answer but nothing was quite exactly what I needed. I found the solution and also learned something else about jQuery in the process. So here is what I came up with.
<br><br>
The solution was to chain a <code><a href="https://api.jquery.com/filter/">filter</a></code> call to the <code><a href="https://api.jquery.com/find/">find</a></code> statement using the <code><a href="https://api.jquery.com/not/#not-selector">not selector</a></code>.
<br><br>
Here is a contrived example on <a href="https://codepen.io/clarmond/pen/QzBwZX">CodePen</a>. In this example, I am selecting all buttons <i>except</i> for the <code>btn-danger</code> and <code>btn-warning</code> classes:
<br>
<pre><code class="javascript">var myButtons = $('#main')
.find('button')
.filter(':not(".btn-danger,.btn-warning")');</code></pre>
<br>
Something I noticed in the console is that there is an object created called <code>prevObject</code>. This object contains the matched elements <i>before</i> the filter is applied which could be useful.
<br><br>
Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-84580212411282433902018-06-07T08:28:00.001-05:002018-06-07T08:30:03.412-05:00Using the VS Code Integrated Terminal with Remote HostsI am in the process of writing some server-side scripts on a remote server. There are three things that would make it easier:
<br><br>
1. If I could write the code directly to the server from VS Code <br>
2. If the integrated terminal in VS Code would automatically log me into that server <br>
3. If I could easily toggle between the code window and the terminal window <br>
<br>
I am going to describe how to do all of those things.
<br>
<h2>Writing Code Directly to the Server in VS Code</h2>
To write code directly to the server, I am using an SFTP extension I found in the VS Code Marketplace. The <a href="https://github.com/liximomo/vscode-sftp">extension I am using</a> is simply called <b>sftp</b>.
<br><br>
To configure it, you open the command palette (<code>Cmd+Shift+P</code>) and select <b>SFTP: Config</b>. This will generate a file called <code>sftp.json</code> within the <code>.vscode</code> directory of your workspace. You can check out <a href="https://github.com/liximomo/vscode-sftp/wiki/config">the full config options</a>, but basically you just need to specify the host, username, and remote path. So that my code is automatically saved to the server, I set the <code>uploadOnSave</code> option to <code>true</code> in the <code>sftp.json</code> file.
<br><br>
And so that I don't have to log in every time, I am using the <code>privateKeyPath</code> option as well. I won't go into the details here of <a href="http://www.rebol.com/docs/ssh-auto-login.html">how to set up SSH for auto login without a password</a> but it is definitely a time saver.
<br><br>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7FCG5Xn-J9PYJ0-_8CU3BHUSPo_jW0NmvjyTaF50PTraVXaF7jcWE3BywI9B2JvQrNzV91Yd1HGkcFKGkHTIp6SpYzsTgnIUUfurl9Ho7EQYFI0TsWtF9w70LmATaLtXsgWAQBJtoWyEE/s1600/Screen+Shot+2018-06-07+at+8.02.13+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7FCG5Xn-J9PYJ0-_8CU3BHUSPo_jW0NmvjyTaF50PTraVXaF7jcWE3BywI9B2JvQrNzV91Yd1HGkcFKGkHTIp6SpYzsTgnIUUfurl9Ho7EQYFI0TsWtF9w70LmATaLtXsgWAQBJtoWyEE/s1600/Screen+Shot+2018-06-07+at+8.02.13+AM.png" data-original-width="473" data-original-height="243" /></a></div>
<br>
<h2>Automatically Logging into the Remote Server</h2>
The next thing I wanted to accomplish was automatically logging into the remote server in the integrated terminal. To do that I need to update my workspace settings.
<br><br>
To start I select <b>Code ▶ Preferences ▶ Settings</b> to open the Workspace Settings. Then within the settings, I add some JSON to tell it to call the ssh command with an argument of the server name.
<br><br>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQ2jCHhivRYuRG-lDTq1KEWqDKVg-Aw6KtVP1u5J3Dx33EX0hUW2I4wOO_dFLOlLIeN_pxuj1K6s7e75dukmSL2S9EqKHGE8I93G3RA89LXKrS9zeSOaVVbHMWajZNlWYxSU6vJ4L7S9ES/s1600/Screen+Shot+2018-06-07+at+7.38.11+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQ2jCHhivRYuRG-lDTq1KEWqDKVg-Aw6KtVP1u5J3Dx33EX0hUW2I4wOO_dFLOlLIeN_pxuj1K6s7e75dukmSL2S9EqKHGE8I93G3RA89LXKrS9zeSOaVVbHMWajZNlWYxSU6vJ4L7S9ES/s1600/Screen+Shot+2018-06-07+at+7.38.11+AM.png" data-original-width="488" data-original-height="356" /></a></div>
<br>
Also, because I like to <a href="http://chads-tech-blog.blogspot.com/2012/04/using-color-to-prevent-accidents.html">color code</a> my terminal windows, I found the <code>workbench.colorCustomizations</code> setting which allows me to set a value for <code>terminal.background</code>.
<br><br>
For this change to take effect, I have to restart VS Code. A security feature prompts me to make sure this is what I really want to do. This only happens the first time.
<br><br>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBbSRjjWsXmb8E1I_NB3SJLLA4yX6_IOIPnkyqk0sZzord5sFulRSCQDzkO6UqKcxQOsZ9YBmXcFifO523agZoPDgGKKD-8x8MvzMyy9dq_aIoUhYDexdbMDrAdgCUeNrhNLvu0wPPBqO_/s1600/Screen+Shot+2018-06-07+at+7.38.46+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBbSRjjWsXmb8E1I_NB3SJLLA4yX6_IOIPnkyqk0sZzord5sFulRSCQDzkO6UqKcxQOsZ9YBmXcFifO523agZoPDgGKKD-8x8MvzMyy9dq_aIoUhYDexdbMDrAdgCUeNrhNLvu0wPPBqO_/s1600/Screen+Shot+2018-06-07+at+7.38.46+AM.png" data-original-width="462" data-original-height="139" /></a></div>
<br>
I have to restart VS Code one more time. This time when I open the terminal, it automatically logs me into the remote server so that I can test my scripts.
<br><br>
<h2>Toggling Between the Code Window and the Terminal Window</h2>
While I like having the terminal window integrated into my IDE, I don't like having to use the mouse to click back and forth between the two windows. Fortunately, <a href="https://stackoverflow.com/a/43012779">someone on Stack Overflow</a> had already solved that problem.
<br><br>
This time I go to <b>Code ▶ Preferences ▶ Keyboard Shortcuts</b>. To make the change, I first have to click on the <code>keybindings.json</code> link:
<br><br>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-F7fUP9soZd9rRpDrm400TneEsTUnOd8Gq6BYnPnkUgGdTd8eNcXRoov5nFPMZkpiEqQwmKwVPl-fT7PDb5ETnn4-dOxt34NcRm6dfigSc_lfKoYE7f_JSSY1uJgyhFEoIjNVSCBzZJmv/s1600/Screen+Shot+2018-06-07+at+8.18.39+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-F7fUP9soZd9rRpDrm400TneEsTUnOd8Gq6BYnPnkUgGdTd8eNcXRoov5nFPMZkpiEqQwmKwVPl-fT7PDb5ETnn4-dOxt34NcRm6dfigSc_lfKoYE7f_JSSY1uJgyhFEoIjNVSCBzZJmv/s1600/Screen+Shot+2018-06-07+at+8.18.39+AM.png" data-original-width="619" data-original-height="324" /></a></div>
<br>
Then I can paste in the code for my settings.
<br><br>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIJgqiaBS3zm2wyneGq6HaXlF3LSoIy572g2Nlihc4up9bc8zw9U2jnkeOQ0z_LlLRXFZFRnN-puUT_nKkwqDVwuULddQjf7bXhZkXwRKRLBMOhCG2LIa7YWO3DkLID_wRa8kbinjJF8ac/s1600/Screen+Shot+2018-06-07+at+8.20.00+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIJgqiaBS3zm2wyneGq6HaXlF3LSoIy572g2Nlihc4up9bc8zw9U2jnkeOQ0z_LlLRXFZFRnN-puUT_nKkwqDVwuULddQjf7bXhZkXwRKRLBMOhCG2LIa7YWO3DkLID_wRa8kbinjJF8ac/s1600/Screen+Shot+2018-06-07+at+8.20.00+AM.png" data-original-width="669" data-original-height="169" /></a></div>
<br>
Now after I save my code, I simply press <code>Ctrl+`</code> to switch to the terminal window, test my code, and then press <code>Ctrl+`</code> again to return back to my code.
<br><br>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-59071214891450509602018-05-22T09:20:00.000-05:002018-05-22T09:20:42.833-05:00Changing Chrome Policies for TestingI recently ran into a situation where I was pretty sure that a group policy was breaking functionality on my intranet app, but I needed a way to test that theory. After some research, I finally figured out a way to do this.
<br><br>
I am working on an <a href="https://www.virtualbox.org/">Oracle Virtual Box</a> VM of Windows 7. Because it's a VM on my laptop, I have complete control over the operating system. To set the policy, I used the Windows Registry Editor <code>regedit</code>.
<br><br>
Within <code>Computer\HKEY_CURRENT_USER\Software\Policies</code>, I created a new key called <code>Google</code>. Then within <code>Google</code>, I created a new key called <code>Chrome</code>.
<br><br>
From there, I just needed to add policies one by one until I figured out which one was breaking my app. The link at <a href="https://www.chromium.org/administrators/policy-list-3">https://www.chromium.org/administrators/policy-list-3</a> provides a full list of policies. After saving a policy, I would restart Chrome and the new policy would be enabled. Sure enough, I found the culprit. By toggling this policy and restarting Chrome I could verify that the application worked and then broke with each change of the policy.
<br><br>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgNaRkNz7hfIVPO8njVGqEtKEN0ck9rixubR5DgFB0ybzZUpAyiXokBhJAbUwpCmzgLfadtIgx7hvTOYKYSrX3ana3sNofanCiGEaLXlqT8EhoC5SP-mMpWJsVdMHLVqf9lDwG8_E-OpocT/s1600/Screen+Shot+2018-05-22+at+8.59.20+AM.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgNaRkNz7hfIVPO8njVGqEtKEN0ck9rixubR5DgFB0ybzZUpAyiXokBhJAbUwpCmzgLfadtIgx7hvTOYKYSrX3ana3sNofanCiGEaLXlqT8EhoC5SP-mMpWJsVdMHLVqf9lDwG8_E-OpocT/s1600/Screen+Shot+2018-05-22+at+8.59.20+AM.png" data-original-width="621" data-original-height="335" alt="Example of Chrome policy set in Windows registry" width="100%" /></a>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-12346236963273030372018-04-24T15:15:00.001-05:002018-04-24T15:15:14.955-05:00Stopping a page redirect to look at JavaScript consoleI am currently assisting another developer troubleshoot an issue he's having. In the code, there is a JavaScript function call tied to the <code>onclick</code> event of the HTML link. Because the JavaScript code is failing, the page is redirecting before I have a change to look at the console log to see what the error is. One solution is to use a breakpoint in the code and that's what I'll ultimately do. But I found this <a href="https://gist.github.com/InstanceOfMichael/f6a509b84fe82a718816">great hack</a>.
<p><code class="brush:js">window.addEventListener("beforeunload", function() { debugger; }, false)</code></p>
By running this code in the console first, it will pause execution and allow me to see the error in the console.Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-12705778086476540342017-10-11T15:30:00.001-05:002017-10-12T07:50:27.819-05:00Rollback with CFTRANSACTIONYou may be surprised to find out that software documentation is not always clear or complete. Shocking, I know. Even when it is, sometimes I just have to see things in action for myself. This is one of those cases.
<br /><br />
A question came up today at work about the <code><cftransaction></code> tag. (Yes, we still use ColdFusion. <a href="https://www.youtube.com/watch?v=lKie-vgUGdI" target="_blank">There are dozens of us. Dozens!</a>) The question was if the tag would automatically rollback changes or if you needed to explicitly type <code><cftransaction action="rollback"></code> to rollback the changes. I have always assumed the latter.
<br /><br />
So here is my test case. First I created 2 test tables in the database for testing each with a test record.
<pre><code class="sql">CREATE TABLE test_table_1 (
tt1_record_id NUMBER(3) NOT NULL,
tt1_text VARCHAR2(100),
CONSTRAINT pk_test_table_1 PRIMARY KEY (tt1_record_id)
);
CREATE TABLE test_table_2 (
tt2_record_id NUMBER(3) NOT NULL,
tt2_text VARCHAR2(100),
CONSTRAINT pk_test_table_2 PRIMARY KEY (tt2_record_id)
);
INSERT INTO test_table_1 (
tt1_record_id,
tt1_text
)
VALUES (
1,
'Blue'
);
INSERT INTO test_table_2 (
tt2_record_id,
tt2_text
)
VALUES (
1,
'Triangle'
);</code></pre>
And now the ColdFusion code. This block of code first outputs the text from the database. Then it updates the first table while intentionally failing to update the second table. Lastly, it displays the text from the database.
<pre><code class="xml"><cfquery name="variables.qryBefore" datasource="#application.dsn#">
SELECT
tt1_text, tt2_text
FROM
test_table_1,
test_table_2
WHERE
tt1_record_id = tt2_record_id
</cfquery>
<p>
<cfoutput>
Before:
#variables.qryBefore.tt1_text#
#variables.qryBefore.tt2_text#
</cfoutput>
</p>
<cftry>
<cftransaction>
<cfquery name="variables.qryUpdate1" datasource="#application.dsn#">
UPDATE
test_table_1
SET
tt1_text = 'Red'
WHERE
tt1_record_id = 1
</cfquery>
<cfquery name="variables.qryUpdate2" datasource="#application.dsn#">
UPDATE
test_table_2
SET
tt2_text = 'Square'
WHERE
foo = bar
</cfquery>
</cftransaction>
<cfcatch type="any">
<p>
Database update failed. Changes should have been rolled back.
</p>
</cfcatch>
</cftry>
<cfquery name="variables.qryAfter" datasource="#application.dsn#">
SELECT
tt1_text, tt2_text
FROM
test_table_1,
test_table_2
WHERE
tt1_record_id = tt2_record_id
</cfquery>
<p>
<cfoutput>
After:
#variables.qryAfter.tt1_text#
#variables.qryAfter.tt2_text#
</cfoutput>
</p></code></pre>
The database values remained the same. The first table was updated but when the second table failed to update, the changes were rolled back. I could have added <code><cftransaction action="rollback"></code> to the <code><cfcatch></code> block and gotten the same result.
<br/ ><br />
So why use the rollback command then? You may only want to rollback to a certain savepoint. Another use case I have found for it is unit testing. In my test, I want to execute the entire method even the database code. However, before I leave the <cfcode><cftransaction></code> block, I rollback my changes:
<pre class="brush:sql"><cfif this.testing>
<cftransaction action="rollback" />
</cfif></pre>
Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-953861510367334542017-04-05T09:58:00.000-05:002018-12-06T09:16:01.193-06:00Clipboard ManagersI do a lot of copying and pasting. A lot. One of the things that often happens is that I copy a piece of information, paste it, copy something else, paste it, then need to go back to that previous information I copied. Or maybe I need to copy two (or more) pieces of information from one source before switching to the other source. The best way to handle this is with a clipboard manager. These tools allow you to keep a history of everything you have copied (up to a limit) and the refer back to them. Here are a couple that I have used in my job:
<br /><br />
<div style="border: 1px solid #ccc; padding: 10px; text-align: center">
<a href="https://github.com/naotaka/ClipMenu/issues/31">ClipMenu</a> for MacOX (Updated 12/06/18 with new link)
</div>
<div style="border: 1px solid #ccc; padding: 10px; margin: 10px 0; text-align: center">
<a href="http://ditto-cp.sourceforge.net/">Ditto</a> for Windows
</div>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-43048738954568413402017-03-09T07:45:00.000-06:002017-03-09T07:45:07.074-06:00ColdFusion 2016 Broke My API (And How I Fixed It)While on ColdFusion 9, I built a REST API for our an application at work. Everything was working fine until we upgraded to ColdFusion 2016. When I went to test the API, I got an HTTP 500 error. The Apache log gave me no clues to what was happening, so my next stop was the ColdFusion logs. In the exception log, I found this:
<br />
<pre>Error","ajp-nio-8015-exec-9","03/09/17","07:22:12",,"Application could not be found. The specific sequence of files included or processed is: '''' "
javax.servlet.ServletException: Application could not be found.
at coldfusion.rest.servlet.CFRestServlet.invoke(CFRestServlet.java:512)
at coldfusion.rest.servlet.RestFilter.invoke(RestFilter.java:60)
at coldfusion.filter.ExceptionFilter.invoke(ExceptionFilter.java:94)
...
</pre>
A quick Google search did not reveal much. However, the fact that it was calling the CFResetServlet gave me an idea. What if the <code>/api</code> directory now had special meaning? So I renamed the <code>/api</code> directory to <code>/API-TEST</code> and sure enough my code was working again.
<br />
I found the solution in the web.xml file:
<pre class="brush:xml">
<servlet-mapping id="coldfusion_mapping_16">
<servlet-name>CFRestServlet</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
</pre>
This mapping is forcing everything in the <code>/api</code> directory to be processed by the ColdFusion REST service. To fix this, I simply commented out this section of the configuration and restarted the ColdFusion service.
<pre class="brush:xml">
<--
<servlet-mapping id="coldfusion_mapping_16">
<servlet-name>CFRestServlet</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
-->
</pre>Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com3tag:blogger.com,1999:blog-7413705187490473473.post-87220941582707685352016-10-28T10:38:00.002-05:002018-09-03T12:14:35.900-05:00Chicken and Sausage Gumbo Recipe<style>
ol.e-instructions {
list-style-type: decimal !important;
}
#ingredient-list {
list-style-type: disc;
}
</style>
Today I take a break from my usual tech blog posts to post a recipe. However, this is still tech-related as I am exploring the <a href="http://schema.org/Recipe" target="_blank">recipe schema</a> to post this. If my code works, then the recipe should be formatted better for Pinterest, search engines, and recipe applications.
<br><br>
<meta property="og:site_name" content="chads-tech-blog.blogspot.com" />
<article class="h-recipe">
<img class="u-photo" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitYwDIckfIw21ngbfGDhjoT_Z4R4nDXyy1uK8u1tLHxyFZLFPLWIjSMQYmmXAwQm4SvvD44ZJjo64YfevMyEuy8UU_DsXE9U5tizsV1a6xtPWcNUo7eMiiEXiiarG5Uay9InJDprAHd14W/s1600/Photo+Oct+28%252C+10+17+30+AM.png">
<h1 class="p-name">Chicken and Sausage Gumbo</h1>
<h3>Ingredients</h3>
<ul id="ingredient-list">
<li class="p-ingredient">1/2 Cup Cooking Oil</li>
<li class="p-ingredient">1/2 Cup Flour</li>
<li class="p-ingredient">1 White Onion</li>
<li class="p-ingredient">1 Bell Pepper</li>
<li class="p-ingredient">4 Stalks Celery</li>
<li class="p-ingredient">2 32-oz Cartons of Chicken Stock</li>
<li class="p-ingredient"><a href="https://www.tonychachere.com/Original-Creole-Seasoning-Multi-Pack-P1268.aspx" target="_blank">Tony Chachere's Creole Seasoning</a> (To taste)</li>
<li class="p-ingredient">2 Bay Leaves</li>
<li class="p-ingredient">1 Package Chicken Breasts</li>
<li class="p-ingredient">1 Package <a href="https://en.wikipedia.org/wiki/Andouille" target="_blank">Andouille</a> Sausage<super>*</super></li>
<li class="p-ingredient">Cooked Rice</li>
</ul>
<h3>Cook Time</h3>
<ul>
<li> <time class="dt-duration" datetime="3H">3 hours</time></li>
</ul>
<h3>Instructions</h3>
<ol class="e-instructions">
<li>Cut up chicken breasts into chunks and place into a bowl. Season with creole season, cover bowl, and put in refrigerator.</li>
<li>Chop onion, bell pepper, and celery. Set aside.</li>
<li>In a heavy 4-quart pot or Dutch oven, warm cooking oil over medium to medium-low heat.</li>
<li>
Add flour to oil and stir continuously until <a href="https://en.wikipedia.org/wiki/Roux" target="_blank">roux</a> becomes a dark brown color.
<em>Do not burn.</em>
If black flecks appears, you will need to start over.
It will take approximately 30 minutes to get a nice brown color.
</li>
<li>Add vegetables and cook in roux until onions begin to wilt.</li>
<li>While stirring the mixture, slowy pour in chicken stock. Bring to a boil and then reduce to a simmer.</li>
<li>Stir in seasoning and bay leaves. Adjust to taste.</li>
<li>Carefully add meat to gumbo.</li>
<li>Cut up sausage and add to gumbo</li>
<li>Simmer gumbo for 1-2 hours or until chicken is tender</li>
<li>Serve over cooked rice. Goes well with French bread and deviled eggs</li>
</ol>
<p>
<super>*</super>
I prefer a real smoky sausage from a local smokehouse for the rich flavor but I usually do not have time to get that. So I like to buy <a href="http://www.holmessmokehouse.com/products/sausages/andouille/item/andouille" target="_blank">Holmes Andouille</a> sausage at the grocery store because it has a nice flavor and it is not greasy at all.
</article>
Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0tag:blogger.com,1999:blog-7413705187490473473.post-22566325097483175472016-10-06T15:06:00.001-05:002016-10-06T15:09:00.640-05:00Format JSON String in ColdFusionI am sure this has probably already been done but I couldn't find it quickly with a Google search. So I wrote my own. This function takes a JSON string and indents it to make it more readable.
<pre class="brush:coldfusion">
<cffunction name="indentJSON" hint="Indents JSON to make it more readable">
<cfargument name="JSONString" default="" hint="JSON string to be formatted">
<cfargument name="indentCharacters" default="#Chr(9)#" hint="Character(s) to use for indention">
<cfset local.inQuotes = false>
<cfset local.indent = 0>
<cfset local.returnString = "">
<cfset local.stringLength = Len(arguments.JSONString)>
<cfloop index="i" from="1" to="#local.stringLength#">
<cfset local.currChar = Mid(arguments.JSONString, i, 1)>
<cfif i lt local.stringLength - 1>
<cfset local.nextChar = Mid(arguments.JSONString, i + 1, 1)>
<cfelse>
<cfset local.nextChar = "">
</cfif>
<cfif local.currChar eq '"'>
<cfset local.inQuotes = !local.inQuotes>
</cfif>
<cfif local.inQuotes>
<cfset local.returnString = local.returnString & local.currChar>
<cfelse>
<cfswitch expression="#local.currChar#">
<cfcase value="{">
<cfset local.indent = local.indent + 1>
<cfset local.returnString = local.returnString & "{" & Chr(10) & RepeatString(arguments.indentCharacters, local.indent)>
</cfcase>
<cfcase value="}">
<cfset local.indent = local.indent - 1>
<cfset local.returnString = local.returnString & Chr(10) & RepeatString(arguments.indentCharacters, local.indent) & "}">
<cfif local.nextChar neq ",">
<cfset local.returnString = local.returnString & Chr(10)>
</cfif>
</cfcase>
<cfcase value="," delimiters="Chr(0)">
<cfset local.returnString = local.returnString & "," & Chr(10) & RepeatString(arguments.indentCharacters, local.indent)>
</cfcase>
<cfcase value=":">
<cfif local.nextChar neq " ">
<cfset local.returnString = local.returnString & ": ">
</cfif>
</cfcase>
<cfdefaultcase>
<cfset local.returnString = local.returnString & local.currChar>
</cfdefaultcase>
</cfswitch>
</cfif>
</cfloop>
<cfreturn trim(local.returnString)>
</cffunction>
</pre>
And here's an example:
<pre class="brush:coldfusion">
<cfset variables.testObject = {}>
<cfset variables.testObject.name.first = "Chad">
<cfset variables.testObject.name.last = "Armond">
<cfset variables.testObject.title = "Software Developer">
<cfset variables.testString = SerializeJSON(variables.testObject)>
<cfoutput>
<h1>With Tabs (Default)</h1>
<cfset variables.json1 = indentJSON(variables.testString)>
<pre>#variables.json1#</pre>
<h1>With Spaces</h1>
<cfset variables.json2 = indentJSON(variables.testString, " ")>
<pre>#variables.json2#</pre>
</cfoutput>
</pre>
And the results:
<h1>With Tabs (Default)</h1>
<pre>{
"NAME": {
"LAST": "Armond",
"FIRST": "Chad"
},
"TITLE": "Software Developer"
}</pre>
<h1>With Spaces</h1>
<pre>{
"NAME": {
"LAST": "Armond",
"FIRST": "Chad"
},
"TITLE": "Software Developer"
}</pre>
Chadhttp://www.blogger.com/profile/04636710455817061758noreply@blogger.com0