Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Initial commit

  • Loading branch information...
commit 923c09ca8768d40fb0ff9881f3e081efefe9528f 0 parents
Adrian Macneil authored

Showing 21 changed files with 1,047 additions and 0 deletions. Show diff stats Hide diff stats

  1. +3 0  .gitignore
  2. +20 0 LICENSE.txt
  3. +42 0 README.md
  4. +70 0 plugins/cocktailrecipes/CocktailRecipesPlugin.php
  5. +49 0 plugins/cocktailrecipes/blocktypes/CocktailRecipes_IngredientsBlockType.php
  6. +23 0 plugins/cocktailrecipes/composer.json
  7. +56 0 plugins/cocktailrecipes/controllers/CocktailRecipes_IngredientsController.php
  8. +36 0 plugins/cocktailrecipes/models/CocktailRecipes_IngredientModel.php
  9. +19 0 plugins/cocktailrecipes/phpunit.xml.dist
  10. +50 0 plugins/cocktailrecipes/records/CocktailRecipes_IngredientRecord.php
  11. +106 0 plugins/cocktailrecipes/services/CocktailRecipesService.php
  12. +7 0 plugins/cocktailrecipes/templates/_blocktypes/ingredients.html
  13. +51 0 plugins/cocktailrecipes/templates/index.html
  14. +44 0 plugins/cocktailrecipes/templates/ingredients/_edit.html
  15. +164 0 plugins/cocktailrecipes/tests/CocktailRecipesServiceTest.php
  16. +42 0 plugins/cocktailrecipes/tests/CocktailRecipesTwigExtensionTest.php
  17. +38 0 plugins/cocktailrecipes/tests/CocktailRecipes_IngredientsBlockTypeTest.php
  18. +127 0 plugins/cocktailrecipes/tests/CocktailRecipes_IngredientsControllerTest.php
  19. +36 0 plugins/cocktailrecipes/tests/bootstrap.php
  20. +34 0 plugins/cocktailrecipes/twigextensions/CocktailRecipesTwigExtension.php
  21. +30 0 plugins/cocktailrecipes/variables/CocktailRecipesVariable.php
3  .gitignore
... ... @@ -0,0 +1,3 @@
  1 +.DS_Store
  2 +composer.lock
  3 +vendor/
20 LICENSE.txt
... ... @@ -0,0 +1,20 @@
  1 +Copyright (c) 2012 Adrian Macneil
  2 +
  3 +Permission is hereby granted, free of charge, to any person obtaining
  4 +a copy of this software and associated documentation files (the
  5 +"Software"), to deal in the Software without restriction, including
  6 +without limitation the rights to use, copy, modify, merge, publish,
  7 +distribute, sublicense, and/or sell copies of the Software, and to
  8 +permit persons to whom the Software is furnished to do so, subject to
  9 +the following conditions:
  10 +
  11 +The above copyright notice and this permission notice shall be
  12 +included in all copies or substantial portions of the Software.
  13 +
  14 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15 +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16 +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17 +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  18 +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  19 +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  20 +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
42 README.md
Source Rendered
... ... @@ -0,0 +1,42 @@
  1 +# Cocktail Recipes Plugin for Blocks CMS
  2 +
  3 +This is an example plugin for the [Blocks CMS Beta](http://blockscms.com/), inspired by
  4 +the documentation. It is well documented, and designed to get you up and running quickly,
  5 +by giving you a working example to start developing from.
  6 +
  7 +The plugin adds an `ingredients` table to the database, with custom control panel pages
  8 +to add new ingredients. It also adds an Ingredients blocktype, which can be added to your
  9 +sections or pages.
  10 +
  11 +Obviously there are many things in this plugin which could be done differently for a plugin
  12 +this simple. However, we prefer to do things the most complete way possible, to give you a
  13 +non-trivial working example.
  14 +
  15 +## Compontents
  16 +
  17 +Cocktail Recipes provides examples of the following Blocks components:
  18 +
  19 +* Ingredients Blocktype (allows user to select from available ingredients)
  20 +* Controller (handles template actions)
  21 +* Ingredients Model (read only data object)
  22 +* Ingredients Record (database definition and write access)
  23 +* Service (provdes API to create/save/delete ingredients)
  24 +* Templates (provides custom control panel section)
  25 +* Twig Extension (provides `shake` twig filter)
  26 +* Variables (provides read only API to access ingredients from within templates)
  27 +
  28 +Blocks provides many extension points, and more examples will be added in future
  29 +(pull requests accepted).
  30 +
  31 +## Unit Tests
  32 +
  33 +In addition, a PHPUnit test suite is provided to ensure all components are behaving correctly.
  34 +To run the tests, run the following commands:
  35 +
  36 + cd plugins/cocktailrecipes
  37 + composer update --dev
  38 + vendor/bin/phpunit
  39 +
  40 +## License
  41 +
  42 +This work is licenced under the MIT license.
70 plugins/cocktailrecipes/CocktailRecipesPlugin.php
... ... @@ -0,0 +1,70 @@
  1 +<?php
  2 +
  3 +namespace Blocks;
  4 +
  5 +class CocktailRecipesPlugin extends BasePlugin
  6 +{
  7 + public function getName()
  8 + {
  9 + return Blocks::t('Cocktail Recipes');
  10 + }
  11 +
  12 + public function getVersion()
  13 + {
  14 + return '1.0';
  15 + }
  16 +
  17 + public function getDeveloper()
  18 + {
  19 + return 'Adrian Macneil';
  20 + }
  21 +
  22 + public function getDeveloperUrl()
  23 + {
  24 + return 'http://adrianmacneil.com';
  25 + }
  26 +
  27 + public function hasCpSection()
  28 + {
  29 + return true;
  30 + }
  31 +
  32 + /**
  33 + * Register control panel routes
  34 + */
  35 + public function hookRegisterCpRoutes()
  36 + {
  37 + return array(
  38 + 'cocktailrecipes\/ingredients\/new' => 'cocktailrecipes/ingredients/_edit',
  39 + 'cocktailrecipes\/ingredients\/(?P<recipeId>\d+)' => 'cocktailrecipes/ingredients/_edit',
  40 + );
  41 + }
  42 +
  43 + /**
  44 + * Register twig extension
  45 + */
  46 + public function hookAddTwigExtension()
  47 + {
  48 + Blocks::import('plugins.cocktailrecipes.twigextensions.CocktailRecipesTwigExtension');
  49 +
  50 + return new CocktailRecipesTwigExtension();
  51 + }
  52 +
  53 + /**
  54 + * Add default ingredients after plugin is installed
  55 + */
  56 + public function onAfterInstall()
  57 + {
  58 + $ingredients = array(
  59 + array('name' => 'Gin'),
  60 + array('name' => 'Tonic'),
  61 + array('name' => 'Lime'),
  62 + array('name' => 'Soda'),
  63 + array('name' => 'Vodka'),
  64 + );
  65 +
  66 + foreach ($ingredients as $ingredient) {
  67 + blx()->db->createCommand()->insert('cocktailrecipes_ingredients', $ingredient);
  68 + }
  69 + }
  70 +}
49 plugins/cocktailrecipes/blocktypes/CocktailRecipes_IngredientsBlockType.php
... ... @@ -0,0 +1,49 @@
  1 +<?php
  2 +
  3 +namespace Blocks;
  4 +
  5 +/**
  6 + * Ingredients Blocktype
  7 + *
  8 + * Allows entries to select associated ingredients
  9 + */
  10 +class CocktailRecipes_IngredientsBlockType extends BaseBlockType
  11 +{
  12 + /**
  13 + * Get the name of this blocktype
  14 + */
  15 + public function getName()
  16 + {
  17 + return Blocks::t('Cocktail Ingredients');
  18 + }
  19 +
  20 + /**
  21 + * Get this blocktype's column type.
  22 + *
  23 + * @return mixed
  24 + */
  25 + public function defineContentAttribute()
  26 + {
  27 + // "Mixed" represents a "text" column type, which can be used to store arrays etc.
  28 + return AttributeType::Mixed;
  29 + }
  30 +
  31 + /**
  32 + * Get this blocktype's form HTML
  33 + *
  34 + * @param string $name
  35 + * @param mixed $value
  36 + * @return string
  37 + */
  38 + public function getInputHtml($name, $value)
  39 + {
  40 + // call our service layer to get a current list of ingredients
  41 + $ingredients = blx()->cocktailRecipes->getAllIngredients();
  42 +
  43 + return blx()->templates->render('cocktailrecipes/_blocktypes/ingredients', array(
  44 + 'name' => $name,
  45 + 'options' => $ingredients,
  46 + 'values' => $value,
  47 + ));
  48 + }
  49 +}
23 plugins/cocktailrecipes/composer.json
... ... @@ -0,0 +1,23 @@
  1 +{
  2 + "authors": [
  3 + {
  4 + "name": "Adrian Macneil",
  5 + "email": "adrian.macneil@gmail.com"
  6 + }
  7 + ],
  8 + "autoload": {
  9 + "classmap": [
  10 + "blocktypes",
  11 + "controllers",
  12 + "models",
  13 + "records",
  14 + "services",
  15 + "twigextensions",
  16 + "variables"
  17 + ]
  18 + },
  19 + "require-dev": {
  20 + "phpunit/phpunit": "3.7.*",
  21 + "mockery/mockery": "0.7.2"
  22 + }
  23 +}
56 plugins/cocktailrecipes/controllers/CocktailRecipes_IngredientsController.php
... ... @@ -0,0 +1,56 @@
  1 +<?php
  2 +
  3 +namespace Blocks;
  4 +
  5 +/**
  6 + * Ingredients Controller
  7 + *
  8 + * Defines actions which can be posted to by forms in our templates.
  9 + */
  10 +class CocktailRecipes_IngredientsController extends BaseController
  11 +{
  12 + /**
  13 + * Save Ingredient
  14 + *
  15 + * Create or update an existing ingredient, based on POST data
  16 + */
  17 + public function actionSaveIngredient()
  18 + {
  19 + $this->requirePostRequest();
  20 +
  21 + if ($id = blx()->request->getPost('ingredientId')) {
  22 + $model = blx()->cocktailRecipes->getIngredientById($id);
  23 + } else {
  24 + $model = blx()->cocktailRecipes->newIngredient($id);
  25 + }
  26 +
  27 + $attributes = blx()->request->getPost('ingredient');
  28 + $model->setAttributes($attributes);
  29 +
  30 + if (blx()->cocktailRecipes->saveIngredient($model)) {
  31 + blx()->user->setNotice(Blocks::t('Ingredient saved.'));
  32 +
  33 + return $this->redirectToPostedUrl(array('ingredientId' => $model->getAttribute('id')));
  34 + } else {
  35 + blx()->user->setError(Blocks::t("Couldn't save ingredient."));
  36 +
  37 + return $this->renderRequestedTemplate(array('ingredient' => $model));
  38 + }
  39 + }
  40 +
  41 + /**
  42 + * Delete Ingredient
  43 + *
  44 + * Delete an existing ingredient
  45 + */
  46 + public function actionDeleteIngredient()
  47 + {
  48 + $this->requirePostRequest();
  49 + $this->requireAjaxRequest();
  50 +
  51 + $id = blx()->request->getRequiredPost('id');
  52 + blx()->cocktailRecipes->deleteIngredientById($id);
  53 +
  54 + $this->returnJson(array('success' => true));
  55 + }
  56 +}
36 plugins/cocktailrecipes/models/CocktailRecipes_IngredientModel.php
... ... @@ -0,0 +1,36 @@
  1 +<?php
  2 +
  3 +namespace Blocks;
  4 +
  5 +/**
  6 + * Ingredient Model
  7 + *
  8 + * Provides a read-only object representing an ingredient, which is returned
  9 + * by our service class and can be used in our templates and controllers.
  10 + */
  11 +class CocktailRecipes_IngredientModel extends BaseModel
  12 +{
  13 + /**
  14 + * Defines what is returned when someone puts {{ ingredient }} directly
  15 + * in their template.
  16 + *
  17 + * @return string
  18 + */
  19 + public function __toString()
  20 + {
  21 + return $this->name;
  22 + }
  23 +
  24 + /**
  25 + * Define the attributes this model will have.
  26 + *
  27 + * @return array
  28 + */
  29 + public function defineAttributes()
  30 + {
  31 + return array(
  32 + 'id' => AttributeType::Number,
  33 + 'name' => AttributeType::String,
  34 + );
  35 + }
  36 +}
19 plugins/cocktailrecipes/phpunit.xml.dist
... ... @@ -0,0 +1,19 @@
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<phpunit backupGlobals="false"
  3 + backupStaticAttributes="false"
  4 + bootstrap="tests/bootstrap.php"
  5 + colors="true"
  6 + convertErrorsToExceptions="true"
  7 + convertNoticesToExceptions="true"
  8 + convertWarningsToExceptions="true"
  9 + processIsolation="false"
  10 + stopOnFailure="false"
  11 + syntaxCheck="false">
  12 +
  13 + <testsuites>
  14 + <testsuite name="Cocktail Recipes Test Suite">
  15 + <directory>./tests/</directory>
  16 + </testsuite>
  17 + </testsuites>
  18 +
  19 +</phpunit>
50 plugins/cocktailrecipes/records/CocktailRecipes_IngredientRecord.php
... ... @@ -0,0 +1,50 @@
  1 +<?php
  2 +
  3 +namespace Blocks;
  4 +
  5 +/**
  6 + * Ingredient Record
  7 + *
  8 + * Provides a definition of the database tables required by our plugin,
  9 + * and methods for updating the database. This class should only be called
  10 + * by our service layer, to ensure a consistent API for the rest of the
  11 + * application to use.
  12 + */
  13 +class CocktailRecipes_IngredientRecord extends BaseRecord
  14 +{
  15 + /**
  16 + * Gets the database table name
  17 + *
  18 + * @return string
  19 + */
  20 + public function getTableName()
  21 + {
  22 + return 'cocktailrecipes_ingredients';
  23 + }
  24 +
  25 + /**
  26 + * Define columns for our database table
  27 + *
  28 + * @return array
  29 + */
  30 + public function defineAttributes()
  31 + {
  32 + return array(
  33 + 'name' => array(AttributeType::String, 'required' => true, 'unique' => true),
  34 + );
  35 + }
  36 +
  37 + /**
  38 + * Create a new instance of the current class. This allows us to
  39 + * properly unit test our service layer.
  40 + *
  41 + * @return BaseRecord
  42 + */
  43 + public function create()
  44 + {
  45 + $class = get_class($this);
  46 + $record = new $class();
  47 +
  48 + return $record;
  49 + }
  50 +}
106 plugins/cocktailrecipes/services/CocktailRecipesService.php
... ... @@ -0,0 +1,106 @@
  1 +<?php
  2 +
  3 +namespace Blocks;
  4 +
  5 +/**
  6 + * Cocktail Recipes Service
  7 + *
  8 + * Provides a consistent API for our plugin to access the database
  9 + */
  10 +class CocktailRecipesService extends BaseApplicationComponent
  11 +{
  12 + protected $ingredientRecord;
  13 +
  14 + /**
  15 + * Create a new instance of the Cocktail Recpies Service.
  16 + * Constructor allows IngredientRecord dependency to be injected to assist with unit testing.
  17 + *
  18 + * @param @ingredientRecord IngredientRecord The ingredient record to access the database
  19 + */
  20 + public function __construct($ingredientRecord = null)
  21 + {
  22 + $this->ingredientRecord = $ingredientRecord;
  23 + if (is_null($this->ingredientRecord)) {
  24 + $this->ingredientRecord = CocktailRecipes_IngredientRecord::model();
  25 + }
  26 + }
  27 +
  28 + /**
  29 + * Get a new blank ingredient
  30 + *
  31 + * @param array $attributes
  32 + * @return CocktailRecipes_IngredientModel
  33 + */
  34 + public function newIngredient($attributes = array())
  35 + {
  36 + $model = new CocktailRecipes_IngredientModel();
  37 + $model->setAttributes($attributes);
  38 +
  39 + return $model;
  40 + }
  41 +
  42 + /**
  43 + * Get all ingredients from the database.
  44 + *
  45 + * @return array
  46 + */
  47 + public function getAllIngredients()
  48 + {
  49 + $records = $this->ingredientRecord->findAll(array('order'=>'t.name'));
  50 +
  51 + return CocktailRecipes_IngredientModel::populateModels($records, 'id');
  52 + }
  53 +
  54 + /**
  55 + * Get a specific ingredient from the database based on ID. If no ingredient exists, null is returned.
  56 + *
  57 + * @param int $id
  58 + * @return mixed
  59 + */
  60 + public function getIngredientById($id)
  61 + {
  62 + if ($record = $this->ingredientRecord->findByPk($id)) {
  63 + return CocktailRecipes_IngredientModel::populateModel($record);
  64 + }
  65 + }
  66 +
  67 + /**
  68 + * Save a new or existing ingredient back to the database.
  69 + *
  70 + * @param CocktailRecipes_IngredientModel $model
  71 + * @return bool
  72 + */
  73 + public function saveIngredient(CocktailRecipes_IngredientModel &$model)
  74 + {
  75 + if ($id = $model->getAttribute('id')) {
  76 + if (null === ($record = $this->ingredientRecord->findByPk($id))) {
  77 + throw new Exception(Blocks::t('Can\'t find ingredient with ID "{id}"', array('id' => $id)));
  78 + }
  79 + } else {
  80 + $record = $this->ingredientRecord->create();
  81 + }
  82 +
  83 + $record->setAttributes($model->getAttributes());
  84 + if ($record->save()) {
  85 + // update id on model (for new records)
  86 + $model->setAttribute('id', $record->getAttribute('id'));
  87 +
  88 + return true;
  89 + } else {
  90 + $model->addErrors($record->getErrors());
  91 +
  92 + return false;
  93 + }
  94 + }
  95 +
  96 + /**
  97 + * Delete an ingredient from the database.
  98 + *
  99 + * @param int $id
  100 + * @return int The number of rows affected
  101 + */
  102 + public function deleteIngredientById($id)
  103 + {
  104 + return $this->ingredientRecord->deleteByPk($id);
  105 + }
  106 +}
7 plugins/cocktailrecipes/templates/_blocktypes/ingredients.html
... ... @@ -0,0 +1,7 @@
  1 +{% import "_includes/forms" as forms %}
  2 +
  3 +{{ forms.checkboxSelect({
  4 + name: name,
  5 + options: options,
  6 + values: values,
  7 +}) }}
51 plugins/cocktailrecipes/templates/index.html
... ... @@ -0,0 +1,51 @@
  1 +{% extends "_layouts/cp" %}
  2 +{% set centered = true %}
  3 +
  4 +{% set ingredients = blx.cocktailrecipes.getAllIngredients %}
  5 +{% set title = "Cocktail Ingredients"|t %}
  6 +
  7 +{% set header %}
  8 + <h1>{{ title }}</h1>
  9 +{% endset %}
  10 +
  11 +{% set content %}
  12 +
  13 + {{ "hi there, test sentence!"|shake }}
  14 +
  15 + <p id="noingredients"{% if ingredients|length %} class="hidden"{% endif %}>
  16 + {{ "No ingredients exist yet."|t }}
  17 + </p>
  18 +
  19 + {% if ingredients|length %}
  20 + <table id="ingredients" class="data">
  21 + <thead>
  22 + <th scope="col">{{ "Name"|t }}</th>
  23 + <th class="thin"></th>
  24 + </thead>
  25 + <tbody>
  26 +
  27 + {% for ingredient in ingredients %}
  28 + <tr data-id="{{ ingredient.id }}" data-name="{{ ingredient.name|t }}">
  29 + <td><a href="{{ url('cocktailrecipes/ingredients/'~ingredient.id) }}">{{ ingredient.name }}</a></td>
  30 + <td><a class="delete icon" title="{{ 'Delete'|t }}"></a></td>
  31 + </tr>
  32 + {% endfor %}
  33 +
  34 + </tbody>
  35 + </table>
  36 + {% endif %}
  37 +
  38 + <div class="buttons">
  39 + <a href="{{ url('cocktailrecipes/ingredients/new') }}" class="btn add icon">{{ "New Ingredient"|t }}</a>
  40 + </div>
  41 +
  42 +{% endset %}
  43 +
  44 +{% set js %}
  45 + new Blocks.ui.AdminTable({
  46 + tableSelector: '#ingredients',
  47 + noObjectsSelector: '#noingredients',
  48 + deleteAction: 'cocktailrecipes/ingredients/deleteIngredient'
  49 + });
  50 +{% endset %}
  51 +{% includeJs js %}
44 plugins/cocktailrecipes/templates/ingredients/_edit.html
... ... @@ -0,0 +1,44 @@
  1 +{% extends "_layouts/cp" %}
  2 +{% import "_includes/forms" as forms %}
  3 +{% set centered = true %}
  4 +
  5 +{% if ingredientId is not defined %}{% set ingredientId = null %}{% endif %}
  6 +{% if ingredient is not defined %}
  7 + {% if ingredientId %}
  8 + {% set ingredient = blx.cocktailrecipes.getIngredientById(ingredientId) %}
  9 + {% if not ingredient %}{% exit 404 %}{% endif %}
  10 + {% else %}
  11 + {% set ingredient = null %}
  12 + {% endif %}
  13 +{% endif %}
  14 +
  15 +{% set title = ingredient ? ingredient.name : "New Ingredient"|t %}
  16 +
  17 +{% set header %}
  18 + <h1>{{ title }}</h1>
  19 + <ul class="left">
  20 + <li><a href="{{ url('cocktailrecipes') }}" class="backbtn">{{ "Cocktail Ingredients"|t }}</a></li>
  21 + </ul>
  22 +{% endset %}
  23 +
  24 +{% set content %}
  25 +
  26 + <form method="post" action="" accept-charset="UTF-8">
  27 + <input type="hidden" name="action" value="cocktailrecipes/ingredients/saveIngredient" />
  28 + <input type="hidden" name="redirect" value="cocktailrecipes/ingredients/{ingredientId}" />
  29 + <input type="hidden" name="ingredientId" value="{{ ingredientId }}" />
  30 +
  31 + {{ forms.textField({
  32 + label: 'Ingredient Name'|t,
  33 + required: true,
  34 + name: 'ingredient[name]',
  35 + value: ingredient ? ingredient.name : null,
  36 + errors: ingredient ? ingredient.errors('name') : null,
  37 + }) }}
  38 +
  39 + <div class="buttons">
  40 + <input type="submit" class="btn submit" value="{{ 'Save'|t }}">
  41 + </div>
  42 + </form>
  43 +
  44 +{% endset %}
164 plugins/cocktailrecipes/tests/CocktailRecipesServiceTest.php
... ... @@ -0,0 +1,164 @@
  1 +<?php
  2 +
  3 +namespace Blocks;
  4 +
  5 +use Mockery as m;
  6 +use PHPUnit_Framework_TestCase;
  7 +
  8 +class CocktailRecipesServiceTest extends PHPUnit_Framework_TestCase
  9 +{
  10 + public function setUp()
  11 + {
  12 + $this->ingredientRecord = m::mock('Blocks\CocktailRecipes_IngredientRecord');
  13 + $this->service = new CocktailRecipesService($this->ingredientRecord);
  14 + }
  15 +
  16 + public function testNewIngredient()
  17 + {
  18 + $result = $this->service->newIngredient();
  19 +
  20 + $this->assertInstanceOf('Blocks\CocktailRecipes_IngredientModel', $result);
  21 + }
  22 +
  23 + public function testNewIngredientWithAttributes()
  24 + {
  25 + $result = $this->service->newIngredient(array('id' => 5));
  26 +
  27 + $this->assertInstanceOf('Blocks\CocktailRecipes_IngredientModel', $result);
  28 + $this->assertEquals(5, $result->id);
  29 + }
  30 +
  31 + public function testGetAllIngredients()
  32 + {
  33 + $fakeResults = array(array('id' => 3), array('id' => 5));
  34 +
  35 + $this->ingredientRecord
  36 + ->shouldReceive('findAll')->with(array('order' => 't.name'))
  37 + ->andReturn($fakeResults);
  38 +
  39 + $results = $this->service->getAllIngredients();
  40 +
  41 + $this->assertEquals(2, count($results));
  42 + $this->assertInstanceOf('Blocks\CocktailRecipes_IngredientModel', $results[5]);
  43 + }
  44 +
  45 + public function testGetIngredientById()
  46 + {
  47 + $attributes = array('id' => 5);
  48 +
  49 + $mockRecord = m::mock('Blocks\CocktailRecipes_IngredientModel');
  50 + $this->ingredientRecord
  51 + ->shouldReceive('findByPk')->with(5)
  52 + ->andReturn($mockRecord);
  53 +
  54 + $mockRecord->shouldReceive('getAttributes')->andReturn($attributes);
  55 +
  56 + $result = $this->service->getIngredientById(5);
  57 +
  58 + $this->assertInstanceOf('Blocks\CocktailRecipes_IngredientModel', $result);
  59 + $this->assertEquals(5, $result->id);
  60 + }
  61 +
  62 + public function testGetIngredientByMissingId()
  63 + {
  64 + $this->ingredientRecord->shouldReceive('findByPk')->with(5)
  65 + ->andReturn(null);
  66 + $result = $this->service->getIngredientById(5);
  67 +
  68 + $this->assertNull($result);
  69 + }
  70 +
  71 + public function testSaveIngredient()
  72 + {
  73 + $mockModel = m::mock('Blocks\CocktailRecipes_IngredientModel');
  74 + $mockModel->shouldReceive('getAttribute')->with('id')->once()->andReturn(5);
  75 +
  76 + $mockRecord = m::mock('Blocks\CocktailRecipes_IngredientRecord');
  77 + $this->ingredientRecord->shouldReceive('findByPk')->with(5)->once()
  78 + ->andReturn($mockRecord);
  79 +
  80 + $attributes = array('name' => 'example');
  81 + $mockModel->shouldReceive('getAttributes')->once()->andReturn($attributes);
  82 + $mockRecord->shouldReceive('setAttributes')->with($attributes)->once();
  83 +
  84 + $mockRecord->shouldReceive('save')->once()->andReturn(true);
  85 +
  86 + $mockRecord->shouldReceive('getAttribute')->with('id')->once()
  87 + ->andReturn(5);
  88 + $mockModel->shouldReceive('setAttribute')->with('id', 5)->once();
  89 +
  90 + $result = $this->service->saveIngredient($mockModel);
  91 + $this->assertTrue($result);
  92 + }
  93 +
  94 + /**
  95 + * @expectedException Blocks\Exception
  96 + */
  97 + public function testSaveIngredientNotFound()
  98 + {
  99 + $mockModel = m::mock('Blocks\CocktailRecipes_IngredientModel');
  100 + $mockModel->shouldReceive('getAttribute')->with('id')->once()->andReturn(5);
  101 +
  102 + $mockRecord = m::mock('Blocks\CocktailRecipes_IngredientRecord');
  103 + $this->ingredientRecord->shouldReceive('findByPk')->with(5)->once()
  104 + ->andReturn(null);
  105 +
  106 + $result = $this->service->saveIngredient($mockModel);
  107 + }
  108 +
  109 + public function testSaveIngredientInvalid()
  110 + {
  111 + $mockModel = m::mock('Blocks\CocktailRecipes_IngredientModel');
  112 + $mockModel->shouldReceive('getAttribute')->with('id')->once()->andReturn(5);
  113 +
  114 + $mockRecord = m::mock('Blocks\CocktailRecipes_IngredientRecord');
  115 + $this->ingredientRecord->shouldReceive('findByPk')->with(5)->once()
  116 + ->andReturn($mockRecord);
  117 +
  118 + $attributes = array('name' => 'example');
  119 + $mockModel->shouldReceive('getAttributes')->once()->andReturn($attributes);
  120 + $mockRecord->shouldReceive('setAttributes')->with($attributes)->once();
  121 +
  122 + $mockRecord->shouldReceive('save')->once()->andReturn(false);
  123 +
  124 + $errors = array('name' => 'error message');
  125 + $mockRecord->shouldReceive('getErrors')->once()->andReturn($errors);
  126 + $mockModel->shouldReceive('addErrors')->with($errors)->once();
  127 +
  128 + $result = $this->service->saveIngredient($mockModel);
  129 + $this->assertFalse($result);
  130 + }
  131 +
  132 + public function testSaveIngredientNewRecord()
  133 + {
  134 + $mockModel = m::mock('Blocks\CocktailRecipes_IngredientModel');
  135 + $mockModel->shouldReceive('getAttribute')->with('id')->once()->andReturn(null);
  136 +
  137 + $mockRecord = m::mock('Blocks\CocktailRecipes_IngredientRecord');
  138 + $this->ingredientRecord->shouldReceive('create')->once()
  139 + ->andReturn($mockRecord);
  140 +
  141 + $attributes = array('name' => 'example');
  142 + $mockModel->shouldReceive('getAttributes')->once()->andReturn($attributes);
  143 + $mockRecord->shouldReceive('setAttributes')->with($attributes)->once();
  144 +
  145 + $mockRecord->shouldReceive('save')->once()->andReturn(true);
  146 +
  147 + $mockRecord->shouldReceive('getAttribute')->with('id')->once()
  148 + ->andReturn(5);
  149 + $mockModel->shouldReceive('setAttribute')->with('id', 5)->once();
  150 +
  151 + $result = $this->service->saveIngredient($mockModel);
  152 + $this->assertTrue($result);
  153 + }
  154 +
  155 + public function testDeleteIngredientById()
  156 + {
  157 + $this->ingredientRecord
  158 + ->shouldReceive('deleteByPk')->with(5)->andReturn(2);
  159 +
  160 + $result = $this->service->deleteIngredientById(5);
  161 +
  162 + $this->assertSame(2, $result);
  163 + }
  164 +}
42 plugins/cocktailrecipes/tests/CocktailRecipesTwigExtensionTest.php
... ... @@ -0,0 +1,42 @@
  1 +<?php
  2 +
  3 +namespace Blocks;
  4 +
  5 +use PHPUnit_Framework_TestCase;
  6 +
  7 +// Ensure Twig classes are loaded
  8 +blx()->templates->registerTwigAutoloader();
  9 +
  10 +class CocktailRecipesTwigExtensionTest extends PHPUnit_Framework_TestCase
  11 +{
  12 + public function setUp()
  13 + {
  14 + $this->extension = new CocktailRecipesTwigExtension();
  15 + }
  16 +
  17 + /**
  18 + * GetFilters should return a "shake" filter
  19 + */
  20 + public function testGetFilters()
  21 + {
  22 + $result = $this->extension->getFilters();
  23 +
  24 + $this->assertArrayHasKey('shake', $result);
  25 + }
  26 +
  27 + public function testShakeFilter()
  28 + {
  29 + $str = "This is an example sentence to be shaken up.";
  30 + $result = $this->extension->shakeFilter($str);
  31 +
  32 + $this->assertEquals(strlen($str), strlen($result));
  33 + $this->assertNotEquals($str, $result);
  34 + }
  35 +
  36 + public function testShakeFilterEmptyString()
  37 + {
  38 + $result = $this->extension->shakeFilter(null);
  39 +
  40 + $this->assertSame('', $result);
  41 + }
  42 +}
38 plugins/cocktailrecipes/tests/CocktailRecipes_IngredientsBlockTypeTest.php
... ... @@ -0,0 +1,38 @@
  1 +<?php
  2 +
  3 +namespace Blocks;
  4 +
  5 +use Mockery as m;
  6 +use PHPUnit_Framework_TestCase;
  7 +
  8 +class CocktailRecipes_IngredientsBlockTypeTest extends PHPUnit_Framework_TestCase
  9 +{
  10 + public function setUp()
  11 + {
  12 + $this->blocktype = new CocktailRecipes_IngredientsBlockType();
  13 +
  14 + // inject service dependencies
  15 + $this->cocktailRecipes = m::mock('Blocks\CocktailRecipesService');
  16 + $this->cocktailRecipes->shouldReceive('getIsInitialized')->andReturn(true);
  17 + blx()->setComponent('cocktailRecipes', $this->cocktailRecipes);
  18 +
  19 + $this->templates = m::mock('Blocks\TemplatesService');
  20 + $this->templates->shouldReceive('getIsInitialized')->andReturn(true);
  21 + blx()->setComponent('templates', $this->templates);
  22 + }
  23 +
  24 + public function testGetName()
  25 + {
  26 + $result = $this->blocktype->getName();
  27 +
  28 + $this->assertInternalType('string', $result);
  29 + $this->assertNotEmpty($result);
  30 + }
  31 +
  32 + public function testGetInputHtml()
  33 + {
  34 + $this->cocktailRecipes->shouldReceive('getAllIngredients')->once()->andReturn(array());
  35 +
  36 + $this->templates->shouldReceive('render')->once();
  37 + }
  38 +}
127 plugins/cocktailrecipes/tests/CocktailRecipes_IngredientsControllerTest.php
... ... @@ -0,0 +1,127 @@
  1 +<?php
  2 +
  3 +namespace Blocks;
  4 +
  5 +use Mockery as m;
  6 +use PHPUnit_Framework_TestCase;
  7 +
  8 +class CocktailRecipes_IngredientsControllerTest extends PHPUnit_Framework_TestCase
  9 +{
  10 + public function setUp()
  11 + {
  12 + // unfortunately we need to stub out some methods on parent class
  13 + $this->controller = m::mock('Blocks\CocktailRecipes_IngredientsController[redirectToPostedUrl,renderRequestedTemplate,returnJson]');
  14 +
  15 + // inject service dependencies
  16 + $this->cocktailRecipes = m::mock('Blocks\CocktailRecipesService');
  17 + $this->cocktailRecipes->shouldReceive('getIsInitialized')->andReturn(true);
  18 + blx()->setComponent('cocktailRecipes', $this->cocktailRecipes);
  19 +
  20 + $this->user = m::mock('Blocks\UsersService');
  21 + $this->user->shouldReceive('getIsInitialized')->andReturn(true);
  22 + blx()->setComponent('user', $this->user);
  23 +
  24 + $this->request = m::mock('Blocks\HttpRequestService');
  25 + $this->request->shouldReceive('getIsInitialized')->andReturn(true);
  26 + blx()->setComponent('request', $this->request);
  27 + }
  28 +
  29 + public function testSaveIngedient()
  30 + {
  31 + $this->request->shouldReceive('getRequestType')->once()
  32 + ->andReturn('POST');
  33 +
  34 + $this->request->shouldReceive('getPost')->with('ingredientId')->once()
  35 + ->andReturn(5);
  36 +
  37 + $mockModel = m::mock('Blocks\CocktailRecipes_IngredientModel');
  38 + $this->cocktailRecipes->shouldReceive('getIngredientById')->with(5)->once()
  39 + ->andReturn($mockModel);
  40 +
  41 + $attributes = array('name' => 'example');
  42 + $this->request->shouldReceive('getPost')->with('ingredient')->once()
  43 + ->andReturn($attributes);
  44 + $mockModel->shouldReceive('setAttributes')->with($attributes);
  45 +
  46 + $this->cocktailRecipes->shouldReceive('saveIngredient')->with($mockModel)->once()
  47 + ->andReturn(true);
  48 + $mockModel->shouldReceive('getAttribute')->with('id')->once()
  49 + ->andReturn(5);
  50 +
  51 + $this->user->shouldReceive('setNotice');
  52 + $this->controller->shouldReceive('redirectToPostedUrl');
  53 +
  54 + $this->controller->actionSaveIngredient();
  55 + }
  56 +
  57 + public function testSaveIngredientNew()
  58 + {
  59 + $this->request->shouldReceive('getRequestType')->once()
  60 + ->andReturn('POST');
  61 +
  62 + $this->request->shouldReceive('getPost')->with('ingredientId')->once()
  63 + ->andReturn(null);
  64 +
  65 + $mockModel = m::mock('Blocks\CocktailRecipes_IngredientModel');
  66 + $this->cocktailRecipes->shouldReceive('newIngredient')->once()
  67 + ->andReturn($mockModel);
  68 +
  69 + $attributes = array('name' => 'example');
  70 + $this->request->shouldReceive('getPost')->with('ingredient')->once()
  71 + ->andReturn($attributes);
  72 + $mockModel->shouldReceive('setAttributes')->with($attributes);
  73 +
  74 + $this->cocktailRecipes->shouldReceive('saveIngredient')->with($mockModel)->once()
  75 + ->andReturn(true);
  76 + $mockModel->shouldReceive('getAttribute')->with('id')->once()
  77 + ->andReturn(5);
  78 +
  79 + $this->user->shouldReceive('setNotice');
  80 + $this->controller->shouldReceive('redirectToPostedUrl');
  81 +
  82 + $this->controller->actionSaveIngredient();
  83 + }
  84 +
  85 + public function testSaveIngedientError()
  86 + {
  87 + $this->request->shouldReceive('getRequestType')->once()
  88 + ->andReturn('POST');
  89 +
  90 + $this->request->shouldReceive('getPost')->with('ingredientId')->once()
  91 + ->andReturn(5);
  92 +
  93 + $mockModel = m::mock('Blocks\CocktailRecipes_IngredientModel');
  94 + $this->cocktailRecipes->shouldReceive('getIngredientById')->with(5)->once()
  95 + ->andReturn($mockModel);
  96 +
  97 + $attributes = array('name' => 'example');
  98 + $this->request->shouldReceive('getPost')->with('ingredient')->once()
  99 + ->andReturn($attributes);
  100 + $mockModel->shouldReceive('setAttributes')->with($attributes);
  101 +
  102 + $this->cocktailRecipes->shouldReceive('saveIngredient')->with($mockModel)->once()
  103 + ->andReturn(false);
  104 +
  105 + $this->user->shouldReceive('setError');
  106 + $this->controller->shouldReceive('renderRequestedTemplate')
  107 + ->with(array('ingredient' => $mockModel));
  108 +
  109 + $this->controller->actionSaveIngredient();
  110 + }
  111 +
  112 + public function testDeleteIngredient()
  113 + {
  114 + $this->request->shouldReceive('getRequestType')->once()
  115 + ->andReturn('POST');
  116 + $this->request->shouldReceive('isAjaxRequest')->once()
  117 + ->andReturn(true);
  118 +
  119 + $this->request->shouldReceive('getRequiredPost')->with('id')->once()
  120 + ->andReturn(5);
  121 + $this->cocktailRecipes->shouldReceive('deleteIngredientById')->with(5)->once();
  122 +
  123 + $this->controller->shouldReceive('returnJson')->with(array('success' => true))->once();
  124 +
  125 + $this->controller->actionDeleteIngredient();
  126 + }
  127 +}
36 plugins/cocktailrecipes/tests/bootstrap.php
... ... @@ -0,0 +1,36 @@
  1 +<?php
  2 +
  3 +// PHP sucks at resolving symlinked directories
  4 +define('BLOCKS_BASE_PATH', realpath(str_replace("plugins/cocktailrecipes", '', exec('pwd'))).'/');
  5 +
  6 +// Define app constants
  7 +defined('BLOCKS_APP_PATH') || define('BLOCKS_APP_PATH', BLOCKS_BASE_PATH.'app/');
  8 +defined('BLOCKS_CONFIG_PATH') || define('BLOCKS_CONFIG_PATH', BLOCKS_BASE_PATH.'config/');
  9 +defined('BLOCKS_PLUGINS_PATH') || define('BLOCKS_PLUGINS_PATH', BLOCKS_BASE_PATH.'plugins/');
  10 +defined('BLOCKS_STORAGE_PATH') || define('BLOCKS_STORAGE_PATH', BLOCKS_BASE_PATH.'storage/');
  11 +defined('BLOCKS_TEMPLATES_PATH') || define('BLOCKS_TEMPLATES_PATH', BLOCKS_BASE_PATH.'templates/');
  12 +defined('BLOCKS_TRANSLATIONS_PATH') || define('BLOCKS_TRANSLATIONS_PATH', BLOCKS_BASE_PATH.'translations/');
  13 +defined('YII_TRACE_LEVEL') || define('YII_TRACE_LEVEL', 3);
  14 +
  15 +// Load Yii
  16 +require_once BLOCKS_APP_PATH.'framework/yiit.php';
  17 +Yii::$enableIncludePath = false;
  18 +require_once BLOCKS_APP_PATH.'Blocks.php';
  19 +require_once BLOCKS_APP_PATH.'App.php';
  20 +require_once BLOCKS_APP_PATH.'Info.php';
  21 +
  22 +// Blocks shits itself if there is no server or request path
  23 +$_SERVER['HTTP_HOST'] = 'example.com';
  24 +$_SERVER['REQUEST_URI'] = '/';
  25 +
  26 +// Load test database
  27 +$config = require_once BLOCKS_APP_PATH.'config/test.php';
  28 +$config['params']['dbConfig'] = array();
  29 +$config['components']['db'] = array();
  30 +
  31 +// Create app instance
  32 +$app = new \Blocks\App($config);
  33 +
  34 +// Load plugin
  35 +require_once BLOCKS_PLUGINS_PATH.'cocktailrecipes/CocktailRecipesPlugin.php';
  36 +require_once BLOCKS_PLUGINS_PATH.'cocktailrecipes/vendor/autoload.php';
34 plugins/cocktailrecipes/twigextensions/CocktailRecipesTwigExtension.php
... ... @@ -0,0 +1,34 @@
  1 +<?php
  2 +
  3 +namespace Blocks;
  4 +
  5 +use Twig_Extension;
  6 +use Twig_Filter_Method;
  7 +
  8 +class CocktailRecipesTwigExtension extends Twig_Extension
  9 +{
  10 + public function getName()
  11 + {
  12 + return 'cocktailrecipes';
  13 + }
  14 +
  15 + public function getFilters()
  16 + {
  17 + return array(
  18 + 'shake' => new Twig_Filter_Method($this, 'shakeFilter'),
  19 + );
  20 + }
  21 +
  22 + /**
  23 + * The "shake" filter shakes up the order of words in a sentence.
  24 + *
  25 + * Usage: {{ "Bartender, I'd like a drink please."|shake }}
  26 + */
  27 + public function shakeFilter($content)
  28 + {
  29 + $words = preg_split('/\s+/', $content);
  30 + shuffle($words);
  31 +
  32 + return implode(' ', $words);
  33 + }
  34 +}
30 plugins/cocktailrecipes/variables/CocktailRecipesVariable.php
... ... @@ -0,0 +1,30 @@
  1