/**
  * @param string $projectId
  * @param string $userId
  * @returns array - the DTO array
  */
 public static function encode($projectId, $userId)
 {
     $userModel = new UserModel($userId);
     $projectModel = new SfchecksProjectModel($projectId);
     $textList = new TextListModel($projectModel);
     $textList->read();
     $list = $projectModel->listUsers();
     $data = array();
     $data['count'] = count($list->entries);
     $data['entries'] = array_values($list->entries);
     // re-index array
     $data['project'] = ProjectSettingsDtoEncoder::encode($projectModel);
     unset($data['project']['users']);
     $data['archivedTexts'] = array();
     foreach ($textList->entries as $entry) {
         $textModel = new TextModel($projectModel, $entry['id']);
         if ($textModel->isArchived) {
             $questionList = $textModel->listQuestionsWithAnswers();
             // Just want count of questions and responses, not whole list
             $entry['questionCount'] = $questionList->count;
             $responseCount = 0;
             // "Responses" = answers + comments
             foreach ($questionList->entries as $q) {
                 foreach ($q['answers'] as $a) {
                     $commentCount = count($a['comments']);
                     $responseCount += $commentCount + 1;
                     // +1 for this answer
                 }
             }
             $entry['responseCount'] = $responseCount;
             $entry['dateModified'] = $textModel->dateModified->asDateTimeInterface()->format(\DateTime::RFC2822);
             $data['archivedTexts'][] = $entry;
         }
     }
     $data['rights'] = RightsHelper::encode($userModel, $projectModel);
     $data['bcs'] = BreadCrumbHelper::encode('settings', $projectModel, null, null);
     return $data;
 }
 public function testCRUD_Works()
 {
     $environ = new MongoTestEnvironment();
     $environ->clean();
     $project = $environ->createProject(SF_TESTPROJECT, SF_TESTPROJECTCODE);
     // List
     $list = new TextListModel($project);
     $list->read();
     $this->assertEquals(0, $list->count);
     // Create
     $text = new TextModel($project);
     $text->title = "Some Text";
     $usx = MongoTestEnvironment::usxSample();
     $text->content = $usx;
     $id = $text->write();
     $this->assertNotNull($id);
     $this->assertInternalType('string', $id);
     $this->assertEquals($text->id->asString(), $id);
     // Read back
     $otherText = new TextModel($project, $id);
     $this->assertEquals($id, $otherText->id->asString());
     $this->assertEquals('Some Text', $otherText->title);
     $this->assertEquals($usx, $otherText->content);
     // Update
     $otherText->title = 'Other Text';
     $otherText->write();
     // Read back
     $otherText = new TextModel($project, $id);
     $this->assertEquals('Other Text', $otherText->title);
     // List
     $list->read();
     $this->assertEquals(1, $list->count);
     // Delete
     TextModel::remove($project->databaseName(), $id);
     // List
     $list->read();
     $this->assertEquals(0, $list->count);
 }
 /**
  * @param string $projectId
  * @param string $userId
  * @returns array - the DTO array
  * @throws ResourceNotAvailableException
  */
 public static function encode($projectId, $userId)
 {
     $user = new UserModel($userId);
     $project = new SfchecksProjectModel($projectId);
     if ($project->isArchived && $project->users[$userId]->role != ProjectRoles::MANAGER) {
         throw new ResourceNotAvailableException("This Project is no longer available. If this is incorrect contact your project manager.");
     }
     $textList = new TextListModel($project);
     $textList->read();
     $data = array();
     $data['rights'] = RightsHelper::encode($user, $project);
     $data['project'] = array('name' => $project->projectName, 'id' => $projectId);
     if ($project->isArchived) {
         $data['project']['name'] .= " [ARCHIVED]";
     }
     $data['texts'] = array();
     foreach ($textList->entries as $entry) {
         $text = new TextModel($project, $entry['id']);
         if (!$text->isArchived) {
             $questionList = $text->listQuestionsWithAnswers();
             // Just want count of questions and responses, not whole list
             $entry['questionCount'] = 0;
             $responseCount = 0;
             // "Responses" = answers + comments
             foreach ($questionList->entries as $q) {
                 $question = new QuestionModel($project, $q['id']);
                 if (!$question->isArchived) {
                     $entry['questionCount']++;
                     foreach ($q['answers'] as $a) {
                         $commentCount = count($a['comments']);
                         $responseCount += $commentCount + 1;
                         // +1 for this answer
                     }
                 }
             }
             $entry['responseCount'] = $responseCount;
             $entry['dateCreated'] = $text->dateCreated->asDateTimeInterface()->format(\DateTime::RFC2822);
             $data['texts'][] = $entry;
         }
     }
     // sort Texts with newest at the top
     usort($data['texts'], function ($a, $b) {
         $sortOn = 'dateCreated';
         if (array_key_exists($sortOn, $a) && array_key_exists($sortOn, $b)) {
             return strtotime($a[$sortOn]) < strtotime($b[$sortOn]) ? 1 : -1;
         } else {
             return 0;
         }
     });
     // future support for members
     $data['members'] = array();
     // unread activity count
     $unreadActivity = new UnreadActivityModel($userId, $projectId);
     $unreadItems = $unreadActivity->unreadItems();
     $data['activityUnreadCount'] = count($unreadItems);
     // unread broadcast messages
     $unreadMessages = new UnreadMessageModel($userId, $projectId);
     $messageIds = $unreadMessages->unreadItems();
     $messages = array();
     foreach ($messageIds as $messageId) {
         $message = new MessageModel($project, $messageId);
         $messages[] = array('id' => $message->id->asString(), 'subject' => $message->subject, 'content' => $message->content);
     }
     $data['broadcastMessages'] = $messages;
     return $data;
 }
 public static function UserEngagementReport($projectId)
 {
     $project = ProjectModel::getById($projectId);
     $output = str_pad('**** User Engagement Report ****', 120, " ", STR_PAD_BOTH) . "\n";
     $output .= str_pad(date(DATE_RFC2822), 120, " ", STR_PAD_BOTH) . "\n\n";
     $data = array();
     $activeUsers = array();
     $managerUsers = array();
     $inactiveUsers = array();
     $invalidUsers = array();
     $listModel = new UserListProjectModel($projectId);
     $listModel->read();
     if ($listModel->count > 0) {
         $textListModel = new TextListModel($project);
         $textListModel->read();
         $questions = array();
         foreach ($textListModel->entries as $text) {
             $questionListModel = new QuestionAnswersListModel($project, $text['id']);
             $questionListModel->read();
             $questions = array_merge($questions, array_map(function ($q) use($text) {
                 $q['textRef'] = $text['id'];
                 return $q;
             }, $questionListModel->entries));
         }
         $answerCtr = 0;
         $commentCtr = 0;
         foreach ($listModel->entries as $user) {
             $userModel = new UserModel($user['id']);
             $user['isActive'] = $userModel->active;
             $user['questions'] = 0;
             $user['texts'] = 0;
             $user['answers'] = 0;
             $user['comments'] = 0;
             $user['responses'] = 0;
             $user['textIds'] = array();
             if (!$user['isActive']) {
                 if (!$user['email']) {
                     $user['email'] = $userModel->emailPending;
                 }
                 array_push($invalidUsers, $user);
                 continue;
             }
             if ($project->users->offsetExists($user['id'])) {
                 $user['role'] = $project->users[$user['id']]->role;
             } else {
                 $user['role'] = ProjectRoles::NONE;
             }
             $answerCtr = 0;
             $commentCtr = 0;
             foreach ($questions as $question) {
                 $responses = 0;
                 foreach ($question['answers'] as $answer) {
                     if (!$answer['content']) {
                         continue;
                     }
                     $answerCtr++;
                     foreach ($answer['comments'] as $comment) {
                         if (!$comment['content']) {
                             continue;
                         }
                         $commentCtr++;
                         if ($comment['userRef'] && strval($comment['userRef']) == $user['id']) {
                             $user['comments']++;
                             $user['responses']++;
                             array_push($user['textIds'], $question['textRef']);
                             $responses++;
                         }
                     }
                     if ($answer['userRef'] && strval($answer['userRef']) == $user['id']) {
                         $user['answers']++;
                         $user['responses']++;
                         array_push($user['textIds'], $question['textRef']);
                         $responses++;
                     }
                 }
                 if ($responses > 0) {
                     $user['questions']++;
                 }
             }
             $user['texts'] = count(array_unique($user['textIds']));
             if ($user['role'] == ProjectRoles::MANAGER) {
                 array_push($managerUsers, $user);
             } elseif ($user['responses'] > 0) {
                 array_push($activeUsers, $user);
             } else {
                 array_push($inactiveUsers, $user);
             }
         }
         $output .= $project->projectName . " Project\n";
         $output .= "Texts (T's) in Project: " . $textListModel->count . "\n";
         $output .= "Questions (Q's) in Project: " . count($questions) . "\n";
         $output .= "Responses (R's) in Project (Answers + Comments): " . ($answerCtr + $commentCtr) . "\n";
         $output .= "Answers (A's) in Project: " . $answerCtr . "\n";
         $output .= "Comments (C's) in Project: " . $commentCtr . "\n";
     } else {
         $output .= "This project has no users\n\n";
     }
     $sortByResponses = function ($a, $b) {
         if ($a['responses'] > $b['responses']) {
             return -1;
         } elseif ($a['responses'] < $b['responses']) {
             return 1;
         } else {
             if ($a['answers'] > $b['answers']) {
                 return -1;
             } elseif ($a['answers'] < $b['answers']) {
                 return 1;
             } else {
                 if ($a['comments'] > $b['comments']) {
                     return -1;
                 } elseif ($a['comments'] < $b['comments']) {
                     return 1;
                 } else {
                     return strcmp($a['username'], $b['username']);
                 }
             }
         }
     };
     $sortByName = function ($a, $b) {
         return strcasecmp($a['name'], $b['name']);
     };
     usort($activeUsers, $sortByResponses);
     usort($managerUsers, $sortByResponses);
     usort($inactiveUsers, $sortByName);
     usort($invalidUsers, $sortByName);
     $output .= "\n\nManagers: " . count($managerUsers) . "\n" . str_pad("Name", 30) . str_pad("Email", 35) . str_pad("Username", 25) . str_pad("R's", 5) . str_pad("A's", 5) . str_pad("C's", 5) . str_pad("Q's", 5) . str_pad("T's", 5) . "\n\n";
     foreach ($managerUsers as $user) {
         $output .= str_pad($user['name'], 30) . str_pad($user['email'], 35) . str_pad($user['username'], 25) . str_pad($user['responses'], 5) . str_pad($user['answers'], 5) . str_pad($user['comments'], 5) . str_pad($user['questions'], 5) . str_pad($user['texts'], 5) . "\n";
     }
     $output .= "\n\nActive Users: " . count($activeUsers) . "\n" . str_pad("Name", 30) . str_pad("Email", 35) . str_pad("Username", 25) . str_pad("R's", 5) . str_pad("A's", 5) . str_pad("C's", 5) . str_pad("Q's", 5) . str_pad("T's", 5) . "\n\n";
     foreach ($activeUsers as $user) {
         $output .= str_pad($user['name'], 30) . str_pad($user['email'], 35) . str_pad($user['username'], 25) . str_pad($user['responses'], 5) . str_pad($user['answers'], 5) . str_pad($user['comments'], 5) . str_pad($user['questions'], 5) . str_pad($user['texts'], 5) . "\n";
     }
     $output .= "\n\nInactive Users (never engaged): " . count($inactiveUsers) . "\n" . str_pad("Name", 30) . str_pad("Email", 35) . str_pad("Username", 25) . "\n\n";
     foreach ($inactiveUsers as $user) {
         $output .= str_pad($user['name'], 30) . str_pad($user['email'], 35) . str_pad($user['username'], 25) . "\n";
     }
     $output .= "\n\nInvited Users (but never validated or logged in): " . count($invalidUsers) . "\n" . str_pad("Name", 30) . str_pad("Email", 35) . str_pad("Username", 25) . "\n\n";
     foreach ($invalidUsers as $user) {
         $output .= str_pad($user['name'], 30) . str_pad($user['email'], 35) . str_pad($user['username'], 25) . "\n";
     }
     $data['output'] = $output;
     $data['result'] = array('managerUsers' => $managerUsers, 'activeUsers' => $activeUsers, 'inactiveUsers' => $inactiveUsers, 'invitedUsers' => $invalidUsers);
     return $data;
 }
 public function run($mode = 'test')
 {
     $testMode = $mode == 'test';
     $message = "";
     $userList = new UserListModel();
     $userList->read();
     $userIds = array_map(function ($e) {
         return $e['id'];
     }, $userList->entries);
     $projectList = new ProjectListModel();
     $projectList->read();
     $projectIds = array_map(function ($e) {
         return $e['id'];
     }, $projectList->entries);
     $deadCommentUserRefs = 0;
     $deadAnswerUserRefs = 0;
     foreach ($projectIds as $projectId) {
         $project = new ProjectModel($projectId);
         $textList = new TextListModel($project);
         $textList->read();
         $textIds = array_map(function ($e) {
             return $e['id'];
         }, $textList->entries);
         foreach ($textIds as $textId) {
             $questionList = new QuestionListModel($project, $textId);
             $questionList->read();
             $questionIds = array_map(function ($e) {
                 return $e['id'];
             }, $questionList->entries);
             foreach ($questionIds as $questionId) {
                 $question = new QuestionModel($project, $questionId);
                 foreach ($question->answers as $answerId => $answer) {
                     foreach ($answer->comments as $commentId => $comment) {
                         /** @var IdReference $ref */
                         $ref = $comment->userRef;
                         if (!empty($ref->id) && !in_array($ref->asString(), $userIds)) {
                             $comment->userRef->id = '';
                             if (!$testMode) {
                                 $question->writeComment($project->databaseName(), $questionId, $answerId, $comment);
                             }
                             $deadCommentUserRefs++;
                             $message .= "Removed dead user-comment ref {$ref} from question {$questionId}, answer {$answerId}, comment {$commentId}\n";
                         }
                     }
                     $ref = $answer->userRef;
                     if (!empty($ref->id) && !in_array($ref->asString(), $userIds)) {
                         $answer->userRef->id = '';
                         if (!$testMode) {
                             $question->writeAnswer($answer);
                         }
                         $deadAnswerUserRefs++;
                         $message .= "Removed dead user-answer ref {$ref} from question {$questionId}, answer {$answerId}\n";
                     }
                 }
             }
         }
     }
     if ($deadAnswerUserRefs > 0) {
         $message .= "\n\nRemoved dead user references from {$deadAnswerUserRefs} answers\n\n";
     } else {
         $message .= "\n\nNo dead user references were found in answers\n\n";
     }
     if ($deadCommentUserRefs > 0) {
         $message .= "\n\nRemoved dead user references from {$deadCommentUserRefs} comments\n\n";
     } else {
         $message .= "\n\nNo dead user references were found in comments\n\n";
     }
     return $message;
 }
 public function run($mode = 'test')
 {
     $testMode = $mode != 'run';
     $message = "";
     $projectlist = new ProjectListModel();
     $projectlist->read();
     $textsExamined = 0;
     $textsUpdated = 0;
     // foreach existing project
     foreach ($projectlist->entries as $projectParams) {
         $projectId = $projectParams['id'];
         if ($projectParams['projectName'] == 'Jamaican Psalms') {
             $project = new SfchecksProjectModel($projectId);
             $textlist = new TextListModel($project);
             $textlist->read();
             // foreach text in project
             foreach ($textlist->entries as $textParams) {
                 $textsExamined++;
                 $textId = $textParams['id'];
                 $legacyText = new TextModel_sf_v0_9_18($project, $textId);
                 $fileName = '';
                 if ($legacyText->audioUrl) {
                     $text = new TextModel($project, $textId);
                     if (!$testMode) {
                         if (!$text->audioFileName) {
                             // legacy audioUrl format "assets/<projectId>/<textId>_<fileName>"
                             $fileNamePrefix = $textId . '_';
                             $pos = strpos($legacyText->audioUrl, $fileNamePrefix);
                             $text->audioFileName = substr($legacyText->audioUrl, $pos + strlen($fileNamePrefix));
                         }
                         $text->write();
                     }
                     $message .= "Changed text: {$text->title}\n";
                     $textsUpdated++;
                 }
             }
             if (!$testMode) {
                 TextModel_sf_v0_9_18::removeAudioProperty($project->databaseName());
                 $message .= "Removed 'audioUrl' property from project: {$project->projectName}\n";
             }
         }
     }
     if ($textsUpdated > 0) {
         $message .= "\n\nChanged {$textsUpdated} legacy texts to only store audio filename\n\n";
     } else {
         $message .= "\n\nNo legacy text audio were found/changed. {$textsExamined} texts examined.\n\n";
     }
     // re-arrange assets folder
     // - remove <siteName> from path for LF
     // - add <appName> to path for SF and change <projectId> in path to <projectSlug (databaseName)>
     $message .= "\n\nRe-arrange assets folder\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n";
     $project = new SfchecksProjectModel();
     $assetsFolderPath = APPPATH . "assets";
     $assetsSubfolders = glob($assetsFolderPath . '/*');
     @mkdir($assetsFolderPath . '/lexicon');
     @mkdir($assetsFolderPath . '/sfchecks');
     foreach ($assetsSubfolders as $assetsSubfolder) {
         if (file_exists($assetsSubfolder) && is_dir($assetsSubfolder)) {
             $assetsSubfolderName = basename($assetsSubfolder);
             if (strpos($assetsSubfolderName, 'languageforge') !== false) {
                 $message .= "Move into lexicon: {$assetsSubfolderName}\n";
                 $oldFolderPath = $assetsSubfolder . '/lexicon';
                 $newFolderPath = $assetsFolderPath . '/lexicon';
             } elseif ($assetsSubfolderName == 'lexicon' || $assetsSubfolderName == 'sfchecks') {
                 $message .= "No change: {$assetsSubfolderName}\n";
                 $oldFolderPath = '';
                 $newFolderPath = '';
                 $assetsSubfolder = '';
             } elseif ($project->exists($assetsSubfolderName)) {
                 $message .= "Move into sfchecks: {$assetsSubfolderName}\n";
                 $oldFolderPath = $assetsSubfolder;
                 $project->read($assetsSubfolderName);
                 $projectSlug = $project->databaseName();
                 $newFolderPath = $assetsFolderPath . "/sfchecks/{$projectSlug}";
                 if (!$testMode) {
                 }
             } else {
                 $message .= "Delete: {$assetsSubfolderName}\n";
                 $oldFolderPath = '';
                 $newFolderPath = '';
             }
             if (!$testMode) {
                 if (file_exists($oldFolderPath) && is_dir($oldFolderPath)) {
                     if (!@rename($oldFolderPath, $newFolderPath)) {
                         $oldFiles = glob($oldFolderPath . '/*');
                         foreach ($oldFiles as $oldFile) {
                             $newFile = $newFolderPath . '/' . basename($oldFile);
                             rename($oldFile, $newFile);
                         }
                     }
                 }
                 if (file_exists($assetsSubfolder) && is_dir($assetsSubfolder)) {
                     $this->recursiveRemoveFolder($assetsSubfolder);
                 }
             }
         }
     }
     return $message;
 }