Joseph Crawford Using wordpress because he is lazy

12Nov/1061

CakePHP ACL Management

ACL Management Plugin ScreenshotRecently I was working on a post for setting up the CakePHP Acl Component and was just walking through Mark Story's tutorial in the documentation to see what people were starting from. For basic ACLs it is pretty much the only way to go. Although many situations will require some tweeking. I realized the main thing lacking from all of the documentation I've read is a way to manage the stupid things. I've seen one or two in the past but if you could get them to work it was a miracle and even then they were not very friendly to use. So I began working on a plugin to manage ACOs and ACLs for the CakePHP AclComponent based on a set up that my friend Kenny Austin built at work. This plugin assumes that you were following the tutorial in the CakePHP documentation and even includes Mark Story's build_acl() function built into a component. I didn't include a way to manage AROs because those are created automatically for your users and groups. If you want help with managing those see my recent Auth Component tutorial. The code from it should work with a few minor changes to the models. Also keep in mind that this plugin (and the tutorial in the book) assumes that your users only belong to one group. If this plugin fits your need feel free to try it out.

Download

To use just extract the contents to your plugins directory and access it by going to http://yoursite/acls. You may need to initially rebuild the ACOs but there is a button for that. Then grant yourself access to the plugin's ACOs and remove the $this->Auth->allow('*'); line from the beforeFilter of the 3 controllers. Please drop a comment with any feedback.

9Nov/100

Un-Reset Stylesheet

The reset stylesheet has been aggravating me for years now. It was originally developed by Eric Meyer in April, 2007. As much as I respect and admire the man and I know he had his reasoning for developing it. I hate that he did it. Only because of who he is did so many people start using it thinking it was the right thing to do. It later received criticism from Jonathan Snook and even more from Jens O. Meiert who said it best "A novice should not use them, an expert would not use them". His article Why “Reset” Style Sheets Are Bad has to be the best argument I have found so far regarding the matter. I was originally going to write about this myself but after reading his take I don't think I can do much better so if you are currently using a reset.css I strongly urge you to go read his post.

I recently started a new job and work with a couple of guys who think the reset stylesheet is the only way to start coding a design. The problem is they do nothing to replace the style of basic elements of html. So in developing something I wanted to make an ordered list only to see it render like regular text with br's after every item. Their theory is if I want the list to appear a certain way then I should write more css and style it accordingly. Which leaves me thinking...not only should I not have to but it almost defeats the purpose of using css at all.

So I present the un-reset.css. A stylesheet to undo the what was done and place the world back in its natural order. Before you ask why not just remove the reset.css? Well I can not in my situation. The current design was built on it and so I needed a stylesheet that would set all of the styles back to default without effecting styles that rely on reset.css. All you have to do is link un-reset.css and add the unreset class to any element you wish. The contents should then be back to normal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
.unreset address,
.unreset blockquote,
.unreset dd, .unreset div,
.unreset dl, .unreset dt, .unreset fieldset, .unreset form,
.unreset frame, .unreset frameset,
.unreset h1, .unreset h2, .unreset h3, .unreset h4,
.unreset h5, .unreset h6, .unreset noframes,
.unreset ol, .unreset p, .unreset ul, .unreset center,
.unreset dir, .unreset hr, .unreset menu, .unreset pre { display: block }
.unreset li { display: list-item }
.unreset head { display: none }
.unreset table { display: table }
.unreset tr { display: table-row }
.unreset thead { display: table-header-group }
.unreset tbody { display: table-row-group }
.unreset tfoot { display: table-footer-group }
.unreset col { display: table-column }
.unreset colgroup { display: table-column-group }
.unreset td, .unreset th { display: table-cell }
.unreset caption { display: table-caption }
.unreset th { font-weight: bolder; text-align: center }
.unreset caption { text-align: center }
.unreset body { margin: 8px }
.unreset h1 { font-size: 2em; margin: .67em 0 }
.unreset h2 { font-size: 1.5em; margin: .75em 0 }
.unreset h3 { font-size: 1.17em; margin: .83em 0 }
.unreset h4, .unreset p,
.unreset blockquote, .unreset ul,
.unreset fieldset, .unreset form,
.unreset ol, .unreset dl, .unreset dir,
.unreset menu { margin: 1.12em 0 }
.unreset h5 { font-size: .83em; margin: 1.5em 0 }
.unreset h6 { font-size: .75em; margin: 1.67em 0 }
.unreset h1, .unreset h2, .unreset h3, .unreset h4,
.unreset h5, .unreset h6, .unreset b,
.unreset strong { font-weight: bolder }
.unreset blockquote { margin-left: 40px; margin-right: 40px }
.unreset i, .unreset cite, .unreset em,
.unreset var, .unreset address { font-style: italic }
.unreset pre, .unreset tt, .unreset code,
.unreset kbd, .unreset samp { font-family: monospace }
.unreset pre { white-space: pre }
.unreset button, .unreset textarea,
.unreset input, .unreset select { display: inline-block }
.unreset big { font-size: 1.17em }
.unreset small, .unreset sub, .unreset sup { font-size: .83em }
.unreset sub { vertical-align: sub }
.unreset sup { vertical-align: super }
.unreset table { border-spacing: 2px; }
.unreset thead, .unreset tbody,
.unreset tfoot { vertical-align: middle }
.unreset td, .unreset th, .unreset tr { vertical-align: inherit }
.unreset s, .unreset strike, .unreset del { text-decoration: line-through }
.unreset hr { border: 1px inset }
.unreset ol, .unreset ul, .unreset dir,
.unreset menu, .unreset dd { margin-left: 40px }
.unreset ol { list-style-type: decimal }
.unreset ol ul, .unreset ul ol,
.unreset ul ul, .unreset ol ol { margin-top: 0; margin-bottom: 0 }
.unreset ul { list-style-type: disc }
.unreset u, .unreset ins { text-decoration: underline }
.unreset br:before { content: "\A"; white-space: pre-line }
.unreset center { text-align: center }
.unreset :link, .unreset :visited { text-decoration: underline }
.unreset :focus { outline: thin dotted invert }

Most of this came from the default stylesheet posted by the w3c for CSS2.1 so there are probably some unnecessary elements. The only real change I had to make was adding ul { list-style-type: disc }. I'm not sure if anybody will even find a real use for this but if you do and have any comments or suggestions please let me know.

8Nov/1019

CakePHP 1.3.* Auth Component Tutorial Basic Authentication

Last year I wrote a tutorial on setting up the Auth Component for basic authentication which is now slightly outdated since the release of CakePHP 1.3. I plan to post several versions of this progressing up to a more complex setup which includes the ACL Component. It seems every website's authentication needs are slightly different but, hopefully, I can cover the most common and give advice on customizing each one.

For this tutorial you will need to have CakePHP 1.3 already set up and connecting to a database. (Note: Some of the php functions I have used here are for PHP5 so if you are still running PHP4 you may encounter some errors.) As before all of the code used here is provided in a zip file at the bottom of the post. I don't want this to be just a lot of code so I will try to explain everything as much as possible along the way. Please feel free to comment with any recommended changes.

Database

The first thing you will want to do is create the users table in your site database or update what you already have. If you would like to name it something other than users go ahead and do so now. (I will address the changes you might need to make later.)

If you already have a users table with existing users you could run into problems, and need to take a few extra steps to update the accounts. Cake uses its own security salt that is set in /app/config/core.php to encrypt each new password. Since the salt for each Cake site should be unique; your existing passwords will not match. If the passwords are not encrypted at all then you could write a script to loop through each account and update the old passwords. I will not get into that here but, if anybody needs help with that, just let me know and I can explain further. If your old passwords are encrypted using md5 you might be able to use Security::setHash('md5').

/app/config/schema/users.sql

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `username` varchar(20) NOT NULL,
  `password` varchar(40) NOT NULL,
  `clear_password` varchar(20) default NULL,
  `first_name` varchar(20) default NULL,
  `last_name` varchar(20) default NULL,
  `email` tinytext NOT NULL,
  `status` enum('Active','Inactive') NOT NULL default 'Active',
  `last_login` datetime default NULL,
  `last_access` datetime default NULL,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  PRIMARY KEY  (`id`),
  KEY `status` (`status`),
  KEY `login` (`username`,`password`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 ;

Your table doesn't have to be exactly like this so feel free to adjust it to your own needs. However, further down, I will include CRUD pages for the users and if you use them (and you change this) you will need to update those also.

User Model

Now that you have your table set up; create your user model. (Again, if you customized your user table at all, you will need to update this accordingly.) I also included a few validation rules. (If your users table is called something different like “people” the easy fix for this would be to set var $uses = 'people' in your user model.)

/app/models/user.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<?php
class User extends AppModel {
    var $name = 'User';
    var $virtualFields = array(
        'full_name' => "CONCAT(User.first_name, ' ', User.last_name)",
    );
    var $validate = array(
        'username' => array(
            'empty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'message' => 'Username is required',
            ),
            'minlength' => array(
                'rule' => array('minLength', 4),
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Usernames must be at least 4 characters long',
            ),
            'maxlength' => array(
                'rule' => array('maxLength', 20),
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Usernames may not be more than 20 characters long',
            ),
            'alphanum' => array(
                'rule' => 'alphaNumeric',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Usernames may only contain letters and numbers',
            ),
            'unique' => array(
                'rule' => 'isUnique',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'That username is already in use',
            ),
        ),
        'clear_password' => array(
            'empty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'on' => 'create',
                'message' => 'Password is required',
            ),
            'length' => array(
                'rule' => array('minLength', 6),
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Passwords must be at least 6 characters long',
            ),
        ),
        'confirm_password' => array(
            'empty_create' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'on' => 'create',
                'message' => 'Please confirm the password 1',
            ),
            'empty_update' => array(
                'rule' => 'validateConfirmPasswordEmptyUpdate',
                'required' => true,
                'allowEmpty' => true,
                'on' => 'update',
                'message' => 'Please confirm the password 2',
            ),
            'match' => array(
                'rule' => 'validateConfirmPasswordMatch',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'The passwords do not match',
            ),
        ),
        'email' => array(
            'empty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'allowEmpty' => false,
                'message' => 'Email is required',
            ),
            'valid' => array(
                'rule' => 'email',
                'required' => true,
                'allowEmpty' => true,
                'message' => 'Please enter a valid email address',
            ),
        ),
    );
   
    /**
     * Callback function for confirm_password
     * Used to check the confirm_password field is not empty on update
     * @return bool
     */

    function validateConfirmPasswordEmptyUpdate() {
        return !empty($this->data['User']['clear_password']) && !empty($this->data['User']['confirm_password']);
    }
   
    /**
     * Callback function for confirm_password
     * Used to check if clear_password and confirm_password match
     * @return bool
     */

    function validateConfirmPasswordMatch() {
        return $this->data['User']['clear_password'] == $this->data['User']['confirm_password'];
    }
}
?>

Virtual Fields

This is a thing I found recently that can be very handy. I like to have the first name and last name in separate columns in my users table and this allows me to combine them into a full name without having to do any additional coding later. The Html Helper's paginator sort functions also work with it, which is nice - definitely something to keep in mind for other applications.

Password

There are two separate validations for the password because I like to always have a confirm password for user management. The clear_password set is pretty straight forward but some additional things are needed for the confirm_password. This will also address an issue with passwords when editing a user. It is nice to edit a user and not have to re-enter a password. So I added some things to allow the password fields to be blank on edit. However if one of them is not empty it will not validate.

Validation Callbacks

These are two methods used for the confirm_password validation.

Users Controller

Now we need a controller to handle user management and the login methods.

/app/controllers/users_controller.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?php
class UsersController extends AppController {
    var $name = 'Users';
    var $paginate = array(
        'User' => array(
            'limit' => 20,
            'order' => array(
                'User.full_name' => 'asc',
            ),
        ),
    );
   
    /**
     * Set this to false if you don't want to store clear passwords in the database
     * @var bool
     * @access private
     */

    var $_store_clear_password = true;
   
    function index() {
        $users = $this->paginate('User');
        $this->set(compact('users'));
    }
   
    function add() {
        if (!empty($this->data)) {
            $this->User->set($this->data);
            if ($this->User->validates()) {
                $this->data['User']['password'] = $this->data['User']['clear_password'];
                $this->data = $this->Auth->hashPasswords($this->data);
                if (!$this->_store_clear_password) {
                    unset($this->data['User']['clear_password']);
                }
                $this->User->save($this->data, false);
                $this->Session->setFlash('User Added');
                $this->redirect('index');
            }
        }
    }
   
    function edit($id = null) {
        if (!empty($this->data)) {
            $fields = array_keys($this->data['User']);
            if (!empty($this->data['User']['clear_password']) || !empty($this->data['User']['confirm_password'])) {
                $fields[] = 'password';
            } else {
                $fields = array_diff($fields, array('clear_password', 'confirm_password'));
            }
            $this->User->set($this->data);
            if ($this->User->validates()) {
                if (!empty($this->data['User']['clear_password'])) {
                    $this->data['User']['password'] = $this->data['User']['clear_password'];
                }
                $this->data = $this->Auth->hashPasswords($this->data);
                if (!$this->_store_clear_password) {
                    unset($this->data['User']['clear_password']);
                }
                $this->User->save($this->data, false, $fields);
                $this->Session->setFlash('User Updated');
                $this->redirect('index');
            }
        } else {
            $user = $this->User->findById($id);
            if (empty($user)) {
                $this->Session->setFlash('Invalid User ID');
                $this->redirect('add');
            } else {
                unset($user['User']['clear_password']);
                $this->data = $user;
            }
        }
    }
   
    function delete() {
        $delete_count = 0;
        if (!empty($this->data['User']['delete'])) {
            foreach($this->data['User']['delete'] as $id => $delete) {
                if ($delete == 1) {
                    if ($this->User->delete($id)) {
                        $delete_count++;
                    }
                }
            }
        }
        $this->Session->setFlash($delete_count . ' User' . (($delete_count == 1) ? ' was' : 's were') . ' deleted');
        $this->redirect('index');
    }
   
    function login() {
        if (!empty($this->data) && $this->Auth->user()) {
            $this->User->id = $this->Auth->user('id');
            $this->User->saveField('last_login', date('Y-m-d H:i:s'));
            $this->redirect($this->Auth->redirect());
        }
    }
   
    function logout() {
        $this->Session->setFlash('You are now logged out');
        $this->redirect($this->Auth->logout());
    }
}
?>

I know the acronym is CRUD but I just interpret it as list, add, edit and delete. They are more common terms for data management to end users and a “view” page is usually kind of pointless. However, if you need one, feel free to add it.

Paginate

I added pagination to this version only to demonstrate some more of Cake's awesomeness. This is nothing new, but nice if you needed it, because now you don't have to retro-fit it.

Store Clear Password

This is an option to store clear passwords in the database which is kind of frowned upon but when you need it-you need it. I have had a lot of people demand for the ability to retrieve passwords and not have to reset them when forgotten. I did not include a reset password section. I might add one in a later post but, for now, its all about authentication and user management. If you don't want to store clear passwords then just set the $_store_clear_password variable to false and forget about it. You may feel the need to change the name of the password field in the views after that but, please don't, it has a different name for a reason.

Index

Really nothing special here. Just grabbing the list of users and tossing them to the view.

Add

The Add and Edit methods are slightly different than usual. Mostly because of the password fields. I changed the name of password to clear_password in the view mainly so, that in case of a validation error, the encrypted password is not sent back to the view. This way the password is not encrypted until after the user data is validated.

Edit

This is very similar to Add with one major difference. I wanted the user's password to stay the same if it was not changed in the form. So initially I blank it out, and then, if neither field has been populated, they are removed from the fields list before validation.

Delete

I see a lot of people doing delete with a GET. For example a link to /users/delete/$id. This might be fine for some applications but seems a little dangerous to me. I wanted to make it a POST and a good way to do that is by adding checkboxes to provide a multiple delete. (You could also add methods for updating the user status similarly. I have done that in the past by creating a drop down list below the users list with some on change javascript to modify the form's action.)

Login

Most of what goes on here is handled by the Auth Component automatically, but I wanted to record the user's last_login date, so I added that here. This code will not execute unless autoRedirect in the Auth Component is set to false. It is true by default so we will change it later in the App Controller.

Logout

The Auth Component will handle destroying our login session, so the only thing to do here is notifying the user (if you want to) and then redirecting to the appropriate place.

User Views

There is not a lot to explain here. You may need to make modifications to fit your needs but it is a good start.

/app/views/users/add.ctp

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
echo '<h2>Add User';
echo $form->create('User');
echo $form->input('username');
echo $form->input('clear_password', array('type' => 'password', 'label' => 'Password'));
echo $form->input('confirm_password', array('type' => 'password'));
echo $form->input('first_name');
echo $form->input('last_name');
echo $form->input('email');
echo $form->input('status', array('options' => array('Active' => 'Active', 'Inactive' => 'Inactive')));
echo $form->submit('Submit', array('after' => ' ' . $html->link('Cancel', array('action' => 'index'))));
echo $form->end();
?>

/app/views/users/edit.ctp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
echo '<h2>Edit User';
echo $form->create('User');
echo $form->hidden('id');
echo $form->input('username');
echo $form->input('clear_password', array('type' => 'password', 'label' => 'Password'));
echo $form->input('confirm_password', array('type' => 'password'));
echo $form->input('first_name');
echo $form->input('last_name');
echo $form->input('email');
echo $form->input('status', array('options' => array('Active' => 'Active', 'Inactive' => 'Inactive')));
echo $form->submit('Submit', array('after' => ' ' . $html->link('Cancel', array('action' => 'index'))));
echo $form->end();
?>

/app/views/users/index.ctp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
echo '<h2>Users';
echo '<p>' . $html->link('Add User', array('action' => 'add')) . '</p>';
if (!empty($users)) {
    echo $form->create('User', array('action' => 'delete'));
    echo '<table width="100%">';
    echo '  <thead>';
    $cells = array(
        $form->checkbox(null, array('id' => 'select-all')),
        null,
        $this->Paginator->sort('Name', 'full_name'),
        $this->Paginator->sort('Email', 'email'),
        $this->Paginator->sort('Status', 'status'),
    );
    echo $this->Html->tableHeaders($cells);
    echo '  </thead>';
    echo '  <tbody>';
    foreach($users as $i) {
        $cells = array(
            $form->checkbox('User.delete.' . $i['User']['id']),
            $html->link('Edit', array('action' => 'edit', $i['User']['id'])),
            $i['User']['full_name'],
            $i['User']['email'],
            $i['User']['status'],
        );
        echo $this->Html->tableCells($cells, array('class' => 'odd'), array('class' => 'even'));
    }
    echo '  </tbody>';
    $numbers = $this->Paginator->numbers();
    if (!empty($numbers)) {
        $counter = $this->Paginator->prev('« Previous', null, null, array('class' => 'disabled'));
        $counter .= ' | '.$numbers.' | ';
        $counter .= $this->Paginator->next('Next »', null, null, array('class' => 'disabled'));
        echo '<tfoot>';
        echo $this->Html->tableCells(array(array(array($counter, array('colspan' => count($cells))))), null, null, true);
        echo '</tfoot>';
    }
    echo '</table>';
    echo $form->end('Delete Selected');
}
?>

/app/views/users/login.ctp

1
2
3
4
5
6
7
<?php
echo '<h2>Login';
echo $form->create('User', array('action' => 'login'));
echo $form->input('username');
echo $form->input('password');
echo $form->end('Login');
?>

App Controller

Now that everything is set up lets enable the Auth Component in the App Controller. If you do not already have one just create it otherwise make the appropriate changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class AppController extends Controller {
    var $components = array(
        'Auth' => array(
            'autoRedirect' => false,
        ),
        'Session',
    );
    var $helpers = array(
        'Html',
        'Form',
        'Session',
    );
   
    function afterFilter() {
        # Update User last_access datetime
       if ($this->Auth->user()) {
            $this->loadModel('User');
            $this->User->id = $this->Auth->user('id');
            $this->User->saveField('last_access', date('Y-m-d H:i:s'));
        }
    }
}
?>

I moved the Auth Component setup to inside the $components array in order to eliminate needing to run parent::beforeFilter() in other controller beforeFilters just to keep validation running correctly. There are a lot of options for the Auth Component that I am not using here. Most of the time the defaults work just fine. But if you are doing anything non-standard the options are fairly well explained in the book.

You might notice the afterFilter but, this isn't too important, it only records the users last_access time. If you want to make sure that is saved all the time, and you have a afterFilter in your other controllers, you will need to have a parent::afterFilter() inside it. But it will not break anything if you don't.

Almost Done...

We need to create some users and right now you probably can't because if you pull up your site it will take you to the login page. And you probably don't have an account to log in with yet. So for now open your Users Controller and add this.

function beforeFilter() {
    $this->Auth->allow('*');
}

Now open /users/add in your browser, and create an account. After that remove the above chunk of code from you users controller otherwise your user management will remain open to the world. It will force you to the login again but that's okay because you have an account now :)

Now You're Done!

Or just beginning, depends on how you look at it. Either way you should have authentication working on your site now. If you have any questions, comments or suggestions you can comment below.

Download

16Oct/100

Stargate Universe Featured Music

I have been trying to come up with a list of the featured songs that are played near the end of almost every Stargate Universe episode. So far this is what I have....I will try to keep this updated

Season One
Episode Song Title Artist
3 "Air" Breathe Alexi Murdoch
7 "Earth" Many Moons, Sincerely, Jane Janelle Monae
9 "Life" The Worst Day Since Yesterday Flogging Molly
Comfort Deb Talan
11 "Space" Now Comes The Night Rob Thomas
12 "Divided" You Won't Know Brand New
13 "Faith" All My Days Alexi Murdoch
14 "Human" English Rose Paul Weller
16 "Sabotage" Only If You Run Julian Plenti
17 "Pain" Agony Eels
What You Want Me To Do The Heavy
Season Two
Episode Song Title Artist
1 "Intervention" After The Storm Mumford and Sons
3 "Awakening" Sort of Revolution Fink
Filed under: Recreation No Comments
13Oct/100

Lake Hefner Golf Club

For the past few years I have had went to Minnesota each summer and attempted to get my fill of playing golf. Because of everything that has been going on I was not able to this year. I didn't even manage to go to the country club in Durant despite efforts of trying to organize my dad and brother for one last game together before I moved away. But this Saturday I slept late and woke up to a beautiful day and thought 'this is the day'. I unpacked my clubs and headed out. I expected to play alone but when I got there I was pushed into a group with others which ended up being okay. I played horrible until the 18th hole. I got a legit birdie on a par 5 which was enough to put a smile on my face for the rest of the weekend :)

Filed under: Recreation No Comments