jQuery solution to the add ingredient row problem

Filed in Recipe Manager Leave a comment

Due to the fact that I could not find a reasonable solution within the CakePHP framework to add a new row of ingredient list inputs to recipe form I solved the problem using jQuery.

This is the script.

$('#add_ingredient_row').click(function()
{
	// Clone the last row of the table.
	var clonedRow = $("#ingredient_list tr:last").clone();
 
	// Generate an incremented index for the new row. -1 because there is a table header row
	var newIndex = (document.getElementById('ingredient_list').rows.length -1);
 
	// Set the ID of the row with the new index
	clonedRow[0].id = 'ingredient_' + newIndex;
 
 
	//console.log('ingredient_' + newIndex);
	// Loop through all of the inputs and select in the cloned row
	// Find the name and ID of the input/select element and find the number in it abd replace it with the
	// new index.
	$.each($('input, select', clonedRow), function(i, val)
	{
		//console.log(val);
 
		//console.log('Index -1: ' + (newIndex -1) + '. Index: ' + newIndex);
		val.name = val.name.replace((newIndex - 1), newIndex);
		val.id = val.id.replace((newIndex - 1), newIndex);
		val.value = '';
 
	});
	// put the row into the table
	$("#ingredient_list").append(clonedRow); 
}
);

This is the table that it operates on.

<table id="ingredient_list">
<tr>
	<th>Amount</th>
 
	<th>Measurement Type</th>
	<th>Description</th>
	<th>Ingredient</th>
</tr>
	<tr id="ingredient_0">
		<td>
			<div class="input text"><input name="data[IngredientList][0][amount]" type="text" maxlength="5" value="" id="IngredientList0Amount" /></div><input type="hidden" name="data[IngredientList][0][id]" value="" id="IngredientList0Id" />		</td>
 
		<td>
			<select name="data[IngredientList][0][measurement_type_id]" id="IngredientList0MeasurementTypeId">
				<option value=""></option>
				<option value="0000000002">cup</option>
				<option value="0000000005">fluid ounce</option>
				<option value="0000000001">ounce</option>
				<option value="0000000004">tablespoon</option>
				<option value="0000000003">teaspoon</option>
			</select>		
		</td>
 
		<td>
			<div class="input text"><input name="data[IngredientList][0][description]" type="text" maxlength="255" value="" id="IngredientList0Description" /></div>		</td>
		<td>
			<select name="data[IngredientList][0][ingredient_id]" id="IngredientList0IngredientId">
				<option value=""></option>
				<option value="0000000006">broccoli</option>
				<option value="0000000008">garlic</option>
				<option value="0000000009">olive oil</option>
				<option value="0000000010">peanut oil</option>
 
				<option value="0000000012">red onion</option>
				<option value="0000000007">white onion</option>
				<option value="0000000011">yellow onion</option>
			</select>		
		</td>
	</tr>
</table>

Here is the final solution I came up with. I abstracted out the cloning and manipulation code into a separate function so that it would be DRY.

 
$(document).ready(function()
{
	$('#add_recipe_row').click(function()
	{
		add_row('recipes_table', 'menu_item_');
	}); 
 
	$('#add_ingredient_row').click(function()
	{
		add_row('ingredient_list', 'ingredient_');
	}
	);
 
}); // ends document.ready
 
 
function add_row(tableID, rowIDprefix)
{
 
	// Clone the last row of the table.
	var clonedRow = $('#'+ tableID + ' tr:last').clone();
 
	// Generate an incremented index for the new row. -1 because there is a table header row
	var newIndex = (document.getElementById(tableID).rows.length -1);
 
	// Set the ID of the row with the new index
	clonedRow[0].id = rowIDprefix + newIndex;
 
 
	//console.log('ingredient_' + newIndex);
	// Loop through all of the inputs and select in the cloned row
	// Find the name and ID of the input/select element and find the number in it abd replace it with the
	// new index.
	$.each($('input, select', clonedRow), function(i, val)
	{
		//console.log(val);
 
		//console.log('Index -1: ' + (newIndex -1) + '. Index: ' + newIndex);
		val.name = val.name.replace((newIndex - 1), newIndex);
		val.id = val.id.replace((newIndex - 1), newIndex);
		val.value = '';
 
	});
	// put the row into the table
	$('#' + tableID).append(clonedRow);
}

Basic user authentication

Filed in Recipe Manager Leave a comment

Today I got the basic user authentication going for the Recipe Manager using this tutorial.

Users Controller

class UsersController extends AppController
{
	var $name = "Users";
	//var $helpers = array('Html', 'Form');
 
	function index()
	{
 
	}
 
	function beforeFilter()
	{
		$this-&gt;__validateLoginStatus();
	}
 
	function login()
	{
		if(empty($this-&gt;data) == false)
		{
			if(($user = $this-&gt;User-&gt;validateLogin($this-&gt;data['User'])) == true)
			{
				$this-&gt;Session-&gt;write('User', $user);
				$this-&gt;Session-&gt;setFlash('You\'ve successfully logged in.');
				$this-&gt;redirect('/recipes/');
				exit();
			}
			else
			{
				$this-&gt;Session-&gt;setFlash('Sorry, the information you\'ve entered is incorrect.');
				exit();
			}
		}
	}
 
	function register()
	{
		if (!empty($this-&gt;data))
		{
			//Sanitize::clean($this-&gt;data);
			$this-&gt;data['User']['password'] = md5($this-&gt;data['User']['password']);
			$this-&gt;User-&gt;create(); // create the model
 
			if ($this-&gt;User-&gt;save($this-&gt;data))
			{
				$this-&gt;Session-&gt;write('User', $this-&gt;User-&gt;findByUsername($this-&gt;data['User']['username']));
				$this-&gt;Session-&gt;setFlash('Thank you for registering.');
				$this-&gt;redirect('login/');
			}
			else
			{
				$this-&gt;Session-&gt;setFlash('The User could not be saved. Please, try again.');
			}
		}
	}
 
	function logout()
	{
		$this-&gt;Session-&gt;destroy('user');
		$this-&gt;Session-&gt;setFlash('You\'ve successfully logged out.', 2);
		$this-&gt;redirect('login');
	}
 
	function __validateLoginStatus()
	{
		if($this-&gt;action != 'login' &amp;&amp; $this-&gt;action != 'logout')
		{
			if($this-&gt;Session-&gt;check('User') == false)
			{
				$this-&gt;redirect('login');
				$this-&gt;Session-&gt;setFlash('The URL you\'ve followed requires you login.');
			}
		}
	}
 
}

Login View

<div class="login">
<h2>Login</h2>
create('User', array('action' =&gt; 'login'));?&gt;
        input('username');?&gt;
        input('password');?&gt;
        submit('Login');?&gt;
    end(); ?&gt;</div>

Register View

<div class="login">
<h2>Login</h2>
create('User', array('action' =&gt; 'register'));?&gt;
        input('email');?&gt;
        input('username');?&gt;
        input('password');?&gt;
        submit('Register');?&gt;
    end(); ?&gt;</div>

Adding this to the app_controller causes each view to authenticate the user.

class AppController extends Controller {
 
	var $helpers = array('Html', 'Javascript', 'Ajax');
 
	function beforeFilter()
	{
            if($this-&gt;Session-&gt;check('User') == false)
            {
                $this-&gt;redirect('/users/login');
                $this-&gt;Session-&gt;setFlash('The URL you\'ve followed requires you login.');
            }
    }
}

I still have to work on showing user messages and sanitizing the data but this is a good start.

CakePHP so far

Filed in Recipe Manager Leave a comment

I have finally started the recipe manager project. I decided upon the CakePHP framework thanks to a recommendation from Anthony. I have found a few places that have some documentation to help get started.

I have most of the framework for the application fleshed out. I can add, edit and delete ingredient_types, ingredients, measurement_types, measurements, recipe_types and recipes.

I am stuck at the moment on adding, deleting items from the ingredients_list for a recipe. Every recipe has one or more ingredients that are stored in the ingredients_list table. This table contains the amount, the measurement_type, ingredient and a description. So you can store:

1 cup chopped broccoli or 2 ounces of shaved garlic

When adding or editing a recipe I want to simply be able to add a new line item for the ingredient. I can picture how to do this without CakePHP but I cant seem to find the trick to do it with. Essentially each item in the recipe edit view should have a button that allows you to delete that particular item. In the add view their should be a button that simply adds a new empty ingredient.

There are some things lacking in the CakePHP documentation, although its better than most. I had one other problem that I had to sort of hack around until I get a better solution. The task was to edit a recipe. This involves getting all the ingredients from the ingredient_list for the recipe to be edited. This is done with the following line:

$list = $this->IngredientList->find('all', array ('recipe_id' => $id));

You would assume that as the recipe id is passed to the IngredientList controller you would only get records for that particular recipe. But not so. This means that I made a mistake somewhere in my table definitions but I have gone over it many times trying to find it but to no avail. My work around is this:

foreach($list as $item)
{
    //debug($item['IngredientList']['recipe_id'], true);
    if($item['IngredientList']['recipe_id'] == $id)
    {
        $ing_list[] = $item;
    }
}

Essentially I just loop through the array looking for the current recipe ID. If I find it I add it to an array that I pass to the view. Here is the whole action for edit:

function edit($id = null)
{
    $this->Recipe->id = $id;
    $this->set_recipe_types();
    $this->set_ingredients();
    $this->set_measurement_types();
 
    $list = $this->IngredientList->find('all', array ('recipe_id' => $id));
 
    //debug($list, true);
 
    foreach($list as $item)
    {
        //debug($item['IngredientList']['recipe_id'], true);
        if($item['IngredientList']['recipe_id'] == $id)
        {
        $ing_list[] = $item;
        }
    }
 
    $this->set('ingredient_list', $ing_list);
 
    if (empty($this->data))
    {
        $this->data = $this->Recipe->read();
    }
    else
    {
        if ($this->Recipe->saveAll($this->data))
        {
            $this->flash('Your recipe has been updated.','/recipes');
        }
    }
}

TOP