*/ ?>

Creating a note taking application with composer and google charts

I set up a quick little application today, for saving meeting notes for the Icarus Project.  It has a lot of cool features, including charting of meeting topics on a graphic automatically - keep reading to learn how to use composer,  a page list block, and a select attribute in concrete 5 to create a chart and a list of links to a search page for each topic.

It really isn't that hard to make a page list template that shows the tag usages underneath a particular page.  The application that I'm making was a super simple little one for the Icarus Project.  We had a meeting Saturday about trying to come up with better ways to keep track of ideas and topics for our support meetings.  One thing that was suggested was keeping meeting notes and writing down what people think of as topics for upcoming meetings based on what was talked about in the original meeting. 

They talked about keeping a communal notebook hidden somewhere that people can't access it at the Minnehaha Free Space where we meet.  That's one way of doing it, but I'm a web developer and I have a tool called concrete 5.  So of course when I think about this I think about it in a totally different way.

To me this is a pretty simple problem.  You have something "Meeting Notes" that you want to keep track of.  Each Meeting Notes object can be a page in the system with attributes.  So I added in two new select attribute types, set to allow multiple options and allow users to add to the list, one for "Suggested Future Topics" and one for "Meeting Topics."  Then I made a page type called "Meeting Notes" and attached those two attributes to it.

In order to make this something that is quick and easy to add pages we need to make a form for data entry.  Concrete 5 makes this very simple, there is a built in tool called composer.  First off I added a page to the site called "Meeting Notes" and added a page list to it that shows 10 sub-pages that are of type Meeting Notes with pagination.  Now that I have my parent page to add sub-pages to I went to the Page Types in the dashboard and set up my Meeting Notes page type in composer.

Composer settings for Meeting Notes page type

This automatically makes a page entry form that you can access from /dashboard/composer/write as an administrator.  The form will autosave drafts.  You can have as many pages unpublished as you like.  If you have more than one page type that can be edited in composer you will see a list of page types that are enabled, each would have different settings.  The form that was generated for my Meeting Notes page type is here:

Composer data entry page for Meeting Notes page type

For people unfamiliar with concrete 5 the select attribute works as an autocomplete when you have it turned on in this configuration.  So you just have to start typing and it suggests existing entries for you.  This should make it easy to categorize stuff.  Each page can have as many individual entries as you want.

Now that I have composer set up I published a few test pages to add in some sample content and then went to the front end to begin working on that.  I added three more pages to my site, "Suggested Future Topics", "Past Topics" and "Search Results."  On the search results page I added in a search box.  On the two other pages I added Page List blocks and set them to show pages from underneath the "Meeting Notes" page that we were publishing our notes pages underneath.  Then I made a custom template for each one.

  1. <?php
  2. defined('C5_EXECUTE') or die(_("Access Denied."));
  4. $searchn= Page::getByPath("/search-results");
  5. $search= $nh->getLinkToCollection($searchn);
  6. if (count($cArray) > 0) { ?>
  7. <div class="ccm-page-list">
  8. <?php
  9. $ak = CollectionAttributeKey::getByHandle('meeting_topics');
  10. $akc = $ak->getController();
  11. if(method_exists($akc, 'getOptionUsageArray')){
  12. $tagCounts = array();
  13. $cThis = $controller->cThis;
  14. if ($cThis){
  15. $pp = Page::getCurrentPage();
  16. } elseif (intval($controller->cParentID)>0) {
  17. $pp = Page::getByID($controller->cParentID);
  18. } else {
  19. $pp = false;
  20. }
  21. $ttags = $akc->getOptionUsageArray($pp);
  22. $tags = array();
  23. foreach($ttags as $t) {
  24. $tags[] = $t;
  25. }
  26. shuffle($tags);?>
  27. <script type="text/javascript" src="https://www.google.com/jsapi"></script>
  28. <script type="text/javascript">
  30. // Load the Visualization API and the piechart package.
  31. google.load('visualization', '1.0', {'packages':['corechart']});
  33. // Set a callback to run when the Google Visualization API is loaded.
  34. google.setOnLoadCallback(drawChart);
  36. // Callback that creates and populates a data table,
  37. // instantiates the pie chart, passes in the data and
  38. // draws it.
  39. function drawChart() {
  41. // Create the data table.
  42. var data = new google.visualization.DataTable();
  43. data.addColumn('string', 'Topic');
  44. data.addColumn('number', 'Count');
  45. data.addRows([
  46. <?php for ($i = 0; $i < $ttags->count(); $i++) {
  47. $akct = $tags[$i];?>
  48. ["<?= $akct->getSelectAttributeOptionValue();?>", <?= $akct->getSelectAttributeOptionUsageCount();?>]<?php if (($i) < $ttags->count()) {?>,<?php } ?>
  49. <?php }?>
  50. ]);
  52. // Set chart options
  53. var options = {'title':'Past Meeting Topics',
  54. 'width':940,
  55. 'height':200};
  57. // Instantiate and draw our chart, passing in some options.
  58. var chart = new google.visualization.BarChart(document.getElementById('chart_div'));
  59. chart.draw(data, options);
  60. }
  61. </script>
  62. <div id="chart_div"></div>
  63. <?php for ($i = 0; $i < $ttags->count(); $i++) {
  64. $akct = $tags[$i];
  65. $qs = $akc->field('atSelectOptionID') . '[]=' . $akct->getSelectAttributeOptionID();
  66. if (is_object($pp)){
  67. echo '<h3><a href="'.$BASE_URL.$search.'?'.$qs.'&search_paths[]=' . urlencode($pp->getCollectionPath()) . '">'.$akct->getSelectAttributeOptionValue().' ('.$akct->getSelectAttributeOptionUsageCount().')</a></h3>';
  68. } else {
  69. echo '<h3><a href="'.$BASE_URL.$search.'?'.$qs .'">'.$akct->getSelectAttributeOptionValue().' ('.$akct->getSelectAttributeOptionUsageCount().')</a></h3>';
  70. }
  71. }
  72. }
  73. ?>
  74. </div>
  75. <?php
  76. }?>

This block of code may give errors when working with all pages on a site, it's designed to take advantage of another feature of the select attribute, a function called on line 21:

$ttags = $akc->getOptionUsageArray($pp);

I'm passing in a page, which is the parent page selected when we added our page list, and what we get back is an instance of SelectAttributeTypeOptionList.  You can use that list to get the name and usage count of each individual option.  It's a pretty simple matter from there to make a list for the google chart api. 

It's also fairly easy to make a list of links back to our search page, which we do after the code for the google charts API.  The code there will create links back to our search page.  The results of the search will be pages underneath our parent page that have the option selected. 

The only thing left to do is to make a template for outputting the meeting notes that looks a little better.  By default page lists show title and description only, this is pretty boring and doesn't give you much information.  Instead I'm going to show the main area's contents on the sub page, title, and a list of topics and a list of suggested future topics.  The topics should also be linked back to the search page so that you can view other meetings with the same topics.  Seems simple enough, it looks a little something like this:

  1. <?php
  2. defined('C5_EXECUTE') or die("Access Denied.");
  3. ?>
  4. <div id="meeting-notes-index">
  5. <?php
  6. $isFirst = true;
  7. $searchn= Page::getByPath("/search-results");
  8. $search= $nh->getLinkToCollection($searchn, true);
  9. $excerptBlocks = ($controller->truncateSummaries ? 1 : null); //1 is the number of blocks to include in the excerpt
  10. $truncateChars = ($controller->truncateSummaries ? $controller->truncateChars : 0);
  11. foreach ($cArray as $cobj):
  12. $title = $cobj->getCollectionName();
  13. $date = $cobj->getCollectionDatePublic('F j, Y');
  14. $author = $cobj->getVersionObject()->getVersionAuthorUserName();
  15. $link = $nh->getLinkToCollection($cobj);
  16. $firstClass = $isFirst ? 'first-entry' : '';
  17. $isFirst = false;
  18. ?>
  19. <div class="entry" <?php echo $firstClass; ?>>
  20. <div class="title">
  21. <h3>
  22. <a href="<?php echo $link; ?>"><?php echo $title; ?></a>
  23. </h3>
  24. <h4>
  25. Posted by <?php echo $author; ?> on <?php echo $date; ?>
  26. </h4>
  27. </div>
  28. <div class="excerpt">
  29. <?php
  30. $a = new Area('Main');
  31. $a->disableControls();
  32. $a->display($cobj);
  33. ?>
  34. </div>
  35. <div class="ccm-spacer"></div>
  36. <div class="meta">
  37. <h2>Meeting Topics</h2>
  38. <ul><?php
  39. $ak = CollectionAttributeKey::getByHandle('meeting_topics');
  40. $akc = $ak->getController();
  41. $cat = $cobj->getCollectionAttributeValue($ak);
  42. $opts = $cat->getOptions();
  43. $str = "";
  44. for ($j = 1; $j <= count($opts); $j++) {
  45. $opt = $opts[$j - 1];
  46. $qs = $akc->field('atSelectOptionID') . '[]=' . $opt->getSelectAttributeOptionID();
  47. $str .= '<li><a href="'.$search.'?'.$qs.'">'.$opt->getSelectAttributeOptionValue().'</a></li>';
  48. if ($j < count($opts)) {
  49. $str .= ", ";
  50. }
  51. }
  52. echo $str;
  53. ?></ul>
  54. <h2>Suggested Future Topics</h2>
  55. <ul><?php
  56. $ak = CollectionAttributeKey::getByHandle('suggested_future_topics');
  57. $akc = $ak->getController();
  58. $cat = $cobj->getCollectionAttributeValue($ak);
  59. $opts = $cat->getOptions();
  60. $str = "";
  61. for ($j = 1; $j <= count($opts); $j++) {
  62. $opt = $opts[$j - 1];
  63. $qs = $akc->field('atSelectOptionID') . '[]=' . $opt->getSelectAttributeOptionID();
  64. $str .= '<li><a href="'.$search.'?'.$qs.'">'.$opt->getSelectAttributeOptionValue().'</a></li>';
  65. if ($j < count($opts)) {
  66. $str .= ", ";
  67. }
  68. }
  69. echo $str;
  70. ?></ul>
  71. </div>
  72. </div>
  73. <?php endforeach; ?>
  74. </div>
  76. <div id="meeting-notes-index-foot">
  77. <?php if(!$previewMode && $controller->rss):
  78. $btID = $b->getBlockTypeID();
  79. $bt = BlockType::getByID($btID);
  80. $uh = Loader::helper('concrete/urls');
  81. $rssUrl = $controller->getRssUrl($b, 'blog_rss');
  82. $rssIcon = $uh->getBlockTypeAssetsURL($bt, 'rss.png');
  83. $rssTitle = $controller->rssTitle;
  84. ?>
  85. <div id="rss">
  86. <a href="<?php echo $rssUrl; ?>" target="_blank"><?php echo t('Subscribe to RSS Feed')?></a>
  87. <a href="<?php echo $rssUrl; ?>" target="_blank"><img src="<?php echo $rssIcon; ?>" width="14" height="14" alt="<?php echo t('RSS Icon')?>" title="<?php echo t('RSS Feed')?>" /></a>
  88. </div>
  89. <link href="<?php echo BASE_URL.$rssUrl; ?>" rel="alternate" type="application/rss+xml" title="<?php echo $rssTitle; ?>" />
  90. <?php endif; ?>
  93. <?php if ($paginate && $num > 0 && is_object($pl)): ?>
  94. <div id="pagination">
  95. <?php
  96. $summary = $pl->getSummary();
  97. if ($summary->pages > 1):
  98. $paginator = $pl->getPagination();
  99. ?>
  100. <span class="pagination-left"><?php echo $paginator->getPrevious('&laquo; Newer Posts'); ?></span>
  101. <span class="pagination-right"><?php echo $paginator->getNext('Older Posts &raquo;'); ?></span>
  102. <?php echo $paginator->getPages(); ?>
  103. <?php endif; ?>
  104. </div>
  105. <?php endif; ?>
  106. </div>

I basically just took the blog index template from the concrete 5 core and updated it a little bit to work with the meeting notes.  Instead of putting the comments in the footer of each post I'm looping over the attribute lists for our select attributes and making links back to the search page, that's really the only main difference. 

All said and done with finding a new theme to use and making a couple tweaks to get it looking nice it took about 3, maybe 4 hours to get everything set up and functioning.  A super simple application but it should do everything that the Icarus Project needs for their data entry needs.

Notes Application - Home Page Notes Application - List Page Notes Application - Suggested Future Topics Notes Application - Past Topics Notes Application - Search Results

It seems like there's very little that you can't do in concrete 5 with some combination of page attributes and page lists.  Maybe that's an oversimplification but most application functionality can be boiled down to pages and lists of pages.  Of course when you start trying to step outside of this convention things get complicated rather quickly.

I really like what you can do with the page lists and attributes.  An application like this would have taken quite awhile to code up from scratch but this was really just a quick afternoon's work, I probably could have done it quicker but I was farting around with some stuff and surfing facebook while I coded.  I also made a batch of chai and cooked up dinner.  I think it took about as long to document it and take screenshots and clean up the code for making this post as it took to actually make the whole website.  I think this might be the quickest that I've coded up any side project website.

Integrating with the google chart API was super easy.  I wasn't expecting it to be hard but the code from the samples just dropped in and work, all I really had to do was change a couple of strings and replace the hard coded array of values with a loop of the the $ttags object.  I'll probably have to go in and adjust the template as the number of entries grows to keep it readable but I think it will be really great.

It's nice that we can suddenly have DATA about stuff.  I like that a lot.  It's easy when looking over a list in a notebook of the last several months and trying to tally up what comes up the most often but you wouldn't have the added benefit of the auto complete so things that might be the same thing might be worded differently.  Even if you went through and digitized everything using a google spreadsheet or xcel you'd still be looking at tons of data entry and possibility for error.  I'm not sure how it would even work for a regular spreadsheet, you'd almost have to have a relational database to really get the kind of data that we're getting here.  And the data that we're getting here is kind of automatically generated, you add it to the pages and it's automatically searchable and viewable in chart format.

I wonder if I can work in chart reporting on the Occupy website as well, there are tags and categories on the posts there so you could do basically the same thing for each neighborhood, showing which topics come up the most in each area.  The code should work the same for User Blog Posts in User Blogs as it does here with Meeting Notes for Icarus. 

I hope that the other people from Icarus like it as much as I do and actually start using it.  I don't know how detailed of notes we need to take but there might be good stuff like foods to eat that are healthier for you or herbs to take that help with depression or any number of other things that people might bring up that we would want to keep track of.  But you also have to be careful to keep out any identifying information.  So what kind of notes can you keep?  Of course the whole thing is going to need to be secret and browsable only by members of Icarus.

I'm probably going to set it up so that there is a group "Icarus Members" who have access to the site, and those members would all have access to both the composer section of the dashboard and the user section of the dashboard.  This would mean that anyone who is a member could approve newly registered people to then be in the Icarus Members group.  Everyone else would be locked out.  This would give users access to each other's emails though, I'm not sure if this is necessarily the best thing.  So maybe there will be two levels, admins and members, members would be able to post notes but not access user data.  I wish it was a task permission in concrete 5 to allow or disallow members or groups to access to other user's email, that would make allowing access to user management a lot friendlier.  Maybe it's just me that wants something like that.

Anyway, locking down permissions and figuring out who will take and how to keep notes are left, most of the hard work is done.  Everything else is details and data entry.

I also made up a flier for our upcoming movie night.  Icarus Project is working to do some outreach to people through showing different films that highlight different aspects of mental illness.  The first film is Daddy of Rock and Roll: The Wesley Willis Documentary.

You must install Adobe Flash to view this content.
Movie Night Flier

Not my best work for a flier but I'm pretty out of practice.  I'm not sure why I volunteered to make the flier, seemed like it would be a cinch for me to do something up in Illustrator and I didn't know what anyone else had for capabilities so I said I do it.  So Saturday I went to Peace Coffee before Icarus for a planning meeting which I thought was going to be mostly about the movie night but turned out to be a lot more organizational about how to set up Icarus and run things.  So that was kind of cool, I like being involved with stuff like that.  That's where I got the idea for coding up this application, they seemed to have a need I could fill with just a couple of hours of custom coding that could be of great benefit to a lot of people.  How can I then not make the application when I see a need like that for a group I care about and I'm part of?

Anyway, I wasn't meaning for my weekend to be spent volunteering for Icarus but that's kind of how it turned out.  I think I did a lot of good with my time and none of my other projects are really hurt by it.

In between the organizational meeting and the support meeting there was another meeting happening at the Minnehaha Free space which I went and took part of.  It was the group that used to do the food swaps last year.  In case you haven't heard of this, they would get 40 people together in a room with a ton of food and you would walk around and bid what you had on other people's items, and hope that they came to you with whatever they had to trade at the end of the night.  It sounded really cool, I wondered how my chai would do when I heard about it but forgot to bookmark the link or somehow forgot about it and never checked back.

Turns out they had been completely shut down by the department of agriculture or something like that.  They were told that since they weren't making the food in licensed kitchens with licensed vendors they were going to kill someone.  I didn't get to hear the whole story because I actually put my chai that I'd brought for the Icarus project meeting in to their potluck.  I also chipped 5 dollars and loaded up a plate of yummy food and listened to them for a bit but I had to run home and get a second bottle of chai.  I had the feeling I was going to need two bottles I just didn't bring them both, in hindsight I should have just brought more and then I could have stayed for more of the meeting about the food swap.  Maybe I could start a peer-to-peer through the mail food swap over the internet, totally anonymously?  Maybe that will be my next project.

I got a lot of good compliments from the foodies on my chai which was really nice.  I know it's good but it's nice to hear it from other people, especially people that are known for being really picky about using only the freshest most local ingredients in making their crazy foodie concoctions.  I probably could have done pretty well in the food swaps if they were still going on.  I didn't get to hear if they had made any progress or anything, the bit that I heard was mostly history and it sounded pretty grim.  Apparently there are laws concerning food serving in nearly any form, even potlucks can't be hosted on a 'regular' basis without a licensed kitchen or something like that.  I thought that was pretty outrageous. 

That was pretty much it for my weekend.  I didn't make it out to any of the biking stuff going on.  I didn't do much of anything except for volunteer and go to group and cook food.  I made up two batches of chai today to keep up my cycle of taking in all my chai in on Monday for the whole week.  So tomorrow will be a cargo run day.  Work should be pretty busy, even if we don't have anything there is stuff to do.  Concrete 5 just released 5.5.2 which has the new image cropping tool for when picnic goes offline and a bunch of other bug fixey things.  I need to get that white labeled and ready for whatever site I program next.  Then I have to get Hutman News updated for 5.5.2.  You wouldn't think that it would be a very big update since we are already 5.5 compatible but you would be wrong.  This is because it's now possible to show two full rich text editors side by side with working concrete 5 toolbars.  At least, this is what I've heard from reading on the leaders board.  It was a last minute fix that got in and it means you can add content to Hutman News even easier than before.  The addon is set up to use two rich text editors which works but you can't show the concrete 5 edit toolbar because the focus of the javascript isn't consistent, both toolbars will target the same editor.  So you are a little limited when it comes to adding content with it, it's one of the main flaws with the add on. 

I've thought about other ways to do it and I'm not really sure how.  I could do it with a custom element for the text editors in the add on that has a page break button and then use that to break things up for the page lists like I do on User Blogs.  This would be the simplest way to get it working with both 5.5 and 5.4.2 and before but then we lose backwards compatibility.  I don't want that.

I did have code working to display two text editors next to eachother in 5.4.2 but then it didn't work in 5.5 so I stopped development on that code, I might be able to find that again and then release an update that is backwards compatible too.  But either way it will be at least a few hours updating the rich text editors to work properly.

blog comments powered by Disqus