diff --git a/config/menu/adminPages.default.php b/config/menu/adminPages.default.php index 9d68b19edd..94e22de10e 100644 --- a/config/menu/adminPages.default.php +++ b/config/menu/adminPages.default.php @@ -16,20 +16,21 @@ * Do NOT change $menu variable name! */ -use src\Utils\Uri\SimpleRouter; use src\Controllers\Admin\CacheSetAdminController; +use src\Utils\Uri\SimpleRouter; /** @var array $links OcConfig::$links is accessible in within this scope */ $menu = [ 'mnu_reports' => '/admin_reports.php', // counters added in MainLayoutCtrl - 'mnu_pendings' => '/viewpendings.php', // counters added in MainLayoutCtrl + 'mnu_pendings' => SimpleRouter::getLink('Admin.GeoCacheApprovalAdmin'), // counters added in MainLayoutCtrl 'mnu_octeamStats' => '/articles.php?page=cog', 'mnu_notFoundCaches' => '/admin_cachenotfound.php', - 'mnu_searchUser' => SimpleRouter::getLink('Admin.UserAdmin','search'), + 'mnu_searchUser' => SimpleRouter::getLink('Admin.UserAdmin', 'search'), 'mnu_ocTeamNews' => SimpleRouter::getLink('News.NewsAdmin'), 'mnu_geoPathAdmin' => '/powerTrailCOG.php', 'mnu_abandonCacheSets' => SimpleRouter::getLink( - CacheSetAdminController::class, 'cacheSetsToArchive' + CacheSetAdminController::class, + 'cacheSetsToArchive' ), ]; diff --git a/lib/languages/en.php b/lib/languages/en.php index db24dde31a..b80dd2a032 100644 --- a/lib/languages/en.php +++ b/lib/languages/en.php @@ -3214,4 +3214,11 @@ 'at_day' => 'NOT recommended at night', 'at_notinwinter' => 'NOT available during winter', 'at_allseasons' => 'Available all seasons', + + 'cache_approval_refresh' => 'Refresh list', + 'cache_approval_refresh_time' => 'Last update', + 'cache_approval_nonamed' => 'no name', + 'cache_approval_no_caches' => 'No geocaches waiting to be accepted', + 'cache_approval_changed_time' => 'status changed', + 'cache_approval_cache_invalid' => 'Cache "%s" is not valid for approval actions', ]; diff --git a/public/css/style_screen.css b/public/css/style_screen.css index 50e787ca4a..1f75b8af8b 100644 --- a/public/css/style_screen.css +++ b/public/css/style_screen.css @@ -1339,3 +1339,7 @@ tr.geoKretLog { .float-right { float: right; } + +.hidden { + display: none; +} diff --git a/public/views/admin/geocacheApproval/geocache_approval.css b/public/views/admin/geocacheApproval/geocache_approval.css new file mode 100644 index 0000000000..9b0105e1ad --- /dev/null +++ b/public/views/admin/geocacheApproval/geocache_approval.css @@ -0,0 +1,147 @@ +.geocacheApproval-truncated { + display: inline-block; + width: 130px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +#geocacheApproval_info { + height: 2.2em; +} + +.geocacheApproval-acceptedInfo { + font-weight: bold; + color: #007000; +} + +.geocacheApproval-rejectedInfo { + font-weight: bold; + color: #550000; +} + +.geocacheApproval-errorInfo { + font-weight: bold; + color: #A00000; +} + +.geocacheApproval-icon { + float: left; + margin-right: 8px; +} + +.geocacheApproval-hidden { + display: none; +} + +.geocacheApproval-refreshBlock { + width: 97%; + clear: both; + padding: 3px 0px; +} + +.geocacheApproval-refreshButton { + display: block; + float: left; + margin-bottom: 3px; + font-size: 1.2em; +} + +.geocacheApproval-refreshInfo { + display: block; + float: right; +} + +.geocacheApproval-emptyList { + text-align: center !important; + padding: 3px 0px !important; + font-weight: bold; +} + +.geocacheApproval-region { + font-weight:bold; + font-size:10px; + color:blue; +} + +.geocacheApproval-confirmDialog-noTitlebar .ui-dialog-titlebar { + display: none; +} + +.geocacheApproval-confirmDialog-content { + text-align: center; +} + +.geocacheApproval-confirmDialog-content2 { + display: inline-block; + font-size: 1.2em; + margin-left: auto; + margin-right: auto; + text-align: justify; +} + +.geocacheApproval-confirmDialog-buttons { + padding-top: 3em; +} + +.geocacheApproval-confirmDialog.ui-widget.ui-widget-content { + border: 1px solid #A0A0A0; +} + +.geocacheApproval-confirmDialog .ui-dialog-buttonpane { + padding-right: .3em !important; +} + +.geocacheApproval-confirmDialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: none !important; +} + +.geocacheApproval-confirmDialog .ui-dialog-buttonpane .ui-dialog-buttonset button { + box-sizing: border-box; + padding: 6px 11px; + margin: 5px 0; + border-radius: 4px; + border: 1px solid; + font-size: 14px; + background: none; +} + +.geocacheApproval-confirmDialog-cancelButton { + float: right !important; + color: #333 !important; + background-color: #fff !important; + border-color: #ccc !important; +} + +.geocacheApproval-confirmDialog-cancelButton:hover { + color: #333 !important; + background-color: #e6e6e6 !important; + border-color: #adadad !important; +} + +.geocacheApproval-confirmDialog-acceptButton, +.geocacheApproval-confirmDialog-blockButton { + float: left !important; + color: #fff !important; +} + +.geocacheApproval-confirmDialog-acceptButton { + background-color: #5cb85c !important; + border-color: #4cae4c !important; +} + +.geocacheApproval-confirmDialog-acceptButton:hover { + background-color: #449d44 !important; + border-color: #398439 !important; +} + +.geocacheApproval-confirmDialog-blockButton { + background-color: #d9534f !important; + border-color: #d43f3a !important; +} + +.geocacheApproval-confirmDialog-blockButton:hover { + color: #fff !important; + background-color: #c9302c !important; + border-color: #ac2925 !important; +} diff --git a/public/views/admin/geocacheApproval/geocache_approval.js b/public/views/admin/geocacheApproval/geocache_approval.js new file mode 100644 index 0000000000..fd1d022505 --- /dev/null +++ b/public/views/admin/geocacheApproval/geocache_approval.js @@ -0,0 +1,301 @@ +/** + * Removes html tags from given input string + */ +function strip_tags(input) { + return input !== null ? input.replace(/<[\s\S]*?>/g, '') : ""; +} + +/** + * Determines what style class to use for caches list row. + * + * rowData - a row resulted from API, + * daysBeforeAlert - maximum days between now and cache submission when alert is + * not set. + */ +function getApprovalTableRowStyle(rowData, daysBeforeAlert) { + result = ""; + + if ( + rowData["assigned_user_id"] === null + && moment(rowData["date_created"]).isBefore(moment().subtract(5, 'd')) + ) { + result = "alert"; + } else if ( + rowData["assigned_user_id"] !== null + && currentUserId == rowData["assigned_user_id"] + ) { + result = "highlighted"; + } + + return result; +} + +/** + * Returns predefined, translated string if given cache name is empty. + */ +function getNonEmptyCacheName(name) { + return name.replace(/\s/g,'') == '' ? cacheNonamedTrans : name; +} + +/** + * Fills up html row contents with row data resulted from API. + * + * row - jQuery row (tr) object + * rowData - one row from API "data" result" + * bgClass - row background class name, applied to cells + */ +function fillRowWithData(row, rowData, bgClass) { + row.attr("cache_id", rowData['cache_id']); + row.find("td").addClass(bgClass); + row.find(".geocacheApproval-cacheName") + .attr("href", viewCacheLink + "?cacheid=" + rowData['cache_id']) + .text(getNonEmptyCacheName(rowData["cachename"])); + row.find(".geocacheApproval-userName") + .attr("href", "viewprofile.php?userid=" + rowData['user_id']) + .text(rowData["username"]); + row.find(".geocacheApproval-region") + .text(rowData["adm3"]); + row.find(".alertable") + .text(rowData["date_created"]); + row.find(".geocacheApproval-lastLogDate") + .text(rowData["last_log_date"]); + if (rowData['last_log_author'] !== null) { + row.find(".geocacheApproval-lastLogUserName") + .attr( + "href", + "viewprofile.php?userid=" + rowData['last_log_author'] + ) + .text(rowData["last_log_username"]); + } + if (rowData['last_log_id'] !== null) { + row.find(".geocacheApproval-lastLogText") + .attr("href", "viewlogs.php?logid=" + rowData['last_log_id']) + .attr("title", strip_tags(rowData['last_log_text'])) + .text(strip_tags(rowData['last_log_text'])); + } + row.find(".geocacheApproval-assignedUser") + .attr("href", "viewprofile.php?userid=" + rowData['assigned_user_id']) + .text(rowData['assigned_user_name']); +} + +function assignCurrentUser(element) { + cacheAction(element, "assign"); +} + +function acceptCache(element) { + cacheAction(element, "accept"); +} + +function rejectCache(element) { + cacheAction(element, "reject"); +} + +/** + * Displays general error information on Api failure + */ +function showApiError(jqXHR, textStatus, errorThrown) { + console.log(textStatus); + console.log(errorThrown); + var tmpl = Handlebars.compile( + $("#geocacheApproval_generalErrorTemplate").html() + ); + var templateData = { + error_thrown: errorThrown, + text_status: textStatus + }; + $("#geocacheApproval_info").html(tmpl(templateData)); +} + +/** + * Performs chosen action on cache waiting for approval. + * + * element - jQuery "clicked" element (a) + * actionType - one of "assign", "accept", "reject" + */ +function cacheAction(element, actionType) { + var params = { + action: null, + successTpl: null, + failureTpl: null + }; + if (actionType == "assign") { + params.action = "assign"; + params.successTpl = "assigned"; + params.failureTpl = "assign"; + } else if (actionType == "accept") { + params.action = "accept"; + params.successTpl = "accepted"; + params.failureTpl = "accept"; + } else if (actionType == "reject") { + params.action = "reject"; + params.successTpl = "rejected"; + params.failureTpl = "reject"; + } + var parentRow = element.closest("tr"); + var cacheId = parentRow.attr("cache_id"); + $.get("/Admin.GeoCacheApprovalAdminApi/" + params.action + "/" + cacheId) + .done(function(result) { + if ("status" in result && result["status"] === "OK") { + var tmpl = Handlebars.compile( + $("#geocacheApproval_" + params.successTpl + "Template").html() + ); + $("#geocacheApproval_info").html(tmpl(result)); + } else if ( + params.failureTpl != null + && "status" in result + && result["status"] === "ERROR" + ) { + var tmpl = Handlebars.compile( + $( + "#geocacheApproval_" + params.failureTpl + "ErrorTemplate" + ).html() + ); + $("#geocacheApproval_info").html(tmpl(result)); + } + }) + .fail(showApiError) + .always(function() { + refreshWaitingTable(); + }); +} + +/** + * Shows jQuery UI dialog with confirmation of accept/reject actions + * + * element - jQuery "clicked" element (a) + * templateId - id of template to create dialog from + * actionButtonText - text of "confirm" button + * actionOnConfirm - function/method to call on confirm button click + */ +function showConfirmDialog( + element, templateId, actionButtonText, actionButtonClass, actionOnConfirm +) { + var tmpl = Handlebars.compile($("#" + templateId).html()); + var parentRow = element.closest("tr"); + var templateData = { + cache_id: parentRow.attr("cache_id"), + cache_name: parentRow.find(".geocacheApproval-cacheName").text(), + cache_owner: parentRow.find(".geocacheApproval-userName").text() + }; + + $("#geocacheApproval_confirmDialogTemplate").dialog({ + dialogClass: "geocacheApproval-confirmDialog" + + " geocacheApproval-confirmDialog-noTitlebar", + resizable: false, + height: "auto", + minHeight: 10, + minWidth: 600, + modal: true, + buttons: [ + { + text: actionButtonText, + class: actionButtonClass, + click: function() { + $(this).dialog("close"); + actionOnConfirm(); + } + }, + { + text: cancelButtonText, + class: "geocacheApproval-confirmDialog-cancelButton", + click: function() { + $(this).dialog("close"); + } + } + ], + open: function() { + $(this).html(tmpl(templateData)); + } + }); +} + +/** + * Regenerates list of waiting caches, clearing existing and assigning new + * actions for corresponding links. + */ +function refreshWaitingTable() { + $.get("/Admin.GeoCacheApprovalAdminApi/getWaiting") + .done(function(result) { + var apprTable = $("#geocacheApproval_waitingTable") + + // clear table and click handlers + apprTable.find(".geocacheApproval-row").remove(); + $(".geocacheApproval-actionAccept").off('click'); + $(".geocacheApproval-actionBlock").off('click'); + $(".geocacheApproval-actionAssign").off('click'); + + if (result["data"].length > 0) { + var inReviewCount = 0; + $.each(result["data"], function(index, rowData) { + var bgClass = "bgcolor" + (2 - (index) % 2); + row = $( + "#geocacheApproval_rowTemplate .geocacheApproval-row" + ).clone( + true, true + ); + row.addClass(getApprovalTableRowStyle(rowData)); + fillRowWithData(row, rowData, bgClass); + apprTable.append(row); + if (rowData["assigned_user_id"] !== null) { + inReviewCount++; + } + }); + $("#adminMenu_waitingForAssignee").text( + result["data"].length - inReviewCount + ); + $("#adminMenu_newPendings").text(result["data"].length); + $("#adminMenu_newPendingsStats").removeClass("hidden"); + $(".geocacheApproval-actionAccept").click(function(ev) { + ev.preventDefault(); + var caller = $(this); + showConfirmDialog( + $(this), + "geocacheApproval_acceptTemplate", + acceptButtonText, + "geocacheApproval-confirmDialog-acceptButton", + function() { + acceptCache(caller); + } + ); + }); + $(".geocacheApproval-actionBlock").click(function(ev) { + ev.preventDefault(); + var caller = $(this); + showConfirmDialog( + $(this), + "geocacheApproval_blockTemplate", + blockButtonText, + "geocacheApproval-confirmDialog-blockButton", + function() { + rejectCache(caller); + } + ); + }); + $(".geocacheApproval-actionAssign").click(function(ev) { + ev.preventDefault(); + assignCurrentUser($(this)); + }); + } else { + row = $( + "#geocacheApproval_rowEmptyTemplate .geocacheApproval-row" + ).clone( + true, true + ); + apprTable.append(row); + $("#adminMenu_waitingForAssignee").text(0); + $("#adminMenu_newPendings").text(0); + $("#adminMenu_newPendingsStats").addClass("hidden"); + } + $("#geocacheApproval_refreshDatetime").text(result["updated"]); + }) + .fail(showApiError); +} + +$(document).ready(function() { + $("#geocacheApproval_refresh").click(function(ev) { + // clear error info if was visible previously + $("#geocacheApproval_info .geocacheApproval-errorInfo").html(""); + refreshWaitingTable(); + }); + $("#geocacheApproval_refresh").trigger("click"); +}); diff --git a/resources/email/admin/approval_action_cache_activated.email.html b/resources/email/admin/approval_action_cache_activated.email.html new file mode 100644 index 0000000000..1cfe045922 --- /dev/null +++ b/resources/email/admin/approval_action_cache_activated.email.html @@ -0,0 +1,9 @@ +
+
+ {{Cacheactivated_02}}: {cacheName} - {wp} + {{Cacheactivated_03}}. +
 
+ {{Cacheactivated_04}}: {{edit_cache}} +
 
+ {{Cacheactivated_05}}. +
diff --git a/resources/email/admin/approval_action_cache_archived.email.html b/resources/email/admin/approval_action_cache_archived.email.html new file mode 100644 index 0000000000..ebdb4de185 --- /dev/null +++ b/resources/email/admin/approval_action_cache_archived.email.html @@ -0,0 +1,9 @@ +
+
+ {{cacheArchived_02}}: {cacheName} - {wp} + {{cacheArchived_03}}. +
 
+ {{cacheArchived_04}}. +
 
+ {{cacheArchived_05}}. +
diff --git a/resources/email/oc_team_notify_new_cache.email.html b/resources/email/oc_team_notify_new_cache.email.html index 5b259e9005..e171fe2c2a 100644 --- a/resources/email/oc_team_notify_new_cache.email.html +++ b/resources/email/oc_team_notify_new_cache.email.html @@ -4,5 +4,5 @@ {cachename} {ocTeamNewCache_03}.

{ocTeamNewCache_04} - {ocTeamNewCache_06} {ocTeamNewCache_05}. + {ocTeamNewCache_06} {ocTeamNewCache_05}. diff --git a/src/Controllers/Admin/GeoCacheApprovalAdminApiController.php b/src/Controllers/Admin/GeoCacheApprovalAdminApiController.php new file mode 100644 index 0000000000..7c3c2a5e93 --- /dev/null +++ b/src/Controllers/Admin/GeoCacheApprovalAdminApiController.php @@ -0,0 +1,320 @@ +isUserLogged() || ! $this->loggedUser->hasOcTeamRole()) { + // this controller is accessible only for OCTeam + $this->ajaxErrorResponse( + 'Not authorized for this operation', + HttpCode::STATUS_UNAUTHORIZED + ); + } + } + + /** + * Retrieves list of caches waiting for approval + */ + public function getWaiting() + { + $this->ajaxJsonResponse([ + 'updated' => Formatter::date(OcDateTime::now(), true), + 'data' => GeoCacheApproval::getWaitingForApproval(), + ]); + } + + /** + * Assigns current user (OC Team member) as a reviewer of give cache. + * + * @param int $cacheId id of the cache to assign user to + */ + public function assign(int $cacheId) + { + $this->wrap($cacheId); + } + + /** + * Accepts (approves) given cache. Current user is assigned as a reviewer, + * then cache status is updated to "not yet available", notification emails + * are sent to a cache owner and current user and admin note is added to the + * geocache log. + * + * @param int $cacheId id of the cache to accept + */ + public function accept(int $cacheId) + { + $this->wrap( + $cacheId, + function (GeoCache $cache): array { + $cache->updateStatus(GeoCacheCommons::STATUS_NOTYETAVAILABLE); + + // currently empty array, may be not empty in future + return []; + }, + function (GeoCache $cache): array { + $this->sendApprovalActionMessages($cache); + GeoCacheLog::newLog( + $cache->getCacheId(), + $this->loggedUser->getUserId(), + GeoCacheLogCommons::LOGTYPE_ADMINNOTE, + htmlspecialchars(tr('viewPending_03')) + ); + + // currently empty array, may be not empty in future + return []; + } + ); + } + + /** + * Rejects (declines) given cache. Current user is assigned as a reviewer, + * then cache status is updated to "blocked", notification emails + * are sent to a cache owner and current user and admin note is added to the + * geocache log. + * + * @param int $cacheId id of the cache to reject + */ + public function reject(int $cacheId) + { + $this->wrap( + $cacheId, + function (GeoCache $cache): array { + $cache->updateStatus(GeoCacheCommons::STATUS_BLOCKED); + + // currently empty array, may be not empty in future + return []; + }, + function (GeoCache $cache): array { + $this->sendApprovalActionMessages($cache, false); + GeoCacheLog::newLog( + $cache->getCacheId(), + $this->loggedUser->getUserId(), + GeoCacheLogCommons::LOGTYPE_ADMINNOTE, + htmlspecialchars(tr('viewPending_06')) + ); + + // currently empty array, may be not empty in future + return []; + } + ); + } + + /** + * A common wrapping function for "assign", "accept" and "reject" operations. + * Assigns current user as a reviewer for given cache, then fills up + * resulting array with data useful for calling client. Then calls a wrapped + * function, if provided, merging its result with resulting array. The + * resulting array is returned in ajax response. + * + * @param int $cacheId id of the cache to accept + * @param callable $wrappedLocked an optional function to call after + * assigning reviewer, but still inside + * the cache lock + * @param callable $wrappedUnLocked an optional function to call after + * assigning reviewer and after + * wrappedLocked, outside of lock + */ + private function wrap( + int $cacheId, + callable $wrappedLocked = null, + callable $wrappedUnlocked = null + ) { + $resultData = []; + + $cache = null; + $cacheAssigned = false; + + // Prevents concurrent work on the same geocache + $lockHandle = Lock::tryLock( + __CLASS__ . '_cache_id_' + . (! empty($cacheId) ? $cacheId : 'no_cache'), + Lock::EXCLUSIVE + ); + + if ($lockHandle) { + $cache = GeoCache::fromCacheIdFactory($cacheId); + + if ( + ! empty($cache) + && $cache->getStatus() === GeoCacheCommons::STATUS_WAITAPPROVERS + ) { + GeoCacheApproval::assignUserToCase( + $cache, + $this->loggedUser + ); + $resultData['cache_id'] = $cacheId; + $resultData['cache_name'] = $cache->getCacheName(); + $resultData['cache_wp'] = $cache->getWaypointId(); + $resultData['cache_owner'] = $cache->getOwner()->getUserName(); + $resultData['assigned_id'] = $this->loggedUser->getUserId(); + $resultData['assigned_username'] + = $this->loggedUser->getUserName(); + $cacheAssigned = true; + + $resultData = $this->invokeWrapped( + $resultData, + $cache, + $wrappedLocked + ); + } + + Lock::unlock($lockHandle); + } + + if ($cacheAssigned) { + $resultData = $this->invokeWrapped( + $resultData, + $cache, + $wrappedUnlocked + ); + $resultData['updated'] = Formatter::date( + OcDateTime::now(), + true + ); + } else { + $this->ajaxErrorResponse( + tr( + 'cache_approval_cache_invalid', + [ + ! empty($cache) + ? $cache->getCacheName() + . ' - ' . $cache->getWaypointId() + : '', + ] + ), + HttpCode::STATUS_OK + ); + } + + $this->ajaxSuccessResponse(null, $resultData); + } + + /** + * Invoked wrapped function if not empty, merging its result with + * $resultData + * + * @return array updated $resultData after invocation + */ + private function invokeWrapped( + array $resultData, + GeoCache $cache, + callable $wrapped = null + ): array { + if (! empty($wrapped)) { + $wrappedResultData = $wrapped($cache); + + if (! empty($wrappedResultData)) { + $resultData = array_merge( + $resultData, + $wrappedResultData + ); + } + } + + return $resultData; + } + + /** + * Prepares a notification email for given user and operation. + * + * @param GeoCacche $cache a geocache to prepare message from + * @param bool $isAccepted true if operation is "accept", false for "reject" + */ + private function prepareApprovalActionMessage( + GeoCache $cache, + bool $isAccepted = true + ): EmailFormatter { + $message = new EmailFormatter( + self::TEMPLATE_PATH . 'approval_action_cache_' + . ($isAccepted ? 'activated' : 'archived') + . '.email.html', + true + ); + $message->setVariable( + 'absoluteServerUri', + OcConfig::getAbsolute_server_URI() + ); + $message->setVariable('cacheName', $cache->getCacheName()); + $message->setVariable('wp', $cache->getWaypointId()); + $message->setVariable('viewCacheUrl', $cache->getCacheUrl()); + + if ($isAccepted) { + $message->setVariable( + 'editCacheUrl', + 'editcache.php?cacheid=' . $cache->getCacheId() + // TODO: IMHO editcache.php shouldn't be hardcoded + ); + } + + $message->addFooterAndHeader( + $cache->getOwner()->getUserName(), + false + ); + + return $message; + } + + /** + * Sends notification emails: to geocache owner and a copy to current user + * (reviewer). + * + * @param GeoCacche $cache a geocache to send message about + * @param bool $isAccepted true if operation is "accept", false for "reject" + */ + private function sendApprovalActionMessages( + GeoCache $cache, + bool $isAccepted = true + ) { + $message = $this->prepareApprovalActionMessage($cache, $isAccepted); + + foreach ([$cache->getOwner(), $this->loggedUser] as $user) { + $email = new Email(); + $email->addToAddr($user->getEmail()); + $email->setFromAddr(OcConfig::getEmailAddrOcTeam()); + $email->setReplyToAddr(OcConfig::getEmailAddrOcTeam()); + $email->addSubjectPrefix(OcConfig::getEmailSubjectPrefix()); + $email->setSubject( + tr($isAccepted ? 'viewPending_01' : 'viewPending_04') + . ': ' + . $cache->getCacheName() + ); + $email->setHtmlBody( + ( + $user == $this->loggedUser + ? ( + tr($isAccepted ? 'viewPending_02' : 'viewPending_05') + . ":

\n" + ) + : '' + ) + . $message->getEmailContent() + ); + $email->send(); + } + } +} diff --git a/src/Controllers/Admin/GeoCacheApprovalAdminController.php b/src/Controllers/Admin/GeoCacheApprovalAdminController.php new file mode 100644 index 0000000000..a0319905bd --- /dev/null +++ b/src/Controllers/Admin/GeoCacheApprovalAdminController.php @@ -0,0 +1,50 @@ +redirectNotLoggedUsers(); + + if (! $this->loggedUser->hasOcTeamRole()) { + $this->view->redirect('/'); + } + } + + public function isCallableFromRouter(string $actionName): bool + { + // all public methods can be called by router + return true; + } + + /** + * Initial, static view. The rest of operations are performed by API + */ + public function index() + { + $this->view->loadJQuery(); + $this->view->loadJQueryUI(); + $this->view->addHeaderChunk('momentJs'); + $this->view->addHeaderChunk('handlebarsJs'); + $this->view->addLocalCss( + Uri::getLinkWithModificationTime( + '/views/admin/geocacheApproval/geocache_approval.css' + ) + ); + $this->view->addLocalJs( + Uri::getLinkWithModificationTime( + '/views/admin/geocacheApproval/geocache_approval.js' + ) + ); + $this->view->setTemplate('admin/geocacheApproval/geocacheApproval'); + $this->view->setVar('currentUserId', $this->loggedUser->getUserId()); + $this->view->buildView(); + } +} diff --git a/src/Controllers/PageLayout/MainLayoutController.php b/src/Controllers/PageLayout/MainLayoutController.php index f921db9fe1..32b0044f52 100644 --- a/src/Controllers/PageLayout/MainLayoutController.php +++ b/src/Controllers/PageLayout/MainLayoutController.php @@ -244,15 +244,19 @@ private function adminMenuHandler(&$key, &$url) break; case 'mnu_pendings': - $new_pendings = GeoCacheApproval::getWaitingForApprovalCount(); - - if ($new_pendings > 0) { - $in_review_count = GeoCacheApproval::getInReviewCount(); - $waitingForAssigne = $new_pendings - $in_review_count; - $key = tr($key) . " ({$waitingForAssigne}/{$new_pendings})"; - } else { - $key = tr($key); - } + $newPendings = GeoCacheApproval::getWaitingForApprovalCount(); + + $inReviewCount = GeoCacheApproval::getInReviewCount(); + $waitingForAssigne = $newPendings - $inReviewCount; + $key + = tr($key) + . ' (' + . $waitingForAssigne + . '/' + . $newPendings + . ')'; break; default: $key = tr($key); // by default menu key is just a translation diff --git a/src/Models/Admin/GeoCacheApproval.php b/src/Models/Admin/GeoCacheApproval.php index 12fbffa7ab..0d494cd878 100644 --- a/src/Models/Admin/GeoCacheApproval.php +++ b/src/Models/Admin/GeoCacheApproval.php @@ -1,31 +1,225 @@ loadByCacheId($cacheId); + } + } + + /** + * Factory + * + * @return GeoCacheApproval|null (null if geocache with given id is not found) + */ + public static function fromCacheIdFactory(int $cacheId) + { + try { + return new self($cacheId); + } catch (Exception $e) { + return; + } + } + + /** + * @throws Exception + */ + private function loadByCacheId(int $cacheId) + { + $s = $this->db->multiVariableQuery( + 'SELECT * FROM approval_status WHERE cache_id = :1 LIMIT 1', + $cacheId + ); + + $cacheApprovalDbRow = $this->db->dbResultFetch($s); + + if (is_array($cacheApprovalDbRow)) { + $this->loadFromRow($cacheApprovalDbRow); + } else { + throw new Exception('Cache approval status not found'); + } + } + + /** + * Load object data based on DB data-row + * + * @throws Exception + */ + private function loadFromRow(array $geocacheApprovalDbRow) + { + $this->cacheId = $geocacheApprovalDbRow['cache_id']; + $this->userId = $geocacheApprovalDbRow['user_id']; + $this->status = $geocacheApprovalDbRow['status']; + + if (! empty($geocacheApprovalDbRow['date_approval'])) { + $this->dateApproval = new DateTime( + $geocacheApprovalDbRow['date_approval'] + ); + } } public static function getWaitingForApprovalCount() { return self::db()->multiVariableQueryValue( - "SELECT COUNT(status) FROM caches WHERE status = :1 ", - 0, GeoCacheCommons::STATUS_WAITAPPROVERS); + 'SELECT COUNT(status) FROM caches WHERE status = :1', + 0, + GeoCacheCommons::STATUS_WAITAPPROVERS + ); + } + + /** + * Retrieves all caches waiting for approval, including additional data + * needed to display in approval view table rows. + * @throws Exception + */ + public static function getWaitingForApproval(): array + { + $stmt = self::db()->multiVariableQuery( + "SELECT + cache_owner.username AS username, + cache_owner.user_id AS user_id, + caches.cache_id AS cache_id, + caches.name AS cachename, + IFNULL(`cache_location`.`adm3`, '') AS `adm3`, + caches.date_created AS date_created, + last_log.id AS last_log_id, + last_log.date AS last_log_date, + last_log.user_id AS last_log_author, + log_author.username AS last_log_username, + last_log.text AS last_log_text, + assigned_user.user_id AS assigned_user_id, + assigned_user.username AS assigned_user_name + FROM + `caches` + LEFT JOIN `cache_location` + ON `caches`.`cache_id` = `cache_location`.`cache_id` + LEFT JOIN ( + SELECT + id, + cache_id, + text, + user_id, + date + FROM + cache_logs logs + WHERE + date = ( + SELECT + MAX(date) + FROM + cache_logs + WHERE + cache_id = logs.cache_id + ) + ) AS last_log + ON caches.cache_id = last_log.cache_id + LEFT JOIN user AS cache_owner + ON caches.user_id = cache_owner.user_id + LEFT JOIN user AS log_author + ON last_log.user_id = log_author.user_id + LEFT JOIN approval_status + ON caches.cache_id = approval_status.cache_id + LEFT JOIN user AS assigned_user + ON approval_status.user_id = assigned_user.user_id + WHERE + caches.status = :1 + GROUP BY + caches.cache_id + ORDER BY + caches.date_created DESC", + GeoCacheCommons::STATUS_WAITAPPROVERS + ); + + return self::db()->dbResultFetchAll($stmt); } public static function getInReviewCount() { return self::db()->multiVariableQueryValue( - "SELECT COUNT(*) + 'SELECT COUNT(*) FROM caches JOIN approval_status USING(cache_id) - WHERE caches.status = :1", - 0, GeoCacheCommons::STATUS_WAITAPPROVERS); + WHERE caches.status = :1', + 0, + GeoCacheCommons::STATUS_WAITAPPROVERS + ); } + /** + * Factory + * @param int $cacheId + * @return GeoCacheApproval|null (null if no $returnInstance) + * @throws Exception + */ + public static function assignUserToCase( + GeoCache $cache, + User $user, + bool $returnInstance = false + ) { + $result = null; + + if (! empty($cache->getCacheId()) && ! empty($user->getUserId())) { + self::db()->multiVariableQuery( + 'INSERT INTO approval_status + (cache_id, user_id, status, date_approval) + VALUES (:1, :2, :3, NOW()) + ON DUPLICATE KEY UPDATE user_id = :2', + $cache->getCacheId(), + $user->getUserId(), + self::STATUS_ASSIGNED + ); + + if ($returnInstance) { + $result = new self($cache->getCacheId()); + } + } + + return $result; + } } diff --git a/src/Views/admin/geocacheApproval/geocacheApproval.tpl.php b/src/Views/admin/geocacheApproval/geocacheApproval.tpl.php new file mode 100644 index 0000000000..ad061aedcc --- /dev/null +++ b/src/Views/admin/geocacheApproval/geocacheApproval.tpl.php @@ -0,0 +1,70 @@ + + + +
 {{pendings}}
+
+
+
+ + {{cache_approval_refresh_time}}: +
+ + + + + + + + +
Cache{{date_created}}{{pendings_last_log}}{{actions}}{{assigned_to}}
+

+ +
+ + + + + + + + +
+
+
+ +

+
+ +
+  {{accept}}
+  {{block}}
+  {{assign_yourself}} +
+
+
+
+ +
+ + + + +
-- {{cache_approval_no_caches}} --
+
+ +callSubTpl('/admin/geocacheApproval/geocacheApprovalMessages'); ?> + +
+
diff --git a/src/Views/admin/geocacheApproval/geocacheApprovalMessages.tpl.php b/src/Views/admin/geocacheApproval/geocacheApprovalMessages.tpl.php new file mode 100644 index 0000000000..37c408689d --- /dev/null +++ b/src/Views/admin/geocacheApproval/geocacheApprovalMessages.tpl.php @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Views/chunks/momentJs.tpl.php b/src/Views/chunks/momentJs.tpl.php new file mode 100644 index 0000000000..49b2931f81 --- /dev/null +++ b/src/Views/chunks/momentJs.tpl.php @@ -0,0 +1,15 @@ + + + + + + + -.truncated { - display: inline-block; - width: 130px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - - -
 {{pendings}}
-
-{confirm} -
- - - - - - - - - {content} -
Cache{{date_created}}{{pendings_last_log}}{{actions}}{{assigned_to}}
-

diff --git a/src/Views/viewpendings_error.tpl.php b/src/Views/viewpendings_error.tpl.php deleted file mode 100644 index f5c62d7959..0000000000 --- a/src/Views/viewpendings_error.tpl.php +++ /dev/null @@ -1 +0,0 @@ -{{noaccess_error_01}}
[{{noaccess_error_02}}] diff --git a/viewpendings.php b/viewpendings.php deleted file mode 100644 index 5160c7d3c0..0000000000 --- a/viewpendings.php +++ /dev/null @@ -1,323 +0,0 @@ -$text"; - case '2': - return "$text"; - case '3': - return "$text"; - default: - return "$text"; - } -} - -function nonEmptyCacheName($cacheName) -{ - if (str_replace(" ", "", $cacheName) == "") - return "[bez nazwy]"; - return $cacheName; -} - -function getUsername($userid) -{ - return XDb::xMultiVariableQueryValue( - "SELECT username FROM user WHERE user_id= :1 LIMIT 1", - null, $userid); -} - -function getCachename($cacheid) -{ - return XDb::xMultiVariableQueryValue( - "SELECT name FROM caches WHERE cache_id= :1 LIMIT 1", - null, $cacheid); -} - -function getCacheOwnername($cacheid) -{ - return XDb::xMultiVariableQueryValue( - "SELECT username FROM user WHERE user_id= :1 LIMIT 1", - null, getCacheOwnerId($cacheid)); -} - -function getCacheOwnerId($cacheid) -{ - return XDb::xMultiVariableQueryValue( - "SELECT user_id FROM caches WHERE cache_id= :1 LIMIT 1", - null, $cacheid); -} - -function actionRequired($cacheid) -{ - // check if cache requires activation - return XDb::xMultiVariableQueryValue( - "SELECT status FROM caches WHERE cache_id= :1 AND status = 4", - null, $cacheid); -} - -function activateCache($cacheid) -{ - // activate the cache by changing its status to yet unavailable - if (actionRequired($cacheid)) { - if (XDb::xSql("UPDATE caches SET status = 5 WHERE cache_id= ? ", $cacheid)) { - return true; - } else - return false; - } - return false; -} - -function declineCache($cacheid) -{ - // activate the cache by changing its status to yet unavailable - if (actionRequired($cacheid)) { - if (XDb::xSql("UPDATE caches SET status = 6 WHERE cache_id= ? ", $cacheid)) { - return true; - } else - return false; - } - return false; -} - -function getAssignedUserId($cacheid) -{ - // check if cache requires activation - return XDb::xMultiVariableQueryValue( - "SELECT user_id FROM approval_status WHERE cache_id= :1 LIMIT 1", - false, $cacheid); -} - -function assignUserToCase($userid, $cacheid) -{ - // check if user is in OC Team Member - $user = User::fromUserIdFactory($userid); - if (!$user || !$user->hasOcTeamRole()) { - return false; - } - - XDb::xSql( - "INSERT INTO approval_status (cache_id, user_id, status, date_approval) - VALUES ( ?, ?, 2, NOW()) - ON DUPLICATE KEY UPDATE user_id = ?", - $cacheid, $userid, $userid); -} - -function notifyOwner($cacheid, $msgType) -{ - // msgType - 0 = cache accepted, 1 = cache declined (=archived) - global $absolute_server_URI; - - $user = ApplicationContainer::GetAuthorizedUser(); - if(!$user) { - return; - } - - $user_id = getCacheOwnerId($cacheid); - - $cachename = getCachename($cacheid); - if ($msgType == 0) { - $email_content = file_get_contents(__DIR__ . '/resources/email/activated_cache.email'); - } else { - $email_content = file_get_contents(__DIR__ . '/resources/email/archived_cache.email'); - } - $email_headers = "Content-Type: text/plain; charset=utf-8\r\n"; - $email_headers .= "From: " . OcConfig::getSiteName() . " <" . OcConfig::getEmailAddrOcTeam() . ">\r\n"; - $email_headers .= "Reply-To: " . OcConfig::getEmailAddrOcTeam() . "\r\n"; - $email_content = mb_ereg_replace('{server}', $absolute_server_URI, $email_content); - $email_content = mb_ereg_replace('{cachename}', $cachename, $email_content); - $email_content = mb_ereg_replace('{cacheid}', $cacheid, $email_content); - $email_content = mb_ereg_replace('{octeamEmailsSignature}', OcConfig::getOcteamEmailsSignature(), $email_content); - $email_content = mb_ereg_replace('{cacheArchived_01}', tr('cacheArchived_01'), $email_content); - $email_content = mb_ereg_replace('{cacheArchived_02}', tr('cacheArchived_02'), $email_content); - $email_content = mb_ereg_replace('{cacheArchived_03}', tr('cacheArchived_03'), $email_content); - $email_content = mb_ereg_replace('{cacheArchived_04}', tr('cacheArchived_04'), $email_content); - $email_content = mb_ereg_replace('{cacheArchived_05}', tr('cacheArchived_05'), $email_content); - $email_content = mb_ereg_replace('{Cacheactivated_01}', tr('Cacheactivated_01'), $email_content); - $email_content = mb_ereg_replace('{Cacheactivated_02}', tr('Cacheactivated_02'), $email_content); - $email_content = mb_ereg_replace('{Cacheactivated_03}', tr('Cacheactivated_03'), $email_content); - $email_content = mb_ereg_replace('{Cacheactivated_04}', tr('Cacheactivated_04'), $email_content); - $email_content = mb_ereg_replace('{Cacheactivated_05}', tr('Cacheactivated_05'), $email_content); - - - $owner_email['email'] = XDb::xMultiVariableQueryValue( - "SELECT `email` FROM `user` WHERE `user_id`= :1 LIMIT 1", '', $user_id); - - if ($msgType == 0) { - //send email to owner - mb_send_mail($owner_email['email'], tr('viewPending_01') . ": " . $cachename, $email_content, $email_headers); - //send email to approver - mb_send_mail($user->getEmail(), tr('viewPending_01') . ": " . $cachename, tr('viewPending_02') . ":\n" . $email_content, $email_headers); - // generate automatic log about status cache - $log_text = htmlspecialchars(tr("viewPending_03")); - $log_uuid = Uuid::create(); - XDb::xSql( - "INSERT INTO `cache_logs` - (`cache_id`, `user_id`, `type`, `date`, `text`, `text_html`, `date_created`, `last_modified`, `uuid`, `node`) - VALUES (?, ?, '12', NOW(), ?, '2', NOW(), NOW(), ?, ?)", - $cacheid, $user->getUserId(), $log_text, $log_uuid, OcConfig::getSiteNodeId()); - - } else { - //send email to owner - mb_send_mail($owner_email['email'], tr('viewPending_04') . ": " . $cachename, $email_content, $email_headers); - //send email to approver - mb_send_mail($user->getEmail(), tr('viewPending_04') . ": " . $cachename, tr('viewPending_05') . ":\n" . $email_content, $email_headers); - - // generate automatic log about status cache - $log_text = htmlspecialchars(tr("viewPending_06")); - $log_uuid = Uuid::create(); - XDb::xSql( - "INSERT INTO `cache_logs` - (`id`, `cache_id`, `user_id`, `type`, `date`, - `text`, `text_html`, `date_created`, `last_modified`, `uuid`, - `node`) - VALUES ('', ?, ?, ?, NOW(), - ?, ?, NOW(), NOW(), ?, - ?)", - $cacheid, $user->getUserId(), 12, - $log_text, 2, $log_uuid, - OcConfig::getSiteNodeId()); - } -} - -require_once(__DIR__ . '/lib/common.inc.php'); - -$view = tpl_getView(); -$user = ApplicationContainer::GetAuthorizedUser(); - -if (empty($user) || !$user->hasOcTeamRole()) { - $view->setTemplate('viewpendings_error'); - $view->buildView(); - exit; -} - -$view->setTemplate('viewpendings'); - -$content = ''; -if (isset($_GET['cacheid'])) { - if (isset($_GET['assign'])) { - if (assignUserToCase($_GET['assign'], $_GET['cacheid'])) { - $confirm = "

" . tr("viewPending_07") . " " . getUsername($_GET['assign']) . " " . tr("viewPending_08") . ".

"; - tpl_set_var('confirm', $confirm); - } else { - tpl_set_var('confirm', ''); - } - } else { - if (actionRequired($_GET['cacheid'])) { - // requires activation - if (isset($_GET['confirm']) && isset($_GET['user_id']) && $_GET['confirm'] == 1) { - // confirmed - change the status and notify the owner now - if (activateCache($_GET['cacheid'])) { - assignUserToCase($user->getUserId(), $_GET['cacheid']); - notifyOwner($_GET['cacheid'], 0); - AdminNote::addAdminNote($user->getUserId(), $_GET['user_id'], true, AdminNote::CACHE_PASS, $_GET['cacheid']); - $confirm = "

" . tr("viewPending_09") . ".

"; - } else { - $confirm = "

" . tr("viewPending_10") . ".

"; - } - } else if (isset($_GET['confirm']) && isset($_GET['user_id']) && $_GET['confirm'] == 2) { - // declined - change status to archived and notify the owner now - if (declineCache($_GET['cacheid'])) { - assignUserToCase($user->getUserId(), $_GET['cacheid']); - notifyOwner($_GET['cacheid'], 1); - AdminNote::addAdminNote($user->getUserId(), $_GET['user_id'], true, AdminNote::CACHE_BLOCKED, $_GET['cacheid']); - $confirm = "

" . tr("viewPending_11") . ".

"; - } else { - $confirm = "

" . tr("viewPending_12") . ".

"; - } - } else if ($_GET['action'] == 1 && isset($_GET['user_id'])) { - // require confirmation - $confirm = "

" . tr("viewPending_13") . " \"" . getCachename($_GET['cacheid']) . "\" " . tr("viewPending_14") . " " . getCacheOwnername($_GET['cacheid']) . ". " . tr("viewPending_15") . ".

"; - $confirm .= "

" . tr("viewPending_16") . " - " . tr("viewPending_17") . "

"; - } else if ($_GET['action'] == 2 && isset($_GET['user_id'])) { - // require confirmation - $confirm = "

" . tr("viewPending_18") . " \"" . getCachename($_GET['cacheid']) . "\" " . tr("viewPending_14") . " " . getCacheOwnername($_GET['cacheid']) . ". " . tr("viewPending_19") . ".

"; - $confirm .= "

" . tr("viewPending_20") . " - " . tr("viewPending_17") . "

"; - } - tpl_set_var('confirm', $confirm); - } else { - tpl_set_var('confirm', '

' . tr('viewPending_21') . '.

'); - } - } -} else { - tpl_set_var('confirm', ''); -} - -$stmt = XDb::xSql( - "SELECT cache_status.id AS cs_id, cache_status.pl AS cache_status, - cache_owner.username AS username, cache_owner.user_id AS user_id, - caches.cache_id AS cache_id, caches.name AS cachename, - IFNULL(`cache_location`.`adm3`, '') AS `adm3`, caches.date_created AS date_created, - last_log.id AS last_log_id, last_log.date AS last_log_date, - last_log.user_id AS last_log_author, log_author.username AS last_log_username, - last_log.text AS last_log_text - FROM cache_status, `caches` - LEFT JOIN `cache_location` ON `caches`.`cache_id` = `cache_location`.`cache_id` - LEFT JOIN ( - SELECT id, cache_id, text, user_id, date - FROM cache_logs logs - WHERE date = (SELECT MAX(date) FROM cache_logs WHERE cache_id = logs.cache_id) - ) AS last_log ON caches.cache_id = last_log.cache_id - LEFT JOIN user AS cache_owner - ON caches.user_id = cache_owner.user_id - LEFT JOIN user AS log_author - ON last_log.user_id = log_author.user_id - WHERE cache_status.id = caches.status - AND caches.status = 4 - GROUP BY caches.cache_id - ORDER BY caches.date_created DESC"); - -$row_num = 0; -while ($report = XDb::xFetchArray($stmt)) { - $assignedUserId = getAssignedUserId($report['cache_id']); - - if (!$assignedUserId && new DateTime($report['date_created']) < new DateTime('5 days ago')) { - //set alert for forgotten cache - $trstyle = "alert"; - } else if ($user->getUserId() == $assignedUserId) { - //highlight caches assigned to current user - $trstyle = "highlighted"; - } else { - $trstyle = ""; - } - - if ($row_num % 2) - $bgcolor = "bgcolor1"; - else - $bgcolor = "bgcolor2"; - - $content .= "\n"; - $content .= " - " . nonEmptyCacheName($report['cachename']) . "
- " . $report['username'] . "
- " . $report['adm3'] . " - \n"; - - $content .= " " . $report['date_created'] . "\n"; - - $content .= "" . $report['last_log_date'] . "
- " . $report['last_log_username'] . "
- " . strip_tags($report['last_log_text']) . " - \n"; - - $content .= "\"\" " . tr('accept') . "
- \"\" " . tr('block') . "
- \"\" " . tr('assign_yourself') . "\n"; - $content .= "" . getUsername($assignedUserId) . "
"; - $content .= "\n"; - $row_num++; -} -tpl_set_var('content', $content); -$view->buildView();