Permalink
Browse files

FEATURE: first commit

  • Loading branch information...
0 parents commit 279d545e66f030f458dcdf6b922a5c39d68fdde2 @mateusz committed Apr 5, 2011
Showing with 558 additions and 0 deletions.
  1. +54 −0 Readme.md
  2. +1 −0 _config.php
  3. +182 −0 code/Poll.php
  4. +12 −0 code/PollAdmin.php
  5. +61 −0 code/PollChoice.php
  6. +102 −0 code/PollForm.php
  7. +34 −0 tests/Base.yml
  8. +74 −0 tests/functional/PollFormTest.php
  9. +17 −0 tests/templates/TestPollForm.ss
  10. +21 −0 tests/unit/PollTest.php
54 Readme.md
@@ -0,0 +1,54 @@
+# Polls module
+
+## Maintainer
+[Saophalkun Ponlu](mailto:phalkunz@silverstripe.com)
+
+## Requirements
+
+SilverStripe 2.4 or higher
+
+## Installation
+
+1. Include the module folder in your project root folder
+1. Rebuild database schema (dev/build?flush=1)
+
+## Features
+
+- Each user, determined by browser cookie, can only vote once
+- Use [Google chart API](http://code.google.com/apis/chart/)
+- Single and multiple-choice polls
+
+## Usage
+
+### Create a poll
+
+1. Log in the CMS
+1. Go to **Poll** section
+1. Create a poll and a few poll choices and attach them to the poll
+
+### Create a poll form
+
+The following code is an example of a simple poll form.
+
+ class Page_Controller extends ContentController {
+
+ ...
+
+ function ExamplePoll() {
+ $pollID = 2;
+ $chartWidth = 500;
+ $chartHeight = 300;
+
+ $form = new PollForm($this, 'ExamplePoll', $pollID, $chartWidth, $chartHeight);
+
+ return $form;
+ }
+
+ ...
+
+ }
+
+## Todos
+
+- Logged-in users only
+- View results links
1 _config.php
@@ -0,0 +1 @@
+<?php
182 code/Poll.php
@@ -0,0 +1,182 @@
+<?php
+/**
+ * This represents a poll data object that should have 2 more {@link PollChoice}s
+ *
+ * @package polls
+ */
+class Poll extends DataObject {
+
+ const COOKIE_PREFIX = 'SSPoll_';
+
+ static $db = Array(
+ 'Title' => 'Varchar(50)',
+ 'Description' => 'HTMLText',
+ 'IsActive' => 'Boolean(1)',
+ 'MultiChoice' => 'Boolean'
+ );
+ static $has_one = array(
+ 'Image' => 'Image'
+ );
+
+ static $has_many = Array(
+ 'Choices' => 'PollChoice'
+ );
+
+ static $searchable_fields = array(
+ 'Title',
+ 'IsActive' => 'PostgresBooleanSearchFilter'
+ );
+
+ static $summary_fields = array(
+ 'Title',
+ 'IsActive'
+ );
+
+ static $default_sort = 'Created DESC';
+
+ function getCMSFields() {
+
+ if($this->ID != 0) {
+ $totalCount = $this->totalVotes();
+ }
+ else {
+ $totalCount = 0;
+ }
+
+ $fields = new FieldSet(
+ $rootTab = new TabSet("Root",
+ new Tab("Data",
+ new TextField('Title', 'Poll title (maximum 50 characters)', null, 50),
+ new OptionsetField('MultiChoice', 'Single answer (radio buttons)/multi-choice answer (tick boxes)', array(0 => 'Single answer', 1 => 'Multi-choice answer')),
+ new OptionsetField('IsActive', 'Poll state', array(1 => 'Active', 0 => 'Inactive')),
+ new HTMLEditorField('Description', 'Description', 12),
+ new ReadonlyField('Total', 'Total votes', $totalCount),
+ $image = new ImageField('Image', 'Poll image')
+ )
+ )
+ );
+ $image->setCanUploadNewFile(false);
+
+ if($this->ID != 0) {
+ $chartTab = new Tab("Chart", new LiteralField('Chart', sprintf(
+ '<h1>%s</h1><p><img src="%s" title="%s" /></p>',
+ $this->Title,
+ $this->chartURL(),
+ $this->Title))
+ );
+
+ $rootTab->push($chartTab);
+ }
+
+ $pollChoicesTable = new ComplexTableField(
+ $this,
+ 'Choices', // relation name
+ 'PollChoice', // object class
+ array(
+ 'Order' => '#',
+ 'Title' => 'Answer option',
+ 'Votes' => 'Votes'
+ ), // fields to show in table
+ PollChoice::getCMSFields_forPopup(), // form that pops up for edit
+ '"PollID" = ' . $this->ID // a filter to only display item associated with this poll
+ );
+ $pollChoicesTable->setAddTitle( 'a poll choice' );
+ $pollChoicesTable->setParentClass('Poll');
+ $fields->addFieldToTab('Root.Data', $pollChoicesTable);
+
+ $this->extend('updateCMSFields', $fields);
+
+ return $fields;
+ }
+
+ function getFormFields() {
+ $data = array();
+
+ foreach($this->Choices() as $choice) {
+ $data[$choice->ID] = $choice->Title;
+ }
+
+ if($this->MultiChoice) {
+ $choiceField = new CheckboxSetField('PollChoices', 'Please select at least one of the checkboxes', $data);
+ }
+ else {
+ $choiceField = new OptionsetField('PollChoices', 'Please select one option', $data);
+ }
+
+ $fields = new FieldSet(
+ new HiddenField('PollID', '', $this->ID),
+ $choiceField
+ );
+
+ $this->extend('updateFormFields', $fields);
+
+ return $fields;
+ }
+
+ /**
+ * URL to an chart image that is render by Google Chart API
+ * @link http://code.google.com/apis/chart/docs/making_charts.html
+ *
+ * @return string
+ */
+ function chartURL($width = null, $height = null) {
+ $apiURL = 'https://chart.googleapis.com/chart';
+
+ // The sort option helps facilitate writing unit test
+ $choices = $this->Choices('', 'Title ASC');
+
+ if(!$width) $width = 840;
+ if(!$height) $height = 300;
+
+ $formattedLabels = array();
+ if ($choices) foreach($choices as $choice) $formattedLabels[] = $choice->Title.'('.$choice->Votes.')';
+ $labels = implode('|', $formattedLabels);
+ $data = implode(',', $choices->map('ID', 'Votes'));
+ return sprintf(
+ "%s?cht=%s&chs=%sx%s&chl=%s&chd=t:%s&chf=bg,s,00000000&chco=%s",
+ $apiURL,
+ 'p3',
+ $width,
+ $height,
+ $labels,
+ $data,
+ 'F9D42D'
+ );
+ }
+
+ /**
+ * Returns the number of total votes, the sum of all votes from {@link PollChoice}s' votes
+ *
+ * @return int
+ */
+ function totalVotes() {
+ $query = DB::query('SELECT SUM("Votes") As "Total" FROM "PollChoice" WHERE "PollID" = ' . $this->ID);
+ $res = $query->nextRecord();
+
+ return $res['Total'];
+ }
+
+ /**
+ * Mark the the poll has been voted by the user, which determined by browser cookie
+ */
+ function markAsVoted() {
+ Cookie::set(self::COOKIE_PREFIX . $this->ID, 1);
+ }
+
+ /**
+ * Check if the user, determined by browser cookie, has been submitted a vote to the poll.
+ *
+ * @param integer
+ * @return bool
+ */
+ function isVoted() {
+ $cookie = Cookie::get(self::COOKIE_PREFIX . $this->ID);
+
+ if($cookie) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+}
12 code/PollAdmin.php
@@ -0,0 +1,12 @@
+<?php
+class PollAdmin extends ModelAdmin {
+
+ static $url_segment = 'polls';
+
+ static $menu_title = 'Polls';
+
+ static $managed_models = array(
+ 'Poll'
+ );
+
+}
61 code/PollChoice.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * This represents a choice that belongs to a {@link Poll}
+ *
+ * @package polls
+ */
+class PollChoice extends DataObject {
+
+ static $db = Array(
+ 'Title' => 'Varchar(255)',
+ 'Votes' => 'Int',
+ 'Order' => 'Int'
+ );
+
+ static $has_one = Array(
+ 'Poll' => 'Poll'
+ );
+
+ static $searchable_fields = array(
+ 'Title'
+ );
+
+ static $default_sort = '"Order" ASC, "Created" ASC';
+
+ function getCMSFields() {
+ $polls = DataObject::get('Poll', '"IsActive" = 1');
+ $pollsMap = array();
+ if($polls) $pollsMap = $polls->toDropDownMap('ID', 'Title', '--- Select a poll ---');
+
+ $fields = new FieldSet(
+ new TextField('Title', '', '', 80),
+ new DropdownField('PollID', 'Belongs to', $pollsMap),
+ new ReadonlyField('Votes')
+ );
+
+ return $fields;
+ }
+
+ static function getCMSFields_forPopup() {
+ $fields = new FieldSet();
+
+ $fields->push(new TextField('Order'));
+ $fields->push(new TextField('Title', 'Answer option', '', 80));
+ $fields->push(new ReadonlyField('Votes'));
+
+ return $fields;
+ }
+
+ /**
+ * Increase vote by one and mark its poll has voted
+ */
+ function addVote() {
+ $poll = $this->Poll();
+
+ if($poll && !$poll->isVoted()) {
+ $this->Votes++;
+ $this->write();
+ $poll->markAsVoted();
+ }
+ }
+}
102 code/PollForm.php
@@ -0,0 +1,102 @@
+<?php
+class PollForm extends Form {
+
+ protected $poll = null;
+
+ function __construct($controller, $name, $pollid, $chartWidth = null, $chartHeight = null) {
+
+ if(!isset($chartWidth)) $chartWidth = 600;
+ if(!isset($chartHeight)) $chartHeight = 300;
+
+ $this->poll = DataObject::get_one('Poll', '"ID" = ' . $pollid . ' AND "IsActive" = 1');
+ if(!$this->poll) {
+ user_error("Poll with id \"$pollid\" doesn't exist or not active.", E_USER_ERROR);
+ }
+
+ if($this->poll->Image()->ID > 0) {
+ $imageTag = sprintf('<img class="poll-image" src="%s" />', $this->poll->Image()->SetWidth(168)->URL);
+ }
+ else {
+ $imageTag = '';
+ }
+
+ if($this->poll->isVoted()) {
+ $output = sprintf(
+ '<div class="poll-chart"><p class="poll-title">%s</p><p class="poll-description">%s</p><p><img src="%s" /></p></div>',
+ $this->poll->Title,
+ $this->poll->Description,
+ $this->poll->chartURL($chartWidth, $chartHeight)
+ );
+
+ $fields = new FieldSet(new LiteralField('PollGraph', $output));
+ $actions = new FieldSet();
+ }
+ else {
+ $choices = array();
+ foreach($this->poll->Choices() as $choice) {
+ $choices[$choice->ID] = $choice->Title;
+ }
+
+ $fields = $this->poll->getFormFields();
+
+ $output = sprintf(
+ '<p class="poll-title">%s</p><p class="poll-description">%s</p>',
+ $this->poll->Title,
+ $this->poll->Description
+ );
+
+ $fields->insertBefore(new LiteralField('Meta', $output), 'PollChoices');
+
+ if($imageTag) {
+ $this->addExtraClass('has-image');
+ $fields->insertBefore(new LiteralField(
+ 'PollImage',
+ $imageTag
+ ), 'PollChoices');
+ }
+
+ $actions = new FieldSet(
+ new FormAction('submitPoll', 'Submit',null, null, 'button')
+ );
+ }
+
+ $validator = new PollForm_Validator('PollChoices');
+
+ parent::__construct($controller, $name, $fields, $actions, $validator);
+ }
+
+ function submitPoll($data, $form) {
+ $pollid = $this->poll->ID;
+ $choiceIDs = is_array($data['PollChoices']) ? $data['PollChoices'] : array($data['PollChoices']);
+ $choicesIDs = implode(',', $choiceIDs);
+ $choices = DataObject::get('PollChoice', sprintf('"ID" IN (%s)', $choicesIDs));
+
+ if($choices) {
+ foreach($choices as $choice) {
+ $choice->addVote();
+ }
+ }
+
+ Director::redirectBack();
+ }
+}
+
+/**
+ * Customise the validation message. Also enforce at least one selection in multi-choice poll (checkboxes!)
+ */
+class PollForm_Validator extends RequiredFields {
+ function php($data) {
+ $this->form->Fields()->dataFieldByName('PollChoices')->setCustomValidationMessage('Please select at least one option.');
+ return parent::php($data);
+ }
+
+ function javascript() {
+ $js = <<<JS
+ $('PollForm_Poll_PollChoices').requiredErrorMsg = 'Please select at least one option.';
+ if (jQuery('#PollForm_Poll_PollChoices').find('input[checked]').length==0) {
+ validationError(jQuery('#PollForm_Poll_PollChoices')[0], 'Please select at least one option.', 'required');
+ }
+JS;
+ return $js . parent::javascript();
+ }
+}
34 tests/Base.yml
@@ -0,0 +1,34 @@
+Poll:
+ mobile-poll:
+ Title: iPhone, Android, others?
+ MultiChoice: 0
+ color-poll:
+ Titlle: What is your favorite color?
+ MultiChoice: 1
+
+PollChoice:
+ iphone:
+ Title: iPhone
+ Votes: 120
+ Poll: =>Poll.mobile-poll
+ android:
+ Title: Android
+ Votes: 80
+ Poll: =>Poll.mobile-poll
+ other-mobile:
+ Title: Others
+ Votes: 12
+ Poll: =>Poll.mobile-poll
+ red:
+ Title: Red
+ Votes: 6
+ Poll: =>Poll.color-poll
+ green:
+ Title: Green
+ Votes: 15
+ Poll: =>Poll.color-poll
+ blue:
+ Title: Blue
+ Votes: 30
+ Poll: =>Poll.color-poll
+
74 tests/functional/PollFormTest.php
@@ -0,0 +1,74 @@
+<?php
+class PollFormTest extends FunctionalTest {
+
+ static $use_draft_site = true;
+
+ function setUp() {
+ ManifestBuilder::load_test_manifest();
+ parent::setUp();
+
+ // Create sample poll and choices
+ $poll = new Poll();
+ $poll->Title = "Poll example";
+ $poll->IsActive = 1;
+ $poll->write();
+
+ $choice1 = new PollChoice();
+ $choice1->Title = "Choice one";
+ $choice1->PollID = $poll->ID;
+ $choice1->write();
+
+ $choice2 = new PollChoice();
+ $choice2->Title = "Choice two";
+ $choice2->PollID = $poll->ID;
+ $choice2->write();
+ }
+
+ function testFormSubmission() {
+ $poll = DataObject::get_one('Poll');
+ $choice1 = DataObject::get_one('PollChoice', '"Title" = \'Choice one\'');
+
+ $response = $this->get('TestPollForm_Controller');
+ $response = $this->submitForm(
+ 'PollForm_Form',
+ null,
+ array('PollChoices' => $choice1->ID)
+ );
+
+ $choice1 = DataObject::get_one('PollChoice', '"Title" = \'Choice one\'');
+ $choice2 = DataObject::get_one('PollChoice', '"Title" = \'Choice two\'');
+
+ $this->assertEquals(1, $choice1->Votes);
+ $this->assertEquals(0, $choice2->Votes);
+ }
+
+ function testDisplayChart() {
+ $poll = DataObject::get_one('Poll');
+ $response = $this->get('TestPollForm_Controller', '', '', array('SSPoll_' . $poll->ID => true));
+
+ $this->assertContains(
+ sprintf('<img src="%s"', $poll->chartURL(600,300)),
+ $response->getBody()
+ );
+ }
+}
+
+class TestPollForm_Controller extends ContentController implements TestOnly {
+
+ protected $template = 'TestPollForm';
+
+ static $url_handlers = array(
+ '$Action//$ID/$OtherID' => "handleAction",
+ );
+
+ function Link($action = null) {
+ return Controller::join_links('TestPollForm_Controller', $this->request->latestParam('Action'), $this->request->latestParam('ID'), $action);
+ }
+
+ function Form() {
+ $poll = DataObject::get_one('Poll');
+
+ return new PollForm($this, "Form", $poll->ID);
+ }
+
+}
17 tests/templates/TestPollForm.ss
@@ -0,0 +1,17 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
+<head>
+<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
+<title>$Title</title>
+<% base_tag %>
+</head>
+<body class="$CSSClasses">
+ <p>This is a test template for just displaying a poll form</p>
+
+ <div class="right">
+ $Form
+ </div>
+
+</body>
+</html>
21 tests/unit/PollTest.php
@@ -0,0 +1,21 @@
+<?php
+class PollTest extends SapphireTest {
+
+ static $fixture_file = 'polls/tests/Base.yml';
+
+ function testTotalVotes() {
+ $mobilePoll = $this->ObjFromFixture('Poll', 'mobile-poll');
+ $this->assertEquals(120 + 80 + 12, $mobilePoll->totalVotes());
+
+ $mobilePoll = $this->ObjFromFixture('Poll', 'color-poll');
+ $this->assertEquals(6 + 15 + 30, $mobilePoll->totalVotes());
+ }
+
+ function testChartURL() {
+ $mobilePoll = $this->ObjFromFixture('Poll', 'mobile-poll');
+ $this->assertEquals(
+ "https://chart.googleapis.com/chart?cht=p3&chs=300x200&chl=Android(80)|iPhone(120)|Others(12)&chd=t:80,120,12&chf=bg,s,00000000&chco=F9D42D",
+ $mobilePoll->chartURL('300', '200')
+ );
+ }
+}

0 comments on commit 279d545

Please sign in to comment.