Today’s article is about concrete5 again after a long time without anything about concrete5 on codeblog.ch. The example we’re going to look at takes a few ideas and code snippets from my book about concrete5.
If you ever had a closer look at the file manager you’ve probably seen that if you open the file properties, you can see a small statistics about the downloads of a file. This is quite nice but what if you wanted to see how many file downloads there are in total? Right now, there’s no such report available in concrete5 which is why we’re going to build the first part of such an addon.
It will use some AJAX to switch between different views, allowing us to extend it even further in the future. At the end you’ll have an additional page in the reports section like this:
This article uses a few different elements of concrete5, having some experience with blocks, templates, themes and packages is highly recommended. I’m not going to explain every detail but I’m happy to answer questions in case something’s missing.
First, let’s have a look at the package installer:
<?php defined('C5_EXECUTE') or die(_("Access Denied.")); class RemoFileReportPackage extends Package { protected $pkgHandle = 'remo_file_report'; protected $appVersionRequired = '5.4'; protected $pkgVersion = '1.0'; public function getPackageDescription() { return t("Adds a file download statistic report"); } public function getPackageName() { return t("File Report"); } public function install() { $pkg = parent::install(); Loader::model('single_page'); $sp = SinglePage::add('dashboard/reports/remo_file_report', $pkg); $sp->update(array('cName' => 'File Report', 'cDescription'=>'File Download Statistic')); } } ?> |
As you can see, the package controller is pretty small. Beside the usual package information fields and methods, we only override the install method to add a custom page located at /dashboard/reports/remo_file_report. By adding out page beneath the dashboard reports, we’ll automatically see our new page as a new register in the dashboard. Nothing else has to be done in order to make sure the page appears where it should.
Page controller & view
Since we’re adding a new page, we’ll have to add a controller as well as a file for the single page output. First, we create a file in packages/remo_file_report/controllers/dashboard/reports/remo_file_report.php with this content:
<?php defined('C5_EXECUTE') or die("Access Denied."); class DashboardReportsRemoFileReportController extends Controller { public function view() { } public function on_start() { $html = Loader::helper('html'); $this->addHeaderItem($html->css('remo.filereport.css', 'remo_file_report')); $this->addHeaderItem($html->javascript('remo.filereport.js', 'remo_file_report')); } } ?> |
Again, this file is small, we only make sure that our CSS and JavaScript file are properly included in the head of the page. If you’ve created single pages before, you’ve probably added some code to the view method in order to pass some data to the output of the single page. In our case, this is not necessary since we load our content with the help of AJAX.
Next, create a file for our output packages/remo_file_report/single_pages/dashboard/reports/remo_file_report.php and put this content in it:
<?php defined('C5_EXECUTE') or die("Access Denied."); ?> <h1><span><?php echo t('File Download Statistics')?></span></h1> <div class="ccm-dashboard-inner"> <div id="remo-file-report-links-1"> <a href="#" data-action="day" class="remo-file-report-link" id="remo-file-report-week"><?php echo t('Day')?></a> <a href="#" data-action="week" class="remo-file-report-link" id="remo-file-report-week"><?php echo t('Week')?></a> </div> <div id="remo-file-report-content"> </div> </div> |
Besides a caption and two links to switch between the two views “day” and “week”, there’s only an empty DIV element with the id “remo-file-report-content” where we’re going to add some content with the help of a little JavaScript.
CSS to format File Report
In the page controller, we’ve included two files, one for a few basic styles and one for the AJAX logic. Let’s create the CSS file first at this location packages/remo_file_report/css/remo.filereport.css:
#remo-file-report-links-1 a { margin-right: 10px; } #remo-file-report-content table { margin-top: 10px; } #remo-file-report-content th { text-align: left; background: #ddd; } |
JavaScript and PHP backend file to handle the AJAX procedures
The next file is the one which holds all the JavaScript and thus the AJAX magic. It has to be created here packages/remo_file_report/js/remo.filereport.js:
$(document).ready(function() { $(".remo-file-report-link").click(function() { $.post(CCM_TOOLS_PATH + "/../packages/remo_file_report/get_data/", {action: $(this).attr("data-action")}, function(data) { // select the correct renderer to print the data switch (data.data.renderer) { case "table": var rowHeader = ""; var tableBody = ""; var rowHeaderPrinted = false; // loop through all rows for (var rowKey in data.data.tableData) { var rowData = data.data.tableData[rowKey]; tableBody += "<tr>"; // loop through all columns for (var colKey in rowData) { if (!rowHeaderPrinted) { rowHeader += "<th>" + colKey.replace('_',' ') + "</th>"; } tableBody += "<td>" + rowData[colKey] + "</td>"; } tableBody += "</tr>"; rowHeaderPrinted = true; } // put generated markup in DIV $("#remo-file-report-content").html("<table><tr>" + rowHeader + "</tr>" + tableBody + "</table>"); break; default: alert("Unsupported rendered: " + data.data.renderer); } }, "json") }); }); |
In this file we’re doing the following things:
- Set an event which is executed when the user clicks on the links (day or week)
- Execute an AJAX call by using $.post
- Once we get the result back, we’re detected the correct rendered. Right now, there’s just “table” but this could be extended to create a different output type like a chart
- We then loop though all the data we’ve gotten from the AJAX tool
- Last, we’re going to add the generated content to our DIV element with the ID remo-file-report-content
Our AJAX call uses a file which we’re accessing using this URL: CCM_TOOLS_PATH + “/../packages/remo_file_report/get_data/”. This file is located in the tools folder of our package which is probably the most common way in concrete5 to generate data for an AJAX script. Create a new file at this location packages/remo_file_report/tools/get_data.php and put this content in it:
<?php defined('C5_EXECUTE') or die("Access Denied."); $ret['error'] = false; try { $jh = Loader::helper("json"); Loader::model("remo_file_report", "remo_file_report"); // make sure the user is allowed to access the file download statistic. $p = new Permissions(Page::getByPath('/dashboard/reports/remo_file_report')); if (!$p->canRead()) { throw new Exception(t('Not allowed to access the JSON tools to fetch the file statistics.')); } // execute action to get the file download data $action = $_REQUEST['action']; $actionObject = RemoFileReportModel::getActionInstance($action); $ret['data'] = $actionObject->getData(); } catch (Exception $ex) { $ret['error'] = $e->getMessage(); } echo $jh->encode($ret); ?> |
What’s in this file?
- We load two elements, the JSON helper because we’re going to return our data in this format,
- the remo_file_report model which is where we put all the database logic
- We also make sure that only users who are allowed to access the dashboard page are allowed to use this file. Without this check it would be possible to access our download statistics by using a direct URL, even if one doesn’t have the permission to do so.
- Next, we’re going to call a method from our model from which we’ll get all the data
- Most of the file has been wrapped in a try…catch block. This is helpful because if you format data in JSON you won’t be able to parse the output in your JavaScript if there’s a PHP error message. By using a try and catch block, we can handle all the possible errors and pass them on to our JavaScript where we can show them to the user
- At the end we’re going to use the JSON helper from concrete5 to return the data echo $jh->encode($ret);
Model to fetch Download Statistics from the database
The only file that’s missing now, is the model which we’re calling from the “tool” to get our download statistics out of the database. Create this file packages/remo_file_report/models/remo_file_report.php with this content:
<?php defined('C5_EXECUTE') or die("Access Denied."); class RemoFileReportModel { public static function getActionInstance($action) { switch ($action) { case 'day': return new RemoFileReportDay(); break; case 'week': return new RemoFileReportWeek(); break; default: throw new Exception(t('Unsupported action in model remo_file_report.php.')); } } } interface IRemoFileReport { public function getData(); } class RemoFileReportDay implements IRemoFileReport { public function getData() { $db = Loader::db(); $ret['renderer'] = 'table'; $ret['tableData'] = $db->GetAll('SELECT fv.fvFilename Filename,count(*) Download_Count FROM DownloadStatistics ds INNER JOIN FileVersions fv ON ds.fID=fv.fID AND ds.fvID=fv.fvID WHERE date(timestamp) = CURRENT_DATE GROUP BY fv.fID, fv.fvID, fv.fvFilename ORDER BY count(*) DESC'); return $ret; } } class RemoFileReportWeek implements IRemoFileReport { public function getData() { $db = Loader::db(); $ret['renderer'] = 'table'; $ret['tableData'] = $db->GetAll('SELECT fv.fvFilename Filename,count(*) Download_Count FROM DownloadStatistics ds INNER JOIN FileVersions fv ON ds.fID=fv.fID AND ds.fvID=fv.fvID WHERE YEARWEEK(timestamp) = YEARWEEK(CURRENT_DATE) GROUP BY fv.fID, fv.fvID, fv.fvFilename ORDER BY count(*) DESC'); return $ret; } } ?> |
This file is a bit bigger but doesn’t do a lot at all, what’s in it?
- The getActionInstance method in the RemoFileReportModel class simply returns the proper class matching the requested action. Splitting every action into a different class isn’t necessary but could be helpful once we add more parts to our report
- Since we’ve splitted our actions into classes, we also define an interface to assure that action classes implement the necessary getData method
- The two classes RemoFileReportDay and RemoFileReportWeek are almost identical, the just execute a simple SQL command to fetch the data and return it as an array
Download and next steps
After all these words, you can download the code and extract it to the “packages” directory of your concrete5 site and install it by using the “Add Functionality” section in the dashboard”.
This Add-on isn’t complete, as you’ve seen on the screenshot at the beginning of this post, you can’t find a lot of information in the output. A few things which are missing:
- Pictures are often helpful to get a first and quick impression, some charts would be nice
- Different sections to split information into different parts of the report
- Automatically display a statistic, right now you have to click on day or week in order to see something
- An export which produces an XLS(X) or PDF report
- Some information about the file types maybe
And a lot more! I’m you have a lot of ideas yourself, if you want to add something or have any questions about this code / tutorials, don’t hesitate to leave a comment!
3 Comments
Amazing idea, can’t wait to try out and give you some feedback… You already stated everything that would be nice to add, but the add-on itself is amazing…
This is a great add on, would it be possible to also track the user name if they are logged in?
Yes! As a matter of fact concrete5 already tracks this information for you! Just have a look at the table called DownloadStatistics and you’ll find a column called uID which is 0 for anonymous users and bigger than that if a user downloaded the file who’s logged in!