From 90343d1e08d47d1ee21e82eac14dc5910ece17ec Mon Sep 17 00:00:00 2001 From: Florent Bruneau Date: Sun, 7 Nov 2010 22:54:18 +0100 Subject: [PATCH] More stuff for the new surveys. Signed-off-by: Florent Bruneau --- core | 2 +- modules/survey.php | 12 ++ modules/survey/answer.inc.php | 163 ++++++++++++++++++++++++++ modules/survey/question.inc.php | 178 +++++++++++++++++++++++++++++ modules/survey/survey.inc.php | 209 ++++++++++++++++++---------------- modules/survey/text.inc.php | 11 ++ templates/survey/question.section.tpl | 31 +++++ templates/survey/question.text.tpl | 28 +++++ templates/survey/vote.tpl | 13 +++ upgrade/1.1.0/10_surveys.sql | 12 +- 10 files changed, 558 insertions(+), 101 deletions(-) create mode 100644 modules/survey/answer.inc.php create mode 100644 modules/survey/question.inc.php create mode 100644 templates/survey/question.section.tpl create mode 100644 templates/survey/question.text.tpl diff --git a/core b/core index 23749c6..cb22cf2 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 23749c61b65d441b631e791005277ec1c69f4c83 +Subproject commit cb22cf2a3b34e4f263dcfb6b51396111f8d6b785 diff --git a/modules/survey.php b/modules/survey.php index 1c451bc..7a34573 100644 --- a/modules/survey.php +++ b/modules/survey.php @@ -85,6 +85,18 @@ class SurveyModule extends PLModule if (!$survey->canSee(S::user())) { return PL_FORBIDDEN; } + if (Post::has('vote')) { + $answers = Post::v('qid'); + $vote = $survey->vote(S::user(), $answers); + if (is_null($vote)) { + $page->kill("Tu n'as pas le droit de voter à ce sondage."); + } else if ($vote->inError()) { + $page->trigError("Certaines réponses sont invalides et doivent être corrigées"); + } else { + $vote->insert(true); + $page->trigSuccess("Ton vote a été enregistré"); + } + } $page->assign('survey', $survey); } } diff --git a/modules/survey/answer.inc.php b/modules/survey/answer.inc.php new file mode 100644 index 0000000..8781a6b --- /dev/null +++ b/modules/survey/answer.inc.php @@ -0,0 +1,163 @@ +survey = $survey; + $this->user = $user; + $this->sid = $survey->id; + } + + protected function postSave() + { + Platal::assert(!is_null($this->vid), "Cannot process a vote without its identifier"); + XDB::execute("REPLACE INTO survey_voters (sid, uid, vid) + VALUES ({?}, {?}, {?})", + $this->survey->id, $this->user->id(), + $this->survey->flags->hasFlag('anonymous') ? null : $this->vid); + + /* Save answers */ + $selector = new SurveyAnswer($this); + $selector->delete(); + + $answers = array(); + foreach ($this->answers as $key=>$answer) { + if (!is_null($answer)) { + $answer->vid = $this->vid; + $answers[] = $answer; + } + } + PlDBTableEntry::insertBatch($answers); + return true; + } + + protected function postFetch() + { + $selector = new SurveyAnswer($this); + foreach ($selector as $answer) { + $question = $this->survey->questionForId($answer->qid); + $this->answers[$answer->qid] = $answer; + } + return true; + } + + public function inError() + { + foreach ($this->answers as $answer) { + if ($answer->inError !== false) { + return true; + } + } + return false; + } + + public function getAnswer(SurveyQuestion $question) + { + if (!isset($this->answers[$question->qid])) { + $val = new SurveyAnswer($this); + $val->qid = $question->qid; + $this->answers[$question->qid] = $val; + } + return $this->answers[$question->qid]; + } + + public function export() + { + $export = array(); + foreach ($this->answers as $qid=>$answer) { + $export[$qid] = $answer->export(); + } + return $export; + } + + public static function getVote(Survey $survey, User $user, $fetchAnswers = true) + { + $vid = XDB::query('SELECT vid + FROM survey_voters + WHERE sid = {?} AND uid = {?}', + $survey->id, $user->id()); + if ($vid->numRows() == 0) { + $vote = new SurveyVote($survey, $user); + $vote->fetchAnswers = $fetchAnswers; + return $vote; + } + $vid = $vid->fetchOneCell(); + if (is_null($vid)) { + /* User already vote, but survey is anonymous and the vote + * cannot be changed + */ + return null; + } + $vote = new SurveyVote($survey, $user); + $vote->vid = $vid; + $vote->fetchAnswers = $fetchAnswers; + $vote->fetch(); + return $vote; + } +} + +class SurveyAnswer extends PlDBTableEntry +{ + public $inError = false; + public $vote; + + public function __construct(SurveyVote $vote) + { + parent::__construct('survey_vote_answers'); + $this->registerFieldFormatter('answer', 'JSonFieldFormatter'); + $this->vote = $vote; + if (!is_null($vote->vid)) { + $this->vid = $vote->vid; + } + } + + protected function preSave() + { + Platal::assert(!$this->inError, "Cannot save an invalid answer"); + $this->sid = $this->vote->sid; + $this->vid = $this->vote->vid; + return true; + } + + public function export() + { + $export = array(); + if (!is_null($this->answer)) { + $export['value'] = $this->answer->export(); + } + if ($this->inError !== false) { + $export['error'] = $this->inError; + } + return $export; + } +} + +// vim:set et sw=4 sts=4 ts=4 foldmethod=marker enc=utf-8: +?> diff --git a/modules/survey/question.inc.php b/modules/survey/question.inc.php new file mode 100644 index 0000000..9a92620 --- /dev/null +++ b/modules/survey/question.inc.php @@ -0,0 +1,178 @@ +registerFieldFormatter('parameters', 'JSonFieldFormatter'); + $this->survey = $survey; + } + + public function typedInstance() + { + $instance = self::instanceForType($this->survey, $this->type); + $instance->copy($this); + return $instance; + } + + public static function instanceForType(Survey $survey, $type) + { + require_once dirname(__FILE__) . '/' . $type . '.inc.php'; + $class = 'SurveyQuestion' . $type; + return new $class($survey); + } + + public function voteTemplate() + { + return 'survey/question.' . $this->type . '.tpl'; + } + + public function editTemplate() + { + return 'survey/edit.' . $this->type . '.tpl'; + } + + protected function buildAnswer(SurveyAnswer $answer, PlDict $answers) + { + Platal::assert(false, "This should not happen"); + } + + public function vote(SurveyVote $vote, PlDict $answers) + { + if ($this->flags->hasFlag('noanswer')) { + if ($answers->has($this->qid)) { + throw new Exception("Des réponses ont été données à une question n'en attendant pas"); + } + return null; + } + $answer = $vote->getAnswer($this); + if (is_null($answer)) { + return null; + } + if (!$this->buildAnswer($answer, $answers)) { + return $answer; + } + if ($this->flags->hasFlag('mandatory') && is_null($answer->answer)) { + $answer->inError = 'Tu dois répondre à cette question'; + } + return $answer; + } +} + +class SurveyQuestionGroup extends SurveyQuestion implements SurveyQuestionContainer +{ + public $children = array(); + + public function __construct(Survey $survey) + { + parent::__construct($survey); + } + + public function addQuestion(SurveyQuestion $question, $pos = null) + { + $question->parentQuestion = $this; + if (is_null($pos)) { + $this->children[] = $question; + } else { + array_splice($this->children, $pos, 0, $question); + } + } + + public function newQuestion($type, $pos = null) + { + $question = SurveyQuestion::instanceForType($this->survey, $type); + $this->addQuestion($question, $pos); + return $question; + } + + public function reassignQuestionIds() + { + $id = $this->qid + 1; + foreach ($this->children as $question) { + $question->qid = $id; + if ($question instanceof SurveyQuestionContainer) { + $id = $question->reassignQuestionIds(); + } else { + $id++; + } + } + return $id; + } + + protected function postSave() + { + foreach ($this->children as $question) { + $question->sid = $this->sid; + $question->parent = $this->qid; + $question->insert(); + } + } + + public function export() + { + $export = parent::export(); + $export['children'] = array(); + foreach ($this->children as $child) { + $export['children'][] = $child->export(); + } + return $export; + } + + public function vote(SurveyVote $vote, PlDict $answers) + { + $a = parent::vote($vote, $answers); + foreach ($this->children as $child) { + $child->vote($vote, $answers); + } + return $a; + } + + public function child($qid) + { + $prev = null; + foreach ($this->children as $question) { + if ($qid == $question->qid) { + return $question; + } else if ($qid < $question->qid) { + Platal::assert($prev instanceof SurveyQuestionGroup); + return $prev->child($qid); + } + $prev = $question; + } + Platal::assert($prev instanceof SurveyQuestionGroup); + return $prev->child($qid); + } +} + +// vim:set et sw=4 sts=4 ts=4 foldmethod=marker enc=utf-8: +?> diff --git a/modules/survey/survey.inc.php b/modules/survey/survey.inc.php index c96a44d..3812d2e 100644 --- a/modules/survey/survey.inc.php +++ b/modules/survey/survey.inc.php @@ -19,10 +19,15 @@ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * ***************************************************************************/ +require_once dirname(__FILE__) . '/question.inc.php'; +require_once dirname(__FILE__) . '/answer.inc.php'; + class Survey extends PlDBTableEntry implements SurveyQuestionContainer { private $fetchQuestions = true; public $questions = array(); + public $viewerFilter = null; + public $voterFilter = null; public function __construct() { @@ -34,6 +39,16 @@ class Survey extends PlDBTableEntry implements SurveyQuestionContainer protected function postFetch() { + if (!is_null($this->voters)) { + $this->voterFilter = UserFilter::fromExportedConditions($this->voters); + } else { + $this->voterFilter = null; + } + if (!is_null($this->viewers)) { + $this->viewerFilter = UserFilter::fromExportedConditions($this->viewers); + } else { + $this->viewerFilter = null; + } if (!$this->fetchQuestions) { return true; } @@ -60,6 +75,21 @@ class Survey extends PlDBTableEntry implements SurveyQuestionContainer return true; } + protected function preSave() + { + if (!is_null($this->voterFilter)) { + $this->voters = $this->voterFilter->exportConditions(); + } else { + $this->voters = null; + } + if (!is_null($this->viewerFilter)) { + $this->viewers = $this->viewerFilter->exportConditions(); + } else { + $this->viewers = null; + } + return true; + } + protected function postSave() { $questions = array(); @@ -91,6 +121,24 @@ class Survey extends PlDBTableEntry implements SurveyQuestionContainer return $question; } + public function questionForId($qid) + { + $prev = null; + foreach ($this->questions as $question) { + if ($qid == $question->qid) { + return $question; + } else if ($qid < $question->qid) { + Platal::assert($prev instanceof SurveyQuestionGroup, + "Id gap must be caused by question groups"); + return $prev->child($qid); + } + $prev = $question; + } + Platal::assert($prev instanceof SurveyQuestionGroup, + "Id gap must be caused by question groups"); + return $prev->child($qid); + } + public function reassignQuestionIds() { $id = 0; @@ -115,14 +163,79 @@ class Survey extends PlDBTableEntry implements SurveyQuestionContainer return $export; } + /* Return an indicator of the progression of the survey: + * negative values means 'the survey is not started' + * 0 means 'the survey is in progress' + * positive values means 'the survey expired' + */ + public function progression() + { + if (!$this->flags->hasFlag('validated')) { + return -2; + } + $now = time(); + if ($this->begin->format('U') > $now) { + return -1; + } + if ($this->end->format('U') <= $now) { + return 1; + } + return 0; + } + public function canSee(User $user) { - if ($this->uid == $user->id() || $user->hasFlag('admin')) { + if ($this->canSeeResults($user) || $this->canVote($user)) { return true; } return false; } + public function canSeeResults(User $user) + { + if ($user->id() == $this->uid || $user->hasFlag('admin')) { + return true; + } + if (is_null($this->viewerFilter)) { + return $this->viewerFilter->checkUser($user); + } + return false; + } + + public function canVote(User $user) + { + $status = $this->progression(); + if ($status < 0) { + return "Ce sondage n'est pas encore commencé"; + } else if ($status > 0) { + return "Ce sondage est terminé"; + } + if (!is_null($this->voterFilter) && !$this->voterFilter->checkUser($user)) { + return "Ce sondage ne s'adresse pas à toi"; + } + $vote = SurveyVote::getVote($this, $user, false); + if (is_null($vote)) { + return "Tu as déjà voté à ce sondage."; + } + return true; + } + + public function vote(User $user, array $answers) + { + if (!$this->canVote($user)) { + return array('survey' => "Tu n'es pas autorisé à voter à ce sondage."); + } + $vote = SurveyVote::getVote($this, $user); + if (is_null($vote)) { + return $vote; + } + $answers = new PlDict($answers); + foreach ($this->questions as $question) { + $question->vote($vote, $answers); + } + return $vote; + } + public static function get($name, $fetchQuestions = true) { if (is_array($name)) { @@ -161,99 +274,5 @@ class ShortNameFieldValidator implements PlDBTableFieldValidator } } -interface SurveyQuestionContainer -{ - public function addQuestion(SurveyQuestion $question, $pos = null); - public function newQuestion($type, $pos = null); - public function reassignQuestionIds(); -} - -class SurveyQuestion extends PlDBTableEntry -{ - protected $survey; - protected $parentQuestion; - - public function __construct(Survey $survey) - { - parent::__construct('survey_questions'); - $this->registerFieldFormatter('parameters', 'JSonFieldFormatter'); - $this->survey = $survey; - } - - public function typedInstance() - { - $instance = self::instanceForType($this->survey, $this->type); - $instance->copy($this); - return $instance; - } - - public static function instanceForType(Survey $survey, $type) - { - require_once dirname(__FILE__) . '/' . $type . '.inc.php'; - $class = 'SurveyQuestion' . $type; - return new $class($survey); - } -} - -class SurveyQuestionGroup extends SurveyQuestion implements SurveyQuestionContainer -{ - public $children = array(); - - public function __construct(Survey $survey) - { - parent::__construct($survey); - } - - public function addQuestion(SurveyQuestion $question, $pos = null) - { - $question->parentQuestion = $this; - if (is_null($pos)) { - $this->children[] = $question; - } else { - array_splice($this->children, $pos, 0, $question); - } - } - - public function newQuestion($type, $pos = null) - { - $question = SurveyQuestion::instanceForType($this->survey, $type); - $this->addQuestion($question, $pos); - return $question; - } - - public function reassignQuestionIds() - { - $id = $this->qid + 1; - foreach ($this->children as $question) { - $question->qid = $id; - if ($question instanceof SurveyQuestionContainer) { - $id = $question->reassignQuestionIds(); - } else { - $id++; - } - } - return $id; - } - - protected function postSave() - { - foreach ($this->children as $question) { - $question->sid = $this->sid; - $question->parent = $this->qid; - $question->insert(); - } - } - - public function export() - { - $export = parent::export(); - $export['children'] = array(); - foreach ($this->children as $child) { - $export['children'][] = $child->export(); - } - return $export; - } -} - // vim:set et sw=4 sts=4 ts=4 foldmethod=marker enc=utf-8: ?> diff --git a/modules/survey/text.inc.php b/modules/survey/text.inc.php index 5bb65d0..3bb8770 100644 --- a/modules/survey/text.inc.php +++ b/modules/survey/text.inc.php @@ -26,6 +26,17 @@ class SurveyQuestionText extends SurveyQuestion parent::__construct($survey); $this->type = "text"; } + + protected function buildAnswer(SurveyAnswer $answer, PlDict $data) + { + $value = $data->t($this->qid); + if (!empty($value)) { + $answer->answer = array('text' => $value); + } else { + $answer->answer = null; + } + return true; + } } // vim:set et sw=4 sts=4 ts=4 foldmethod=marker enc=utf-8: diff --git a/templates/survey/question.section.tpl b/templates/survey/question.section.tpl new file mode 100644 index 0000000..acdf836 --- /dev/null +++ b/templates/survey/question.section.tpl @@ -0,0 +1,31 @@ +{**************************************************************************} +{* *} +{* Copyright (C) 2003-2010 Polytechnique.org *} +{* http://opensource.polytechnique.org/ *} +{* *} +{* This program is free software; you can redistribute it and/or modify *} +{* it under the terms of the GNU General Public License as published by *} +{* the Free Software Foundation; either version 2 of the License, or *} +{* (at your option) any later version. *} +{* *} +{* This program is distributed in the hope that it will be useful, *} +{* but WITHOUT ANY WARRANTY; without even the implied warranty of *} +{* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *} +{* GNU General Public License for more details. *} +{* *} +{* You should have received a copy of the GNU General Public License *} +{* along with this program; if not, write to the Free Software *} +{* Foundation, Inc., *} +{* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *} +{* *} +{**************************************************************************} + +
+ {$question->label} + +{foreach from=$question->children item=child} + {include file=$child->voteTemplate() question=$child} +{/foreach} +
+ +{* vim:set et sw=2 sts=2 ts=8 enc=utf-8: *} diff --git a/templates/survey/question.text.tpl b/templates/survey/question.text.tpl new file mode 100644 index 0000000..a843c01 --- /dev/null +++ b/templates/survey/question.text.tpl @@ -0,0 +1,28 @@ +{**************************************************************************} +{* *} +{* Copyright (C) 2003-2010 Polytechnique.org *} +{* http://opensource.polytechnique.org/ *} +{* *} +{* This program is free software; you can redistribute it and/or modify *} +{* it under the terms of the GNU General Public License as published by *} +{* the Free Software Foundation; either version 2 of the License, or *} +{* (at your option) any later version. *} +{* *} +{* This program is distributed in the hope that it will be useful, *} +{* but WITHOUT ANY WARRANTY; without even the implied warranty of *} +{* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *} +{* GNU General Public License for more details. *} +{* *} +{* You should have received a copy of the GNU General Public License *} +{* along with this program; if not, write to the Free Software *} +{* Foundation, Inc., *} +{* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *} +{* *} +{**************************************************************************} + +
+
{$question->label}
+ +
+ +{* vim:set et sw=2 sts=2 ts=8 enc=utf-8: *} diff --git a/templates/survey/vote.tpl b/templates/survey/vote.tpl index 6ba0d81..9c927ae 100644 --- a/templates/survey/vote.tpl +++ b/templates/survey/vote.tpl @@ -24,4 +24,17 @@

{$survey->description|miniwiki}

+
+
+ {foreach from=$survey->questions item=question} + {include file=$question->voteTemplate() question=$question} + {/foreach} + +
+ {xsrf_token_field} + +
+
+
+ {* vim:set et sw=2 sts=2 ts=8 enc=utf-8: *} diff --git a/upgrade/1.1.0/10_surveys.sql b/upgrade/1.1.0/10_surveys.sql index 010a72b..e6e5f47 100644 --- a/upgrade/1.1.0/10_surveys.sql +++ b/upgrade/1.1.0/10_surveys.sql @@ -16,12 +16,11 @@ CREATE TABLE surveys ( description TEXT NOT NULL, begin DATE NOT NULL, end DATE NOT NULL, - anonymous TINYINT(1) DEFAULT 0, voters TEXT DEFAULT NULL COMMENT "Filter users who can vote", viewers TEXT DEFAULT NULL COMMENT "Filter users who can see the results", - flags SET('validated'), + flags SET('validated', 'anonymous'), PRIMARY KEY id (id), UNIQUE KEY shortname (shortname), @@ -47,10 +46,10 @@ CREATE TABLE survey_questions ( ) ENGINE=InnoDB, CHARSET=utf8, COMMENT="Describe the questions of the surveys"; CREATE TABLE survey_votes ( + vid INT(11) UNSIGNED NOT NULL auto_increment, sid INT(11) UNSIGNED NOT NULL, - vid INT(11) UNSIGNED NOT NULL, - PRIMARY KEY id (sid, vid), + PRIMARY KEY vid (vid), FOREIGN KEY (sid) REFERENCES surveys (id) ON UPDATE CASCADE ON DELETE CASCADE @@ -72,12 +71,15 @@ CREATE TABLE survey_voters ( CREATE TABLE survey_vote_answers ( sid INT(11) UNSIGNED NOT NULL, - vid INT(11) UNSIGNED NOT NULL, qid INT(11) UNSIGNED NOT NULL, + vid INT(11) UNSIGNED NOT NULL, answer TEXT DEFAULT NULL, PRIMARY KEY id (sid, vid, qid), + FOREIGN KEY (vid) REFERENCES survey_votes (vid) + ON UPDATE CASCADE + ON DELETE CASCADE, FOREIGN KEY (sid, qid) REFERENCES survey_questions (sid, qid) ON UPDATE CASCADE ON DELETE CASCADE -- 2.1.4