Category: Coding

Home / Category: Coding

LocalStorage Expiry Mechanism

October 28, 2019 | Coding | No Comments

programming code

HTML5 LocalStorage is a great tool, but it never expires. To contrast, SessionStorage expires when the browser tab is closed. But what if you want a longer expiry while preserving data if the browser is closed?

Here are two simple functions to add and check a expiry value to your storage object.

// get from LocalStorage (if the value expired it is destroyed)
/**
 * @return {null}
 */
function LocalStorageGet(key) {
  let stringValue = window.LocalStorage.getItem(key);
    if (stringValue !== null) {
      let value = JSON.parse(stringValue);
        let expirationDate = new Date(value.expirationDate);
        if (expirationDate > new Date()) {
          return value.value
        } else {
          window.LocalStorage.removeItem(key);
        }
    }
    return null;
}

// add into LocalStorage
function LocalStorageSet(key, value, expirationInMin = 10) {
  let expirationDate = new Date(new Date().getTime() + (60000 * expirationInMin));
    let newValue = {
    value: value,
    expirationDate: expirationDate.toISOString()
  };
  window.LocalStorage.setItem(key, JSON.stringify(newValue))
}

GIT: Remove File History

October 24, 2019 | Coding, System Administration | No Comments

Shell

Ever accidentally commit a file that you meant to have in your .gitignore? Perhaps a file which includes an API key? It’s easier than you are probably thinking.

$ git rm --cached the-file-i-want-to-remove
$ git commit --amend -CHEAD
$ git filter-branch --force --index-filter "git rm --cached --ignore-unmatch FILE-TO_REMOVE" --prune-empty --tag-name-filter cat -- --all

But, you’re better off just not committing files that you don’t want in the first place.

code coder coding

I’m still new to WordPress development put plugging along at a good pace. We recently started developing our business toward online marketing and web development, which has pushed me into this domain. In out custom themes we want the ability to add specific meta tags to the page header for SEO.

There are actually paid plugins that do exactly this. In my case I’m building it into a custom theme, but it could easily be a standalone plugin you could drop into any project.

This is what I’ve got so far…

In my themes functions.php I’ve created a new class to wrap all the functions necessary.

abstract class MyTheme_Meta_Box
{
....
}

I’ve then defined a array of meta tags we want to be available on a per post basis.

....
private static $fields = [
        // Search Engine
        "name=description",
        "name=image",
        // Schema.org for Google
        "itemprop=name",
        "itemprop=description",
        "itemprop=image",
        //  Twitter
        "name=twitter:card",
        "name=twitter:title",
        "name=twitter:description",
        "name=twitter:image:src",
        // Open Graph general (Facebook, Pinterest & Google+)
        "name=og:title",
        "name=og:description",
        "name=og:image",
        "name=og:url",
        "name=og:site_name",
        "name=og:locale",
        "name=og:type",
    ];
....

The next step was to define the function to add the metabox to the page editor and define the html.

    public static function add()
    {
        $pagetypes = ['post', 'page'];
        foreach ($pagetypes as $pagetype) {
            add_meta_box(
                'MyTheme_box_id',        
                'Custom Meta Data',      
                [self::class, 'html'],   
                $pagetype                
            );
        }
    }

    public static function html($post)
    {
        foreach (self::$fields as $field) {
            $value = get_post_meta($post->ID, $field, true);
            ?>
            <label for="<?php echo $field ?>"><?php echo $field ?></label>
            <input name="<?php echo $field ?>" id="<?php echo $field ?>" value="<?php echo $value ?>" style="width: 100%">
            <br>
        <?php
        }
    }

At this point we could register the function, but we’ll save that for the end and add them all at once. We also need to add a function to handle saving the new data to the database.

The official documentation has multiple ways of doing this, but recommends using the update function as such:

    public static function save($post_id)
    {
        foreach (self::$fields as $field) {
            if (array_key_exists($field, $_POST)) {
                update_post_meta(
                    $post_id,
                    $field,
                    $_POST[$field]
                );
            }
        }
    }

With that, all the data is saved. Looking at it now, I should probably put in a step to remove empty tags from the database. Mental note for later.

We now need a function to pull and display that data, which can be hooked into wp_head.

public static function head()
    {
        foreach (self::$fields as $field) {
            // get meta key
            $value = get_post_meta(get_queried_object_id(), $field, true);

            //Output
            if (!empty($value)) {
                printf(
                    '<meta %s content="%s" />',
                    esc_attr((trim($field))),
                    esc_attr((trim($value)))
                );
            }
        }
    }

The only thing left to do is register the functions on the appropriate hooks. This we do outside of the class, if there is any confusion.

add_action('add_meta_boxes', ['MyTheme_Meta_Box', 'add']);
add_action('save_post', ['MyTheme_Meta_Box', 'save']);
add_action('wp_head', ['MyTheme_Meta_Box', 'head']);

That’s it, all the meta fields will now be displayed in the header if they aren’t empty.

A minimal WordPress Theme

July 20, 2019 | Coding | No Comments

WordPress Logo

The company I work for is growing, and part of that growth has me learning WordPress. Id honestly rather not touch it, I do not enjoy working in PHP. But there it is anyway.

I was quickly surprised at how simple, and how little you need to make a theme. You only need two files: index.php and style.css. So I decided to make one that was that simple to demonstrate here.

Shortly into it I decided to add a menu and a sidebar, so two more files: functions.php and sidebar.php. How functional is a theme really, if you don’t have a customizable menu or widget area?

All the code is below, comments inline and github repo is available.

style.css

/*
Theme Name: Minimal WordPress
Theme URI: https://wordpress.org/themes/twentyseventeen/
Author: the WordPress team
Author URI: https://wordpress.org/
Description: Minimal WordPress brings your site to life with immersive featured images and subtle animations. With a focus on business sites, it features multiple sections on the front page as well as widgets, navigation and social menus, a logo, and more. Personalize its asymmetrical grid with a custom color scheme and showcase your multimedia content with post formats. Our default theme for 2017 works great in many languages, for any abilities, and on any device.
Version: 1.0
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: minimaltheme
Tags: one-column, two-columns, right-sidebar, flexible-header, accessibility-ready, custom-colors, custom-header, custom-menu, custom-logo, editor-style, featured-images, footer-widgets, post-formats, rtl-language-support, sticky-post, theme-options, threaded-comments, translation-ready
This theme, like WordPress, is licensed under the GPL.
Use it to make something cool, have fun, and share what you've learned with others.
*/

index.php

<?php
/*
Everything is going to be displayed on this one page,
we don't want to put potentially thousands of posts on 
our one page... so we redefine the query to paginate.
But only if we are not on a single item page.
*/

if (!is_singular()) {
    $paged = (get_query_var('paged')) ? get_query_var('paged') : '1';
    $args = array(
        'nopaging'               => false,
        'paged'                  => $paged,
        'posts_per_page'         => '2',
        'post_type'              => 'post',
    );
    $query = new WP_Query($args);
} else {
    $query = $wp_query;
}


?>

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>My Theme</title>
    <!-- wp_head will inject all of wordpress's specific header info
    and anything else we define, like custome css.  We could just write that
    in here ourselves, but we could end up with mulitple headers and this
    helps keep us DRY
    -->
    <?php wp_head() ?>
</head>

<body>
    <nav>
        <div class="nav-wrapper  container">
            <a href="/" class="brand-logo"><?php echo get_bloginfo('name'); ?></a>
            <?php
            wp_nav_menu(array(
                'theme_location' => 'main-menu',
                'container_class' => 'right'
            ));
            ?>
        </div>
    </nav>
    <main class="container">
        <div class="row">
            <div class="col s12 m8">
                <?php if ($query->have_posts()) : while ($query->have_posts()) : $query->the_post(); ?>
                        <article class="card-panel">
                            <h3>
                                <a href="<?php the_permalink(); ?>" rel="bookmark" title="Permanent Link to <?php the_title_attribute(); ?>"><?php the_title(); ?></a>
                            </h3>

                            <small><?php the_time('F jS, Y'); ?> by <?php the_author(); ?></small>

                            <div class="entry">
                                <!-- we only want to show the full post if we aren't on a list -->
                                <?php if (is_singular()) {
                                    the_content();
                                    // If comments are open or we have at least one comment, 
                                    // load up the comment template.
                                    if (comments_open() || get_comments_number()) {
                                        comments_template();
                                    }
                                } else {
                                    the_excerpt();
                                } ?>
                            </div>
                            <p class="postmetadata"><?php _e('Posted in'); ?> <?php the_category(', '); ?></p>

                        </article>

                    <?php endwhile;
                    // show pagination links
                    echo paginate_links(array(
                        'total' => $query->max_num_pages,
                        'mid_size' => 2
                    ));

                else : ?>
                    <p><?php esc_html_e('Sorry, no posts matched your criteria.'); ?></p>
                <?php endif; ?>
            </div>
            <div class="col s12 m4">
                <?php get_sidebar() ?>
            </div>
        </div>
    </main>


    <footer>
        <!-- like wp_head wp_footer injects necessary script stuff
            such as the code that gives us the WordPress Admin bar
         -->
        <?php wp_footer(); ?>
    </footer>
</body>

</html>

functions.php

<?php
/* 
You can register your stylesheets and scripts in the same function
but I prefer to keep them seperate, it just feels cleaner to me
*/
function minimaltheme_enqueue_styles()
{
    wp_enqueue_style('materialize-css', "https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css");
}
add_action('wp_enqueue_scripts', 'minimaltheme_enqueue_styles');

function minimaltheme_enqueue_scripts()
{
    wp_enqueue_script('materialize-js', "https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js");
}
add_action('wp_enqueue_scripts', 'minimaltheme_enqueue_scripts');
wp_enqueue_style('style', get_stylesheet_uri());


/*
  We need one menu for our nav bar, but you can register
  as many as you like and put them anywhere
*/
function register_menus()
{
    register_nav_menu('main-menu', __('Main Menu'));
}
add_action('init', 'register_menus');


// register a sidebar to user throughout the site
add_action('widgets_init', 'minimaltheme_register_sidebars');
function minimaltheme_register_sidebars()
{
    register_sidebar(array(
        'name' => __('Main Sidebar', 'minimaltheme'),
        'id' => 'sidebar',
        'description' => __('Widgets in this area will be shown on all posts and pages.', 'minimaltheme'),
        'before_widget' => '<div class="card"><div class="card-content">',
        'after_widget'  => '</div></div>',
        'before_title'  => '<div class="card-title">',
        'after_title'   => '</div>',
    ));
}

sidebar.php

<?php
if (!is_active_sidebar('sidebar')) {
    return;
}

dynamic_sidebar('sidebar');

Available at https://github.com/wsimmerson/minimaltheme

PipEnv

July 10, 2019 | Coding | No Comments

python programming language

Here I am, apparently late to the party again. Set in my ways, using the same old — trustworthy — tools and along comes another one, seemingly better.

Pipenv will eventually be included in pip itself, but for now we have to pip install it. Hopefully you’re using Python 3, because that’s where you need to get it, although once you have it you can use it to create Python 2 environments.

Pipenv gets rid of the necessity for a requirements.txt file — but can both use and generate them — instead using two files a Pipfile and Pipfile.lock. The pipfile to me, is very reminiscent of a npm package.json while the Pipfile.lock “locks” the specific package versions, dependencies and hashs.

Instead of creating the virtual env specifically pipenv creates one in your home directory based on the current folder name. You can get around this by creating a .venv directory in your current folder, which pipenv will prioritize.

C:\code\myproject>pipenv shell
Creating a virtualenv for this project…
Pipfile: C:\code\myproject\Pipfile                                                                                      Using c:\users\wayne\appdata\local\programs\python\python37-32\python.exe (3.7.0) to create virtualenv…
[ ===] Creating virtual environment...Already using interpreter 
....
Installing setuptools, pip, wheel...
done.

Successfully created virtual environment!                                                                               Virtualenv location: C:\Users\Wayne\.virtualenvs\myproject--MmAU6cW
Creating a Pipfile for this project…                                                                                    Launching subshell in virtual environment…

You will see that a .virtualenvs with the current folder name was created in my home directory. But if I create a .venv directory and run pipenv shell again…

C:\code\myproject>pipenv shell
Creating a virtualenv for this project…
Pipfile: C:\code\myproject\Pipfile                                                                                      Using c:\users\wayne\appdata\local\programs\python\python37-32\python.exe (3.7.0) to create virtualenv…
[ ===] Creating virtual environment...Already using interpreter 
...
Installing setuptools, pip, wheel...
done.

Successfully created virtual environment!                                                                               Virtualenv location: C:\Users\Wayne\.virtualenvs\myproject--MmAU6cW
Creating a Pipfile for this project…                                                                                    Launching subshell in virtual environment…

Now we have a new one in the .venv

We can also specify a specific python version by pipenv –python=version.number

We now have a Pipfile in our project directory that looks like:

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]

[requires]
python_version = "3.7"

Notice the seperation of dev-packages and packages. We can now deploy our projects and not include test runners that found their way into requirements.txt in production. Lets install something.

$ pipenv install flask

Pipenv has been updated to:

[packages]
flask = “*”

We also now have a Pipfile.lock file that looks like:

{
    "_meta": {
        "hash": {
            "sha256": "a82b674d67d29678775ff6b773de1686a9593749ec14483b0d8e05131b662286"
        },
        "pipfile-spec": 6,
        "requires": {
            "python_version": "3.7"
        },
        "sources": [
            {
                "name": "pypi",
                "url": "https://pypi.org/simple",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "click": {
            "hashes": [
                "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
                "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
            ],
            "version": "==7.0"
        },
        "flask": {
            "hashes": [
                "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
                "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
            ],
            "index": "pypi",
            "version": "==1.1.1"
        },
        "itsdangerous": {
            "hashes": [
                "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
                "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
            ],
            "version": "==1.1.0"
        },
        "jinja2": {
            "hashes": [
                "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
                "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
            ],
            "version": "==2.10.1"
        },
        "markupsafe": {
            "hashes": [
                "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
                "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
                "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
                "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
                "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
                "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
                "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
                "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
                "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
                "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
                "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
                "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
                "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
                "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
                "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
                "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
                "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
                "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
                "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
                "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
                "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
                "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
                "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
                "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
                "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
                "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
                "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
                "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
            ],
            "version": "==1.1.1"
        },
        "werkzeug": {
            "hashes": [
                "sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c",
                "sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6"
            ],
            "version": "==0.15.4"
        }
    },
    "develop": {}
}

This file includes the specific version of flask and all of its dependencies. This file will be updated every time we install, upgrade or remove a package.

This would be a incredibly long post if I continued to post demonstrations for everything this can do, so below is a descriptive cheat sheet that should be self explanatory.

Cheat Sheet

Activate the shell
pipenv shell

Initialize environment with a specific python version
pipenv –python=2.7.3

Install from Pipfiles
pipenv install

Install in production
pipenv install –deploy

Install a development dependancy
pipenv install nose –dev

Install all packages specified in Pipfile.lock
pipenv sync

Install from a requirements.txt file
pipenv install -r ./requirements.txt

Check for known security issues in your installed packages
pipenv check

View a graph of all your dependencies
pipenv graph

Update the Lock file before deploying
pipenv lock

Ignore the pip file
pipenv install –ignore-pipfile

Installs all packages specified in Pipfile.lock
pipenv –rm

Show the path to the virtual env
pipenv –venv

Create a requirements.txt file (if your team refuses to change to pipenv)
pipenv run pip freeze > requirements.txt

Deactivate the shell
exit

PyCharm Save Actions

July 9, 2019 | Coding | No Comments

programming code

I think I’m late to the party on this, but wanted to throw out my gleeful support of this plugin. Automatically cleanup and/or format your code when you save a file, or run against several files.

This should be part of PyCharm itself and not just a plugin, its that useful. https://plugins.jetbrains.com/plugin/7642-save-actions

If you use PyCharm on a daily basis like me, just do yourself the favor and install it.

TwitterBot!

July 9, 2019 | Coding | No Comments

twitter

I’ve been a bit bored, haven’t been able to come up with a new personal side project for a while. Last night I realized I hadn’t done any browser automation stuff, and what better way of learning it than building a Twitter bot?

Caveat: This is probably a violation of Twitters terms of service so use for educational purposes only!

So what I’ve done is taken Python and with Selenium automated the process of signing into Twitter, searching a specific hashtag, get a list of tweets and like them. All this interspersed with some short, random weight times to mimic more human like behavior.

The repo can be found at https://github.com/wsimmerson/twitterbot but it is only two files, the bot itself and a config file with variables such as username and password, excluded from git of course.

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import random
import time
import sys

import config


class TwitterBot():
    def __init__(self, username, password):
        self.username = username
        self.password = password

        try:
            self.hashtag = config.HASHTAG
        except:
            print("config.HASHTAG must be defined")
            sys.exit(1)

        try:
            self.pages_to_scroll = config.PAGES
        except:
            self.pages_to_scroll = 10

        self.bot = webdriver.Chrome()

    def activate(self):
        print("running...")
        self.bot.get("https://twitter.com/login")
        time.sleep(3)

        usernameBox = self.bot.find_element_by_class_name("js-username-field")
        usernameBox.clear()
        usernameBox.send_keys(self.username)

        passwordBox = self.bot.find_element_by_class_name(
            "js-password-field")
        passwordBox.clear()
        passwordBox.send_keys(self.password)
        passwordBox.submit()

        time.sleep(15)
        self.bot.get(
            "https://twitter.com/search?f=tweets&vertical=default&q=%23{}&src=tyah".format(self.hashtag))

        for x in range(1, self.pages_to_scroll):
            self.bot.execute_script(
                "window.scrollTo(0, document.body.scrollHeight);")
            # for more humanesque scrolling behavior
            time.sleep(random.randrange(10, 30))

        links = []
        for tweet in self.bot.find_elements_by_class_name("tweet"):
            links.append(tweet.get_attribute("data-permalink-path"))

        for link in links:
            self.bot.get("https://twitter.com/{}".format(link))
            favBtn = self.bot.find_element_by_class_name("js-actionFavorite")
            try:
                favBtn.click()
            except Exception as e:
                # sometimes the clicks fail, not sure why yet.
                print("Error: ", e)
            # How long does it take for a human to read a tweet?
            time.sleep(random.randrange(20, 60))


if __name__ == '__main__':
    bot = TwitterBot(config.USERNAME, config.PASSWORD)
    bot.activate()

The config.py file requires:
USERNAME=
PASSWORD=
HASHTAG= (don’t actually add the #, its in the search string by default)
and an optional
PAGES= (this defaults to 10 if not present; determines how many tweets you will find on the page)

It took me about an hour or so to get this working and I may extend it out further. I.E. retweet, if the number of likes is over a specific threshold or exclude tweets that contain certain words, etc. Otherwise, this was a very short project and my quest to find something better continues.

Convert strings to HEX Colors

July 4, 2019 | Coding | No Comments

programming code

I’ve been working with charts.js and data being generated from the python backed of the API. Since the data is changing all the time, and the nature of the different queries, I never know how many data sets will be generated to plot.

This makes it difficult to define a specific set of colors to go with the unknown quantity of labels. At first I wrote a function to generate a random color code, but then, on every page reload, everything is a different color.

This of course, makes the experience rough and kinda annoying the longer you’re on the page, pulling different queries. A different approach was in order.

The individual data sets have unique names, which get used for labels. So I thought, why not try to use those names to create hex color codes? Well it turns out its really simple to do.

def convertToHexColor(word):
    word = "AB" + word.upper()
    buff = ""
    for c in word:
        buff += "{}".format(ord(c))
    word = "000000" + hex(int(buff)).lstrip("0x")

    return "#" + word[-6:]

That’s it. It’s small, but I created a github repo for it. If you need it, enjoy.
https://github.com/wsimmerson/convertToHexColor

programming code

A while back I had to write an app to parse data from an old billing system to be used with a new banks credit card processing system via batch upload. Most of what I do is either on Linux systems and only need to be run directly by me, or are web applications of one form or another, so a standalone application for our billing department was something really new — well not really new, but something I haven’t done in a very, very long time and not with web technologies — and exciting.

At the time I was completely obsessed with Angular and wanted to use it for everything, so of course, I chose it to write the app and after some research, decided on ElectronJS to do the stand alone part of it.

Today we got notice from the bank that the format was going to be changing due to changes in how credit cards have to be handled, so I opened up the code to take a look and refresh my memory only to find that I had completely forgotten how everything worked with Electron. So I fired up a new Angular app and reverse engineered my own code, to record here for posterity.

Start an Angular project and install Electron.

$ ng new electron-app
$ cd electron-app
$ npm install --save electron
$ npm install -D electron-packager

Electron’s main configuration.

// main.js

const { app, BrowserWindow } = require('electron');

let win;

function createWindow () {
  // Create the browser window.
  win = new BrowserWindow({
    width: 900, 
    height: 900,
    backgroundColor: '#ffffff',
    icon: `file://${__dirname}/dist/electron-app/assets/logo.png`
  });

  win.loadURL(`file://${__dirname}/dist/electron-app/index.html`);

  // uncomment below to open the DevTools.
  // win.webContents.openDevTools()

  // Event when the window is closed.
  win.on('closed', function () {
    win = null;
  });
}

// Create window on electron initialization
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', function () {
  // On macOS specific close process
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', function () {
  // macOS specific close process
  if (win === null) {
    createWindow();
  }
});

Update files to use Electron

// package.json

{
  "name": "electron-app",
  "version": "0.1.0",
  "main": "main.js", // <-- ADD THIS
// package.json
  "scripts": {
    ....
    "electron": "electron .",                       // <-- ADD THIS
    "package": "electron-packager .",               // <-- ADD THIS
<!-- index.hmtl -->
<base href="./">

Build and run the application.

$ ng build
$ npm run electron

Build the final, distributable package.

$ ng build --production
$ npm run package

This will create a directory name like electron-app-win32-x64 (or similar depending on your platform), containing all the necessary files to run and an electron-app.exe

Django: Extending the User Model

June 22, 2019 | Coding | No Comments

programming code

I love Django. It’s a wonderful framework and ORM. Whether you’re building a full website or a restful api, its got you covered. But if you are like me, you HATE the default user model. This generally comes down to it having both a username & e-mail field; and the e-mail field isn’t even unique.

In my world, the e-mail address is the perfect, unique username. So how do you make this happen?

If your app is out, live in the world all ready, you’re out of luck. But if you are firing up a new one, there are a couple of options. Just DON’T RUN THE MIGRATIONS until the new user model is ready.

First thing after starting the new project is starting the users app.

(ENV) $: python manage.py startapp users

Next we are going to create our new manager and user model in users. DO NOT RUN MIGRATIONS! Did you get that? I’ll tell you when.

#users/managers.py

from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import ugettext_lazy as _


class UserManager(BaseUserManager):
    """
    Our custom user model that uses email as the unique username
    """
    def create_user(self, email, password, **extra_fields):
        """
        Create user.
        """
        if not email:
            raise ValueError(_('The Email must be set'))
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password, **extra_fields):
        """
        Create superuser.
        """
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError(_('Superuser must have is_staff=True.'))
        if extra_fields.get('is_superuser') is not True:
            raise ValueError(_('Superuser must have is_superuser=True.'))
        return self.create_user(email, password, **extra_fields)

When we create our user model, we are going to extend AbstractUser. We can extend BaseAbstractUser as well, but that is a lot more work and I really don’t see a need to ever do that at all.

# users/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import ugettext_lazy as _

from .managers import UserManager

class User(AbstractUser):
    username = None
    email = models.EmailField(_('email address'), unique=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = UserManager()

    def __str__(self):
        return self.email

Notice the username = None and USERNAME_FIELD = ’email’. We can also add any other fields we want here now. Instead of adding a separate 1 to 1 profile model, we can put things right on the user. Such as phone number, or company name, customer id #, etc.

Now we can add it to our settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'users',
]

AUTH_USER_MODEL = 'users.User'

You can go ahead and create and run the migrations now and run python manage.py createsuperuser to set yourself up.

Now we need Forms and Admin integration.

# users/forms.py
from django.contrib.auth.forms import UserCreationForm, UserChangeForm

from .models import User

class UserCreationForm(UserCreationForm):
    class Meta(UserCreationForm):
        model = User
        fields = ('email',)

class UserChangeForm(UserChangeForm):
    class Meta:
        model = User
        fields = ('email',)
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .forms import UserCreationForm, UserChangeForm
from .models import User

class UserAdmin(UserAdmin):
    add_form = UserCreationForm
    form = UserChangeForm
    model = User
    list_display = ('email', 'is_staff', 'is_active',)
    list_filter = ('email', 'is_staff', 'is_active',)
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Permissions', {'fields': ('is_staff', 'is_active')}),
    )
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'password1', 'password2', 'is_staff', 'is_active')}
        ),
    )
    search_fields = ('email',)
    ordering = ('email',)


admin.site.register(User, UserAdmin)

That’s it. You have your own customized user model that you can extend further at will.

Referencing the User model:

There are two ways to get the user model now. You can’t simply import AbstractUser anymore. The preferred method is to use get_user_model() but you can also use settings.AUTH_USER_MODEL. For instance, if you want a foreign key relationship to the user.

# Example
User = get_user_model()
## or User = settings.AUTH_USER_MODEL ## but get_user_model() is preferred

class OurTestModel(models.Model):
     customer = models.ForeignKey(User, on_delete=models.CASCADE)

Edit:

To make this a simpler process I’ve created a GitHub repo that you can drop into your Django project and get started right away.
https://github.com/wsimmerson/DjangoExtendedUser