diff --git a/.travis.yml b/.travis.yml
index d3aa010937..00b520b42e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,15 +1,7 @@
sudo: false
-notifications:
- hipchat:
- rooms:
- secure: fb2eYR4DVBestPl/DSG7lnUWT46Rmr9BfRsZFe4dhrS3ZetkZ1XZueF6SCtB4yukaJoDlA7y98FgQ7eGx6OGToc4ALnPpwd3BdfJ5RV/PK/6LG7u5Mp4+DfB4lG1q9IjuHF7dhBPSk2sRd8ewtwV5JR4SHKYfHxSR9Ekwmn1Xyo=
- template:
- - '%{repository}#%{build_number} (%{branch} - %{commit}
- : %{author}): %{message}'
- format: html
- notify: true
- slack:
- secure: NxhzhKGYnZYZiWVZM4w1PlZpTy9bbmNlvbqjL+9d4PvK4hFzPwQU12CchhBPeXNGLWBHg/ti6Scn/t5XIS3YJrkk4ydWmJBht5UId8uFZ1A7GCUnNNPTt2RG55lbJJdZSj01rOGZjEQbE5RyckEjgRzhZjdZ+HsjoPaRzWIBrvE=
+#notifications:
+# slack:
+# secure: NxhzhKGYnZYZiWVZM4w1PlZpTy9bbmNlvbqjL+9d4PvK4hFzPwQU12CchhBPeXNGLWBHg/ti6Scn/t5XIS3YJrkk4ydWmJBht5UId8uFZ1A7GCUnNNPTt2RG55lbJJdZSj01rOGZjEQbE5RyckEjgRzhZjdZ+HsjoPaRzWIBrvE=
language: php
php:
- '5.5'
diff --git a/application/classes/Ushahidi/Console/Webhook.php b/application/classes/Ushahidi/Console/Webhook.php
index 3126844583..98f8f27be5 100644
--- a/application/classes/Ushahidi/Console/Webhook.php
+++ b/application/classes/Ushahidi/Console/Webhook.php
@@ -126,7 +126,7 @@ private function generateRequest($webhook_request)
// This is an asynchronous request, we don't expect a result
// this can be extended to allow for handling of the returned promise
- $promise = $this->client->requestAsync('POST', $webhook->url, [
+ $promise = $this->client->request('POST', $webhook->url, [
'headers' => [
'X-Platform-Signature' => $signature,
'Accept' => 'application/json'
diff --git a/application/classes/Ushahidi/Core.php b/application/classes/Ushahidi/Core.php
index 55f3c578f2..4774fbf91c 100644
--- a/application/classes/Ushahidi/Core.php
+++ b/application/classes/Ushahidi/Core.php
@@ -351,53 +351,8 @@ public static function init()
// Set Formatter factory
$di->params['Ushahidi\Factory\FormatterFactory']['factory'] = $di->newFactory('Ushahidi_Formatter_Collection');
- // Helpers, tools, etc
- $di->set('tool.hasher.password', $di->lazyNew('Ushahidi_Hasher_Password'));
- $di->set('tool.signer', $di->lazyNew('Ushahidi\Core\Tool\Signer'));
- $di->set('tool.authenticator.password', $di->lazyNew('Ushahidi_Authenticator_Password'));
-
$di->set('tool.validation', $di->lazyNew('Ushahidi_ValidationEngine'));
$di->set('tool.jsontranscode', $di->lazyNew('Ushahidi\Core\Tool\JsonTranscode'));
- $di->set('tool.acl', $di->lazyNew('Ushahidi_Acl'));
- $di->setter['Ushahidi_Acl']['setRoleRepo'] = $di->lazyGet('repository.role');
-
- // Register filesystem adpater types
- // Currently supported: Local filesysten, AWS S3 v3, Rackspace
- // the naming scheme must match the cdn type set in config/cdn
- $di->set('adapter.local', $di->lazyNew(
- 'Ushahidi_FilesystemAdapter_Local',
- [
- 'config' => $di->lazyGet('cdn.config')
- ]
- )
- );
- $di->set('adapter.aws', $di->lazyNew(
- 'Ushahidi_FilesystemAdapter_AWS',
- [
- 'config' => $di->lazyGet('cdn.config')
- ]
- )
- );
- $di->set('adapter.rackspace', $di->lazyNew(
- 'Ushahidi_FilesystemAdapter_Rackspace',
- [
- 'config' => $di->lazyGet('cdn.config')
- ]
- )
- );
-
- // Media Filesystem
- // The Ushahidi filesystem adapter returns a flysystem adapter for a given
- // cdn type based on the provided configuration
- $di->set('tool.filesystem', $di->lazyNew('Ushahidi_Filesystem'));
- $di->params['Ushahidi_Filesystem'] = [
- 'adapter' => $di->lazy(function () use ($di) {
- $adapter_type = $di->get('cdn.config');
- $fsa = $di->get('adapter.' . $adapter_type['type']);
-
- return $fsa->getAdapter();
- })
- ];
// Formatters
$di->set('formatter.entity.api', $di->lazyNew('Ushahidi_Formatter_API'));
@@ -472,6 +427,17 @@ public static function init()
'upload' => $di->lazyGet('tool.uploader'),
];
+ // Form Stage repository parameters
+ $di->params['Ushahidi_Repository_Form_Stage'] = [
+ 'form_repo' => $di->lazyGet('repository.form')
+ ];
+
+ // Form Attribute repository parameters
+ $di->params['Ushahidi_Repository_Form_Attribute'] = [
+ 'form_stage_repo' => $di->lazyGet('repository.form_stage'),
+ 'form_repo' => $di->lazyGet('repository.form')
+ ];
+
// Post repository parameters
$di->params['Ushahidi_Repository_Post'] = [
'form_attribute_repo' => $di->lazyGet('repository.form_attribute'),
@@ -572,7 +538,8 @@ public static function init()
'user_repo' => $di->lazyGet('repository.user'),
'collection_repo' => $di->lazyGet('repository.set'),
'savedsearch_repo' => $di->lazyGet('repository.savedsearch'),
- ];$di->params['Ushahidi_Validator_Webhook_Update'] = [
+ ];
+ $di->params['Ushahidi_Validator_Webhook_Update'] = [
'user_repo' => $di->lazyGet('repository.user'),
];
$di->params['Ushahidi_Validator_SavedSearch_Create'] = [
@@ -663,7 +630,7 @@ public static function init()
'point' => $di->lazyGet('validator.post.point'),
'relation' => $di->lazyGet('validator.post.relation'),
'varchar' => $di->lazyGet('validator.post.varchar'),
- 'markdown' => $di->lazyGet('validator.post.markdown'),
+ 'markdown' => $di->lazyGet('validator.post.markdown'),
'title' => $di->lazyGet('validator.post.title'),
'media' => $di->lazyGet('validator.post.media'),
'video' => $di->lazyGet('validator.post.video'),
@@ -680,12 +647,6 @@ public static function init()
$di->setter['Ushahidi_Transformer_CSVPostTransformer']['setRepo'] =
$di->lazyGet('repository.post');
- $di->set('filereader.csv', $di->lazyNew('Ushahidi_FileReader_CSV'));
- $di->setter['Ushahidi_FileReader_CSV']['setReaderFactory'] =
- $di->lazyGet('csv.reader_factory');
-
- $di->set('csv.reader_factory', $di->lazyNew('Ushahidi_CSVReaderFactory'));
-
$di->set('tool.mailer', $di->lazyNew('Ushahidi_Mailer'));
// Event listener for the Set repo
@@ -711,70 +672,6 @@ public static function init()
$di->setter['Ushahidi_Listener_PostListener']['setWebhookRepo'] =
$di->lazyGet('repository.webhook');
- // Defined memcached
- $di->set('memcached', $di->lazy(function () use ($di) {
- $config = $di->get('ratelimiter.config');
-
- $memcached = new Memcached();
- $memcached->addServer($config['memcached']['host'], $config['memcached']['port']);
-
- return $memcached;
- }));
-
- // Set up login rate limiter
- $di->set('ratelimiter.login.flap', $di->lazyNew('BehEh\Flaps\Flap'));
-
- $di->params['BehEh\Flaps\Flap'] = [
- 'storage' => $di->lazyNew('BehEh\Flaps\Storage\DoctrineCacheAdapter'),
- 'name' => 'login'
- ];
-
- $di->set('ratelimiter.login.strategy', $di->lazyNew('BehEh\Flaps\Throttling\LeakyBucketStrategy'));
-
- // 3 requests every 1 minute by default
- $di->params['BehEh\Flaps\Throttling\LeakyBucketStrategy'] = [
- 'requests' => 3,
- 'timeSpan' => '1m'
- ];
-
- $di->set('ratelimiter.login', $di->lazyNew('Ushahidi_RateLimiter'));
-
- $di->params['Ushahidi_RateLimiter'] = [
- 'flap' => $di->lazyGet('ratelimiter.login.flap'),
- 'throttlingStrategy' => $di->lazyGet('ratelimiter.login.strategy'),
- ];
-
- $di->params['BehEh\Flaps\Storage\DoctrineCacheAdapter'] = [
- 'cache' => $di->lazyGet('ratelimiter.cache')
- ];
-
- // Rate limit storage cache
- $di->set('ratelimiter.cache', function() use ($di) {
- $config = $di->get('ratelimiter.config');
- $cache = $config['cache'];
-
- if ($cache === 'memcached') {
- $di->setter['Doctrine\Common\Cache\MemcachedCache']['setMemcached'] =
- $di->lazyGet('memcached');
-
- return $di->newInstance('Doctrine\Common\Cache\MemcachedCache');
- }
- elseif ($cache === 'filesystem') {
- $di->params['Doctrine\Common\Cache\FilesystemCache'] = [
- 'directory' => $config['filesystem']['directory'],
- ];
-
- return $di->newInstance('Doctrine\Common\Cache\FilesystemCache');
- }
-
- // Fall back to using in-memory cache if none is configured
- return $di->newInstance('Doctrine\Common\Cache\ArrayCache');
- });
-
- // Rate limiter violation handler
- $di->setter['BehEh\Flaps\Flap']['setViolationHandler'] =
- $di->lazyNew('Ushahidi_ThrottlingViolationHandler');
-
/**
* 1. Load the plugins
*/
diff --git a/application/classes/Ushahidi/Formatter/Post/CSV.php b/application/classes/Ushahidi/Formatter/Post/CSV.php
index 899a5af7ac..fbcf155cc8 100644
--- a/application/classes/Ushahidi/Formatter/Post/CSV.php
+++ b/application/classes/Ushahidi/Formatter/Post/CSV.php
@@ -88,7 +88,6 @@ protected function generateCSVRecords($records)
// Sort the keys so that they match with columns from the CSV heading
ksort($record);
- Kohana::$log->add(Log::ERROR, print_r($record, true));
fputcsv($fp, $record);
}
diff --git a/application/classes/Ushahidi/Formatter/Tag.php b/application/classes/Ushahidi/Formatter/Tag.php
index c23d171618..d603131ed8 100644
--- a/application/classes/Ushahidi/Formatter/Tag.php
+++ b/application/classes/Ushahidi/Formatter/Tag.php
@@ -21,4 +21,15 @@ protected function format_color($value)
$value = ltrim($value, '#');
return $value ? '#' . $value : null;
}
+
+ protected function format_forms($forms)
+ {
+ $output = [];
+ foreach ($forms as $formid)
+ {
+ $output[] = $this->get_relation('forms', $formid);
+ }
+
+ return $output;
+ }
}
diff --git a/application/classes/Ushahidi/FormsTagsTrait.php b/application/classes/Ushahidi/FormsTagsTrait.php
new file mode 100644
index 0000000000..20fdbc5272
--- /dev/null
+++ b/application/classes/Ushahidi/FormsTagsTrait.php
@@ -0,0 +1,122 @@
+
+ * @package Ushahidi\Application\Controllers
+ * @copyright 2013 Ushahidi
+ * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3)
+*/
+
+trait Ushahidi_FormsTagsTrait
+{
+ //returning forms for a specific Tag-id
+ private function getFormsForTag($id)
+ {
+ $result = DB::select('form_id')
+ ->from('forms_tags')
+ ->where('tag_id', '=', $id)
+ ->execute($this->db);
+ return $result->as_array(NULL, 'form_id');
+ }
+ //returning tags for a specific Form-id
+ private function getTagsForForm($id)
+ {
+ $result = DB::select('tag_id')
+ ->from('forms_tags')
+ ->where('form_id', '=', $id)
+ ->execute($this->db);
+ return $result->as_array(NULL, 'tag_id');
+ }
+
+ // updating/adding tags to a form
+ private function updateFormsTags($form_id, $tags)
+ {
+ if (!$tags) {
+ DB::delete('forms_tags')
+ ->where('form_id', '=', $form_id)
+ ->execute($this->db);
+ } else if ($tags) {
+ $existing = $this->getTagsForForm($form_id);
+ $insert = DB::insert('forms_tags', ['form_id', 'tag_id']);
+ $tag_ids = [];
+ $new_tags = FALSE;
+ foreach ($tags as $tag) {
+ if (!in_array($tag, $existing)) {
+ $insert->values([$form_id, $tag]);
+ $new_tags = TRUE;
+ }
+ $tag_ids[] = $tag;
+ }
+ if ($new_tags) {
+ $insert->execute($this->db);
+ }
+ if (!empty($tag_ids)) {
+ DB::delete('forms_tags')
+ ->where('tag_id', 'NOT IN', $tag_ids)
+ ->and_where('form_id', '=', $form_id)
+ ->execute($this->db);
+ }
+ }
+ }
+
+ //updating/adding forms to a tag
+ private function updateTagForms($tag_id, $forms)
+ {
+ if (empty($forms)) {
+ DB::delete('forms_tags')
+ ->where('tag_id', '=', $tag_id)
+ ->execute($this->db);
+ } else {
+ $existing = $this->getFormsForTag($tag_id);
+ $insert = DB::insert('forms_tags', ['form_id', 'tag_id']);
+ $form_ids = [];
+ $new_forms = FALSE;
+ foreach ($forms as $form) {
+ if (isset($form['id'])) {
+ $id = $form['id'];
+ } else {
+ $id = $form;
+ }
+ if (!in_array($form, $existing)) {
+ $insert->values([$id, $tag_id]);
+ $new_forms = TRUE;
+ }
+ $form_ids[] = $id;
+ }
+
+ if ($new_forms) {
+ $insert->execute($this->db);
+ }
+
+ if (!empty($form_ids)) {
+ DB::delete('forms_tags')
+ ->where('form_id', 'NOT IN', $form_ids)
+ ->and_where('tag_id', '=', $tag_id)
+ ->execute($this->db);
+ }
+ }
+ }
+
+ private function updateFormAttributes($id)
+ {
+ $attr = DB::select('id', 'options')
+ ->from('form_attributes')
+ ->where('input', '=', 'tags')
+ ->execute($this->db)
+ ->as_array('id', 'options');
+ foreach ($attr as $attr_id => $value) {
+ $value = json_decode($value);
+ if (in_array($id, $value)) {
+ $index = array_search($id, $value);
+ array_splice($value, $index, 1);
+ $value = json_encode($value);
+ DB::update('form_attributes')
+ ->set(array('options' => $value))
+ ->where('id', '=', $attr_id)
+ ->execute($this->db);
+ }
+ }
+ }
+}
diff --git a/application/classes/Ushahidi/Repository/Form.php b/application/classes/Ushahidi/Repository/Form.php
index 61f14a66d4..cc240e9e94 100644
--- a/application/classes/Ushahidi/Repository/Form.php
+++ b/application/classes/Ushahidi/Repository/Form.php
@@ -17,6 +17,8 @@
class Ushahidi_Repository_Form extends Ushahidi_Repository implements
FormRepository
{
+ use Ushahidi_FormsTagsTrait;
+
// Ushahidi_Repository
protected function getTable()
{
@@ -27,11 +29,12 @@ protected function getTable()
// ReadRepository
public function getEntity(Array $data = null)
{
- if (isset($data["id"])) {
+ if (isset($data["id"])) {
$can_create = $this->getRolesThatCanCreatePosts($data['id']);
$data = $data + [
- 'can_create' => $can_create['roles'],
+ 'can_create' => $can_create['roles'],
];
+ $data['tags'] = $this->getTagsForForm($data['id']);
}
return new Form($data);
}
@@ -46,7 +49,6 @@ public function getSearchFields()
protected function setSearchConditions(SearchData $search)
{
$query = $this->search_query;
-
if ($search->parent) {
$query->where('parent_id', '=', $search->parent);
}
@@ -60,15 +62,31 @@ protected function setSearchConditions(SearchData $search)
// CreateRepository
public function create(Entity $entity)
{
- // todo ensure default group is created
- return parent::create($entity->setState(['created' => time()]));
+ $tags = $entity->tags;
+ unset($entity->tags);
+ $id = parent::create($entity->setState(['created' => time()]));
+ //updating forms_tags-table
+ if ($tags && $id !== null) {
+ $this->updateFormsTags($id, $tags);
+ }
+ // todo ensure default group is created
+ return $id;
}
// UpdateRepository
public function update(Entity $entity)
{
- return parent::update($entity->setState(['updated' => time()]));
+ $tags = $entity->tags;
+ unset($entity->tags);
+ unset($entity->children);
+ $id = parent::update($entity->setState(['updated' => time()]));
+ // updating forms_tags-table
+ if ($tags && $entity->id !== null) {
+ $this->updateFormsTags($entity->id, $tags);
+ }
+
+ return $id;
}
/**
@@ -81,6 +99,23 @@ public function getTotalCount(Array $where = [])
return $this->selectCount($where);
}
+ /**
+ * Get value of Form property hide_author
+ * if no form is found return false
+ * @param $form_id
+ * @return Boolean
+ */
+ public function isAuthorHidden($form_id)
+ {
+ $query = DB::select('hide_author')
+ ->from('forms')
+ ->where('id', '=', $form_id);
+
+ $results = $query->execute($this->db)->as_array();
+
+ return count($results) > 0 ? $results[0]['hide_author'] : false;
+ }
+
/**
* Get `everyone_can_create` and list of roles that have access to post to the form
* @param $form_id
@@ -103,7 +138,7 @@ public function getRolesThatCanCreatePosts($form_id)
$roles = [];
- foreach($results as $role) {
+ foreach ($results as $role) {
if (!is_null($role['name'])) {
$roles[] = $role['name'];
}
@@ -113,7 +148,5 @@ public function getRolesThatCanCreatePosts($form_id)
'everyone_can_create' => $everyone_can_create,
'roles' => $roles,
];
-
}
-
}
diff --git a/application/classes/Ushahidi/Repository/Form/Attribute.php b/application/classes/Ushahidi/Repository/Form/Attribute.php
index 27328d2745..ba15c0fa1e 100644
--- a/application/classes/Ushahidi/Repository/Form/Attribute.php
+++ b/application/classes/Ushahidi/Repository/Form/Attribute.php
@@ -13,6 +13,10 @@
use Ushahidi\Core\SearchData;
use Ushahidi\Core\Entity\FormAttribute;
use Ushahidi\Core\Entity\FormAttributeRepository;
+use Ushahidi\Core\Entity\FormStageRepository;
+use Ushahidi\Core\Entity\FormRepository;
+use Ushahidi\Core\Traits\PostValueRestrictions;
+use Ushahidi\Core\Traits\UserContext;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\Exception\UnsatisfiedDependencyException;
@@ -20,8 +24,38 @@
class Ushahidi_Repository_Form_Attribute extends Ushahidi_Repository implements
FormAttributeRepository
{
+ use UserContext;
+
+ use PostValueRestrictions;
+
+ protected $form_stage_repo;
+
+ protected $form_repo;
+
+ protected $form_id;
+
// Use the JSON transcoder to encode properties
use Ushahidi_JsonTranscodeRepository;
+ use Ushahidi_FormsTagsTrait;
+
+ /**
+ * Construct
+ * @param Database $db
+ * @param FormStageRepository $form_stage_repo
+ * @param FormRepository $form_repo
+ */
+ public function __construct(
+ Database $db,
+ FormStageRepository $form_stage_repo,
+ FormRepository $form_repo
+ )
+ {
+ parent::__construct($db);
+
+ $this->form_stage_repo = $form_stage_repo;
+ $this->form_repo = $form_repo;
+
+ }
// Ushahidi_JsonTranscodeRepository
protected function getJsonProperties()
@@ -29,6 +63,27 @@ protected function getJsonProperties()
return ['options', 'config'];
}
+ protected function getFormId($form_stage_id)
+ {
+ $form_id = $this->form_stage_repo->getFormByStageId($form_stage_id);
+ if ($form_id) {
+ return $form_id;
+ }
+ return null;
+ }
+
+ // Override selectQuery to fetch attribute 'key' too
+ protected function selectQuery(Array $where = [], $form_id = null, $form_stage_id = null)
+ {
+ $query = parent::selectQuery($where);
+
+ if (!$form_id && $form_stage_id) {
+ $form_id = $this->getFormId();
+ }
+
+ return $query;
+ }
+
// CreateRepository
public function create(Entity $entity)
{
@@ -42,6 +97,37 @@ public function create(Entity $entity)
}
return $this->executeInsertAttribute($this->removeNullValues($record));
}
+
+ // Override SearchRepository
+ public function setSearchParams(SearchData $search)
+ {
+ $form_id = null;
+ if ($search->form_id) {
+ $form_id = $search->form_id;
+ }
+
+ $this->search_query = $this->selectQuery([], $form_id);
+
+ $sorting = $search->getSorting();
+
+ if (!empty($sorting['orderby'])) {
+ $this->search_query->order_by(
+ $this->getTable() . '.' . $sorting['orderby'],
+ Arr::get($sorting, 'order')
+ );
+ }
+
+ if (!empty($sorting['offset'])) {
+ $this->search_query->offset($sorting['offset']);
+ }
+
+ if (!empty($sorting['limit'])) {
+ $this->search_query->limit($sorting['limit']);
+ }
+
+ // apply the unique conditions of the search
+ $this->setSearchConditions($search);
+ }
// SearchRepository
protected function setSearchConditions(SearchData $search)
@@ -84,7 +170,8 @@ public function getSearchFields()
// FormAttributeRepository
public function getByKey($key, $form_id = null, $include_no_form = false)
{
- $query = $this->selectQuery()
+
+ $query = $this->selectQuery([], $form_id)
->select('form_attributes.*')
->join('form_stages', 'LEFT')
->on('form_stages.id', '=', 'form_attributes.form_stage_id')
@@ -122,7 +209,7 @@ public function getByForm($form_id)
{
$query = $this->selectQuery([
'form_stages.form_id' => $form_id,
- ])
+ ], $form_id)
->select('form_attributes.*')
->join('form_stages', 'INNER')
->on('form_stages.id', '=', 'form_attributes.form_stage_id');
@@ -135,10 +222,12 @@ public function getByForm($form_id)
// FormAttributeRepository
public function getRequired($stage_id)
{
+ $form_id = $this->getFormId($stage_id);
+
$query = $this->selectQuery([
'form_attributes.form_stage_id' => $stage_id,
'form_attributes.required' => true
- ])
+ ], $form_id)
->select('form_attributes.*');
$results = $query->execute($this->db);
diff --git a/application/classes/Ushahidi/Repository/Form/Stage.php b/application/classes/Ushahidi/Repository/Form/Stage.php
index 7bdb52760b..86c0139032 100644
--- a/application/classes/Ushahidi/Repository/Form/Stage.php
+++ b/application/classes/Ushahidi/Repository/Form/Stage.php
@@ -13,16 +13,68 @@
use Ushahidi\Core\SearchData;
use Ushahidi\Core\Entity\FormStage;
use Ushahidi\Core\Entity\FormStageRepository;
+use Ushahidi\Core\Entity\FormRepository;
+use Ushahidi\Core\Traits\PostValueRestrictions;
+use Ushahidi\Core\Traits\UserContext;
+
+use Ushahidi\Core\Traits\AdminAccess;
+use Ushahidi\Core\Traits\PermissionAccess;
+use Ushahidi\Core\Traits\Permissions\ManagePosts;
class Ushahidi_Repository_Form_Stage extends Ushahidi_Repository implements
FormStageRepository
{
+ use UserContext;
+
+ use PostValueRestrictions;
+
+ // Checks if user is Admin
+ use AdminAccess;
+
+ // Provides `hasPermission`
+ use PermissionAccess;
+
+ // Provides `getPermission`
+ use ManagePosts;
+
+ protected $form_id;
+ protected $form_repo;
+
+ /**
+ * Construct
+ * @param Database $db
+ * @param FormRepository $form_repo
+ */
+ public function __construct(
+ Database $db,
+ FormRepository $form_repo
+ )
+ {
+ parent::__construct($db);
+
+ $this->form_repo = $form_repo;
+
+ }
+
// Ushahidi_Repository
protected function getTable()
{
return 'form_stages';
}
+ // Override selectQuery to fetch attribute 'key' too
+ protected function selectQuery(Array $where = [], $form_id = null)
+ {
+ $query = parent::selectQuery($where);
+
+ $user = $this->getUser();
+ if (!$this->canUserEditForm($form_id, $user)) {
+ $query->where('show_when_published', '=', "1");
+ }
+
+ return $query;
+ }
+
// CreateRepository
// ReadRepository
public function getEntity(Array $data = null)
@@ -36,6 +88,37 @@ public function getSearchFields()
return ['form_id', 'label'];
}
+ // Override SearchRepository
+ public function setSearchParams(SearchData $search)
+ {
+ $form_id = null;
+ if ($search->form_id) {
+ $form_id = $search->form_id;
+ }
+
+ $this->search_query = $this->selectQuery([], $form_id);
+
+ $sorting = $search->getSorting();
+
+ if (!empty($sorting['orderby'])) {
+ $this->search_query->order_by(
+ $this->getTable() . '.' . $sorting['orderby'],
+ Arr::get($sorting, 'order')
+ );
+ }
+
+ if (!empty($sorting['offset'])) {
+ $this->search_query->offset($sorting['offset']);
+ }
+
+ if (!empty($sorting['limit'])) {
+ $this->search_query->limit($sorting['limit']);
+ }
+
+ // apply the unique conditions of the search
+ $this->setSearchConditions($search);
+ }
+
// Ushahidi_Repository
protected function setSearchConditions(SearchData $search)
{
@@ -51,15 +134,51 @@ protected function setSearchConditions(SearchData $search)
}
}
+ public function getFormByStageId($id)
+ {
+ $query = DB::select('form_id')
+ ->from('form_stages')
+ ->where('id', '=', $id);
+
+ $results = $query->execute($this->db);
+
+ return count($results) > 0 ? $results[0]['form_id'] : false;
+ }
+
// FormStageRepository
public function getByForm($form_id)
{
- $query = $this->selectQuery(compact($form_id));
+ $query = $this->selectQuery(compact($form_id), $form_id);
$results = $query->execute($this->db);
return $this->getCollection($results->as_array());
}
+ /**
+ * Retrieve Hidden Stage IDs for a given form
+ * if no form is found return false
+ * @param $form_id
+ * @return Array
+ */
+ public function getHiddenStageIds($form_id)
+ {
+ $stages = [];
+
+ $query = DB::select('id')
+ ->from('form_stages')
+
+ ->where('form_id', '=', $form_id)
+ ->where('show_when_published', '=', 0);
+
+ $results = $query->execute($this->db)->as_array();
+
+ foreach($results as $stage) {
+ array_push($stages, $stage['id']);
+ }
+
+ return $stages;
+ }
+
// FormStageRepository
public function existsInForm($id, $form_id)
{
@@ -72,7 +191,7 @@ public function getRequired($form_id)
$query = $this->selectQuery([
'form_stages.form_id' => $form_id,
'form_stages.required' => true
- ])
+ ], $form_id)
->select('form_stages.*');
$results = $query->execute($this->db);
diff --git a/application/classes/Ushahidi/Repository/Post.php b/application/classes/Ushahidi/Repository/Post.php
index 4795240d00..e875ee555f 100644
--- a/application/classes/Ushahidi/Repository/Post.php
+++ b/application/classes/Ushahidi/Repository/Post.php
@@ -28,6 +28,7 @@
use Ushahidi\Core\Traits\PermissionAccess;
use Ushahidi\Core\Traits\AdminAccess;
use Ushahidi\Core\Tool\Permissions\Permissionable;
+use Ushahidi\Core\Traits\PostValueRestrictions;
use Aura\DI\InstanceFactory;
@@ -57,15 +58,22 @@ class Ushahidi_Repository_Post extends Ushahidi_Repository implements
// Checks if user is Admin
use AdminAccess;
+ // Check for value restrictions
+ // provides canUserReadPostsValues
+ use PostValueRestrictions;
+
protected $form_attribute_repo;
protected $form_stage_repo;
protected $form_repo;
protected $post_value_factory;
protected $bounding_box_factory;
protected $tag_repo;
+ // By default remove all private responses
+ protected $restricted = true;
protected $include_value_types = [];
protected $include_attributes = [];
+ protected $exclude_stages = [];
protected $listener;
@@ -106,6 +114,20 @@ protected function getTable()
// Ushahidi_Repository
public function getEntity(Array $data = null)
{
+ // Ensure we are dealing with a structured Post
+
+ $user = $this->getUser();
+ if ($data['form_id'])
+ {
+
+ if ($this->canUserReadPostsValues(new Post($data), $user, $this->form_repo)) {
+ $this->restricted = false;
+ }
+ // Get Hidden Stage Ids to be excluded from results
+ $this->exclude_stages = $this->form_stage_repo->getHiddenStageIds($data['form_id']);
+
+ }
+
if (!empty($data['id']))
{
$data += [
@@ -115,6 +137,21 @@ public function getEntity(Array $data = null)
'completed_stages' => $this->getCompletedStagesForPost($data['id']),
];
}
+ // NOTE: This and the restriction above belong somewhere else,
+ // ideally in their own step
+ //Check if author information should be returned
+ if ($data['author_realname'] || $data['user_id'] || $data['author_email'])
+ {
+
+
+ if (!$this->canUserSeeAuthor(new Post($data), $this->form_repo, $user))
+ {
+ unset($data['author_realname']);
+ unset($data['author_email']);
+ unset($data['user_id']);
+ }
+ }
+
return new Post($data);
}
@@ -142,12 +179,11 @@ protected function selectQuery(Array $where = [])
protected function getPostValues($id)
{
+
// Get all the values for the post. These are the EAV values.
$values = $this->post_value_factory
->proxy($this->include_value_types)
- ->getAllForPost($id, $this->include_attributes);
-
-
+ ->getAllForPost($id, $this->include_attributes, $this->exclude_stages, $this->restricted);
$output = [];
foreach ($values as $value) {
@@ -163,11 +199,18 @@ protected function getPostValues($id)
protected function getCompletedStagesForPost($id)
{
- $result = DB::select('form_stage_id', 'completed')
+ $query = DB::select('form_stage_id', 'completed')
->from('form_stages_posts')
->where('post_id', '=', $id)
- ->where('completed', '=', 1)
- ->execute($this->db);
+ ->where('completed', '=', 1);
+
+ if ($this->restricted) {
+ if ($this->exclude_stages) {
+ $query->where('form_stage_id', 'NOT IN', $this->exclude_stages);
+ }
+ }
+
+ $result = $query->execute($this->db);
return $result->as_array(NULL, 'form_stage_id');
}
@@ -220,7 +263,7 @@ protected function setSearchConditions(SearchData $search)
$query = $this->search_query;
$table = $this->getTable();
-
+
// Filter by status
$status = $search->getFilter('status', ['published']);
//
@@ -324,7 +367,7 @@ protected function setSearchConditions(SearchData $search)
// Convert to UTC (needed in case date came with a tz)
$date_after->setTimezone(new DateTimeZone('UTC'));
$query->where("$table.post_date", '>=', $date_after->format('Y-m-d H:i:s'));
- }
+ }
if ($search->date_before)
{
@@ -528,7 +571,7 @@ protected function setSearchConditions(SearchData $search)
->where("$table.status", '=', 'published')
->or_where("$table.user_id", '=', $user->id)
->and_where_close();
- }
+ }
}
// SearchRepository
@@ -1067,4 +1110,4 @@ public function doesPostRequireApproval($formId)
return true;
}
-}
+}
\ No newline at end of file
diff --git a/application/classes/Ushahidi/Repository/Post/Description.php b/application/classes/Ushahidi/Repository/Post/Description.php
index 8e077c74c3..36d7633216 100644
--- a/application/classes/Ushahidi/Repository/Post/Description.php
+++ b/application/classes/Ushahidi/Repository/Post/Description.php
@@ -14,7 +14,7 @@
class Ushahidi_Repository_Post_Description extends Ushahidi_Repository_Post_Text
{
- public function getAllForPost($post_id, Array $include_attributes = [])
+ public function getAllForPost($post_id, Array $include_attributes = [], Array $exclude_stages = [], $restricted = false)
{
return [];
}
diff --git a/application/classes/Ushahidi/Repository/Post/Title.php b/application/classes/Ushahidi/Repository/Post/Title.php
index 9e13a2fe34..e34c52a2af 100644
--- a/application/classes/Ushahidi/Repository/Post/Title.php
+++ b/application/classes/Ushahidi/Repository/Post/Title.php
@@ -14,7 +14,7 @@
class Ushahidi_Repository_Post_Title extends Ushahidi_Repository_Post_Varchar
{
- public function getAllForPost($post_id, Array $include_attributes = [])
+ public function getAllForPost($post_id, Array $include_attributes = [], Array $exclude_stages = [], $restricted = false)
{
return [];
}
diff --git a/application/classes/Ushahidi/Repository/Post/Value.php b/application/classes/Ushahidi/Repository/Post/Value.php
index aac659efcc..b0e31bfacf 100644
--- a/application/classes/Ushahidi/Repository/Post/Value.php
+++ b/application/classes/Ushahidi/Repository/Post/Value.php
@@ -40,7 +40,9 @@ protected function selectQuery(Array $where = [])
// Select 'key' too
$query->select(
$this->getTable().'.*',
- 'form_attributes.key'
+ 'form_attributes.key',
+ 'form_attributes.form_stage_id',
+ 'form_attributes.response_private'
)
->join('form_attributes')->on('form_attribute_id', '=', 'form_attributes.id');
@@ -55,7 +57,7 @@ public function get($id, $post_id = null, $form_attribute_id = null)
}
// ValuesForPostRepository
- public function getAllForPost($post_id, Array $include_attributes = [])
+ public function getAllForPost($post_id, Array $include_attributes = [], Array $exclude_stages = [], $restricted = false)
{
$query = $this->selectQuery(compact('post_id'));
@@ -63,6 +65,13 @@ public function getAllForPost($post_id, Array $include_attributes = [])
$query->where('form_attributes.key', 'IN', $include_attributes);
}
+ if ($restricted) {
+ $query->where('form_attributes.response_private', '!=', '1');
+ if ($exclude_stages) {
+ $query->where('form_attributes.form_stage_id', 'NOT IN', $exclude_stages);
+ }
+ }
+
$results = $query->execute($this->db);
return $this->getCollection($results->as_array());
}
diff --git a/application/classes/Ushahidi/Repository/Post/ValueProxy.php b/application/classes/Ushahidi/Repository/Post/ValueProxy.php
index cafa242e95..b564a70ca6 100644
--- a/application/classes/Ushahidi/Repository/Post/ValueProxy.php
+++ b/application/classes/Ushahidi/Repository/Post/ValueProxy.php
@@ -23,12 +23,12 @@ public function __construct(Ushahidi_Repository_Post_ValueFactory $factory, Arra
}
// ValuesForPostRepository
- public function getAllForPost($post_id, Array $include_attributes = [])
+ public function getAllForPost($post_id, Array $include_attributes = [], Array $exclude_stages = [], $restricted = false)
{
$results = [];
- $this->factory->each(function ($repo) use ($post_id, $include_attributes, &$results) {
- $results = array_merge($results, $repo->getAllForPost($post_id, $include_attributes));
+ $this->factory->each(function ($repo) use ($post_id, $include_attributes, &$results, $exclude_stages, $restricted) {
+ $results = array_merge($results, $repo->getAllForPost($post_id, $include_attributes, $exclude_stages, $restricted));
}, $this->include_types);
return $results;
diff --git a/application/classes/Ushahidi/Repository/Tag.php b/application/classes/Ushahidi/Repository/Tag.php
index bebf0ad70e..8aa1de1bc5 100644
--- a/application/classes/Ushahidi/Repository/Tag.php
+++ b/application/classes/Ushahidi/Repository/Tag.php
@@ -23,7 +23,8 @@ class Ushahidi_Repository_Tag extends Ushahidi_Repository implements
{
// Use the JSON transcoder to encode properties
use Ushahidi_JsonTranscodeRepository;
-
+ // Use trait to for updating forms_tags-table
+ use Ushahidi_FormsTagsTrait;
private $created_id;
private $created_ts;
@@ -39,6 +40,20 @@ protected function getTable()
// ReadRepository
public function getEntity(Array $data = null)
{
+ if (!empty($data['id']))
+ {
+ $data['forms'] = $this->getFormsForTag($data['id']);
+
+ if(empty($data['parent_id'])) {
+
+ $data['children'] =
+ DB::select('id')
+ ->from('tags')
+ ->where('parent_id','=',$data['id'])
+ ->execute($this->db)
+ ->as_array();
+ }
+ }
return new Tag($data);
}
@@ -51,25 +66,41 @@ protected function getJsonProperties()
// SearchRepository
public function getSearchFields()
{
- return ['tag', 'type', 'parent_id', 'q', /* LIKE tag */];
+ return ['tag', 'type', 'parent_id', 'q', 'level', 'formId' /* LIKE tag */];
}
// Ushahidi_Repository
- protected function setSearchConditions(SearchData $search)
+ protected function setSearchConditions(SearchData $search)
+ {
+ $query = $this->search_query;
+ foreach (['tag', 'type', 'parent_id'] as $key)
+ {
+ if ($search->$key) {
+ $query->where($key, '=', $search->$key);
+ }
+ }
+ if ($search->q) {
+ // Tag text searching
+ $query->where('tag', 'LIKE', "%{$search->q}%");
+ }
+ if($search->level) {
+ //searching for top-level-tags
+ if($search->level === 'parent') {
+ $query->where('parent_id', '=', null);
+ }
+ }
+ if($search->formId){
+ $query->join('forms_tags')
+ ->on('tags.id', '=', 'forms_tags.tag_id')
+ ->where('form_id','=', $search->formId);
+ }
+ }
+ // SearchRepository
+ public function getSearchResults()
{
- $query = $this->search_query;
-
- foreach (['tag', 'type', 'parent_id'] as $key)
- {
- if ($search->$key) {
- $query->where($key, '=', $search->$key);
- }
- }
-
- if ($search->q) {
- // Tag text searching
- $query->where('tag', 'LIKE', "%{$search->q}%");
- }
+ $query = $this->getSearchQuery();
+ $results = $query->distinct(TRUE)->execute($this->db);
+ return $this->getCollection($results->as_array());
}
// CreateRepository
@@ -77,9 +108,35 @@ public function create(Entity $entity)
{
$record = $entity->asArray();
$record['created'] = time();
- return $this->executeInsert($this->removeNullValues($record));
+
+ unset($record['forms']);
+
+ $id = $this->executeInsert($this->removeNullValues($record));
+
+ if($entity->forms) {
+ //updating forms_tags-table
+ $this->updateTagForms($id, $entity->forms);
+ }
+
+ return $id;
+ }
+
+ public function update(Entity $entity)
+ {
+ $tag = $entity->getChanged();
+ unset($tag['forms']);
+
+ $count = $this->executeUpdate(['id' => $entity->id], $tag);
+ // updating forms_tags-table
+ if($entity->hasChanged('forms'))
+ {
+ $this->updateTagForms($entity->id, $entity->forms);
+ }
+
+ return $count;
}
+
// UpdatePostTagRepository
public function getByTag($tag)
{
@@ -103,10 +160,18 @@ public function isSlugAvailable($slug)
{
return $this->selectCount(compact('slug')) === 0;
}
+ public function delete(Entity $entity)
+ {
+ $this->updateFormAttributes($entity->id);
+ return $this->executeDelete([
+ 'id' => $entity->id
+ ]);
+ }
// DeleteTagRepository
public function deleteTag($id)
{
+ $this->updateFormAttributes($entity->id);
return $this->delete(compact('id'));
}
}
diff --git a/application/classes/Ushahidi/Signer/Signature.php b/application/classes/Ushahidi/Signer/Signature.php
deleted file mode 100644
index 5936c8fc85..0000000000
--- a/application/classes/Ushahidi/Signer/Signature.php
+++ /dev/null
@@ -1,76 +0,0 @@
-
- * @package Ushahidi\Application
- * @copyright 2014 Ushahidi
- * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3)
- */
-
-use Ushahidi\Core\Tool\Signer;
-
-class Ushahidi_Signer_Signature implements Signer
-{
- protected $authToken;
-
- public function __construct($authToken)
- {
- $this->authToken = $authToken;
- }
-
- public function sign($fullUrl, $data)
- {
- return computeSignature($fullUrl, $data);
- }
-
- public function computeSignature($url, $data = array())
- {
- // sort the array by keys
- ksort($data);
- // append them to the data string in order
- // with no delimiters
- foreach ($data as $key => $value) {
- $url .= "$key$value";
- }
-
- // This function calculates the HMAC hash of the data with the key
- // passed in
- // Note: hash_hmac requires PHP 5 >= 5.1.2 or PECL hash:1.1-1.5
- // Or http://pear.php.net/package/Crypt_HMAC/
- return base64_encode(hash_hmac("sha256", $url, $this->authToken, true));
- }
-
- public function validate($expectedSignature, $url, $data = array())
- {
- return self::compare(
- $this->computeSignature($url, $data),
- $expectedSignature
- );
- }
- /**
- * Time insensitive compare, function's runtime is governed by the length
- * of the first argument, not the difference between the arguments.
- * @param $a string First part of the comparison pair
- * @param $b string Second part of the comparison pair
- * @return bool True if $a == $b, false otherwise.
- */
- public static function compare($a, $b)
- {
- $result = true;
- if (strlen($a) != strlen($b)) {
- return false;
- }
- if (!$a && !$b) {
- return true;
- }
- $limit = strlen($a);
- for ($i = 0; $i < $limit; ++$i) {
- if ($a[$i] != $b[$i]) {
- $result = false;
- }
- }
- return $result;
- }
-}
diff --git a/application/classes/Ushahidi/Validator/Form/Attribute/Update.php b/application/classes/Ushahidi/Validator/Form/Attribute/Update.php
index 8898612f6b..2cae01966c 100644
--- a/application/classes/Ushahidi/Validator/Form/Attribute/Update.php
+++ b/application/classes/Ushahidi/Validator/Form/Attribute/Update.php
@@ -53,7 +53,8 @@ protected function getRules()
'relation',
'upload',
'video',
- 'markdown'
+ 'markdown',
+ 'tags',
]]],
],
'type' => [
@@ -70,7 +71,8 @@ protected function getRules()
'relation',
'media',
'title',
- 'description'
+ 'description',
+ 'tags',
]]],
],
'required' => [
diff --git a/application/classes/Ushahidi/Validator/Post/Create.php b/application/classes/Ushahidi/Validator/Post/Create.php
index 82367d3ac8..0eafdf8b77 100644
--- a/application/classes/Ushahidi/Validator/Post/Create.php
+++ b/application/classes/Ushahidi/Validator/Post/Create.php
@@ -185,6 +185,10 @@ public function checkApprovalRequired (Validation $validation, $status, $fullDat
return;
}
+ if ($status === 'draft' && !isset($fullData['id'])) {
+ return;
+ }
+
$user = $this->getUser();
// Do we have permission to publish this post?
$userCanChangeStatus = ($this->isUserAdmin($user) or $this->hasPermission($user));
@@ -195,6 +199,9 @@ public function checkApprovalRequired (Validation $validation, $status, $fullDat
$requireApproval = $this->repo->doesPostRequireApproval($fullData['form_id']);
+ Kohana::$log->add(Log::ERROR, print_r($requireApproval, true));
+ Kohana::$log->add(Log::ERROR, print_r($status, true));
+
// Are we trying to change publish a post that requires approval?
if ($requireApproval && $status !== 'draft') {
$validation->error('status', 'postNeedsApprovalBeforePublishing');
diff --git a/application/tests/datasets/ushahidi/Base.yml b/application/tests/datasets/ushahidi/Base.yml
index 6101d43007..e72e69a85f 100644
--- a/application/tests/datasets/ushahidi/Base.yml
+++ b/application/tests/datasets/ushahidi/Base.yml
@@ -64,7 +64,8 @@ forms:
name: "Test Form"
type: "report"
description: "Testing form"
- require_approval: 1
+ require_approval: 0
+ hide_author: 0
everyone_can_create: 1
-
id: 2
@@ -72,6 +73,7 @@ forms:
type: "report"
description: "Missing persons"
require_approval: 0
+ hide_author: 0
everyone_can_create: 0
-
id: 3
@@ -79,33 +81,65 @@ forms:
type: "report"
description: "Test video embed"
require_approval: 0
+ hide_author: 0
+ everyone_can_create: 0
+ -
+ id: 4
+ name: "Role Restriction"
+ type: "report"
+ description: "Test role restriction and private responses"
+ require_approval: 1
+ hide_author: 1
everyone_can_create: 0
form_roles:
-
id: 1
form_id: 2
role_id: 1
+ -
+ id: 2
+ form_id: 4
+ role_id: 2
+ -
+ id: 3
+ form_id: 4
+ role_id: 3
form_stages:
-
id: 1
form_id: 1
label: "Main"
+ show_when_published: 1
-
id: 2
form_id: 1
label: "2nd step"
+ show_when_published: 1
-
id: 3
form_id: 1
label: "3rd step"
+ show_when_published: 1
-
id: 4
form_id: 2
label: "Main"
+ show_when_published: 1
-
id: 5
form_id: 3
label: "Post"
+ show_when_published: 1
+ -
+ id: 6
+ form_id: 4
+ label: "restricted"
+ show_when_published: 0
+ -
+ id: 7
+ form_id: 4
+ label: "Post"
+ show_when_published: 1
form_attributes:
-
id: 1
@@ -113,6 +147,7 @@ form_attributes:
key: "test_varchar"
type: "varchar"
input: "text"
+ response_private: 0
required: 0
priority: 1
options: ""
@@ -124,6 +159,7 @@ form_attributes:
key: "test_point"
type: "point"
input: "location"
+ response_private: 0
required: 0
priority: 1
options: ""
@@ -135,6 +171,7 @@ form_attributes:
key: "full_name"
type: "varchar"
input: "text"
+ response_private: 0
required: 0
priority: 1
options: ""
@@ -146,6 +183,7 @@ form_attributes:
key: "description"
type: "description"
input: "textarea"
+ response_private: 0
required: 0
priority: 0
options: ""
@@ -157,6 +195,7 @@ form_attributes:
key: "date_of_birth"
type: "datetime"
input: "date"
+ response_private: 0
required: 0
priority: 3
options: ""
@@ -168,6 +207,7 @@ form_attributes:
key: "missing_date"
type: "datetime"
input: "date"
+ response_private: 0
required: 0
priority: 4
options: ""
@@ -179,6 +219,7 @@ form_attributes:
key: "last_location"
type: "varchar"
input: "text"
+ response_private: 0
required: 1
priority: 5
options: ""
@@ -190,6 +231,7 @@ form_attributes:
key: "last_location_point"
type: "point"
input: "location"
+ response_private: 0
required: 0
priority: 5
options: ""
@@ -201,6 +243,7 @@ form_attributes:
key: "geometry_test"
type: "geometry"
input: "text"
+ response_private: 0
required: 0
priority: 5
options: ""
@@ -212,6 +255,7 @@ form_attributes:
key: "missing_status"
type: "varchar"
input: "select"
+ response_private: 0
required: 0
options: '["information_sought","is_note_author","believed_alive","believed_missing","believed_dead"]'
priority: 5
@@ -223,6 +267,7 @@ form_attributes:
key: "links"
type: "varchar"
input: "text"
+ response_private: 0
required: 0
priority: 7
cardinality: 0
@@ -233,6 +278,7 @@ form_attributes:
key: "second_point"
type: "point"
input: "location"
+ response_private: 0
required: 0
priority: 5
options: ""
@@ -244,6 +290,7 @@ form_attributes:
key: "person_status"
type: "varchar"
input: "select"
+ response_private: 0
required: 0
options: '["information_sought","is_note_author","believed_alive","believed_missing","believed_dead"]'
priority: 5
@@ -255,6 +302,7 @@ form_attributes:
key: "media_test"
type: "media"
input: "upload"
+ response_private: 0
required: 0
priority: 7
cardinality: 1
@@ -265,6 +313,7 @@ form_attributes:
key: "possible_actions"
type: "varchar"
input: "checkbox"
+ response_private: 0
required: 0
options: '["ground_search","medical_evacuation"]'
priority: 5
@@ -277,6 +326,7 @@ form_attributes:
key: "video_field"
type: "video"
input: "video"
+ response_private: 0
required: 0
options: '[]'
priority: 5
@@ -289,6 +339,7 @@ form_attributes:
key: "title"
type: "title"
input: "text"
+ response_private: 0
required: 0
priority: 0
options: ""
@@ -296,10 +347,95 @@ form_attributes:
form_stage_id: 1
-
id: 18
+ label: "Test Field Level Locking 1"
+ key: "test_field_locking_hidden_1"
+ type: "text"
+ input: "text"
+ response_private: 1
+ required: 0
+ priority: 0
+ options: ""
+ cardinality: 1
+ form_stage_id: 6
+ -
+ id: 19
+ label: "Test Field Level Locking 2"
+ key: "test_field_locking_visible_1"
+ type: "varchar"
+ input: "text"
+ response_private: 0
+ required: 0
+ priority: 0
+ options: ""
+ cardinality: 1
+ form_stage_id: 6
+ -
+ id: 20
+ label: "Test Field Level Locking 3"
+ key: "test_field_locking_visible_2"
+ type: "title"
+ input: "text"
+ response_private: 0
+ required: 0
+ priority: 0
+ options: ""
+ cardinality: 1
+ form_stage_id: 6
+ -
+ id: 21
+ label: "Test Field Level Locking 4"
+ key: "test_field_locking_hidden_2"
+ type: "text"
+ input: "text"
+ response_private: 1
+ required: 0
+ priority: 0
+ options: ""
+ cardinality: 1
+ form_stage_id: 6
+ -
+ id: 22
+ label: "Test Field Level Locking 5"
+ key: "test_field_locking_visible_3"
+ type: "varchar"
+ input: "text"
+ response_private: 0
+ required: 0
+ priority: 0
+ options: ""
+ cardinality: 1
+ form_stage_id: 6
+ -
+ id: 23
+ label: "Test Field Level Locking 6"
+ key: "test_field_locking_hidden_3"
+ type: "text"
+ input: "text"
+ response_private: 1
+ required: 0
+ priority: 0
+ options: ""
+ cardinality: 1
+ form_stage_id: 7
+ -
+ id: 24
+ label: "Test Field Level Locking 7"
+ key: "test_field_locking_visible_4"
+ type: "varchar"
+ input: "text"
+ response_private: 0
+ required: 0
+ priority: 0
+ options: ""
+ cardinality: 1
+ form_stage_id: 7
+ -
+ id: 25
label: "Test markdown"
key: "markdown"
type: "markdown"
input: "text"
+ response_private: 0
required: 0
priority: 0
options: ""
@@ -605,6 +741,36 @@ posts:
created: 1412025016
published_to: '["user"]'
post_date: "2014-09-29 14:10:16"
+ -
+ id: 121
+ form_id: 4
+ parent_id:
+ user_id: 3
+ author_email: "test@ushahidi.com"
+ author_realname: "Test Name"
+ title: "Post published to members"
+ type: "report"
+ content: "Post published to members"
+ status: "published"
+ locale: "en_us"
+ created: 1412025016
+ published_to: '[]'
+ post_date: "2014-09-29 14:10:16"
+ -
+ id: 122
+ form_id: 4
+ parent_id:
+ user_id: 3
+ author_email: "test@ushahidi.com"
+ author_realname: "Test Name"
+ title: "Post published to members"
+ type: "report"
+ content: "Post published to members"
+ status: "published"
+ locale: "en_us"
+ created: 1412025016
+ published_to: '["user"]'
+ post_date: "2014-09-29 14:10:16"
post_datetime:
post_decimal:
post_geometry:
@@ -678,6 +844,56 @@ post_varchar:
post_id: 1
form_attribute_id: 11
value: "http://ushahidi.com"
+ -
+ id: 13
+ post_id: 121
+ form_attribute_id: 20
+ value: "arabic string"
+ -
+ id: 14
+ post_id: 121
+ form_attribute_id: 21
+ value: "http://google.com"
+ -
+ id: 15
+ post_id: 121
+ form_attribute_id: 22
+ value: "http://ushahidi.com"
+ -
+ id: 16
+ post_id: 121
+ form_attribute_id: 23
+ value: "arabic string"
+ -
+ id: 17
+ post_id: 121
+ form_attribute_id: 24
+ value: "arabic string"
+ -
+ id: 18
+ post_id: 122
+ form_attribute_id: 20
+ value: "arabic string"
+ -
+ id: 19
+ post_id: 122
+ form_attribute_id: 21
+ value: "http://google.com"
+ -
+ id: 20
+ post_id: 122
+ form_attribute_id: 22
+ value: "http://ushahidi.com"
+ -
+ id: 21
+ post_id: 122
+ form_attribute_id: 23
+ value: "arabic string"
+ -
+ id: 22
+ post_id: 122
+ form_attribute_id: 24
+ value: "arabic string"
post_comments:
tags:
-
diff --git a/application/tests/features/api.acl.feature b/application/tests/features/api.acl.feature
index 89bbb1f913..24cdb97dbe 100644
--- a/application/tests/features/api.acl.feature
+++ b/application/tests/features/api.acl.feature
@@ -1,12 +1,204 @@
@acl
Feature: API Access Control Layer
+
+ Scenario: Anonymous users can create posts
+ Given that I want to make a new "Post"
+ And that the request "Authorization" header is "Bearer testanon"
+ And that the request "data" is:
+ """
+ {
+ "form_id": 1,
+ "status": "draft",
+ "title": "Test creating anonymous post",
+ "content": "testing post for oauth",
+ "locale": "en_us",
+ "values": {
+ "last_location" : ["Somewhere"]
+ }
+ }
+ """
+ When I request "/posts"
+ Then the guzzle status code should be 204
+
+ Scenario: Anonymous user can not see restricted fields of public posts
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testanon"
+ And that its "id" is "121"
+ When I request "/posts"
+ Then the response is JSON
+ And the response has a "values" property
+ And the response has a "values.test_field_locking_visible_4" property
+ And the response does not have a "values.test_field_locking_hidden_3" property
+ Then the guzzle status code should be 200
+
+ Scenario: Anonymous user can not see hidden tasks of public posts
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testanon"
+ And that its "id" is "121"
+ When I request "/posts"
+ Then the response is JSON
+ And the response has a "values" property
+ And the response has a "values.test_field_locking_visible_4" property
+ And the response does not have a "values.test_field_locking_visible_2" property
+ Then the guzzle status code should be 200
+
+ Scenario: Anonymous user can not see hidden author field of public posts
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testanon"
+ And that its "id" is "121"
+ When I request "/posts"
+ Then the response is JSON
+ And the response does not have a "author_realname" property
+ And the response does not have a "author_email" property
+ And the response does not have a "user_id" property
+ Then the guzzle status code should be 200
+
+ Scenario: User can not see hidden tasks of public posts
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testbasicuser"
+ And that its "id" is "121"
+ When I request "/posts"
+ Then the response is JSON
+ And the response has a "values" property
+ And the response has a "values.test_field_locking_visible_4" property
+ And the response does not have a "values.test_field_locking_visible_2" property
+ Then the guzzle status code should be 200
+
+ Scenario: User can not see restricted fields of public posts
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testbasicuser"
+ And that its "id" is "121"
+ When I request "/posts"
+ Then the response is JSON
+ And the response has a "values" property
+ And the response has a "values.test_field_locking_visible_4" property
+ And the response does not have a "values.test_field_locking_hidden_3" property
+ Then the guzzle status code should be 200
+
+ Scenario: User can not see hidden author field of public posts
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testanon"
+ And that its "id" is "121"
+ When I request "/posts"
+ Then the response is JSON
+ And the response does not have a "author_realname" property
+ And the response does not have a "author_email" property
+ And the response does not have a "user" property
+ Then the guzzle status code should be 200
+
+ Scenario: User can see restricted fields of posts published to their role when survey restricted to their role
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testmanager"
+ And that its "id" is "121"
+ When I request "/posts"
+ Then the response is JSON
+ And the response has a "values" property
+ And the response has a "values.test_field_locking_visible_4" property
+ And the response has a "values.test_field_locking_hidden_3" property
+ Then the guzzle status code should be 200
+
+ Scenario: User can see hidden author field of posts published to their role when survey restricted to their role
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testmanager"
+ And that its "id" is "121"
+ When I request "/posts"
+ Then the response is JSON
+ And the response has a "author_realname" property
+ And the response has a "author_email" property
+ And the response has a "user" property
+ Then the guzzle status code should be 200
+
+ Scenario: User can not see hidden tasks of posts published to their role
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testbasicuser"
+ And that its "id" is "122"
+ When I request "/posts"
+ Then the response is JSON
+ And the response has a "values" property
+ And the response has a "values.test_field_locking_visible_4" property
+ And the response does not have a "values.test_field_locking_visible_2" property
+ Then the guzzle status code should be 200
+
+ Scenario: User can see restricted fields of posts published to their role
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testbasicuser"
+ And that its "id" is "122"
+ When I request "/posts"
+ Then the response is JSON
+ And the response has a "values" property
+ And the response has a "values.test_field_locking_visible_4" property
+ And the response does not have a "values.test_field_locking_hidden_3" property
+ Then the guzzle status code should be 200
+
+ Scenario: User can not see hidden author field of posts published to their role
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testbasicuser"
+ And that its "id" is "122"
+ When I request "/posts"
+ Then the response is JSON
+ And the response does not have a "author_realname" property
+ And the response does not have a "author_email" property
+ And the response does not have a "user" property
+ Then the guzzle status code should be 200
+
+ Scenario: Listing All Stages for a form with hidden stages
+ Given that I want to get all "Stages"
+ And that the request "Authorization" header is "Bearer testbasicuser"
+ When I request "/forms/4/stages"
+ Then the response is JSON
+ And the response has a "count" property
+ And the type of the "count" property is "numeric"
+ And the "count" property equals "1"
+ Then the guzzle status code should be 200
+
+ Scenario: Listing All Stages for a form with hidden stages with edit permission
+ Given that I want to get all "Stages"
+ And that the request "Authorization" header is "Bearer testmanager"
+ When I request "/forms/4/stages"
+ Then the response is JSON
+ And the response has a "count" property
+ And the type of the "count" property is "numeric"
+ And the "count" property equals "2"
+ Then the guzzle status code should be 200
+
+ Scenario: Listing All Attributes for a form with hidden stages
+ Given that I want to get all "Stages"
+ And that the request "Authorization" header is "Bearer testbasicuser"
+ When I request "/forms/4/attributes"
+ Then the response is JSON
+ And the response has a "count" property
+ And the type of the "count" property is "numeric"
+ And the "count" property equals "7"
+ Then the guzzle status code should be 200
+
+ Scenario: Listing All Attributes for a form with hidden stages with edit permission
+ Given that I want to get all "Stages"
+ And that the request "Authorization" header is "Bearer testmanager"
+ When I request "/forms/4/attributes"
+ Then the response is JSON
+ And the response has a "count" property
+ And the type of the "count" property is "numeric"
+ And the "count" property equals "7"
+ Then the guzzle status code should be 200
+
+ Scenario: User can see hidden tasks of posts published when survey restricted to their role
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testmanager"
+ And that its "id" is "121"
+ When I request "/posts"
+ Then the response is JSON
+ And the response has a "values" property
+ And the response has a "values.test_field_locking_visible_4" property
+ And the response has a "values.test_field_locking_visible_2" property
+ Then the guzzle status code should be 200
+
Scenario: Anonymous user can access public posts
Given that I want to get all "Posts"
And that the request "Authorization" header is "Bearer testanon"
When I request "/posts"
Then the guzzle status code should be 200
And the response is JSON
- And the "count" property equals "10"
+ And the "count" property equals "11"
Scenario: All users can view public posts
Given that I want to get all "Posts"
@@ -15,7 +207,7 @@ Feature: API Access Control Layer
When I request "/posts"
Then the guzzle status code should be 200
And the response is JSON
- And the "count" property equals "11"
+ And the "count" property equals "13"
Scenario: User can view public and own private posts in collection
Given that I want to get all "Posts"
@@ -24,7 +216,7 @@ Feature: API Access Control Layer
When I request "/posts"
Then the guzzle status code should be 200
And the response is JSON
- And the "count" property equals "13"
+ And the "count" property equals "15"
Scenario: Admin can view all posts in collection
Given that I want to get all "Posts"
@@ -33,7 +225,7 @@ Feature: API Access Control Layer
When I request "/posts"
Then the guzzle status code should be 200
And the response is JSON
- And the "count" property equals "16"
+ And the "count" property equals "19"
Scenario: Admin user can view private posts
Given that I want to find a "Post"
@@ -103,27 +295,6 @@ Feature: API Access Control Layer
And the response is JSON
And the response has an "errors" property
- Scenario: Anonymous users can create posts
- Given that I want to make a new "Post"
- And that the request "Authorization" header is "Bearer testanon"
- And that the request "data" is:
- """
- {
- "form_id": 1,
- "status": "draft",
- "title": "Test creating anonymous post",
- "content": "testing post for oauth",
- "locale": "en_us",
- "values": {
- "last_location" : ["Somewhere"]
- }
- }
- """
- When I request "/posts"
- Then the guzzle status code should be 204
-
-
-
Scenario: Anonymous users can not edit posts
Given that I want to update a "Post"
And that the request "Authorization" header is "Bearer testanon"
@@ -365,6 +536,16 @@ Feature: API Access Control Layer
And the response is JSON
And the "id" property equals "120"
+ @resetFixture
+ Scenario: Anonymous can not view private responses
+ Given that I want to find a "Post"
+ And that the request "Authorization" header is "Bearer testanon"
+ And that its "id" is "121"
+ When I request "/posts"
+ Then the guzzle status code should be 200
+ And the response is JSON
+ And the "id" property equals "121"
+
@resetFixture
Scenario: Anonymous can not view post published to members
Given that I want to find a "Post"
@@ -434,7 +615,7 @@ Feature: API Access Control Layer
When I request "/posts"
Then the guzzle status code should be 200
And the response is JSON
- And the "count" property equals "16"
+ And the "count" property equals "18"
@rolesEnabled
Scenario: User with Manage Posts permission can view private posts
@@ -556,4 +737,3 @@ Feature: API Access Control Layer
And the response has a "columns" property
And the "columns.0" property equals "title"
Then the guzzle status code should be 200
-
diff --git a/application/tests/features/api.posts.feature b/application/tests/features/api.posts.feature
index 8dd04435ae..f27105a74b 100644
--- a/application/tests/features/api.posts.feature
+++ b/application/tests/features/api.posts.feature
@@ -178,7 +178,7 @@ Feature: Testing the Posts API
Then the guzzle status code should be 200
@create
- Scenario: Creating a Post with a form that does not require approval but try to set status should fail
+ Scenario: Creating a Post with a form that does not require approval but try to set status should pass
Given that I want to make a new "Post"
And that the request "Authorization" header is "Bearer testbasicuser"
And that the request "data" is:
@@ -199,7 +199,7 @@ Feature: Testing the Posts API
"""
When I request "/posts"
Then the response is JSON
- Then the guzzle status code should be 422
+ Then the guzzle status code should be 200
@create
Scenario: Creating an Post with invalid data returns an error
@@ -921,8 +921,8 @@ Feature: Testing the Posts API
Then the response is JSON
And the response has a "count" property
And the type of the "count" property is "numeric"
- And the "count" property equals "12"
- And the "total_count" property equals "12"
+ And the "count" property equals "14"
+ And the "total_count" property equals "14"
Then the guzzle status code should be 200
@resetFixture @search
@@ -977,8 +977,8 @@ Feature: Testing the Posts API
Then the response is JSON
And the response has a "count" property
And the type of the "count" property is "numeric"
- And the "count" property equals "1"
- And the "total_count" property equals "1"
+ And the "count" property equals "3"
+ And the "total_count" property equals "3"
Then the guzzle status code should be 200
@resetFixture @search
diff --git a/application/tests/features/forms/api.attributes.feature b/application/tests/features/forms/api.attributes.feature
index b88bf72e6f..a1ed03473c 100644
--- a/application/tests/features/forms/api.attributes.feature
+++ b/application/tests/features/forms/api.attributes.feature
@@ -184,7 +184,7 @@ Feature: Testing the Form Attributes API
Then the response is JSON
And the response has a "count" property
And the type of the "count" property is "numeric"
- And the "count" property equals "20"
+ And the "count" property equals "27"
Then the guzzle status code should be 200
Scenario: Search for point attributes
diff --git a/docs/arch-layers.png b/docs/arch-layers.png
new file mode 100644
index 0000000000..c058326c4f
Binary files /dev/null and b/docs/arch-layers.png differ
diff --git a/docs/architecture.md b/docs/architecture.md
index e4f3a293be..13b1dfc693 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -40,24 +40,13 @@ Within the API there are two layers: the delivery and the business logic (core a
#### Core Application
-Within the core application, we use generally follow the [Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html). The central part of the business logic is defined as use cases and entities. All dependencies flow inwards towards the entities, which have no dependencies. In order to bring user input to the use cases, we pass create simple request data structures to pass from the delivery layer into the use case. The request structure is a simple array and contains all of the inputs for that specific use case. Once the usecase is complete it returns another simple data structure (response) back to the delivery layer for conversion via a Formatter. Data flow within the platform can be visualized as:
+Within the core application, we use generally follow the [Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html). The central part of the business logic is defined as use cases and entities. All dependencies flow inwards towards the entities, which have no dependencies.
-![api-request-flow](./api-request-flow.png "API Request Flow")
-[source](http://www.nomnoml.com/#view/%23title%3A%20General%20API%20request%20flow%0A%0A%5B%3Cstart%3Eapp%5D-%3E%5BKohana%5D%0A%5BKohana%5D-%3E%5BController%5D%0A%5BController%5D-%3E%5B%3Cstate%3Erequest%5D%0A%5B%3Cstate%3Erequest%5D-%3E%5BUsecase%5D%0A%5BUsecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%5BOutputFormatter%5D-%3E%5B%3Cend%3Ejson%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BDependencies%7C%0A%20Repository%3B%0A%20Validator%3B%0A%20Authorizer%3B%0A%20etc...%0A%5Do-%3E%5BUsecase%5D%0A%0A%23direction%3A%20right)
-
-Specific use cases follow 5 high level patterns for Create, Read, Update, Delete and Search (CRUDS)
-
-![create-usecase](./create-usecase.png "Create Usecase")
-[create](http://www.nomnoml.com/#view/%23title%3A%20Create%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BCreate%20Usecase%5D%0A%5BCreate%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BCreate%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Create%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Create%20Auth%5D-%3E%5BVerify%20Valid%5D%0A%20%20%20%20%20%5BVerify%20Valid%5D-%3E%5BCreate%20Entity%5D%0A%20%20%20%20%20%5BCreate%20Entity%5D-%3E%5BGet%20Created%5D%0A%20%20%20%20%20%5BGet%20Created%5D-%3E%5B%3Cchoice%3E%20Can%20Read%3F%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right)
+![architecture-layers](./arch-layers.png "Software architecture layers")
-![read-usecase](./read-usecase.png "Read Usecase")
-[read](http://www.nomnoml.com/#view/%23title%3A%20Read%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BRead%20Usecase%5D%0A%5BRead%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BRead%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Read%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right)
+In order to bring user input to the use cases, we pass simple data structures from the delivery layer into the use case. The request structure is a simple array and contains all of the inputs for that specific use case. Once the usecase is complete it returns another simple data structure (response) back to the delivery layer for conversion via a Formatter. Data flow within the platform can be visualized as:
-![update-usecase](./update-usecase.png "UpdateUsecase")
-[update](http://www.nomnoml.com/#view/%23title%3A%20Update%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BUpdate%20Usecase%5D%0A%5BUpdate%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BUpdate%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BUpdate%20State%5D%0A%20%20%20%20%20%5BUpdate%20State%5D-%3E%5BVerify%20Update%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Update%20Auth%5D-%3E%5BVerify%20Valid%5D%0A%20%20%20%20%20%5BVerify%20Valid%5D-%3E%5BUpdate%20Entity%5D%0A%20%20%20%20%20%5BUpdate%20Entity%5D-%3E%5B%3Cchoice%3E%20Can%20Read%3F%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right)
-
-![delete-usecase](./delete-usecase.png "Delete Usecase")
-[delete](http://www.nomnoml.com/#view/%23title%3A%20Delete%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BDelete%20Usecase%5D%0A%5BDelete%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BDelete%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Delete%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Delete%20Auth%5D-%3E%5BDelete%20Entity%5D%0A%20%20%20%20%20%5BDelete%20Entity%5D-%3E%5BVerify%20Read%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right)
+![api-request-flow](./api-request-flow.png "API Request Flow")
+[source](http://www.nomnoml.com/#view/%23title%3A%20General%20API%20request%20flow%0A%0A%5B%3Cstart%3Eapp%5D-%3E%5BKohana%5D%0A%5BKohana%5D-%3E%5BController%5D%0A%5BController%5D-%3E%5B%3Cstate%3Erequest%5D%0A%5B%3Cstate%3Erequest%5D-%3E%5BUsecase%5D%0A%5BUsecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%5BOutputFormatter%5D-%3E%5B%3Cend%3Ejson%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BDependencies%7C%0A%20Repository%3B%0A%20Validator%3B%0A%20Authorizer%3B%0A%20etc...%0A%5Do-%3E%5BUsecase%5D%0A%0A%23direction%3A%20right)
-![search-usecase](./search-usecase.png "Search Usecase")
-[search](http://www.nomnoml.com/#view/%23title%3A%20Search%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BSearch%20Usecase%5D%0A%5BSearch%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BSearch%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Search%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Search%20Auth%5D-%3E%5BSet%20Search%20Params%5D%0A%20%20%20%20%20%5BSet%20Search%20Params%5D-%3E%5BGet%20Search%20Sesults%5D%0A%20%20%20%20%20%5BGet%20Search%20Sesults%5D-%3E%5BVerify%20Read%20Auth%7C%0A%20%20%20%20%20%20%20%20%5B%3Cstart%3E%20foreach%5D-%3E%5B%3Cchoice%3Ewhile%20results%3F%5D%0A%20%20%20%20%20%20%20%20%5B%3Cchoice%3Ewhile%20results%3F%5D-%3E%5Bcheck%20auth%5D%0A%20%20%20%20%20%20%20%20%5Bcheck%20auth%5D-%3E%5B%3Cchoice%3Ewhile%20results%3F%5D%0A%20%20%20%20%20%20%20%20%5B%3Cchoice%3Ewhile%20results%3F%5D-%3E%5B%3Cend%3E%5D%0A%20%20%20%20%20%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Results%5D%0A%20%20%20%20%20%5BFormat%20Results%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right)
+See [Use Case Internals](./use-case-internals.md) for more detail
diff --git a/docs/create-usecase-collab.png b/docs/create-usecase-collab.png
new file mode 100644
index 0000000000..8de17327e4
Binary files /dev/null and b/docs/create-usecase-collab.png differ
diff --git a/docs/use-case-internals.md b/docs/use-case-internals.md
new file mode 100644
index 0000000000..57b9ff1664
--- /dev/null
+++ b/docs/use-case-internals.md
@@ -0,0 +1,87 @@
+# Use Case Internals
+
+## What is a use case?
+
+Part of Hexagonal Architecture is the concept of the Application Boundary. This boundary separates our application as a whole from everything else (both framework and communication with the outside world).
+
+> A Use Case (sometimes called a Command) is an explicitly defined way in which an application can be used.
+
+We define how the outside world can communicate with our application by creating "Use Cases". These essentially are classes which name actions that can be taken. For example, our CreatePostUsecase defines that our application can create a post.
+
+Defining Use Cases has some useful side-affects. For example, we clearly and explicitly can see how our application "wants" to be interacted with. We can plan use cases ahead of time, or add them as needed, but use cases should capture the operations which can happen within our application.
+
+> Aside: Platform uses some generic CRUDS usecases. These aren't tied to a specific Domain Model (Entity) ie. a Post but rather have the entity and repo injected into them. This makes Use Cases significantly less well defined. A developer can no longer glance at the Use Case directory and see what actions are available. This might be something we can improve in future
+
+## Anatomy of a Use Case (in platform)
+
+Use Cases in platform all follow a high level interface. In short they all have a `interact()` method.
+
+```
+interface Usecase
+{
+ /**
+ * @return Array
+ */
+ public function interact();
+}
+```
+
+To enable building of some generic use cases they also have `isSearch` and `isWrite` methods.
+
+```
+interface Usecase
+{
+ /**
+ * Will this usecase write any data?
+ *
+ * @return Boolean
+ */
+ public function isWrite();
+
+ /**
+ * Will this usecase search for data?
+ *
+ * @return Boolean
+ */
+ public function isSearch();
+
+ /**
+ * @return Array
+ */
+ public function interact();
+}
+```
+
+The actual parameters for each UseCase are injected through setter methods, commonly: `setPayload()`, `setIdentifiers` and `setFilters`.
+
+## CRUDS use cases
+
+Most of our use cases follow 5 high level patterns for Create, Read, Update, Delete and Search (CRUDS)
+
+### Create
+
+![create-usecase](./create-usecase.png "Create Usecase")
+[create](http://www.nomnoml.com/#view/%23title%3A%20Create%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BCreate%20Usecase%5D%0A%5BCreate%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BCreate%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Create%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Create%20Auth%5D-%3E%5BVerify%20Valid%5D%0A%20%20%20%20%20%5BVerify%20Valid%5D-%3E%5BCreate%20Entity%5D%0A%20%20%20%20%20%5BCreate%20Entity%5D-%3E%5BGet%20Created%5D%0A%20%20%20%20%20%5BGet%20Created%5D-%3E%5B%3Cchoice%3E%20Can%20Read%3F%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right)
+
+![create-usecase-collab](./create-usecase-collab.png "Create Usecase Collaborators")
+[collaborators](http://www.nomnoml.com/#view/%23title%3A%20Create%20UseCase%20Collaborators%0A%0A%5BCreateUsecase%7C%7C%0Ainteract()%0AsetPayload()%5D%0A%0A%5BValidator%5D%3C-%2B%5BCreateUsecase%5D%0A%5BAuthorizer%5D%3C-%2B%5BCreateUsecase%5D%0A%5BFormatter%5D%3C-%2B%5BCreateUsecase%5D%0A%5BRepository%5D%3C-%2B%5BCreateUsecase%5D%0A%0A%5BCreateUsecase%5D-%3E%5B%3Cinput%3E%20payload%5D%0A%0A%23direction%3A%20right)
+
+### Read
+
+![read-usecase](./read-usecase.png "Read Usecase")
+[read](http://www.nomnoml.com/#view/%23title%3A%20Read%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BRead%20Usecase%5D%0A%5BRead%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BRead%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Read%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right)
+
+### Update
+
+![update-usecase](./update-usecase.png "UpdateUsecase")
+[update](http://www.nomnoml.com/#view/%23title%3A%20Update%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BUpdate%20Usecase%5D%0A%5BUpdate%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BUpdate%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BUpdate%20State%5D%0A%20%20%20%20%20%5BUpdate%20State%5D-%3E%5BVerify%20Update%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Update%20Auth%5D-%3E%5BVerify%20Valid%5D%0A%20%20%20%20%20%5BVerify%20Valid%5D-%3E%5BUpdate%20Entity%5D%0A%20%20%20%20%20%5BUpdate%20Entity%5D-%3E%5B%3Cchoice%3E%20Can%20Read%3F%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right)
+
+### Delete
+
+![delete-usecase](./delete-usecase.png "Delete Usecase")
+[delete](http://www.nomnoml.com/#view/%23title%3A%20Delete%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BDelete%20Usecase%5D%0A%5BDelete%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BDelete%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Delete%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Delete%20Auth%5D-%3E%5BDelete%20Entity%5D%0A%20%20%20%20%20%5BDelete%20Entity%5D-%3E%5BVerify%20Read%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right)
+
+### Search
+
+![search-usecase](./search-usecase.png "Search Usecase")
+[search](http://www.nomnoml.com/#view/%23title%3A%20Search%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BSearch%20Usecase%5D%0A%5BSearch%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BSearch%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Search%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Search%20Auth%5D-%3E%5BSet%20Search%20Params%5D%0A%20%20%20%20%20%5BSet%20Search%20Params%5D-%3E%5BGet%20Search%20Sesults%5D%0A%20%20%20%20%20%5BGet%20Search%20Sesults%5D-%3E%5BVerify%20Read%20Auth%7C%0A%20%20%20%20%20%20%20%20%5B%3Cstart%3E%20foreach%5D-%3E%5B%3Cchoice%3Ewhile%20results%3F%5D%0A%20%20%20%20%20%20%20%20%5B%3Cchoice%3Ewhile%20results%3F%5D-%3E%5Bcheck%20auth%5D%0A%20%20%20%20%20%20%20%20%5Bcheck%20auth%5D-%3E%5B%3Cchoice%3Ewhile%20results%3F%5D%0A%20%20%20%20%20%20%20%20%5B%3Cchoice%3Ewhile%20results%3F%5D-%3E%5B%3Cend%3E%5D%0A%20%20%20%20%20%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Results%5D%0A%20%20%20%20%20%5BFormat%20Results%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right)
diff --git a/migrations/20170315231639_add_post_markdown_table.php b/migrations/20170315231639_add_post_markdown_table.php
index 8e28a2b4ff..7710fd0a5f 100644
--- a/migrations/20170315231639_add_post_markdown_table.php
+++ b/migrations/20170315231639_add_post_markdown_table.php
@@ -12,7 +12,7 @@ public function up()
$this->table('post_markdown')
->addColumn('post_id', 'integer')
->addColumn('form_attribute_id', 'integer')
- ->addColumn('value', 'string', ['null' => true])
+ ->addColumn('value', 'text', ['null' => true])
->addColumn('created', 'integer', ['default' => 0])
->addColumn('updated', 'integer', ['null' => true])
->addForeignKey('form_attribute_id', 'form_attributes', 'id', [
diff --git a/migrations/20170317174120_add_private_response_to_form_attribute.php b/migrations/20170317174120_add_private_response_to_form_attribute.php
new file mode 100644
index 0000000000..8af35a567d
--- /dev/null
+++ b/migrations/20170317174120_add_private_response_to_form_attribute.php
@@ -0,0 +1,29 @@
+table('form_attributes')
+ ->addColumn('response_private', 'boolean', [
+ 'default' => false,
+ 'null' => false,
+ ])
+ ->update();
+ }
+
+ /**
+ * Migrate Down.
+ */
+ public function down()
+ {
+ $this->table('form_attributes')
+ ->removeColumn('response_private')
+ ->update();
+ }
+}
diff --git a/migrations/20170322160413_add_hide_author_to_form.php b/migrations/20170322160413_add_hide_author_to_form.php
new file mode 100644
index 0000000000..6f89e3d820
--- /dev/null
+++ b/migrations/20170322160413_add_hide_author_to_form.php
@@ -0,0 +1,29 @@
+table('forms')
+ ->addColumn('hide_author', 'boolean', [
+ 'default' => false,
+ 'null' => false,
+ ])
+ ->update();
+ }
+
+ /**
+ * Migrate Down.
+ */
+ public function down()
+ {
+ $this->table('forms')
+ ->removeColumn('hide_author')
+ ->update();
+ }
+}
diff --git a/migrations/20170322162237_add_description_field_to_form_attribute.php b/migrations/20170322162237_add_description_field_to_form_attribute.php
new file mode 100644
index 0000000000..c09fd577ca
--- /dev/null
+++ b/migrations/20170322162237_add_description_field_to_form_attribute.php
@@ -0,0 +1,28 @@
+table('form_attributes')
+ ->addColumn('description', 'string', [
+ 'null' => true
+ ])
+ ->update();
+ }
+
+ /**
+ * Migrate Down.
+ */
+ public function down()
+ {
+ $this->table('form_attributes')
+ ->removeColumn('description')
+ ->update();
+ }
+}
diff --git a/migrations/20170325110408_add_language_to_user.php b/migrations/20170325110408_add_language_to_user.php
new file mode 100644
index 0000000000..7a05ad3d34
--- /dev/null
+++ b/migrations/20170325110408_add_language_to_user.php
@@ -0,0 +1,28 @@
+table('users')
+ ->addColumn('language', 'string', [
+ 'null' => true,
+ ])
+ ->update();
+ }
+
+ /**
+ * Migrate Down.
+ */
+ public function down()
+ {
+ $this->table('users')
+ ->removeColumn('language')
+ ->update();
+ }
+}
diff --git a/migrations/20170328080656_add_forms_tags_table.php b/migrations/20170328080656_add_forms_tags_table.php
new file mode 100644
index 0000000000..97a3545934
--- /dev/null
+++ b/migrations/20170328080656_add_forms_tags_table.php
@@ -0,0 +1,52 @@
+table('forms_tags', [
+ 'id' => false,
+ 'primary_key' => ['form_id', 'tag_id'],
+ ])
+ ->addColumn('form_id', 'integer')
+ ->addForeignKey('form_id', 'forms', 'id', [
+ 'delete' => 'CASCADE',
+ 'update' => 'CASCADE'
+ ])
+ ->addColumn('tag_id', 'integer')
+ ->addForeignKey('tag_id', 'tags', 'id', [
+ 'delete' => 'CASCADE',
+ 'update' => 'CASCADE'
+ ])
+ ->create();
+
+ }
+
+ /**
+ * Migrate Up.
+ */
+ public function up()
+ {
+
+
+ }
+
+ /**
+ * Migrate Down.
+ */
+ public function down()
+ {
+
+
+
+ }
+}
diff --git a/migrations/20170331210813_add_show_when_published_to_form_stage.php b/migrations/20170331210813_add_show_when_published_to_form_stage.php
new file mode 100644
index 0000000000..430c205f55
--- /dev/null
+++ b/migrations/20170331210813_add_show_when_published_to_form_stage.php
@@ -0,0 +1,29 @@
+table('form_stages')
+ ->addColumn('show_when_published', 'boolean', [
+ 'null' => false,
+ 'default' => false
+ ])
+ ->update();
+ }
+
+ /**
+ * Migrate Down.
+ */
+ public function down()
+ {
+ $this->table('form_stages')
+ ->removeColumn('show_when_published')
+ ->update();
+ }
+}
diff --git a/migrations/20170412191954_convert_form_tags_to_form_attribute.php b/migrations/20170412191954_convert_form_tags_to_form_attribute.php
new file mode 100644
index 0000000000..7742b73197
--- /dev/null
+++ b/migrations/20170412191954_convert_form_tags_to_form_attribute.php
@@ -0,0 +1,59 @@
+getAdapter()->getConnection();
+ $rows = $this->fetchAll(
+ "SELECT id
+ FROM form_stages"
+ );
+ $tag_rows = $this->fetchAll(
+ "SELECT id
+ FROM tags"
+ );
+ $insert = $pdo->prepare(
+ "INSERT into form_attributes
+ (`label`,`type`, `required`, `priority`, `cardinality`, `input`, `options`, `key`, `form_stage_id`)
+ VALUES
+ ('Categories', 'varchar', 0, 3, 0, 'tags', :tags, :key, :form_stage_id)"
+ );
+
+ $tags = [];
+
+ foreach ($tag_rows as $tag_row) {
+ array_push($tags, (int)$tag_row['id']);
+ }
+ $tags = json_encode($tags);
+
+ foreach ($rows as $row) {
+ $uuid = Uuid::uuid4();
+ $key = $uuid->toString();
+ $insert->execute(
+ [
+ ':tags' => $tags,
+ ':key' => $key,
+ ':form_stage_id' => $row['id']
+ ]
+ );
+ }
+ }
+
+ /**
+ * Migrate Down.
+ */
+ public function down()
+ {
+ $this->execute("DELETE from form_attributes where input = 'tags'");
+
+ }
+}
diff --git a/migrations/20170417082819_add_tags_to_forms_tags.php b/migrations/20170417082819_add_tags_to_forms_tags.php
new file mode 100644
index 0000000000..2fed17221d
--- /dev/null
+++ b/migrations/20170417082819_add_tags_to_forms_tags.php
@@ -0,0 +1,49 @@
+getAdapter()->getConnection();
+
+ $forms = $this->fetchAll(
+ "SELECT id
+ FROM forms"
+ );
+ $tags = $this->fetchAll(
+ "SELECT id
+ FROM tags"
+ );
+
+ $insert = $pdo->prepare(
+ "INSERT into
+ forms_tags
+ (`tag_id`, `form_id`)
+ VALUES(:tag_id, :form_id)
+ "
+ );
+
+ foreach ($forms as $form) {
+ foreach ($tags as $tag) {
+ $insert->execute(
+ [':tag_id' => $tag['id'],
+ ':form_id' => $form['id']
+ ]
+ );
+ }
+ }
+ }
+
+ /**
+ * Migrate Down.
+ */
+ public function down()
+ {
+ $this->execute("DELETE from forms_tags");
+ }
+}
diff --git a/migrations/20170417090621_move_post_tag_values_to_post_varchar.php b/migrations/20170417090621_move_post_tag_values_to_post_varchar.php
new file mode 100644
index 0000000000..fed076d255
--- /dev/null
+++ b/migrations/20170417090621_move_post_tag_values_to_post_varchar.php
@@ -0,0 +1,89 @@
+getAdapter()->getConnection();
+ // fetching posts with form_attribute_id
+ $posts = $this->fetchAll(
+ "SELECT posts.id, posts.form_id, form_attributes.id as form_attribute_id
+ FROM posts
+ INNER JOIN form_stages
+ ON form_stages.form_id = posts.form_id
+ INNER JOIN form_attributes
+ ON form_attributes.form_stage_id = form_stages.id"
+ );
+ $insert = $pdo->prepare(
+ "INSERT into
+ post_varchar
+ (`post_id`, `form_attribute_id`, `value`, `created`)
+ VALUES(:post_id, :form_attribute_id, :value, :created)"
+ );
+ foreach ($posts as $post) {
+ $post_tags = $pdo->prepare(
+ "SELECT tag_id
+ FROM posts_tags
+ WHERE post_id = :post_id"
+ );
+ $post_tags->execute([':post_id' => $post['id']]);
+ // inserting post_ids and tag_ids(value) in post_varchar
+ $tags = $post_tags->fetchAll();
+ foreach ($tags as $tag) {
+ $insert->execute(
+ [
+ ':post_id' => $post['id'],
+ ':form_attribute_id' => $post['form_attribute_id'],
+ ':value' => $tag['tag_id'],
+ ':created' => time()
+ ]
+ );
+ }
+ }
+ }
+
+ /**
+ * Migrate Down.
+ */
+ public function down()
+ {
+ $pdo = $this->getAdapter()->getConnection();
+ $posts = $this->fetchAll(
+ "SELECT posts.id, posts.form_id, form_attributes.id as form_attribute_id
+ FROM posts
+ INNER JOIN form_stages
+ ON form_stages.form_id = posts.form_id
+ INNER JOIN form_attributes
+ ON form_attributes.form_stage_id = form_stages.id"
+ );
+ $delete = $pdo->prepare(
+ "DELETE from post_varchar
+ WHERE post_id = :post_id AND form_attribute_id = :form_attribute_id AND value = :value"
+ );
+
+ foreach ($posts as $post) {
+ $post_tags = $pdo->prepare(
+ "SELECT tag_id
+ FROM posts_tags
+ WHERE post_id = :post_id"
+ );
+ $post_tags->execute([':post_id' => $post['id']]);
+ // inserting post_ids and tag_ids(value) in post_varchar
+ $tags = $post_tags->fetchAll();
+ foreach ($tags as $tag) {
+ $delete->execute(
+ [
+ ':post_id' => $post['id'],
+ ':form_attribute_id' => $post['form_attribute_id'],
+ ':value' => $tag['tag_id']
+ ]
+ );
+ }
+ }
+ }
+}
diff --git a/application/classes/Ushahidi/Acl.php b/src/App/Acl.php
similarity index 83%
rename from application/classes/Ushahidi/Acl.php
rename to src/App/Acl.php
index af2e03ab8b..c5b0e609ca 100644
--- a/application/classes/Ushahidi/Acl.php
+++ b/src/App/Acl.php
@@ -1,4 +1,4 @@
-role) {
return false;
}
-
+
$role = $this->role_repo->getByName($user->role);
// Does the user have the permission?
diff --git a/application/classes/Ushahidi/Authenticator/Password.php b/src/App/Authenticator/Password.php
similarity index 81%
rename from application/classes/Ushahidi/Authenticator/Password.php
rename to src/App/Authenticator/Password.php
index 37f7f10a19..796e6d2641 100644
--- a/application/classes/Ushahidi/Authenticator/Password.php
+++ b/src/App/Authenticator/Password.php
@@ -1,4 +1,4 @@
-fetchOne());
- $reader->addFilter(function($row) use ($nbColumns) {
+ $reader->addFilter(function ($row) use ($nbColumns) {
return count($row) == $nbColumns;
});
@@ -51,6 +53,6 @@ public function process($file)
$reader->setLimit($this->limit);
}
- return new ArrayIterator($reader->fetchAssoc());
+ return new \ArrayIterator($reader->fetchAssoc());
}
}
diff --git a/application/classes/Ushahidi/CSVReaderFactory.php b/src/App/FileReader/CSVReaderFactory.php
similarity index 64%
rename from application/classes/Ushahidi/CSVReaderFactory.php
rename to src/App/FileReader/CSVReaderFactory.php
index b38e329d5a..44ae1cca52 100644
--- a/application/classes/Ushahidi/CSVReaderFactory.php
+++ b/src/App/FileReader/CSVReaderFactory.php
@@ -1,4 +1,4 @@
-config['media_upload_dir']);
+ return new LocalAdapter($this->config['media_upload_dir']);
}
}
-
diff --git a/application/classes/Ushahidi/FilesystemAdapter/Rackspace.php b/src/App/FilesystemAdapter/Rackspace.php
similarity index 80%
rename from application/classes/Ushahidi/FilesystemAdapter/Rackspace.php
rename to src/App/FilesystemAdapter/Rackspace.php
index 446dd91387..6bd8f775a4 100644
--- a/application/classes/Ushahidi/FilesystemAdapter/Rackspace.php
+++ b/src/App/FilesystemAdapter/Rackspace.php
@@ -1,4 +1,4 @@
- $this->config['apiKey'],
));
- $store = $client->objectStoreService(null,$this->config['region']);
+ $store = $client->objectStoreService(null, $this->config['region']);
$container = $store->getContainer($this->config['container']);
return new Adapter($container);
}
}
-
diff --git a/application/classes/Ushahidi/Hasher/Password.php b/src/App/Hasher/Password.php
similarity index 78%
rename from application/classes/Ushahidi/Hasher/Password.php
rename to src/App/Hasher/Password.php
index f485ef80a8..ead74656e9 100644
--- a/application/classes/Ushahidi/Hasher/Password.php
+++ b/src/App/Hasher/Password.php
@@ -1,4 +1,4 @@
-set('tool.acl', $di->lazyNew('Ushahidi\App\Acl'));
+$di->setter['Ushahidi\App\Acl']['setRoleRepo'] = $di->lazyGet('repository.role');
+
+$di->set('tool.hasher.password', $di->lazyNew('Ushahidi\App\Hasher\Password'));
+$di->set('tool.authenticator.password', $di->lazyNew('Ushahidi\App\Authenticator\Password'));
+
+$di->set('filereader.csv', $di->lazyNew('Ushahidi\App\FileReader\CSV'));
+$di->setter['Ushahidi\App\FileReader\CSV']['setReaderFactory'] =
+ $di->lazyGet('csv.reader_factory');
+
+$di->set('csv.reader_factory', $di->lazyNew('Ushahidi\App\FileReader\CSVReaderFactory'));
+
+// Register filesystem adapter types
+// Currently supported: Local filesysten, AWS S3 v3, Rackspace
+// the naming scheme must match the cdn type set in config/cdn
+$di->set('adapter.local', $di->lazyNew(
+ 'Ushahidi\App\FilesystemAdapter\Local',
+ ['config' => $di->lazyGet('cdn.config')]
+));
+
+$di->set('adapter.aws', $di->lazyNew(
+ 'Ushahidi\App\FilesystemAdapter\AWS',
+ ['config' => $di->lazyGet('cdn.config')]
+));
+
+$di->set('adapter.rackspace', $di->lazyNew(
+ 'Ushahidi\App\FilesystemAdapter\Rackspace',
+ ['config' => $di->lazyGet('cdn.config')]
+));
+
+// Media Filesystem
+// The Ushahidi filesystem adapter returns a flysystem adapter for a given
+// cdn type based on the provided configuration
+$di->set('tool.filesystem', $di->lazyNew('Ushahidi\App\Filesystem'));
+$di->params['Ushahidi\App\Filesystem'] = [
+ 'adapter' => $di->lazy(function () use ($di) {
+ $adapter_type = $di->get('cdn.config');
+ $fsa = $di->get('adapter.' . $adapter_type['type']);
+
+ return $fsa->getAdapter();
+ })
+];
+
+// Defined memcached
+$di->set('memcached', $di->lazy(function () use ($di) {
+ $config = $di->get('ratelimiter.config');
+
+ $memcached = new Memcached();
+ $memcached->addServer($config['memcached']['host'], $config['memcached']['port']);
+
+ return $memcached;
+}));
+
+// Set up login rate limiter
+$di->set('ratelimiter.login.flap', $di->lazyNew('BehEh\Flaps\Flap'));
+
+$di->params['BehEh\Flaps\Flap'] = [
+ 'storage' => $di->lazyNew('BehEh\Flaps\Storage\DoctrineCacheAdapter'),
+ 'name' => 'login'
+];
+
+$di->set('ratelimiter.login.strategy', $di->lazyNew('BehEh\Flaps\Throttling\LeakyBucketStrategy'));
+
+// 3 requests every 1 minute by default
+$di->params['BehEh\Flaps\Throttling\LeakyBucketStrategy'] = [
+ 'requests' => 3,
+ 'timeSpan' => '1m'
+];
+
+$di->set('ratelimiter.login', $di->lazyNew('Ushahidi\App\RateLimiter'));
+
+$di->params['Ushahidi\App\RateLimiter'] = [
+ 'flap' => $di->lazyGet('ratelimiter.login.flap'),
+ 'throttlingStrategy' => $di->lazyGet('ratelimiter.login.strategy'),
+];
+
+$di->params['BehEh\Flaps\Storage\DoctrineCacheAdapter'] = [
+ 'cache' => $di->lazyGet('ratelimiter.cache')
+];
+
+// Rate limit storage cache
+$di->set('ratelimiter.cache', function () use ($di) {
+ $config = $di->get('ratelimiter.config');
+ $cache = $config['cache'];
+
+ if ($cache === 'memcached') {
+ $di->setter['Doctrine\Common\Cache\MemcachedCache']['setMemcached'] =
+ $di->lazyGet('memcached');
+
+ return $di->newInstance('Doctrine\Common\Cache\MemcachedCache');
+ } elseif ($cache === 'filesystem') {
+ $di->params['Doctrine\Common\Cache\FilesystemCache'] = [
+ 'directory' => $config['filesystem']['directory'],
+ ];
+
+ return $di->newInstance('Doctrine\Common\Cache\FilesystemCache');
+ }
+
+ // Fall back to using in-memory cache if none is configured
+ return $di->newInstance('Doctrine\Common\Cache\ArrayCache');
+});
+
+// Rate limiter violation handler
+$di->setter['BehEh\Flaps\Flap']['setViolationHandler'] =
+ $di->lazyNew('Ushahidi\App\ThrottlingViolationHandler');
diff --git a/application/classes/Ushahidi/RateLimiter.php b/src/App/RateLimiter.php
similarity index 85%
rename from application/classes/Ushahidi/RateLimiter.php
rename to src/App/RateLimiter.php
index 02dda4b0e6..98da9f689c 100644
--- a/application/classes/Ushahidi/RateLimiter.php
+++ b/src/App/RateLimiter.php
@@ -1,4 +1,4 @@
- 'bool',
'created' => 'int',
'updated' => 'int',
+ 'hide_author' => 'bool',
'require_approval' => 'bool',
'everyone_can_create' => 'bool',
'can_create' => 'array',
+ 'tags' => 'array',
];
}
diff --git a/src/Core/Entity/FormAttribute.php b/src/Core/Entity/FormAttribute.php
index fac76c818b..28c16724b2 100644
--- a/src/Core/Entity/FormAttribute.php
+++ b/src/Core/Entity/FormAttribute.php
@@ -28,6 +28,7 @@ class FormAttribute extends StaticEntity
protected $cardinality;
protected $config = [];
protected $form_stage_id;
+ protected $response_private;
// StatefulData
protected function getDerived()
@@ -55,6 +56,7 @@ protected function getDefinition()
'config' => '*json',
'form_stage' => false, /* alias */
'form_stage_id' => 'int',
+ 'response_private' => 'bool',
];
}
diff --git a/src/Core/Entity/FormStage.php b/src/Core/Entity/FormStage.php
index ba74a88c3a..a42dd1c8d3 100644
--- a/src/Core/Entity/FormStage.php
+++ b/src/Core/Entity/FormStage.php
@@ -22,6 +22,7 @@ class FormStage extends StaticEntity
protected $icon;
protected $type;
protected $required;
+ protected $show_when_published;
protected $description;
// DataTransformer
@@ -30,6 +31,7 @@ protected function getDefinition()
return [
'id' => 'int',
'description' => 'string',
+ 'show_when_published' => 'boolean',
'type' => 'string',
'form_id' => 'int',
'label' => 'string',
diff --git a/src/Core/Entity/Tag.php b/src/Core/Entity/Tag.php
index 5c0f1554c9..50fc758853 100644
--- a/src/Core/Entity/Tag.php
+++ b/src/Core/Entity/Tag.php
@@ -26,6 +26,8 @@ class Tag extends StaticEntity
protected $priority;
protected $created;
protected $role;
+ protected $forms;
+ protected $children;
// StatefulData
protected function getDerived()
@@ -55,6 +57,8 @@ protected function getDefinition()
'priority' => 'int',
'created' => 'int',
'role' => '*json',
+ 'forms' => 'array',
+ 'children' =>'array',
];
}
diff --git a/src/Core/Entity/User.php b/src/Core/Entity/User.php
index 74d4716301..ec0e3a745d 100644
--- a/src/Core/Entity/User.php
+++ b/src/Core/Entity/User.php
@@ -26,6 +26,7 @@ class User extends StaticEntity
protected $created;
protected $updated;
protected $role;
+ protected $language;
// DataTransformer
protected function getDefinition()
@@ -42,6 +43,7 @@ protected function getDefinition()
'created' => 'int',
'updated' => 'int',
'role' => 'string',
+ 'language' => 'string',
];
}
diff --git a/src/Core/Traits/PostValueRestrictions.php b/src/Core/Traits/PostValueRestrictions.php
new file mode 100644
index 0000000000..a4d71bdcb2
--- /dev/null
+++ b/src/Core/Traits/PostValueRestrictions.php
@@ -0,0 +1,55 @@
+
+ * @package Ushahidi\Application
+ * @copyright 2014 Ushahidi
+ * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3)
+ */
+
+namespace Ushahidi\Core\Traits;
+
+use Ushahidi\Core\Entity\User;
+use Ushahidi\Core\Entity\Post;
+use Ushahidi\Core\Entity\FormRepository;
+
+trait PostValueRestrictions
+{
+
+
+ public function canUserSeeAuthor(Post $post, FormRepository $form_repo, $user)
+ {
+
+ if ($post->form_id) {
+
+ if ($this->canUserEditForm($post->form_id, $user)) {
+ return true;
+ }
+
+ return !$form_repo->isAuthorHidden($post->form_id);
+ }
+
+ return true;
+ }
+
+
+ /**
+ * Test whether the post instance requires value restriction
+ * @param Post $post
+ * @return Boolean
+ */
+ public function canUserReadPostsValues(Post $post, $user)
+ {
+ return $this->canUserEditForm($post->form_id, $user);
+ }
+
+ /* FormRole */
+ protected function canUserEditForm($form_id, $user)
+ {
+ return $this->isUserAdmin($user) || $this->hasPermission($user, $this->getPermission());
+ }
+}
diff --git a/src/Core/Usecase/Post/ValuesForPostRepository.php b/src/Core/Usecase/Post/ValuesForPostRepository.php
index c0fd608109..d0f7153634 100644
--- a/src/Core/Usecase/Post/ValuesForPostRepository.php
+++ b/src/Core/Usecase/Post/ValuesForPostRepository.php
@@ -19,7 +19,12 @@ interface ValuesForPostRepository
* @param Array $include_attributes
* @return [Ushahidi\Core\Entity\PostValue, ...]
*/
- public function getAllForPost($post_id, Array $include_attributes = []);
+ public function getAllForPost(
+ $post_id,
+ Array $include_attributes = [],
+ Array $exclude_stages = [],
+ $restricted = false
+ );
/**
* @param int $post_id
diff --git a/src/Init.php b/src/Init.php
index 5711b52e30..06b8047ca4 100644
--- a/src/Init.php
+++ b/src/Init.php
@@ -347,6 +347,7 @@ function feature($name)
$di->setter['Ushahidi\Core\Traits\DataImportAccess']['setEnabled'] = $di->lazyGet('data-import.enabled');
// Tools
+$di->set('tool.signer', $di->lazyNew('Ushahidi\Core\Tool\Signer'));
$di->set('tool.uploader', $di->lazyNew('Ushahidi\Core\Tool\Uploader'));
$di->params['Ushahidi\Core\Tool\Uploader'] = [
'fs' => $di->lazyGet('tool.filesystem'),
@@ -396,3 +397,5 @@ function feature($name)
];
$di->set('authorizer.console', $di->lazyNew('Ushahidi\Console\Authorizer\ConsoleAuthorizer'));
+
+require __DIR__ . '/App/Init.php';