diff --git a/.~lock.Assessment Brief Form 2024-25.docx# b/.~lock.Assessment Brief Form 2024-25.docx# old mode 100755 new mode 100644 index 983f89c..8d50ed3 --- a/.~lock.Assessment Brief Form 2024-25.docx# +++ b/.~lock.Assessment Brief Form 2024-25.docx# @@ -1 +1 @@ -,boris,boris-ThinkPad-T480,07.11.2024 12:14,file:///home/boris/.config/libreoffice/4; \ No newline at end of file +,boris,boris-ThinkPad-T480,28.11.2024 18:42,file:///home/boris/.config/libreoffice/4; \ No newline at end of file diff --git a/.~lock.eco database design v3.docx# b/.~lock.eco database design v3.docx# deleted file mode 100644 index 7d813a8..0000000 --- a/.~lock.eco database design v3.docx# +++ /dev/null @@ -1 +0,0 @@ -,boris,boris-ThinkPad-T480,08.11.2024 12:14,file:///home/boris/.config/libreoffice/4; \ No newline at end of file diff --git a/MVCTemplate/MVCtemplateWithBasicLogin/css/my-style.css b/MVCTemplate/MVCtemplateWithBasicLogin/css/my-style.css index f906498..6181fea 100644 --- a/MVCTemplate/MVCtemplateWithBasicLogin/css/my-style.css +++ b/MVCTemplate/MVCtemplateWithBasicLogin/css/my-style.css @@ -6,7 +6,7 @@ } #menu { border-top: solid 6px #000; - background-color: #fff + background-color: #fff; color: #fff; height: 400px; } diff --git a/Models/Database.php b/Models/Database.php index 95c4175..47b9365 100644 --- a/Models/Database.php +++ b/Models/Database.php @@ -24,6 +24,8 @@ class Database { private function __construct() { try { $this->_dbHandle = new PDO("sqlite:Databases/ecobuddy.sqlite"); + $this->_dbHandle->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->_dbHandle->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); } catch (PDOException $e) { echo $e->getMessage(); diff --git a/Models/FacilityData.php b/Models/FacilityData.php index 20875c7..44e3cd5 100644 --- a/Models/FacilityData.php +++ b/Models/FacilityData.php @@ -1,11 +1,12 @@ _id = $dbRow['_id']; + $this->_id = $dbRow['id']; $this->_title = $dbRow['title']; $this->_category = $dbRow['category']; + $this->_status = $dbRow['status']; $this->_description = $dbRow['description']; $this->_houseNumber = $dbRow['houseNumber']; $this->_streetName = $dbRow['streetName']; @@ -14,6 +15,7 @@ class FacilityData { $this->_postcode = $dbRow['postcode']; $this->_lng = $dbRow['lng']; $this->_lat = $dbRow['lat']; + $this->_contributor = $dbRow['contributor']; } public function getId() { @@ -25,6 +27,10 @@ class FacilityData { public function getCategory() { return $this->_category; } + + public function getStatus() { + return $this->_status; + } public function getDescription() { return $this->_description; } @@ -49,4 +55,7 @@ class FacilityData { public function getLat() { return $this->_lat; } + public function getContributor() { + return $this->_contributor; + } } \ No newline at end of file diff --git a/Models/FacilityDataSet.php b/Models/FacilityDataSet.php index ae3e133..90ba943 100644 --- a/Models/FacilityDataSet.php +++ b/Models/FacilityDataSet.php @@ -10,21 +10,132 @@ class FacilityDataSet { $this->_dbHandle = $this->_dbInstance->getDbConnection(); } - public function fetchAll(): array + /** + * @param $filterArray + * @param $rowCount + * @param $offset + * @return array + * Function to allow fetching of facility data. Data objects are created and held in an array + * Count of rows for pagination returned alongside data objects. + */ + public function fetchAll($filterArray, $rowCount, $offset): array { - $sqlQuery = 'SELECT * FROM ecoFacilities;'; + /** + * COUNT(DISTINCT ecoFacilities.id) required due to multiple status comments possible. + */ + $sqlCount = "SELECT COUNT(DISTINCT ecoFacilities.id) AS total FROM ecoFacilities"; - $statement = $this->_dbHandle->prepare($sqlQuery); // prepare a PDO statement - $statement->execute(); // execute the PDO statemen + /** + * DISTINCT used again for prior reasoning, although data is handled properly regardless later. + * GROUP_CONCAT is used to handle multiple status comments under one facility. Without this, DISTINCT + * drops the additional comment. + */ + $sqlData = "SELECT DISTINCT ecoFacilities.id, + title, + GROUP_CONCAT(ecoFacilityStatus.statusComment, ', ') AS status, + ecoCategories.name AS category, + description, + houseNumber, + streetName, + county, + town, + postcode, + lng, + lat, + ecoUser.username AS contributor + FROM ecoFacilities"; + /** + * ? Parameters used here over named parameters so logic can be modular, more + * columns can be added in the future + */ + $sqlWhere = " + LEFT JOIN ecoCategories ON ecoCategories.id = ecoFacilities.category + LEFT JOIN ecoUser ON ecoUser.id = ecoFacilities.contributor + LEFT JOIN ecoFacilityStatus ON ecoFacilityStatus.facilityid = ecoFacilities.id + WHERE (ecoFacilityStatus.statusComment LIKE ? OR ? IS NULL) + AND ecoFacilities.title LIKE ? + AND ecoCategories.name LIKE ? + AND ecoFacilities.description LIKE ? + AND ecoFacilities.streetName LIKE ? + AND ecoFacilities.county LIKE ? + AND ecoFacilities.town LIKE ? + AND ecoFacilities.postcode LIKE ? + AND ecoUser.username LIKE ? + "; + + /** + * GROUP BY required to ensure status comments are displayed under the same ID + * Named parameters used here for prior reasoning, columns can be added above without + * effecting the bindIndex. + */ + $sqlLimits = " + GROUP BY ecoFacilities.id + LIMIT :limit OFFSET :offset;"; + $sqlLimits = " + GROUP BY ecoFacilities.id + ;"; + + // Concatenate query snippets for data and row count + $dataQuery = $sqlData . $sqlWhere . $sqlLimits; + $countQuery = $sqlCount . $sqlWhere . ";"; + + // Prepare, bind and execute data query + $stmt = $this->populateFields($dataQuery, $rowCount, $offset, $filterArray); + $stmt->execute(); + + // Create data objects $dataSet = []; - // loop through and read the results of the query and cast - // them into a matching object - while ($row = $statement->fetch()) { + while ($row = $stmt->fetch()) { $dataSet[] = new FacilityData($row); } - return $dataSet; + + // Prepare, bind then execute count query + $stmt = $this->populateFields($countQuery, null, null, $filterArray); + $stmt->execute(); + $totalCount = $stmt->fetch()['total']; + + return [ + 'dataset' => $dataSet, + 'count' => $totalCount + ]; } + /** + * @param $sqlQuery + * @param $rowCount + * @param $offset + * @param $filterArray + * @return false|PDOStatement + * Function for fetchAll() to de-dupe code. Performs binding on PDO statements to facilitate + * filtering of facilities. Returns a bound PDO statement. + */ + private function populateFields($sqlQuery, $rowCount, $offset, $filterArray) + { + $stmt = $this->_dbHandle->prepare($sqlQuery); + // Ensures only one value is returned per column name + $stmt->setFetchMode(\PDO::FETCH_ASSOC); + + // Initialize index for binding + $bindIndex = 1; + + // Bind statusComment filter, required due to comments not being so. + $statusComment = !empty($filterArray[0]) ? "%" . $filterArray[0] . "%" : null; + $stmt->bindValue($bindIndex++, $statusComment ?? "%", \PDO::PARAM_STR); // First ? + $stmt->bindValue($bindIndex++, $statusComment, $statusComment === null ? \PDO::PARAM_NULL : \PDO::PARAM_STR); // Second ? + + // Bind other filters + for ($i = 1; $i <= 8; $i++) { // Assuming 8 other filters + $value = !empty($filterArray[$i]) ? "%" . $filterArray[$i] . "%" : "%"; + $stmt->bindValue($bindIndex++, $value, \PDO::PARAM_STR); + } + + // Bind LIMIT and OFFSET + if (!$rowCount == null || !$offset == null) { + $stmt->bindValue(':limit', $rowCount, \PDO::PARAM_INT); // LIMIT ? + $stmt->bindValue(':offset', $offset, \PDO::PARAM_INT); // OFFSET ? + } + return $stmt; + } } diff --git a/Models/Paginator.php b/Models/Paginator.php index b3d9bbc..b2f6717 100644 --- a/Models/Paginator.php +++ b/Models/Paginator.php @@ -1 +1,84 @@ _rowLimit = $rowLimit; + $this->_offset = $offset; + $this->_totalPages = $this->calculateTotalPages($dataset['count']); + $this->_rowCount = $dataset['count']; + $this->_pages = $dataset['dataset']; + $this->_pageMatrix = $this->Paginate(); + } + + private function calculateTotalPages(int $count): int { + return $count > 0 ? ceil($count / $this->_rowLimit) : 0; + } + + public function Paginate(): array { + $pageMatrix = []; + for ($i = 0; $i < $this->_totalPages; $i++) { + $page = []; + $start = $i * $this->_rowLimit; + $end = min($start + $this->_rowLimit, $this->_rowCount); // Ensure within bounds + + for ($j = $start; $j < $end; $j++) { + $page[] = $this->_pages[$j]; + } + + $pageMatrix[$i] = $page; + } + return $pageMatrix; + } + + function getPageFromUri(): int { + // Retrieve 'page' parameter and default to 0 if missing or invalid + return filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [ + 'options' => ['default' => 0, 'min_range' => 0] // Default to 0 if invalid or missing + ]); + } + + public function setPageUri(int $page): void + { + $uri = $_SERVER['REQUEST_URI']; + $uriComp = parse_url($uri); + $params = []; + + // Parse existing query parameters + if (isset($uriComp['query'])) { + parse_str($uriComp['query'], $params); + } + + // Avoid unnecessary redirection if the page is already correct + if (isset($params['page']) && (int)$params['page'] === $page) { + return; // Do nothing if already on the correct page + } + + // Update the 'page' parameter + $params['page'] = $page; + + // Rebuild the query string + $newUri = http_build_query($params); + + // Redirect to the updated URI + $path = $uriComp['path'] ?? '/'; // Use the current path or root + header("Location: $path?$newUri"); + exit; + } + + public function getPage(int $pageNumber): array { + + if ($pageNumber < 0 || $pageNumber >= $this->_totalPages) { + return []; // Return an empty array if the page number is invalid + } + return $this->_pageMatrix[$pageNumber]; + } + + public function countPageResults(int $pageNumber): int { + if ($pageNumber < 0 || $pageNumber >= $this->_totalPages) { + return 0; // Return 0 if the page number is invalid + } + return count($this->_pageMatrix[$pageNumber]); + } +} \ No newline at end of file diff --git a/Models/User.php b/Models/User.php index 30edaee..2b12d62 100644 --- a/Models/User.php +++ b/Models/User.php @@ -2,7 +2,7 @@ require_once('UserDataSet.php'); class User { - protected $_username, $_loggedIn, $_userId; + protected $_username, $_loggedIn, $_userId, $_accessLevel; public function getUsername() { return $this->_username; @@ -16,11 +16,13 @@ class User { $this->_username = "None"; $this->_loggedIn = false; $this->_userId = "0"; + $this->_accessLevel = null; if(isset($_SESSION['login'])) { $this->_username = $_SESSION['login']; $this->_userId = $_SESSION['uid']; $this->_loggedIn = true; + $this->_accessLevel = $_SESSION['accessLevel']; } } @@ -35,13 +37,22 @@ class User { $this->_loggedIn = true; } } + private function setAccessLevel($level) { + $this->_accessLevel = $level; + $_SESSION['accessLevel'] = $level; + } + public function getAccessLevel() { + return $this->_accessLevel; + } public function Authenticate($username, $password): bool { $users = new UserDataSet(); $userDataSet = $users->checkUserCredentials($username, $password); + $accessLevel = $users->checkAccessLevel($username); if(count($userDataSet) > 0) { $_SESSION['login'] = $username; $_SESSION['uid'] = $userDataSet[0]->getId(); + $this->setAccessLevel($accessLevel); $this->_loggedIn = true; $this->_username = $username; $this->_userId = $userDataSet[0]->getId(); diff --git a/Models/UserData.php b/Models/UserData.php index 738e71b..32b028b 100644 --- a/Models/UserData.php +++ b/Models/UserData.php @@ -3,11 +3,10 @@ class UserData { protected $_id, $_username, $_name, $_password, $_usertype; public function __construct($dbRow) { - $this->_id = $dbRow['_id']; + $this->_id = $dbRow['id']; $this->_username = $dbRow['username']; - $this->_name = $dbRow['name']; $this->_password = $dbRow['password']; - $this->_usertype = $dbRow['usertype']; + $this->_usertype = $dbRow['userType']; } public function getId() { @@ -18,8 +17,4 @@ class UserData { return $this->_username; } - public function getName() { - return $this->_name; - } - } \ No newline at end of file diff --git a/Models/UserDataSet.php b/Models/UserDataSet.php index 43b528f..f8cfa95 100644 --- a/Models/UserDataSet.php +++ b/Models/UserDataSet.php @@ -9,7 +9,15 @@ class UserDataSet { $this->_dbInstance = Database::getInstance(); $this->_dbHandle = $this->_dbInstance->getDbConnection(); } - + public function checkAccessLevel($username) { + $sqlQuery = "SELECT ecoUser.userType FROM ecoUser + LEFT JOIN ecoUsertypes ON ecoUser.userType = ecoUsertypes.userType + WHERE ecoUser.username = ?"; + $statement = $this->_dbHandle->prepare($sqlQuery); + $statement->bindValue(1, $username); + $statement->execute(); + return $statement->fetch(PDO::FETCH_ASSOC)['userType']; + } public function fetchAll(): array { $sqlQuery = 'SELECT * FROM ecoUser;'; diff --git a/Views/index.phtml b/Views/index.phtml index 3c1f71a..ede9d90 100644 --- a/Views/index.phtml +++ b/Views/index.phtml @@ -1,32 +1,63 @@ - +
dbMessage; ?>
Current script
Facility ID | Title | Category | Description | Address | Postcode | Lat/Long | Contributor |
---|---|---|---|---|---|---|---|
' . $facilityData->getId() . - ' | ' . $facilityData->getTitle() . - ' | ' . $facilityData->getCategory() . - ' | ' . $facilityData->getDescription() . - ' | ' . $facilityData->getHouseNumber() . " " . $facilityData->getStreetName() . " " . $facilityData->getCounty() . " " . $facilityData->getTown() . - ' | ' . $facilityData->getPostcode() . - ' | ' . $facilityData->getLatitude() . " " . $facilityData->getLongitude() . - ' | ' . $facilityData->getContributor() . - ' |
Facility ID | +Title | +Category | +Status | +Description | +Address | +Postcode | +Lat/Long | +Contributor | + user->getAccessLevel() == 1): ?> +Actions | + +
---|---|---|---|---|---|---|---|---|---|
= htmlspecialchars($facilityData->getId() ?? 'N/A') ?> | += htmlspecialchars($facilityData->getTitle() ?? 'N/A') ?> | += htmlspecialchars($facilityData->getCategory() ?? 'N/A') ?> | += htmlspecialchars($facilityData->getStatus() ?? 'N/A') ?> | += htmlspecialchars($facilityData->getDescription() ?? 'N/A') ?> | += htmlspecialchars(trim(($facilityData->getHouseNumber() ?? '') . ' ' . + ($facilityData->getStreetName() ?? '') . ' ' . + ($facilityData->getCounty() ?? '') . ' ' . + ($facilityData->getTown() ?? ''))) ?> | += htmlspecialchars($facilityData->getPostcode() ?? 'N/A') ?> | += htmlspecialchars(($facilityData->getLat() ?? 'N/A') . ', ' . + ($facilityData->getLng() ?? 'N/A')) ?> | += htmlspecialchars($facilityData->getContributor() ?? 'N/A') ?> | + user->getAccessLevel() == 1): ?> ++ + + | + +