*/ ?>

This new stuff keeps coming in handy in more and more places

I'm getting a lot done with the new system, and it's useful in pretty much every new project that I do.

It was a kind of grey day yesterday, seeming like it was going to rain but just being kind of grey and misty.  The ride into work was pretty nice, but it was a little annoying with all the wet leaves on the trail.  I didn't want to fall over so I kept it kind of slow.  Noticing that I'm really not that great at cornering any more, not sure why that is.  I've felt like I'm getting less and less stable on my bike for the last couple of years, I'm not sure why that is.  Feeling like I'll fall over for no reason, slowing down when I should be speeding up, stuff like that. I was a few minutes late heading out, but somehow made it up on the ride in. 

I spent most of the morning going through the application I've been programming for the last few weeks, getting ready for our review with the agency we're doing it for.  There were a few different things that I was trying to figure out, the biggest one was trying to set the sub-page permissions on the events page when it's copied to allow the page owner to add event pages underneath it.  I didn't get that one figured out.  But I found a bug in concrete 5.5 that I spent an hour trying to fix.  I eventually figured it out, but it took a lot of trying different things out, until I finally found that it was simply a class name conflict with the theme I was using from the marketplace.  I sent the developer a message letting them know how to fix it.  It's not an issue in 5.6 because they changed the permissions dialog and javascript, but it does show up in older versions.

The review went really well, which made me pretty happy.  We used a screen share application to demo everything to them, going through each different part of the functionality, and they didn't really have any questions or changes - the only thing we might do differently is change how the address is geocoded.  But overall, they were just like "yeah, that looks good, that looks good, this looks good, too."  I had been a kind of nervous about it, not a lot but a bit.  They gave us a pretty detailed IA document for the front end functionality, and we had a conference call where we went through everything.  Maybe there were two conference calls.  But the back end editing interface, user roles and how the pieces fit together on a code basis wasn't really defined.  It was nice to have a lot of leeway in how that all was set up, but that also meant that I could have gotten it completely wrong and it wouldn't be like they wanted it to be at all.  From everything I know about user interface design and creating dashboard pages that use the concrete5 built in styles I had the feeling that they'd like it, but you never know. 

It's good to know that I can take a front end design and figure out exactly how the back end should work. It just keeps making me want to do more and more projects using the new knowledge, I hope we start to get some more application style projects as opposed to just site themes with a couple custom pages and a few custom blocks.

After that I started in on a project for another site.  We have a few weeks before the next check in on the big project, and we are waiting on design files before we can really do much of anything more on it.

The new project was really easy to apply several of the things I learned in the big project to the new one.  I got a bit of stubbing in files for the package yesterday, then picked up the rest of it at home today.  It's again a system for custom database objects again, with two different groups of users who can create the objects.  In this case they are details for newspaper advertisements for a real estate company.  So a bunch of text fields, one plain textarea, an image picker, your user id in a hidden field if you are a 'creator' and a user picker if you are an 'administrator.'  Not really all that complicated at all.  

I ended up kind of combining some code from our Hutman News package and the Custom Objects package.  There's a model for the database rows, and then a list object for those models.  It's a front end application, so the styles are all different.  I'm also using FlexiGrid for the add / edit / list filter page.  So there's a tools file that returns a json array of table rows instead of the tools file returning formatted HTML, pulling in the list off of the page type controller.  There's also an initialization function for the grid generated in the controller.  I'm using the onSuccess callback for FlexiGrid to run a dialog() function on the edit / delete buttons.  Then those open up a 'edit' tools file that either displays the add, edit or delete forms, or an error if they don't have permissions to edit something.  Those screens submit to a 'save' tools file, which either returns json "OK" or "Error" for status. If there is an error, I use the output function of the error helper to output an html list of the error if it exists.  Since you can't just alert an html list, I use the javascript function ccmAlert.notice("", dat.error); to dispaly a dialog window with the error nicely formatted.

  1. // on start function of the page type controller to load up
  2. // the proper javascript to run the flexigrid and popups
  3. // when users do not have edit access
  5. public function on_start() {
  6. parent::on_start();
  7. $html = Loader::helper('html');
  8. $css = $html->css('flexigrid.css', 'cf_ad_builder');
  9. $this->addHeaderItem($css);
  10. $css = $html->css('hutcms.css', 'cf_ad_builder');
  11. $this->addHeaderItem($css);
  12. $js = $html->javascript('flexigrid.js', 'cf_ad_builder');
  13. $this->addHeaderItem($js);
  14. $this->addHeaderItem($html->javascript('jquery.js'));
  15. $this->addHeaderItem($html->javascript('ccm.base.js'));
  16. $this->addFooterItem('<script type="text/javascript" src="' . REL_DIR_FILES_TOOLS_REQUIRED . '/i18n_js"></script>');
  17. $this->addFooterItem($html->javascript('jquery.form.js'));
  18. $this->addFooterItem($html->javascript('jquery.ui.js'));
  19. $this->addFooterItem($html->javascript('ccm.app.js'));
  20. $this->addHeaderItem($html->css('ccm.app.css'));
  21. $this->addHeaderItem($html->css('jquery.ui.css'));
  23. // Create flexigrid init script and load that to header too
  24. }
  27. // php in save tools file
  28. $error = Loader::helper('validation/error');
  29. $json = Loader::helper('json');
  31. if ($uID !== $form['uID']) {
  32. $error->add(t("You do not have permissions to delete this Object"));
  33. }
  35. if (!$error->has()) {
  36. // do whatever here
  37. } else {
  38. $retval['status'] = 'ERROR';
  39. $error->output();
  40. $retval['error'] = ob_get_clean();
  41. }
  1. // javascript in the edit tools file to submit form and display errors
  3. $(function() {
  4. // close the dialog if cancel is clicked;
  5. $("#form-cancel").click(function() {
  6. jQuery.fn.dialog.closeTop();
  7. });
  9. // if save is clicked submit the form
  10. $("#form-save").click(function() {
  11. sendform();
  12. });
  14. // if the form is submitted somehow - send it
  15. $("#myform").submit(function() {
  16. sendform();
  17. return false;
  18. });
  19. });
  20. // submit the form via ajax submit call
  21. function sendform()
  22. {
  23. $("#myform").ajaxSubmit({
  24. dataType: 'json',
  25. type: 'post',
  26. success: update_screen
  27. });
  28. }
  30. function update_screen(dat) {
  31. if (dat.status == "OK") {
  32. jQuery.fn.dialog.closeTop();
  33. // refresh the flexigrid with new data
  34. $("#flex1").flexReload();
  35. } else {
  36. ccmAlert.notice("<?php echo t("Error Processing"); ?>", dat.error);
  37. }
  38. }

There's more to it, but those are some of the basics.  It's pretty simple when you get down to it.

In this application I was copying in a bunch more of the validation stuff from the big project's code and using it in the tools files to show much more relevant error messages.  Actually in our Hutman News add on, there's not really any verification or error messages or anything, it just saves anything.  I'm actually thinking of re-writing a lot of the code for it to use the new system, everything works as is, but it could be tighter.  There are also a lot of other little things that I've learned since we first ported the code over from our old in-house CMS.  I'm hoping I can keep things compatible with the current system, but we might release a second version that offers more features.  Depends on if we have time, too.  Things have been a bit busy lately.

The other part of the application I made today is just for the administrators, it allows them to list of the custom ads in a bunch of 3 column wide rows.  These have name, photo, details, basically everything in one view.  There are also "Modify" and "Delete" buttons at the top of each object. 

There are 3 different statuses for the ads, Active, Pending and Archived.  There's also a field for "Category" in the database.  On the detail view, those options are all shown as check boxes.  Clicking on each one of them does an ajax refresh of the list and updates the HTML immediately.  That's pretty simple to do as well.

  1. <?php
  3. defined('C5_EXECUTE') or die(_("Access Denied."));
  5. class FilterPageTypeController extends Controller {
  6. public function on_start() {
  7. Loader::model('custom_object_list', 'custom_objects_demo');
  8. Loader::model('custom_object', 'custom_objects_demo');
  9. $html = Loader::helper('html');
  11. $this->addHeaderItem($html->javascript('jquery.js'));
  12. $this->addHeaderItem($html->javascript('ccm.base.js'));
  13. $this->addFooterItem('<script type="text/javascript" src="' . REL_DIR_FILES_TOOLS_REQUIRED . '/i18n_js"></script>');
  14. $this->addFooterItem($html->javascript('jquery.form.js'));
  15. $this->addFooterItem($html->javascript('jquery.ui.js'));
  16. $this->addFooterItem($html->javascript('ccm.app.js'));
  17. $this->addHeaderItem($html->css('ccm.app.css'));
  18. $this->addHeaderItem($html->css('jquery.ui.css'));
  20. // session_destroy();
  22. if (!is_array($_SESSION['category_filters'])) {
  23. $_SESSION['category_filters'] = array();
  24. }
  26. if (!is_array($_SESSION['status_filters'])) {
  27. $_SESSION['status_filters'] = array();
  28. }
  29. }
  31. public function view() {
  33. $statuses = array('Active', 'Pending', 'Archived');
  34. $this->set('statuses', $statuses);
  36. $categories = CustomObject::getAllCategories();
  37. $this->set('categories', $categories);
  39. $ads = $this->get_object_list();
  40. $this->set('ads', $ads);
  41. }
  43. public function filter_status($status, $add = "true") {
  44. $add = $add == "true" ? true : false;
  45. if ($add) {
  46. $_SESSION['status_filters'][$status] = $status;
  47. } else {
  48. unset($_SESSION['status_filters'][$status]);
  49. }
  50. $objects = $this->get_object_list();
  51. Loader::packageElement("generator_search_results", 'custom_objects_demo', array('objects' => $objects));
  52. }
  54. public function filter_category($category, $add = "true") {
  55. $add = $add == "true" ? true : false;
  56. if ($add) {
  57. $_SESSION['category_filters'][$category] = $category;
  58. } else {
  59. unset($_SESSION['category_filters'][$category]);
  60. }
  61. $objects = $this->get_object_list();
  62. Loader::packageElement("generator_search_results", 'custom_objects_demo', array('objects' => $objects));
  63. }
  65. public function get_object_list() {
  66. $objectList = new CustomObjectList();
  68. if (count($_SESSION['category_filters']) > 0) {
  69. foreach ($_SESSION['category_filters'] as $categoryIndex => $category) {
  70. $objectList->filterByCategory($category);
  71. }
  72. }
  74. if (count($_SESSION['status_filters']) > 0) {
  75. foreach ($_SESSION['status_filters'] as $statusIndex => $status) {
  76. $objectList->filterByStatus($status);
  77. }
  78. }
  80. // $objectList->debug();
  81. $objects = $objectList->get();
  82. return $objects;
  83. }
  84. }

And then something like this in the view:

  1. <?php
  2. defined('C5_EXECUTE') or die("Access Denied.");
  3. $c = Page::getCurrentPage();
  4. $form = Loader::helper('form');
  5. $tool_helper = Loader::helper('concrete/urls');
  6. $filter_status_url = BASE_URL . View::url($c->getCollectionPath(), "filter_status");
  7. $filter_categories_url = BASE_URL . View::url($c->getCollectionPath(), "filter_category");
  8. $this->inc('functions.php');
  9. ?>
  10. <!-- Left column -->
  11. <div class="three columns">
  12. <h2><?php echo t("Filter Status);?></h2>
  13. <div id="status-options-wrap" class="three columns padding-top-20" style="margin-left: 15px;">
  14. <?php
  15. $i = 0;
  16. foreach ($statuses as $status) {
  17. echo "<label for='status_filter_" . $status . "'>";
  18. if (isset($_SESSION['status_filters'][$status])) {
  19. echo $form->checkbox("status_filters[]", $status, true);
  20. } else {
  21. echo $form->checkbox("status_filters[]", $status, false);
  22. }
  23. echo $status . "</label>";
  24. }
  25. ?>
  26. </div>
  27. <h2><?php echo t("Filter Category");?></h2>
  28. <div id="categories-options-wrap" class="three columns padding-top-20" style="margin-left: 15px;">
  29. <?php
  30. foreach ($categories as $categoryID => $category) {
  31. echo "<label for='category_filter_" . $category['category'] . "'>";
  32. if (isset($_SESSION['category_filters'][$category['category']])) {
  33. echo $form->checkbox("category_filters[]", $category['category'], true);
  34. } else {
  35. echo $form->checkbox("category_filters[]", $category['category'], false);
  36. }
  37. echo $category['category'] . "</label>";
  38. }
  39. ?>
  40. </div>
  42. </div>
  43. <div class="sub-content">
  44. <?php Loader::packageElement("generator_search_results", "custom_objects_list", array("objects" => $objects)); ?>
  45. </div>
  47. <script type="text/javascript">
  48. $(document).ready(function() {
  50. $('#status-options-wrap input:checkbox').click(function() {
  51. $('#object-results-list').html('');
  52. // $('.ajax_loader').show();
  53. var filter = $(this).val();
  54. var is_selected = $(this).attr('checked') == "checked" ? true : false;
  55. var url = '<?php echo $filter_status_url ?>' + filter + '/' + is_selected;
  56. // alert(url);
  57. $.get(url, function(data) {
  58. // $('.ajax_loader').hide();
  59. $('#object-results-list').html(data);
  60. });
  61. });
  63. $('#categories-options-wrap input:checkbox').click(function() {
  64. $('#object-results-list').html('');
  65. // $('.ajax_loader').show();
  66. var filter = $(this).val();
  67. var is_selected = $(this).attr('checked') == "checked" ? true : false;
  68. var url = '<?php echo $filter_categories_url ?>' + filter + '/' + is_selected;
  69. // alert(url);
  70. $.get(url, function(data) {
  71. // $('.ajax_loader').hide();
  72. $('#object-results-list').html(data);
  73. });
  74. });
  75. });
  76. </script>

You just make sure that whatever you output in the elements/generator_search_results.php file is inside of a div or other element with an id of #object-results-list. 

I'll try and get some samples of doing this with page lists and attributes soon - as you can probably guess, it's pretty similar.  I'm using something kind of like it on this site and have on a couple of others using code that's based on how Ajax Page Tools does it.  After doing a bit of research, though, I'm thinking that my code would be a lot simpler and more accurate. 

Next up for today's application is to make it export out a RTF document of all the ads that are shown in the filtered list, and make a zip file of all the images that go with them.  Robert said that he was going to take care of that portion of it because I don't actually know what to do there, so my part is pretty much done. 

I was pretty happy with how well everything came together.  About 8.75 hours for a whole lot of functionality.  When you know what the pieces are and what they should do, sophisticated ajax interactive applications become something you can create quite quickly. 

I don't really know of any systems outside of cocnrete5 that really make it this simple, but then again, I may not know everything.  I haven't really used many other systems besides concrete5.  But part of that is actually the fact that concrete5 pretty much does everything that I want it to do, so figuring out different systems doesn't really show up on my radar too often.

In other news, I didn't drink any beer at all today.  It's been awhile since that happened.  Hope it doesn't mean that I'll be up all night long.

blog comments powered by Disqus