<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[11917] CalendarServer/trunk/contrib</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/11917">11917</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2013-11-08 13:59:31 -0800 (Fri, 08 Nov 2013)</dd>
</dl>

<h3>Log Message</h3>
<pre>webpoll prototype/demo webapp.</pre>

<h3>Added Paths</h3>
<ul>
<li>CalendarServer/trunk/contrib/webpoll/</li>
<li><a href="#CalendarServertrunkcontribwebpollMakefile">CalendarServer/trunk/contrib/webpoll/Makefile</a></li>
<li><a href="#CalendarServertrunkcontribwebpollREADMEtxt">CalendarServer/trunk/contrib/webpoll/README.txt</a></li>
<li><a href="#CalendarServertrunkcontribwebpollcaldavdtestwebpollplist">CalendarServer/trunk/contrib/webpoll/caldavd-test-webpoll.plist</a></li>
<li>CalendarServer/trunk/contrib/webpoll/webapp/</li>
<li>CalendarServer/trunk/contrib/webpoll/webapp/css/</li>
<li><a href="#CalendarServertrunkcontribwebpollwebappcsswebpollcss">CalendarServer/trunk/contrib/webpoll/webapp/css/webpoll.css</a></li>
<li><a href="#CalendarServertrunkcontribwebpollwebappindexhtml">CalendarServer/trunk/contrib/webpoll/webapp/index.html</a></li>
<li>CalendarServer/trunk/contrib/webpoll/webapp/js/</li>
<li><a href="#CalendarServertrunkcontribwebpollwebappjscaldavjs">CalendarServer/trunk/contrib/webpoll/webapp/js/caldav.js</a></li>
<li><a href="#CalendarServertrunkcontribwebpollwebappjsjcaljs">CalendarServer/trunk/contrib/webpoll/webapp/js/jcal.js</a></li>
<li><a href="#CalendarServertrunkcontribwebpollwebappjsutilsjs">CalendarServer/trunk/contrib/webpoll/webapp/js/utils.js</a></li>
<li><a href="#CalendarServertrunkcontribwebpollwebappjswebpolljs">CalendarServer/trunk/contrib/webpoll/webapp/js/webpoll.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcontribwebpollMakefile"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/contrib/webpoll/Makefile (0 => 11917)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/contrib/webpoll/Makefile                                (rev 0)
+++ CalendarServer/trunk/contrib/webpoll/Makefile        2013-11-08 21:59:31 UTC (rev 11917)
</span><span class="lines">@@ -0,0 +1,36 @@
</span><ins>+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+webpoll:
+        curl http://code.jquery.com/jquery-2.0.3.js -o webapp/js/jquery-2.0.3.js
+        curl http://code.jquery.com/ui/1.10.3/jquery-ui.js -o webapp/js/jquery-ui-1.10.3.js
+        curl https://raw.github.com/douglascrockford/JSON-js/master/json2.js -o webapp/js/json2.js
+        curl http://trentrichardson.com/examples/timepicker/jquery-ui-timepicker-addon.js -o webapp/js/datetimepicker.js
+        
+        curl http://trentrichardson.com/examples/timepicker/jquery-ui-timepicker-addon.css -o webapp/css/datetimepicker.css
+        curl http://jqueryui.com/resources/download/jquery-ui-themes-1.10.3.zip -o /tmp/jquery-ui-themes-1.10.3.zip
+        unzip /tmp/jquery-ui-themes-1.10.3.zip jquery-ui-themes-1.10.3/themes/cupertino/* -d /tmp
+        mv /tmp/jquery-ui-themes-1.10.3/themes/cupertino webapp/css
+        rm -rf /tmp/jquery-ui-themes-1.10.3
+
+clean:
+        rm -f webapp/js/jquery-2.0.3.js
+        rm -f webapp/js/jquery-ui-1.10.3.js
+        rm -f webapp/js/json2.js
+        rm -f webapp/js/datetimepicker.js
+        
+        rm -f webapp/css/datetimepicker.css
+        rm -rf webapp/css/cupertino
</ins></span></pre></div>
<a id="CalendarServertrunkcontribwebpollREADMEtxt"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/contrib/webpoll/README.txt (0 => 11917)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/contrib/webpoll/README.txt                                (rev 0)
+++ CalendarServer/trunk/contrib/webpoll/README.txt        2013-11-08 21:59:31 UTC (rev 11917)
</span><span class="lines">@@ -0,0 +1,29 @@
</span><ins>+WebPoll Package for CalendarServer
+==================================
+
+*** IMPORTANT: this a prototype/demo and is not intended for production use

+WebPoll is a set of webbrowser javascript files that implement a prototype webapp for
+a client that supports the new iCalendar VPOLL component for doing &quot;consensus&quot; scheduling
+- i.e., a scheduling process where users can vote on which of a series of potential events
+are best for them. The VPOLL technology has been developed by CalConnect to open up new
+methods of scheduling with iCalendar that allows for not only polls, but potentially
+booking systems and much more.
+
+The WebPoll webapp uses jQuery and jQueryUI (which have to be downloaded separately). It
+uses AJAX calls to make CalDAV queries to the server hosting the webapp. The CalDAV server
+must support jCal (iCalendar-in-JSON) format calendar data.
+
+Use with CalendarServer
+=======================
+
+Use &quot;make webpoll&quot; in this directory to download all the relevant dependencies.
+
+In the CalendarServer directory do &quot;./run -f ./contrib/webpoll/caldavd-test-webpoll.plist&quot; to run the server.
+
+In a browser navigate to &quot;/webpoll&quot;.
+
+Using the webapp
+================
+
+TBD
</ins></span></pre></div>
<a id="CalendarServertrunkcontribwebpollcaldavdtestwebpollplist"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/contrib/webpoll/caldavd-test-webpoll.plist (0 => 11917)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/contrib/webpoll/caldavd-test-webpoll.plist                                (rev 0)
+++ CalendarServer/trunk/contrib/webpoll/caldavd-test-webpoll.plist        2013-11-08 21:59:31 UTC (rev 11917)
</span><span class="lines">@@ -0,0 +1,39 @@
</span><ins>+&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+
+&lt;!--
+    Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  --&gt;
+
+&lt;!DOCTYPE plist PUBLIC &quot;-//Apple Computer//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
+&lt;plist version=&quot;1.0&quot;&gt;
+  &lt;dict&gt;
+
+    &lt;!-- Import a parent config before this one --&gt;
+    &lt;key&gt;ImportConfig&lt;/key&gt;
+    &lt;string&gt;./conf/caldavd-test.plist&lt;/string&gt;
+
+    &lt;!-- Child aliases --&gt;
+    &lt;key&gt;Aliases&lt;/key&gt;
+    &lt;array&gt;
+      &lt;dict&gt;
+        &lt;key&gt;url&lt;/key&gt;
+        &lt;string&gt;/webpoll&lt;/string&gt;
+        &lt;key&gt;path&lt;/key&gt;
+        &lt;string&gt;./contrib/webpoll/webapp&lt;/string&gt;
+      &lt;/dict&gt;
+    &lt;/array&gt;
+
+  &lt;/dict&gt;
+&lt;/plist&gt;
</ins></span></pre></div>
<a id="CalendarServertrunkcontribwebpollwebappcsswebpollcss"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/contrib/webpoll/webapp/css/webpoll.css (0 => 11917)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/contrib/webpoll/webapp/css/webpoll.css                                (rev 0)
+++ CalendarServer/trunk/contrib/webpoll/webapp/css/webpoll.css        2013-11-08 21:59:31 UTC (rev 11917)
</span><span class="lines">@@ -0,0 +1,310 @@
</span><ins>+@CHARSET &quot;UTF-8&quot;;
+
+html,body {
+        font: 80% &quot;Trebuchet MS&quot;, sans-serif;
+}
+
+/* Main Panel settings */
+#main-panel {
+        position: absolute;
+        left: 0px;
+        top: 0px;
+        right: 0px;
+        bottom: 0px;
+}
+
+/* Title Text */
+#title {
+        font: 14pt &quot;Trebuchet MS&quot;, sans-serif;
+        position: absolute;
+        margin-top: 5px;
+        margin-left: 20px;
+        height: 32px;
+        left: 0px;
+        top: 0px;
+}
+
+#loading {
+        font: 14pt &quot;Trebuchet MS&quot;, sans-serif;
+        position: absolute;
+        margin-top: 5px;
+        margin-right: 20px;
+        height: 20px;
+        width: 180px;
+        right: 0px;
+        top: 0px;
+}
+
+#progressbar {
+        position: absolute;
+        width: 100px;
+        height: 20px;
+        top: 0px;
+        right: 0px;
+}
+
+/* Sidebar Panel */
+#sidepanel {
+        position: absolute;
+        width: 210px;
+        left: 5px;
+        top: 32px;
+        bottom: 0px;
+        border-right-style:solid;
+        border-width: 1px;
+        border-color:#ccc;
+}
+
+#sidebar {
+        position: absolute;
+        left: 0px;
+        right: 2px;
+        top: 0px;
+        bottom: 20px;
+}
+
+.sidebar-title {
+        position: relative;
+        font: 12pt &quot;Trebuchet MS&quot;, sans-serif;
+}
+
+#sidebar-new-poll-count, #sidebar-vote-poll-count {
+        position: absolute;
+        background-color:#3366ff;
+        font: 8pt &quot;Trebuchet MS&quot;, sans-serif;
+        color:#fff;
+        text-align: center;
+        border:2px solid;
+        border-color: #fff;
+        border-radius: 30px;
+        right: 10px;
+        padding-top: 2px;
+        padding-bottom: 2px;
+        padding-left: 5px;
+        padding-right: 5px;
+}
+
+/* Refresh Button */
+#refresh {
+        position: absolute;
+        right: 5px;
+        bottom: 4px;
+}
+
+/* Detail Panel */
+#detail {
+        position: absolute;
+        left: 220px;
+        right: 5px;
+        top: 32px;
+        bottom: 0px;
+}
+
+#detail-nocontent {
+        position: absolute;
+        left: 50px;
+        top: 50px;
+        font-size: 2.0em;
+}
+
+#editpoll-description {
+        position: absolute;
+        left: 4px;
+        right: 0px;
+        top: 0px;
+        height: 32px;
+}
+
+#editpoll-title-edit-panel {
+        float: left;
+        font-size: 1.5em;
+}
+
+#editpoll-title-panel {
+        float: left;
+        font-size: 1.5em;
+}
+
+#editpoll-organizer-panel {
+        position: relative;
+        float: left;
+        left: 20px;
+        font-size: 1.5em;
+}
+
+#editpoll-status-panel {
+        position: relative;
+        float: left;
+        left: 40px;
+        font-size: 1.5em;
+}
+
+#editpoll-title, #editpoll-organizer, #editpoll-status {
+        font-weight: bold;
+}
+
+#editpoll-details {
+        position: absolute;
+        left: 0px;
+        right: 0px;
+        top: 28px;
+}
+
+#sidebar-owned, #editpoll-tabs {
+        margin-bottom: 4px;
+}
+
+#editpoll-delete {
+        position: relative;
+        left: 10px;
+}
+
+#editpoll-autofill {
+        position: relative;
+        left: 50px;
+}
+
+.event,.voter {
+        position: relative;
+        background-color:#eee;
+        box-shadow: 5px 5px 2px #888;
+        margin: 8px;
+        padding: 4px;
+}
+
+.edit-datetime {
+        margin: 4px;
+}
+
+.edit-datetime label, .edit-voter label {
+        display: inline-block;
+        width: 40px;
+}
+
+.voter-address {
+        width: 250px;
+}
+
+.input-remove {
+        position: absolute;
+        right: 4px;
+        bottom: 4px;
+}
+
+.active-voter {
+        font-weight:bold;
+}
+
+#editpoll-resulttable {
+        font: 11pt &quot;Trebuchet MS&quot;, sans-serif;
+        border-collapse:collapse;
+        border: 1px solid #ccc;
+}
+
+#editpoll-resulttable thead, #editpoll-resulttable tfoot {
+        text-align:right;        
+}
+
+#editpoll-resulttable td {
+        border: 1px solid #ccc;        
+        padding: 4px;        
+}
+
+#editpoll-resulttable tfoot {
+        border: 2px solid;        
+}
+
+.response-btns, .response-btns label {
+        height: 24px;        
+}
+
+#winner-text {
+        position: relative;
+}
+
+#winner-icon-left {
+        position: absolute;
+        left: 0px;
+        top: 50%;
+        margin-top: -8px;
+}
+
+#winner-icon-right {
+        position: absolute;
+        right: 0px;
+        top: 50%;
+        margin-top: -8px;
+}
+
+.center-td {
+        text-align:center;        
+}
+
+.poll-winner-td {
+        background-color:#D9D9D9;        
+}
+
+.best-td {
+        background-color:#CCCCFF;        
+}
+
+.ok-td {
+        background-color:#CCE6CC;        
+}
+
+.maybe-td {
+        background-color:#FFFFCC;        
+}
+
+.no-td {
+        background-color:#FFCCCC;        
+}
+
+.no-response-td {
+        background-color:white;        
+}
+
+.no-close .ui-dialog-titlebar-close {
+  display: none;
+}
+
+#hover-grid {
+        position: relative;
+        width: 100%;
+        margin: 10px;
+        border-collapse:collapse;
+        border: 1px solid #ccc;
+}
+
+#hover-grid tr {
+        position: relative;
+        height: 30px;
+}
+
+#hover-grid td {
+        border: 1px solid #ccc;
+        padding: 4px;
+}
+
+#hover-grid .hover-grid-td-time {
+        width: 70px;
+        text-align: right;
+}
+
+.hover-event {
+        position: absolute;
+        font: 8pt &quot;Trebuchet MS&quot;, sans-serif;
+        left: 81px;
+        width: 250px;
+        padding-top: 2px;
+        padding-bottom: 2px;
+        padding-left: 5px;
+        padding-right: 5px;
+}
+
+#response-key {
+        position: relative;
+        width: 100px;
+        top: 20px;
+}
+
</ins></span></pre></div>
<a id="CalendarServertrunkcontribwebpollwebappindexhtml"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/contrib/webpoll/webapp/index.html (0 => 11917)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/contrib/webpoll/webapp/index.html                                (rev 0)
+++ CalendarServer/trunk/contrib/webpoll/webapp/index.html        2013-11-08 21:59:31 UTC (rev 11917)
</span><span class="lines">@@ -0,0 +1,110 @@
</span><ins>+&lt;!doctype html&gt;
+&lt;html lang=&quot;us&quot;&gt;
+&lt;head&gt;
+        &lt;meta charset=&quot;utf-8&quot;&gt;
+        &lt;title&gt;WebPoll&lt;/title&gt;
+        &lt;link href=&quot;css/cupertino/jquery-ui-1.10.3.css&quot; rel=&quot;stylesheet&quot;&gt;
+        &lt;link href=&quot;css/datetimepicker.css&quot; rel=&quot;stylesheet&quot;&gt;
+        &lt;link href=&quot;css/webpoll.css&quot; rel=&quot;stylesheet&quot;&gt;
+        &lt;script src=&quot;js/jquery-2.0.3.js&quot;&gt;&lt;/script&gt;
+        &lt;script src=&quot;js/jquery-ui-1.10.3.js&quot;&gt;&lt;/script&gt;
+        &lt;script src=&quot;js/datetimepicker.js&quot;&gt;&lt;/script&gt;
+        &lt;script src=&quot;js/json2.js&quot;&gt;&lt;/script&gt;
+        &lt;script src=&quot;js/utils.js&quot;&gt;&lt;/script&gt;
+        &lt;script src=&quot;js/jcal.js&quot;&gt;&lt;/script&gt;
+        &lt;script src=&quot;js/caldav.js&quot;&gt;&lt;/script&gt;
+        &lt;script src=&quot;js/webpoll.js&quot;&gt;&lt;/script&gt;
+&lt;/head&gt;
+&lt;body&gt;
+
+&lt;div id=&quot;main-panel&quot;&gt;
+
+&lt;div id=&quot;title&quot;&gt;WebPoll&lt;/div&gt;
+
+&lt;div id=&quot;loading&quot;&gt;
+Loading
+&lt;div id=&quot;progressbar&quot;&gt;&lt;/div&gt;
+&lt;/div&gt;
+
+&lt;!-- Sidebar Panel--&gt;
+&lt;div id=&quot;sidepanel&quot;&gt;
+        &lt;div id=&quot;sidebar&quot;&gt;
+                &lt;h3 class=&quot;sidebar-title&quot;&gt;&lt;span&gt;Your Polls&lt;/span&gt;&lt;span id=&quot;sidebar-new-poll-count&quot;&gt;0&lt;/span&gt;&lt;/h3&gt;
+                &lt;div&gt;
+                        &lt;ul id=&quot;sidebar-owned&quot;&gt;&lt;/ul&gt;
+                        &lt;button id=&quot;sidebar-new-poll&quot;&gt;New Poll&lt;/button&gt;
+                &lt;/div&gt;
+                &lt;h3 class=&quot;sidebar-title&quot;&gt;&lt;span&gt;Polls to Vote On&lt;span id=&quot;sidebar-vote-poll-count&quot;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
+                &lt;div&gt;
+                        &lt;ul id=&quot;sidebar-voter&quot;&gt;&lt;/ul&gt;
+                &lt;/div&gt;
+        &lt;/div&gt;
+        &lt;div id=&quot;refresh&quot;&gt;&lt;button id=&quot;refresh-btn&quot;&gt;Refresh&lt;/button&gt;&lt;/div&gt;
+&lt;/div&gt;
+
+&lt;!-- Detail Panel --&gt;
+&lt;div id=&quot;detail&quot;&gt;
+        &lt;div id=&quot;detail-nocontent&quot; class=&quot;ui-widget&quot;&gt;Select a poll in the side-bar to view its details.&lt;/div&gt;
+        &lt;div id=&quot;editpoll&quot;&gt;
+                &lt;div id=&quot;editpoll-description&quot;&gt;
+                        &lt;div id=&quot;editpoll-title-edit-panel&quot; class=&quot;ui-widget&quot;&gt;
+                                &lt;label for=&quot;editpoll-title-edit&quot;&gt;Title: &lt;/label&gt;
+                                &lt;input id=&quot;editpoll-title-edit&quot; /&gt;
+                        &lt;/div&gt;
+                        &lt;div id=&quot;editpoll-title-panel&quot; class=&quot;ui-widget&quot;&gt;Title: &lt;span id=&quot;editpoll-title&quot;&gt;&lt;/span&gt;&lt;/div&gt;
+                        &lt;div id=&quot;editpoll-organizer-panel&quot; class=&quot;ui-widget&quot;&gt;Organizer: &lt;span id=&quot;editpoll-organizer&quot;&gt;&lt;/span&gt;&lt;/div&gt;
+                        &lt;div id=&quot;editpoll-status-panel&quot; class=&quot;ui-widget&quot;&gt;Status: &lt;span id=&quot;editpoll-status&quot;&gt;&lt;/span&gt;&lt;/div&gt;
+                &lt;/div&gt;
+                &lt;div id=&quot;editpoll-details&quot;&gt;
+                        &lt;div id=&quot;editpoll-tabs&quot;&gt;
+                                &lt;ul&gt;
+                                        &lt;li id=&quot;editpoll-events-tab&quot;&gt;&lt;a href=&quot;#editpoll-events&quot;&gt;Events&lt;/a&gt;&lt;/li&gt;
+                                        &lt;li id=&quot;editpoll-voters-tab&quot;&gt;&lt;a href=&quot;#editpoll-voters&quot;&gt;Voters&lt;/a&gt;&lt;/li&gt;
+                                        &lt;li&gt;&lt;a href=&quot;#editpoll-results&quot;&gt;Results&lt;/a&gt;&lt;/li&gt;
+                                &lt;/ul&gt;
+                                &lt;div id=&quot;editpoll-events&quot;&gt;
+                                        &lt;div id=&quot;editpoll-eventlist&quot;&gt;&lt;/div&gt;
+                                        &lt;button id=&quot;editpoll-addevent&quot;&gt;Add Event&lt;/button&gt;
+                                &lt;/div&gt;
+                                &lt;div id=&quot;editpoll-voters&quot;&gt;
+                                        &lt;div id=&quot;editpoll-voterlist&quot;&gt;&lt;/div&gt;
+                                        &lt;button id=&quot;editpoll-addvoter&quot;&gt;Add Voter&lt;/button&gt;
+                                &lt;/div&gt;
+                                &lt;div id=&quot;editpoll-results&quot;&gt;
+                                        &lt;table id=&quot;editpoll-resulttable&quot;&gt;
+                                                &lt;thead&gt;
+                                                        &lt;tr&gt;&lt;/tr&gt;
+                                                        &lt;tr&gt;&lt;/tr&gt;
+                                                        &lt;tr&gt;&lt;/tr&gt;
+                                                &lt;/thead&gt;
+                                                &lt;tbody&gt;
+                                                &lt;/tbody&gt;
+                                                &lt;tfoot&gt;
+                                                        &lt;tr&gt;&lt;/tr&gt;
+                                                        &lt;tr&gt;&lt;/tr&gt;
+                                                &lt;/tfoot&gt;
+                                        &lt;/table&gt;
+                                &lt;/div&gt;
+                        &lt;/div&gt;
+                        &lt;button id=&quot;editpoll-save&quot;&gt;Save&lt;/button&gt;
+                        &lt;button id=&quot;editpoll-cancel&quot;&gt;Cancel&lt;/button&gt;
+                        &lt;button id=&quot;editpoll-done&quot;&gt;Done&lt;/button&gt;
+                        &lt;button id=&quot;editpoll-delete&quot;&gt;Delete Poll&lt;/button&gt;
+                        &lt;button id=&quot;editpoll-autofill&quot;&gt;Auto Fill&lt;/button&gt;
+                        &lt;div id=&quot;response-key&quot; class=&quot;ui-widget&quot;&gt;
+                                Possible Responses:
+                                &lt;ul id=&quot;response-menu&quot;&gt;
+                                        &lt;li&gt;&lt;a href=&quot;#&quot;&gt;&lt;span class=&quot;ui-icon ui-icon-close&quot;&gt;&lt;/span&gt;No&lt;/a&gt;&lt;/li&gt;
+                                        &lt;li&gt;&lt;a href=&quot;#&quot;&gt;&lt;span class=&quot;ui-icon ui-icon-help&quot;&gt;&lt;/span&gt;Maybe&lt;/a&gt;&lt;/li&gt;
+                                        &lt;li&gt;&lt;a href=&quot;#&quot;&gt;&lt;span class=&quot;ui-icon ui-icon-check&quot;&gt;&lt;/span&gt;Ok&lt;/a&gt;&lt;/li&gt;
+                                        &lt;li&gt;&lt;a href=&quot;#&quot;&gt;&lt;span class=&quot;ui-icon ui-icon-circle-check&quot;&gt;&lt;/span&gt;Best&lt;/a&gt;&lt;/li&gt;
+                                &lt;/ul&gt;
+                        &lt;/div&gt;
+                &lt;/div&gt;
+        &lt;/div&gt;
+&lt;/div&gt;
+
+&lt;/div&gt;
+
+&lt;/body&gt;
+&lt;/html&gt;
</ins><span class="cx">Property changes on: CalendarServer/trunk/contrib/webpoll/webapp/index.html
</span><span class="cx">___________________________________________________________________
</span></span></pre></div>
<a id="svnexecutable"></a>
<div class="addfile"><h4>Added: svn:executable</h4></div>
<a id="CalendarServertrunkcontribwebpollwebappjscaldavjs"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/contrib/webpoll/webapp/js/caldav.js (0 => 11917)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/contrib/webpoll/webapp/js/caldav.js                                (rev 0)
+++ CalendarServer/trunk/contrib/webpoll/webapp/js/caldav.js        2013-11-08 21:59:31 UTC (rev 11917)
</span><span class="lines">@@ -0,0 +1,1028 @@
</span><ins>+/**
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+*/
+
+/**
+ * Classes to model a CalDAV service using XHR requests.
+ */
+
+// Do an AJAX request
+function Ajax(params) {
+        $.extend(params, {
+                processData : false,
+                crossDomain: true,
+                username: gSession.auth,
+                password: gSession.auth
+        })
+        return $.ajax(params);
+}
+
+// A generic PROPFIND request
+function Propfind(url, depth, props) {
+        var nsmap = {};
+        addNamespace(&quot;D&quot;, nsmap);
+
+        var propstr = addElements(props, nsmap);
+
+        return Ajax({
+                url : url,
+                type : &quot;PROPFIND&quot;,
+                contentType : &quot;application/xml; charset=utf-8&quot;,
+                headers : {
+                        &quot;Prefer&quot; : &quot;return=minimal&quot;,
+                        &quot;Depth&quot; : depth
+                },
+                data : '&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;' + '&lt;D:propfind' + buildXMLNS(nsmap) + '&gt;' + '&lt;D:prop&gt;' + propstr + '&lt;/D:prop&gt;' + '&lt;/D:propfind&gt;',
+        });
+}
+
+// A calendar-query REPORT request for VPOLLs only
+function PollQueryReport(url, props) {
+        var nsmap = {};
+        addNamespace(&quot;D&quot;, nsmap);
+        addNamespace(&quot;C&quot;, nsmap);
+
+        var propstr = addElements(props, nsmap);
+        propstr += '&lt;C:calendar-data content-type=&quot;application/calendar+json&quot; /&gt;';
+
+        return Ajax({
+                url : url,
+                type : &quot;REPORT&quot;,
+                contentType : &quot;application/xml; charset=utf-8&quot;,
+                headers : {
+                        &quot;Prefer&quot; : &quot;return=minimal&quot;,
+                        &quot;Depth&quot; : &quot;0&quot;
+                },
+                data : '&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;' +
+                        '&lt;C:calendar-query' + buildXMLNS(nsmap) + '&gt;' +
+                                '&lt;D:prop&gt;' + propstr + '&lt;/D:prop&gt;' +
+                                '&lt;C:filter&gt;' +
+                                        '&lt;C:comp-filter name=&quot;VCALENDAR&quot;&gt;' +
+                                                '&lt;C:comp-filter name=&quot;VPOLL&quot; /&gt;' +
+                                        '&lt;/C:comp-filter&gt;' +
+                                '&lt;/C:filter&gt;' +
+                        '&lt;/C:calendar-query&gt;',
+        });
+}
+
+// A calendar-query REPORT request for VEVENTs in time-range, expanded
+function TimeRangeExpandedSummaryQueryReport(url, start, end) {
+        var nsmap = {};
+        addNamespace(&quot;D&quot;, nsmap);
+        addNamespace(&quot;C&quot;, nsmap);
+
+        return Ajax({
+                url : url,
+                type : &quot;REPORT&quot;,
+                contentType : &quot;application/xml; charset=utf-8&quot;,
+                headers : {
+                        &quot;Prefer&quot; : &quot;return=minimal&quot;,
+                        &quot;Depth&quot; : &quot;0&quot;
+                },
+                data : '&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;' +
+                        '&lt;C:calendar-query' + buildXMLNS(nsmap) + '&gt;' +
+                                '&lt;D:prop&gt;' +
+                                '&lt;C:calendar-data content-type=&quot;application/calendar+json&quot;&gt;' +
+                                        '&lt;C:expand start=&quot;' + jcaldate.jsDateToiCal(start) + '&quot; end=&quot;' + jcaldate.jsDateToiCal(end) + '&quot;/&gt;' +
+                                        '&lt;C:comp name=&quot;VCALENDAR&quot;&gt;' + 
+                                                '&lt;C:allprop/&gt;' +
+                                                '&lt;C:comp name=&quot;VEVENT&quot;&gt;' +
+                                                        '&lt;C:prop name=&quot;UID&quot;/&gt;&lt;C:prop name=&quot;DTSTART&quot;/&gt;&lt;C:prop name=&quot;DTEND&quot;/&gt;&lt;C:prop name=&quot;DURATION&quot;/&gt;&lt;C:prop name=&quot;SUMMARY&quot;/&gt;' +
+                                                '&lt;/C:comp&gt;' +
+                                        '&lt;/C:comp&gt;' +
+                                '&lt;/C:calendar-data&gt;' +
+                                '&lt;/D:prop&gt;' +
+                                '&lt;C:filter&gt;' +
+                                        '&lt;C:comp-filter name=&quot;VCALENDAR&quot;&gt;' +
+                                                '&lt;C:comp-filter name=&quot;VEVENT&quot;&gt;' +
+                                                        '&lt;C:time-range start=&quot;' + jcaldate.jsDateToiCal(start) + '&quot; end=&quot;' + jcaldate.jsDateToiCal(end) + '&quot;/&gt;' +
+                                                '&lt;/C:comp-filter&gt;' +
+                                        '&lt;/C:comp-filter&gt;' +
+                                '&lt;/C:filter&gt;' +
+                        '&lt;/C:calendar-query&gt;'
+        });
+}
+
+// A freebusy POST request
+function Freebusy(url, fbrequest) {
+        return Ajax({
+                url : url,
+                type : &quot;POST&quot;,
+                contentType : &quot;application/calendar+json; charset=utf-8&quot;,
+                data : fbrequest.toString(),
+        });
+}
+
+// A calendar-user-search REPORT request
+function UserSearchReport(url, text) {
+        var nsmap = {};
+        addNamespace(&quot;D&quot;, nsmap);
+        addNamespace(&quot;C&quot;, nsmap);
+        addNamespace(&quot;CS&quot;, nsmap);
+
+        return Ajax({
+                url : url,
+                type : &quot;REPORT&quot;,
+                contentType : &quot;application/xml; charset=utf-8&quot;,
+                headers : {
+                        &quot;Depth&quot; : &quot;0&quot;
+                },
+                data : '&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;'
+                        + '&lt;CS:calendarserver-principal-search context=&quot;user&quot;' + buildXMLNS(nsmap) + '&gt;' + '&lt;CS:search-token&gt;' + xmlEncode(text) + '&lt;/CS:search-token&gt;'
+                        + '&lt;CS:limit&gt;&lt;CS:nresults&gt;20&lt;/CS:nresults&gt;&lt;/CS:limit&gt;' + '&lt;D:prop&gt;' + '&lt;D:displayname /&gt;' + '&lt;C:calendar-user-address-set /&gt;' + '&lt;/D:prop&gt;'
+                        + '&lt;/CS:calendarserver-principal-search&gt;',
+        });
+}
+
+// Multistatus response processing
+MultiStatusResponse = function(response, parent_url) {
+        this.response = response;
+        this.parentURL = parent_url;
+}
+
+// Get property text value from the overall multistatus
+MultiStatusResponse.prototype.getPropertyText = function(prop) {
+        return this._getPropertyText($(this.response), prop, &quot;D:multistatus/D:response/D:propstat/D:prop/&quot;);
+}
+
+// Get property href text value from the overall multistatus
+MultiStatusResponse.prototype.getPropertyHrefTextList = function(prop) {
+        return this._getPropertyHrefTextList($(this.response), prop, &quot;D:multistatus/D:response/D:propstat/D:prop/&quot;);
+}
+
+// Get property text value from the specified response node
+MultiStatusResponse.prototype.getResourcePropertyText = function(response_node, prop) {
+        return this._getPropertyText(response_node, prop, &quot;D:propstat/D:prop/&quot;);
+}
+
+// Get property href text value from the specified response node
+MultiStatusResponse.prototype.getResourcePropertyHrefTextList = function(response_node, prop) {
+        return this._getPropertyHrefTextList(response_node, prop, &quot;D:propstat/D:prop/&quot;);
+}
+
+// Get property text value from the specified node
+MultiStatusResponse.prototype._getPropertyText = function(node, prop, prefix) {
+        return getElementText(node, prefix + prop);
+}
+
+// Get all property href text values as an array from the specified node
+MultiStatusResponse.prototype._getPropertyHrefTextList = function(node, prop, prefix) {
+        var items = findElementPath(node, prefix + prop + &quot;/D:href&quot;);
+        if (items.length == 0) {
+                return null;
+        } else {
+                var results = []
+                $.each(items, function(index, item) {
+                        results.push(item.text());
+                });
+                return results;
+        }
+}
+
+// Apply specified function to each response (other than the parent)
+MultiStatusResponse.prototype.doToEachChildResource = function(doIt) {
+        var items = findElementPath($(this.response), &quot;D:multistatus/D:response&quot;);
+        var msr = this;
+        $.each(items, function(index, item) {
+                var href = getElementText(item, &quot;D:href&quot;);
+                if (!compareURLs(href, msr.parentURL)) {
+                        doIt(href, item);
+                }
+        });
+}
+
+// Schedule response processing
+ScheduleResponse = function(response) {
+        this.response = response;
+}
+
+// Apply specified function to each recipient response
+ScheduleResponse.prototype.doToEachRecipient = function(doIt) {
+        var items = findElementPath($(this.response), &quot;C:schedule-response/C:response&quot;);
+        $.each(items, function(index, item) {
+                doIt(getElementText(item, &quot;C:recipient/D:href&quot;), item);
+        });
+}
+
+// A CalDAV session for a specific principal
+CalDAVSession = function(user) {
+        this.currentPrincipal = null;
+        //this.host = &quot;http://172.16.105.104:8080/ucaldav&quot;;
+        //this.host = &quot;https://cyrus.local:8543&quot;;
+        this.host = &quot;&quot;;
+        
+        if (user === undefined) {
+                this.auth = null;
+        } else {
+                this.auth = user;
+        }
+}
+
+// Setup session
+CalDAVSession.prototype.init = function(whenDone) {
+        this.currentUserPropfind(whenDone);
+}
+
+gWellKnown = &quot;/.well-known/caldav&quot;;
+//gWellKnown = &quot;/&quot;;
+
+// Discover current principal from /.well-known/caldav, then load the principal data
+CalDAVSession.prototype.currentUserPropfind = function(whenDone) {
+        var session = this;
+        Propfind(joinURLs(this.host, gWellKnown), &quot;0&quot;, [
+                &quot;D:current-user-principal&quot;
+        ]).done(function(response) {
+                var msr = new MultiStatusResponse(response, gWellKnown);
+                var href = msr.getPropertyText(&quot;D:current-user-principal/D:href&quot;);
+                if (href == null) {
+                        alert(&quot;Could not determine current user.&quot;);
+                } else {
+                        // Load the principal
+                        session.currentPrincipal = new CalDAVPrincipal(href);
+                        session.currentPrincipal.init(whenDone);
+                }
+        }).fail(function(jqXHR, status, error) {
+                alert(status + error);
+        });
+}
+
+// Search for calendar users matching a string
+CalDAVSession.prototype.calendarUserSearch = function(item, whenDone) {
+        UserSearchReport(joinURLs(this.host, &quot;/principals/&quot;), item).done(function(response) {
+                var msr = new MultiStatusResponse(response, &quot;/principals/&quot;);
+                var results = [];
+                msr.doToEachChildResource(function(url, response_node) {
+                        var cn = msr.getResourcePropertyText(response_node, &quot;D:displayname&quot;);
+                        var cuaddr = CalDAVPrincipal.bestCUAddress(msr.getResourcePropertyHrefTextList(response_node, &quot;C:calendar-user-address-set&quot;));
+                        if (cuaddr) {
+                                results.push((cn ? cn + &quot; &quot; : &quot;&quot;) + &quot;&lt;&quot; + cuaddr + &quot;&gt;&quot;);
+                        }
+                });
+
+                if (whenDone) {
+                        whenDone(results.sort());
+                }
+        }).fail(function(jqXHR, status, error) {
+                alert(status + error);
+        });
+}
+
+// Represents a calendar user on the server
+CalDAVPrincipal = function(url) {
+        this.url = url;
+        this.cn = null;
+        this.home_url = null;
+        this.inbox_url = null;
+        this.outbox_url = null;
+        this.calendar_user_addresses = [];
+        this.default_address = null;
+        this.poll_calendars = [];
+        this.event_calendars = [];
+}
+
+// Return the best calendar user address from the set. Prefer mailto over urn over anything else.
+CalDAVPrincipal.bestCUAddress = function(cuaddress_set) {
+        var results = $.grep(cuaddress_set, function(cuaddr, index) {
+                return cuaddr.startsWith(&quot;mailto:&quot;);
+        });
+        if (results.length == 0) {
+                results = $.grep(cuaddress_set, function(cuaddr, index) {
+                        return cuaddr.startsWith(&quot;urn:uuid:&quot;);
+                });
+        }
+
+        if (results.length != 0) {
+                return results[0];
+        }
+        return null;
+}
+
+// Load principal details for this user, then load all the calendars
+CalDAVPrincipal.prototype.init = function(whenDone) {
+                // Get useful properties
+        var principal = this;
+        Propfind(joinURLs(gSession.host, principal.url), &quot;0&quot;, [
+                &quot;D:displayname&quot;, &quot;C:calendar-home-set&quot;, &quot;C:schedule-inbox-URL&quot;, &quot;C:schedule-outbox-URL&quot;, &quot;C:calendar-user-address-set&quot;
+        ]).done(function(response) {
+                var msr = new MultiStatusResponse(response, principal.url);
+                principal.cn = msr.getPropertyText(&quot;D:displayname&quot;);
+                principal.home_url = msr.getPropertyText(&quot;C:calendar-home-set/D:href&quot;);
+                principal.inbox_url = msr.getPropertyText(&quot;C:schedule-inbox-URL/D:href&quot;);
+                principal.outbox_url = msr.getPropertyText(&quot;C:schedule-outbox-URL/D:href&quot;);
+                principal.calendar_user_addresses = msr.getPropertyHrefTextList(&quot;C:calendar-user-address-set&quot;);
+                // Load the calendars
+                principal.loadCalendars(whenDone);
+        }).fail(function(jqXHR, status, error) {
+                alert(status + error);
+        });
+}
+
+// For a reload of all calendar data
+CalDAVPrincipal.prototype.refresh = function(whenDone) {
+        this.poll_calendars = [];
+        this.event_calendars = [];
+        this.loadCalendars(whenDone);
+}
+
+// The most suitable calendar user address for the user
+CalDAVPrincipal.prototype.defaultAddress = function() {
+        if (!this.default_address) {
+                this.default_address = CalDAVPrincipal.bestCUAddress(this.calendar_user_addresses);
+        }
+        return this.default_address;
+}
+
+// Indicate whether the specified calendar-user-address matches the current user
+CalDAVPrincipal.prototype.matchingAddress = function(cuaddr) {
+        return this.calendar_user_addresses.indexOf(cuaddr) != -1;
+}
+
+// Load all VPOLL and VEVENT capable calendars for this user
+CalDAVPrincipal.prototype.loadCalendars = function(whenDone) {
+        var this_principal = this;
+        Propfind(joinURLs(gSession.host, this.home_url), &quot;1&quot;, [
+                &quot;D:resourcetype&quot;, &quot;D:displayname&quot;, &quot;D:add-member&quot;, &quot;C:supported-calendar-component-set&quot;
+        ]).done(function(response) {
+                var msr = new MultiStatusResponse(response, this_principal.home_url);
+                msr.doToEachChildResource(function(url, response_node) {
+                        if (!hasElementPath(response_node, &quot;D:propstat/D:prop/D:resourcetype/D:collection&quot;)
+                                || !hasElementPath(response_node, &quot;D:propstat/D:prop/D:resourcetype/C:calendar&quot;))
+                                return;
+
+                        // Separate out support for VPOLL and VEVENT
+                        var comps = findElementPath(response_node, &quot;D:propstat/D:prop/C:supported-calendar-component-set/C:comp&quot;);
+                        var has_vpoll = true;
+                        var has_vevent = true;
+                        if (comps.length != 0) {
+                                has_vpoll = false;
+                                has_vevent = false;
+                                $.each(comps, function(index, comp) {
+                                        if (comp.attr(&quot;name&quot;) == &quot;VPOLL&quot;) {
+                                                has_vpoll = true;
+                                        }
+                                        if (comp.attr(&quot;name&quot;) == &quot;VEVENT&quot;) {
+                                                has_vevent = true;
+                                        }
+                                });
+                        }
+
+                        // Build the calendar and assign to appropriate arrays
+                        var cal = new CalendarCollection(url);
+                        cal.displayname = getElementText(response_node, &quot;D:propstat/D:prop/D:resourcetype/D:displayname&quot;)
+                        if (!cal.displayname) {
+                                cal.displayname = basenameURL(url);
+                        }
+                        cal.addmember = getElementText(response_node, &quot;D:propstat/D:prop/D:add-member/D:href&quot;)
+                        if (has_vpoll) {
+                                this_principal.poll_calendars.push(cal);
+                        }
+                        if (has_vevent) {
+                                this_principal.event_calendars.push(cal);
+                        }
+                });
+                // Load the resources from all VPOLL calendars
+                this_principal.loadResources(whenDone);
+        }).fail(function(jqXHR, status, error) {
+                alert(status + error);
+        });
+}
+
+// Start loading all VPOLL resources
+CalDAVPrincipal.prototype.loadResources = function(whenDone) {
+        var principal = this;
+        var process = [].concat(principal.poll_calendars);
+        process.reverse();
+        this.loadCalendarResources(whenDone, process);
+}
+
+// Iteratively load all resources from VPOLL calendars
+CalDAVPrincipal.prototype.loadCalendarResources = function(whenDone, process) {
+        var this_principal = this;
+        var calendar = process.pop();
+        calendar.loadResources(function() {
+                if (process.length != 0) {
+                        this_principal.loadCalendarResources(whenDone, process);
+                } else {
+                        this_principal.addResources(whenDone);
+                }
+        });
+}
+
+// After all resources are loaded, add each VPOLL to view controller
+CalDAVPrincipal.prototype.addResources = function(whenDone) {
+
+        // Scan each resource to see if it is organized or not
+        $.each(this.poll_calendars, function(index1, calendar) {
+                $.each(calendar.resources, function(index2, resource) {
+                        var poll = new Poll(resource);
+                        gViewController.addPoll(poll);
+                })
+        });
+        if (whenDone) {
+                whenDone();
+        }
+}
+
+// Do a freebusy query for the specified user for the specified time range and indicate whether busy or not
+CalDAVPrincipal.prototype.isBusy = function(user, start, end, whenDone) {
+        var fbrequest = jcal.newCalendar();
+        fbrequest.newProperty(&quot;method&quot;, &quot;REQUEST&quot;);
+        var fb = fbrequest.newComponent(&quot;vfreebusy&quot;, true);
+        fb.newProperty(
+                &quot;organizer&quot;,
+                this.defaultAddress(),
+                { &quot;cn&quot; : this.cn },
+                &quot;cal-address&quot;
+        );
+        fb.newProperty(
+                &quot;attendee&quot;,
+                user,
+                {},
+                &quot;cal-address&quot;
+        );
+        fb.newProperty(
+                &quot;dtstart&quot;,
+                jcaldate.jsDateTojCal(start),
+                {},
+                &quot;date-time&quot;
+        );
+        fb.newProperty(
+                &quot;dtend&quot;,
+                jcaldate.jsDateTojCal(end),
+                {},
+                &quot;date-time&quot;
+        );
+
+        Freebusy(
+                joinURLs(gSession.host, this.outbox_url),
+                fbrequest
+          ).done(function(response) {
+                  var sched = new ScheduleResponse(response);
+                  var result = null;
+                  sched.doToEachRecipient(function(url, response_node) {
+                          var caldata = getElementText(response_node, &quot;C:calendar-data&quot;);
+                          if (caldata) {
+                                  caldata = jcal.fromString(caldata);
+                                  if (caldata.mainComponent().name() == &quot;vfreebusy&quot;) {
+                                          // Any FREEBUSY property means busy sometime during the requested period
+                                          result = caldata.mainComponent().hasProperty(&quot;freebusy&quot;)
+                                  }
+                          }
+                  });
+                  
+                  if (whenDone) {
+                          whenDone(result)
+                  }
+          }).fail(function(jqXHR, status, error) {
+                  alert(status + error);
+          });
+}
+
+// Get a summary of events for the specified time-range
+CalDAVPrincipal.prototype.eventsForTimeRange = function(start, end, whenDone) {
+        var this_principal = this;
+        TimeRangeExpandedSummaryQueryReport(
+                joinURLs(gSession.host, this.event_calendars[0].url), start, end
+        ).done(function(response) {
+                var results = []
+                var msr = new MultiStatusResponse(response, this_principal.event_calendars[0].url);
+                msr.doToEachChildResource(function(url, response_node) {
+                        var caldata = jcal.fromString(msr.getResourcePropertyText(response_node, &quot;C:calendar-data&quot;));
+                        results.push(new CalendarObject(caldata));
+                });
+                if (whenDone) {
+                        whenDone(results);
+                }
+                
+        }).fail(function(jqXHR, status, error) {
+                  alert(status + error);
+          });
+}
+
+// A calendar collection on the server
+CalendarCollection = function(url) {
+        this.url = url;
+        this.displayname = null;
+        this.addmember = null;
+        this.resources = [];
+}
+
+// Load a calendar's VPOLL resources
+CalendarCollection.prototype.loadResources = function(whenDone) {
+        var calendar = this;
+        PollQueryReport(joinURLs(gSession.host, calendar.url), [
+                &quot;D:getetag&quot;
+        ]).done(function(response) {
+                var msr = new MultiStatusResponse(response, calendar.url);
+                msr.doToEachChildResource(function(url, response_node) {
+                        var etag = msr.getResourcePropertyText(response_node, &quot;D:getetag&quot;);
+                        var caldata = jcal.fromString(msr.getResourcePropertyText(response_node, &quot;C:calendar-data&quot;));
+                        calendar.resources.push(new CalendarResource(calendar, url, etag, caldata));
+                });
+                if (whenDone) {
+                        whenDone();
+                }
+        }).fail(function(jqXHR, status, error) {
+                alert(status + error);
+        });
+}
+
+// A calendar resource object
+CalendarResource = function(calendar, url, etag, data) {
+        this.calendar = calendar;
+        this.url = url;
+        this.etag = etag;
+        this.object = (data instanceof CalendarObject ? data : new CalendarObject(data));
+}
+
+// Create a brand new poll and add to the default calendar
+CalendarResource.newPoll = function(title) {
+        // Add to calendar
+        resource = new CalendarResource(gSession.currentPrincipal.poll_calendars[0], null, null, CalendarPoll.newPoll(title));
+        resource.calendar.resources.push(resource);
+        return resource;
+}
+
+// Save this resource to the server - might be brand new or an update
+CalendarResource.prototype.saveResource = function(whenDone) {
+
+        if (!this.object.changed())
+                return;
+
+        // Always set to accepted
+        if (this.object.mainComponent().data.name() == &quot;vpoll&quot;) {
+                this.object.mainComponent().acceptInvite();
+        }
+
+        if (!this.url) {
+                if (this.calendar.addmember) {
+                        // Do POST;add-member
+                        Ajax({
+                                context : this,
+                                url : joinURLs(gSession.host, this.calendar.addmember),
+                                type : &quot;POST&quot;,
+                                contentType : &quot;application/calendar+json; charset=utf-8&quot;,
+                                headers : {
+                                        &quot;Prefer&quot; : &quot;return=representation&quot;,
+                                        &quot;Accept&quot; : &quot;application/calendar+json&quot;
+                                },
+                                data : this.object.toString(),
+                        }).done(function(response, textStatus, jqXHR) {
+                                // Get Content-Location header as new url
+                                this.url = jqXHR.getResponseHeader(&quot;Content-Location&quot;);
+
+                                // Check for returned data and ETag
+                                this.etag = jqXHR.getResponseHeader(&quot;Etag&quot;);
+                                this.object = new CalendarObject(response);
+                                
+                                if (whenDone) {
+                                        whenDone();
+                                }
+
+                        }).fail(function(jqXHR, status, error) {
+                                alert(status + error);
+                        });
+
+                        return;
+                }
+
+                // Have to PUT a new resource
+                this.url = joinURLs(this.calendar.url, this.data.getComponent(&quot;vpoll&quot;).getPropertyText(&quot;uid&quot;) + &quot;.ics&quot;);
+        }
+
+        // Do conditional PUT
+        Ajax({
+                context : this,
+                url : joinURLs(gSession.host, this.url),
+                type : &quot;PUT&quot;,
+                contentType : &quot;application/calendar+json; charset=utf-8&quot;,
+                headers : {
+                        &quot;Prefer&quot; : &quot;return=representation&quot;,
+                        &quot;Accept&quot; : &quot;application/calendar+json&quot;
+                },
+                data : this.object.toString(),
+        }).done(function(response, textStatus, jqXHR) {
+                // Check for returned data and ETag
+                this.etag = jqXHR.getResponseHeader(&quot;Etag&quot;);
+                this.object = new CalendarObject(response);
+                
+                if (whenDone) {
+                        whenDone();
+                }
+
+        }).fail(function(jqXHR, status, error) {
+                alert(status + error);
+        });
+}
+
+// Remove this resource from the server
+CalendarResource.prototype.removeResource = function(whenDone) {
+        if (!this.url) {
+                if (whenDone) {
+                        whenDone();
+                }
+                return;
+        }
+        Ajax({
+                context : this,
+                url : joinURLs(gSession.host, this.url),
+                type : &quot;DELETE&quot;,
+        }).done(function(response) {
+                var index = this.calendar.resources.indexOf(this);
+                this.calendar.resources.splice(index, 1);
+                if (whenDone) {
+                        whenDone();
+                }
+        }).fail(function(jqXHR, status, error) {
+                alert(status + error);
+        });
+}
+
+// A generic container for an iCalendar component
+CalendarComponent = function(caldata, parent) {
+        this.data = (caldata instanceof jcal ? caldata : new jcal(caldata));
+        this.parent = parent;
+}
+
+// Maintain a registry of component types so the right class can be created when parsing
+CalendarComponent.createComponentType = {}
+
+CalendarComponent.registerComponentType = function(name, cls) {
+        CalendarComponent.createComponentType[name] = cls;
+}
+
+CalendarComponent.buildComponentType = function(caldata, parent) {
+        return new CalendarComponent.createComponentType[caldata.name()](caldata, parent);
+}
+
+CalendarComponent.prototype.duplicate = function(parent) {
+        if (parent === undefined) {
+                parent = this.parent;
+        }
+        return CalendarComponent.buildComponentType(this.data.duplicate(), parent);
+}
+
+CalendarComponent.prototype.toString = function() {
+        return this.data.toString();
+}
+
+// Tell component whether it has changed or not
+CalendarComponent.prototype.changed = function(value) {
+        if (value === undefined) {
+                return this.parent ? this.parent.changed() : false;
+        } else {
+                if (this.parent) {
+                        this.parent.changed(value);
+                }
+        }
+}
+
+CalendarComponent.prototype.uid = function() {
+        return this.data.getPropertyValue(&quot;uid&quot;);
+}
+
+// Indicate whether this object is owned by the current user.
+CalendarComponent.prototype.isOwned = function() {
+        return gSession.currentPrincipal.matchingAddress(this.organizer());
+}
+
+CalendarComponent.prototype.organizer = function() {
+        return this.data.getPropertyValue(&quot;organizer&quot;);
+}
+
+CalendarComponent.prototype.organizerDisplayName = function() {
+        return new CalendarUser(this.data.getProperty(&quot;organizer&quot;), this).nameOrAddress();
+}
+
+CalendarComponent.prototype.status = function() {
+        return this.data.getPropertyValue(&quot;status&quot;);
+}
+
+CalendarComponent.prototype.summary = function(value) {
+        if (value === undefined) {
+                return this.data.getPropertyValue(&quot;summary&quot;);
+        } else {
+                if (this.summary() != value) {
+                        this.data.updateProperty(&quot;summary&quot;, value);
+                        this.changed(true);
+                }
+        }
+}
+
+CalendarComponent.prototype.dtstart = function(value) {
+        if (value === undefined) {
+                return jcaldate.jCalTojsDate(this.data.getPropertyValue(&quot;dtstart&quot;));
+        } else {
+                if (this.dtstart() - value !== 0) {
+                        this.data.updateProperty(&quot;dtstart&quot;, jcaldate.jsDateTojCal(value), {}, &quot;date-time&quot;);
+                        this.changed(true);
+                }
+        }
+}
+
+CalendarComponent.prototype.dtend = function(value) {
+        if (value === undefined) {
+                var dtend = this.data.getPropertyValue(&quot;dtend&quot;);
+                if (dtend === null) {
+                        var offset = 0;
+                        var duration = this.data.getPropertyValue(&quot;duration&quot;);
+                        if (duration === null) {
+                                dtend = new Date(this.dtstart().getTime());
+                                dtend.setHours(0, 0, 0, 0);
+                                dtend.setDate(dtend.getDate() + 1);
+                        } else {
+                                offset = jcalduration.parseText(duration).getTotalSeconds() * 1000;
+                                dtend = new Date(this.dtstart().getTime() + offset);
+                        }
+                } 
+                return jcaldate.jCalTojsDate(dtend);
+        } else {
+                if (this.dtend() - value !== 0) {
+                        this.data.updateProperty(&quot;dtend&quot;, jcaldate.jsDateTojCal(value), {}, &quot;date-time&quot;);
+                        this.data.removeProperties(&quot;duration&quot;);
+                        this.changed(true);
+                }
+        }
+}
+
+CalendarComponent.prototype.pollitemid = function(value) {
+        if (value === undefined) {
+                return this.data.getPropertyValue(&quot;poll-item-id&quot;);
+        } else {
+                if (this.politemid() != value) {
+                        this.data.updateProperty(&quot;poll-item-id&quot;, value);
+                        this.changed(true);
+                }
+        }
+}
+
+CalendarComponent.prototype.voter_responses = function() {
+        var voter_results = {}
+        $.each(this.data.properties(&quot;voter&quot;), function(index, voter) {
+                voter_results[voter[3]] = parseInt(voter[1][&quot;response&quot;]);
+        });
+        return voter_results;
+}
+
+// Change active user's response to this event
+CalendarComponent.prototype.changeVoterResponse = function(response) {
+        if (response !== null) {
+                var matches = $.grep(this.data.properties(&quot;voter&quot;), function(voter, index) {
+                        return gSession.currentPrincipal.matchingAddress(voter[3]);
+                });
+                if (matches.length == 1) {
+                        new CalendarUser(matches[0], this).response(response.toString());
+                } else {
+                        this.data.newProperty(
+                                &quot;voter&quot;,
+                                gSession.currentPrincipal.defaultAddress(),
+                                { &quot;response&quot; : response.toString() },
+                                &quot;cal-address&quot;
+                        );
+                        this.changed(true);
+                }
+        } else {
+                this.data.removePropertiesMatchingValue(function(propdata) {
+                        return propdata[0] == &quot;voter&quot; &amp;&amp; gSession.currentPrincipal.matchingAddress(propdata[3]); 
+                });
+        }
+}
+
+// A container class for VCALENDAR objects
+CalendarObject = function(caldata) {
+        CalendarComponent.call(this, caldata, null);
+        this._changed = false;
+}
+
+CalendarObject.prototype = new CalendarComponent();
+CalendarObject.prototype.constructor = CalendarObject;
+CalendarComponent.registerComponentType(&quot;vcalendar&quot;, CalendarObject);
+
+// This is the top-level object for changing tracking
+CalendarObject.prototype.changed = function(value) {
+        if (value === undefined) {
+                return this._changed;
+        } else {
+                this._changed = value;
+        }
+}
+
+// Get the main component type as one of our model classes
+CalendarObject.prototype.mainComponent = function() {
+        var main = this.data.mainComponent();
+        return new CalendarComponent.buildComponentType(main, this);
+}
+
+// A container class for VPOLL objects
+CalendarPoll = function(caldata, parent) {
+        CalendarComponent.call(this, caldata, parent);
+}
+
+CalendarPoll.prototype = new CalendarComponent();
+CalendarPoll.prototype.constructor = CalendarPoll;
+CalendarComponent.registerComponentType(&quot;vpoll&quot;,  CalendarPoll);
+
+// Create a brand new poll, defaulting various properties
+CalendarPoll.newPoll = function(title) {
+        var calendar = jcal.newCalendar();
+        var vpoll = calendar.newComponent(&quot;vpoll&quot;, true);
+        vpoll.newProperty(&quot;summary&quot;, title);
+        vpoll.newProperty(&quot;poll-mode&quot;, &quot;BASIC&quot;);
+        vpoll.newProperty(&quot;poll-properties&quot;, [&quot;DTSTART&quot;,&quot;DTEND&quot;]);
+        vpoll.newProperty(
+                &quot;organizer&quot;,
+                gSession.currentPrincipal.defaultAddress(),
+                { &quot;cn&quot; : gSession.currentPrincipal.cn },
+                &quot;cal-address&quot;
+        );
+        vpoll.newProperty(
+                &quot;voter&quot;,
+                gSession.currentPrincipal.defaultAddress(),
+                {
+                        &quot;cn&quot; : gSession.currentPrincipal.cn,
+                        &quot;partstat&quot; : &quot;ACCEPTED&quot;
+                },
+                &quot;cal-address&quot;
+        );
+
+        return new CalendarObject(calendar);
+}
+
+// Whether or not current user can make changes (depends on their role as owner too)
+CalendarPoll.prototype.editable = function() {
+        var status = this.status();
+        return status ? status == &quot;IN-PROCESS&quot; : true;
+}
+
+CalendarComponent.prototype.pollwinner = function(value) {
+        if (value === undefined) {
+                return this.data.getPropertyValue(&quot;poll-winner&quot;);
+        } else {
+                if (this.pollwinner() != value) {
+                        this.data.updateProperty(&quot;poll-winner&quot;, value);
+                        this.changed(true);
+                }
+        }
+}
+
+CalendarComponent.prototype.ispollwinner = function() {
+        var pollid = this.pollitemid();
+        return pollid === undefined ? false : pollid == this.parent.pollwinner();
+}
+
+// Get an array of child VEVENTs in the VPOLL
+CalendarPoll.prototype.events = function() {
+        var this_vpoll = this;
+        return $.map(this.data.components(&quot;vevent&quot;), function(event, index) {
+                return new CalendarEvent(event, this_vpoll);
+        });
+}
+
+// Add a new VEVENT to the VPOLL
+CalendarPoll.prototype.addEvent = function(dtstart, dtend) {
+        this.changed(true);
+        var vevent = this.data.newComponent(&quot;vevent&quot;, true);
+        vevent.newProperty(&quot;dtstart&quot;, jcaldate.jsDateTojCal(dtstart), {}, &quot;date-time&quot;);
+        vevent.newProperty(&quot;dtend&quot;, jcaldate.jsDateTojCal(dtend), {}, &quot;date-time&quot;);
+        vevent.newProperty(&quot;summary&quot;, this.summary());
+        vevent.newProperty(&quot;poll-item-id&quot;, (this.data.components(&quot;vevent&quot;).length).toString());
+        vevent.newProperty(
+                &quot;voter&quot;,
+                this.organizer(),
+                {&quot;response&quot; : &quot;80&quot;},
+                &quot;cal-address&quot;
+        );
+        return new CalendarEvent(vevent, this);
+}
+
+// Get an array of voters in the VPOLL
+CalendarPoll.prototype.voters = function() {
+        var this_vpoll = this;
+        return $.map(this.data.properties(&quot;voter&quot;), function(voter) {
+                return new CalendarUser(voter, this_vpoll);
+        });
+}
+
+// Add a voter to the VPOLL
+CalendarPoll.prototype.addVoter = function() {
+        this.changed(true);
+        return new CalendarUser(this.data.newProperty(&quot;voter&quot;, &quot;&quot;, {}, &quot;cal-address&quot;), this);
+}
+
+// Mark current user as accepted
+CalendarPoll.prototype.acceptInvite = function() {
+        if (!this.isOwned()) {
+                var voters = $.grep(this.data.properties(&quot;voter&quot;), function(voter) {
+                        return gSession.currentPrincipal.matchingAddress(voter[3]);
+                });
+                $.each(voters, function(index, voter) {
+                        voter[1][&quot;partstat&quot;] = &quot;ACCEPTED&quot;;
+                        delete voter[1][&quot;rsvp&quot;];
+                })
+        }
+}
+
+// An actual VEVENT object we can manipulate
+CalendarEvent = function(caldata, parent) {
+        CalendarComponent.call(this, caldata, parent);
+}
+
+CalendarEvent.prototype = new CalendarComponent();
+CalendarEvent.prototype.constructor = CalendarEvent;
+CalendarComponent.registerComponentType(&quot;vevent&quot;, CalendarEvent);
+
+// Create this event as the poll winner
+CalendarEvent.prototype.pickAsWinner = function() {
+        
+        // Adjust VPOLL to mark winner and set status
+        var vpoll = this.parent;
+        vpoll.data.updateProperty(&quot;status&quot;, &quot;CONFIRMED&quot;);
+        vpoll.data.newProperty(&quot;poll-winner&quot;, this.pollitemid());
+        vpoll.changed(true);
+
+        // Create the new event resource with voters mapped to attendees
+        var calendar = new CalendarObject(jcal.newCalendar());
+        var vevent = calendar.data.newComponent(&quot;vevent&quot;, true);
+        vevent.updateProperty(&quot;uid&quot;, this.uid());
+        vevent.copyProperty(&quot;summary&quot;, this.data);
+        vevent.copyProperty(&quot;dtstart&quot;, this.data);
+        vevent.copyProperty(&quot;dtend&quot;, this.data);
+        vevent.copyProperty(&quot;organizer&quot;, vpoll.data);
+        $.each(vpoll.data.properties(&quot;voter&quot;), function(index, voter) {
+                var attendee = vevent.newProperty(
+                        &quot;attendee&quot;,
+                        voter[3],
+                        {},
+                        &quot;cal-address&quot;
+                );
+                $.each(voter[1], function(key, value) {
+                        if (key == &quot;partstat&quot;) {
+                                value = gSession.currentPrincipal.matchingAddress(voter[3]) ? &quot;ACCEPTED&quot; : &quot;NEEDS-ACTION&quot;;
+                        }
+                        attendee[1][key] = value
+                });
+        });
+        calendar.changed(true);
+
+        return new CalendarResource(gSession.currentPrincipal.event_calendars[0], null, null, calendar);
+}
+
+// An iCalendar calendar user (ORGANIZER/ATTENDEE/VOTER) property
+CalendarUser = function(caldata, parent) {
+        this.data = caldata;
+        this.parent = parent;
+}
+
+// Get or set the user name and/or cu-address
+CalendarUser.prototype.addressDescription = function(value) {
+        if (value === undefined) {
+                var cn = this.data[1][&quot;cn&quot;] ? this.data[1][&quot;cn&quot;] + &quot; &quot; : &quot;&quot;;
+                return addressDescription(cn, this.data[3]);
+        } else {
+                if (this.addressDescription() != value) {
+                        var splits = splitAddressDescription(value);
+                        if (splits[0]) {
+                                this.data[1][&quot;cn&quot;] = splits[0];
+                        } else {
+                                delete this.data[1][&quot;cn&quot;];
+                        }
+                        this.data[3] = splits[1];
+                        this.parent.changed(true);
+                }
+        }
+}
+
+// Get a suitable display string for this user
+CalendarUser.prototype.nameOrAddress = function() {
+        return this.data[1][&quot;cn&quot;] ? this.data[1][&quot;cn&quot;] : this.data[3];
+}
+
+CalendarUser.prototype.cuaddr = function() {
+        return this.data[3];
+}
+
+// Get or set the voter response
+CalendarUser.prototype.response = function(value) {
+        if (value === undefined) {
+                return this.data[1][&quot;response&quot;];
+        } else {
+                if (this.response != value) {
+                        this.data[1][&quot;response&quot;] = value;
+                        this.parent.changed(true);
+                }
+        }
+}
</ins></span></pre></div>
<a id="CalendarServertrunkcontribwebpollwebappjsjcaljs"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/contrib/webpoll/webapp/js/jcal.js (0 => 11917)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/contrib/webpoll/webapp/js/jcal.js                                (rev 0)
+++ CalendarServer/trunk/contrib/webpoll/webapp/js/jcal.js        2013-11-08 21:59:31 UTC (rev 11917)
</span><span class="lines">@@ -0,0 +1,478 @@
</span><ins>+/**
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+*/
+
+gProdID = &quot;-//calendarserver.org//jcal v1//EN&quot;;
+
+jcalparser = {
+        PARSER_ALLOW : 0,         // Pass the &quot;suspect&quot; data through to the object model
+        PARSER_IGNORE : 1,         // Ignore the &quot;suspect&quot; data
+        PARSER_FIX: 2,                 // Fix (or if not possible ignore) the &quot;suspect&quot; data
+        PARSER_RAISE: 3         // Raise an exception
+}
+
+// Some clients escape &quot;:&quot; - fix
+jcalparser.INVALID_COLON_ESCAPE_SEQUENCE = jcalparser.PARSER_FIX;
+
+// Other escape sequences - raise
+jcalparser.INVALID_ESCAPE_SEQUENCES = jcalparser.PARSER_RAISE;
+
+// Some client generate empty lines in the body of the data
+jcalparser.BLANK_LINES_IN_DATA = jcalparser.PARSER_FIX;
+
+// Some clients still generate vCard 2 parameter syntax
+jcalparser.VCARD_2_NO_PARAMETER_VALUES = jcalparser.PARSER_ALLOW;
+
+// Use this to fix v2 BASE64 to v3 ENCODING=b - only PARSER_FIX or PARSER_ALLOW
+jcalparser.VCARD_2_BASE64 = jcalparser.PARSER_FIX;
+
+// Allow DATE values when DATETIME specified (and vice versa)
+jcalparser.INVALID_DATETIME_VALUE = jcalparser.PARSER_FIX;
+
+// Allow slightly invalid DURATION values
+jcalparser.INVALID_DURATION_VALUE = jcalparser.PARSER_FIX;
+
+// Truncate over long ADR and N values
+jcalparser.INVALID_ADR_N_VALUES = jcalparser.PARSER_FIX;
+
+// REQUEST-STATUS values with \; as the first separator or single element
+jcalparser.INVALID_REQUEST_STATUS_VALUE = jcalparser.PARSER_FIX;
+
+// Remove \-escaping in URI values when parsing - only PARSER_FIX or PARSER_ALLOW
+jcalparser.BACKSLASH_IN_URI_VALUE = jcalparser.PARSER_FIX;
+
+
+jcal = function(caldata) {
+        this.caldata = caldata;
+}
+
+jcal.newCalendar = function() {
+        var calendar = new jcal([&quot;vcalendar&quot;, [], []]);
+        calendar.newProperty(&quot;version&quot;, &quot;2.0&quot;);
+        calendar.newProperty(&quot;prodid&quot;, gProdID);
+        return calendar;
+}
+
+jcal.fromString = function(data) {
+        return new jcal($.parseJSON(data));
+}
+
+jcal.prototype.toString = function() {
+        return JSON.stringify(this.caldata);
+}
+
+jcal.prototype.duplicate = function() {
+        return new jcal($.parseJSON(this.toString()));
+}
+
+jcal.prototype.name = function() {
+        return this.caldata[0];
+}
+
+// Return the primary (master) component
+// TODO: currently ignores recurrence - i.e. assumes one top-level component only
+jcal.prototype.mainComponent = function() {
+        var results = $.grep(this.caldata[2], function(component, index) {
+                return (component[0] != &quot;vtimezone&quot;);
+        });
+        return (results.length == 1) ? new jcal(results[0]) : null;
+}
+
+jcal.prototype.newComponent = function(name, defaultProperties) {
+        var jcomp = new jcal([name.toLowerCase(), [], []]);
+        this.caldata[2].push(jcomp.caldata);
+        if (defaultProperties) {
+                // Add UID and DTSTAMP
+                jcomp.newProperty(&quot;uid&quot;, generateUUID());
+                jcomp.newProperty(&quot;dtstamp&quot;, new jcaldate().toString(), {}, &quot;date-time&quot;);
+        }
+        return jcomp;
+}
+
+jcal.prototype.addComponent = function(component) {
+        this.caldata[2].push(component.caldata);
+        return component;
+}
+
+// Get one component 
+jcal.prototype.getComponent = function(name) {
+        name = name.toLowerCase();
+        var results = $.grep(this.caldata[2], function(component, index) {
+                return (component[0] == name);
+        });
+        return (results.length == 1) ? new jcal(results[0]) : null;
+}
+
+// Get all matching components 
+jcal.prototype.components = function(name) {
+        name = name.toLowerCase();
+        var results = $.grep(this.caldata[2], function(component, index) {
+                return (component[0] == name);
+        });
+        return $.map(results, function(component, index) {
+                return new jcal(component);
+        });
+}
+
+// Remove all matching components
+jcal.prototype.removeComponents = function(name) {
+        name = name.toLowerCase();
+        this.caldata[2] = $.grep(this.caldata[2], function(comp, index) {
+                return comp[0] != name;
+        });
+}
+
+jcal.prototype.newProperty = function(name, value, params, value_type) {
+        var prop = [name.toLowerCase(), params === undefined ? {} : params, value_type == undefined ? &quot;text&quot; : value_type];
+        if (value instanceof Array) {
+                $.each(value, function(index, single) {
+                        prop.push(single);
+                });
+        } else {
+                prop.push(value);
+        }
+        this.caldata[1].push(prop);
+        return prop;
+}
+
+jcal.prototype.copyProperty = function(name, component) {
+        var propdata = component.getProperty(name);
+        this.caldata[1].push([propdata[0], propdata[1], propdata[2], propdata[3]]);
+        return propdata;
+}
+
+jcal.prototype.hasProperty = function(name) {
+        var result = false;
+        name = name.toLowerCase();
+        $.each(this.caldata[1], function(index, property) {
+                if (property[0] == name) {
+                        result = true;
+                        return false;
+                }
+        });
+        return result;
+}
+
+jcal.prototype.getProperty = function(name) {
+        var result = null;
+        name = name.toLowerCase();
+        $.each(this.caldata[1], function(index, property) {
+                if (property[0] == name) {
+                        result = property;
+                        return false;
+                }
+        });
+        return result;
+}
+
+jcal.prototype.getPropertyValue = function(name) {
+        var result = null;
+        name = name.toLowerCase();
+        $.each(this.caldata[1], function(index, propdata) {
+                if (propdata[0] == name) {
+                        result = propdata[3];
+                        return false;
+                }
+        });
+        return result;
+}
+
+jcal.prototype.updateProperty = function(name, value, params, value_type) {
+        if (params === undefined) {
+                params = {};
+        }
+        if (value_type === undefined) {
+                value_type = &quot;text&quot;;
+        }
+        var props = this.properties(name);
+        if (props.length == 1) {
+                props[0][1] = params;
+                props[0][2] = value_type;
+                props[0][3] = value;
+                
+                return props[0];
+        } else if (props.length == 0) {
+                return this.newProperty(name, value, params, value_type);
+        }
+}
+
+jcal.prototype.properties = function(name) {
+        return $.grep(this.caldata[1], function(propdata, index) {
+                return propdata[0] == name;
+        });
+}
+
+jcal.prototype.removeProperties = function(name) {
+        name = name.toLowerCase();
+        this.caldata[1] = $.grep(this.caldata[1], function(propdata, index) {
+                return propdata[0] != name;
+        });
+}
+
+// Remove properties for which test() returns true
+jcal.prototype.removePropertiesMatching = function(test) {
+        name = name.toLowerCase();
+        this.caldata[1] = $.grep(this.caldata[1], function(propdata, index) {
+                return !test(propdata);
+        });
+}
+
+// Date/time utility functions
+jcaldate = function() {
+        this.date = new Date();
+        this.tzid = &quot;utc&quot;;
+}
+
+jcaldate.jsDateTojCal = function(date) {
+        return date.toISOString().substr(0, 19) + &quot;Z&quot;;
+}
+
+jcaldate.jsDateToiCal = function(date) {
+        return jcaldate.jsDateTojCal(date).replace(/\-/g, &quot;&quot;).replace(/\:/g, &quot;&quot;);
+}
+
+jcaldate.jCalTojsDate = function(value) {
+        var result = new Date(value);
+        result.setMilliseconds(0);
+        return result;
+}
+
+jcaldate.prototype.toString = function() {
+        return this.date.toISOString().substr(0, 19) + &quot;Z&quot;;
+}
+
+// Duration utility functions
+jcalduration = function(duration) {
+        
+        if (duration === undefined) {
+                this.mForward = true
+        
+                this.mWeeks = 0
+                this.mDays = 0
+        
+                this.mHours = 0
+                this.mMinutes = 0
+                this.mSeconds = 0
+        } else {
+                this.setDuration(duration);                
+        }
+}
+
+jcalduration.parseText = function(data) {
+        var duration = new jcalduration();
+        duration.parse(data);
+        return duration;
+}
+
+jcalduration.prototype.getTotalSeconds = function() {
+        return (this.mForward ? 1 : -1) * (this.mSeconds + (this.mMinutes + (this.mHours + (this.mDays + (this.mWeeks * 7)) * 24) * 60) * 60);
+}
+
+jcalduration.prototype.setDuration = function(seconds) {
+        this.mForward = seconds &gt;= 0;
+
+        var remainder = seconds;
+        if (remainder &lt; 0) {
+                remainder = -remainder;
+        }
+
+        // Is it an exact number of weeks - if so use the weeks value, otherwise
+        // days, hours, minutes, seconds
+        if (remainder % (7 * 24 * 60 * 60) == 0) {
+                this.mWeeks = remainder / (7 * 24 * 60 * 60);
+                this.mDays = 0;
+
+                this.mHours = 0;
+                this.mMinutes = 0;
+                this.mSeconds = 0;
+        } else {
+                this.mSeconds = remainder % 60;
+                remainder -= this.mSeconds;
+                remainder /= 60;
+
+                this.mMinutes = remainder % 60;
+                remainder -= this.mMinutes;
+                remainder /= 60;
+
+                this.mHours = remainder % 24;
+                remainder -= this.mHours;
+
+                this.mDays = remainder / 24;
+
+                this.mWeeks = 0;
+        }
+}
+
+jcalduration.prototype.parse = function(data) {
+        // parse format ([+]/-) &quot;P&quot; (dur-date / dur-time / dur-week)
+        try {
+                var offset = 0;
+                var maxoffset = data.length;
+        
+                // Look for +/-
+                this.mForward = true;
+                if (data[offset] == '+') {
+                        this.mForward = true;
+                        offset += 1;
+                } else if (data[offset] == '-') {
+                        this.mForward = false;
+                        offset += 1;
+                }
+        
+                // Must have a 'P'
+                if (data[offset] != &quot;P&quot;)
+                        throw &quot;Invalid duration&quot;;
+                offset += 1;
+        
+                // Look for time
+                if (data[offset] != &quot;T&quot;) {
+                        // Must have a number
+                        var strnum = data.strtoul(offset);
+                        var num = strnum.num;
+                        offset = strnum.offset;
+        
+                        // Now look at character
+                        if (data[offset] == &quot;W&quot;) {
+                                // Have a number of weeks
+                                this.mWeeks = num;
+                                offset += 1;
+        
+                                // There cannot be anything else after this so just exit
+                                if (offset != maxoffset) {
+                                        if (ParserContext.INVALID_DURATION_VALUE != ParserContext.PARSER_RAISE)
+                                                return;
+                                        throw &quot;Invalid duration&quot;;
+                                }
+                                return;
+                        } else if (data[offset] == &quot;D&quot;) {
+                                // Have a number of days
+                                this.mDays = num;
+                                offset += 1;
+        
+                                // Look for more data - exit if none
+                                if (offset == maxoffset)
+                                        return;
+        
+                                // Look for time - exit if none
+                                if (data[offset] != &quot;T&quot;)
+                                        throw &quot;Invalid duration&quot;;
+                        } else {
+                                // Error in format
+                                throw &quot;Invalid duration&quot;;
+                        }
+                }
+        
+                // Have time
+                offset += 1;
+        
+                // Strictly speaking T must always be followed by time values, but some clients
+                // send T with no additional text
+                if (offset == maxoffset) {
+                        if (jcalparser.INVALID_DURATION_VALUE == jcalparser.PARSER_RAISE)
+                                throw &quot;Invalid duration&quot;;
+                        else
+                                return;
+                }
+                var strnum = data.strtoul(offset);
+                var num = strnum.num;
+                offset = strnum.offset;
+        
+                // Look for hour
+                if (data[offset] == &quot;H&quot;) {
+                        // Get hours
+                        this.mHours = num;
+                        offset += 1;
+        
+                        // Look for more data - exit if none
+                        if (offset == maxoffset)
+                                return;
+        
+                        // Parse the next number
+                        strnum = data.strtoul(offset);
+                        num = strnum.num;
+                        offset = strnum.offset;
+                }
+        
+                // Look for minute
+                if (data[offset] == &quot;M&quot;) {
+                        // Get hours
+                        this.mMinutes = num;
+                        offset += 1;
+        
+                        // Look for more data - exit if none
+                        if (offset == maxoffset)
+                                return;
+        
+                        // Parse the next number
+                        strnum = data.strtoul(offset);
+                        num = strnum.num;
+                        offset = strnum.offset;
+                }
+        
+                // Look for seconds
+                if (data[offset] == &quot;S&quot;) {
+                        // Get hours
+                        this.mSeconds = num;
+                        offset += 1;
+        
+                        // No more data - exit
+                        if (offset == maxoffset)
+                                return;
+                }
+                
+                throw &quot;Invalid duration&quot;;
+        } catch(err) {
+                throw &quot;Invalid duration&quot;;
+        }
+}
+
+jcalduration.prototype.generate = function(self, os) {
+        var result = &quot;&quot;;
+
+        if (!this.mForward &amp;&amp; (this.mWeeks || this.mDays || this.mHours || this.mMinutes || this.mSeconds)) {
+                result += &quot;-&quot;;
+        }
+        result += &quot;P&quot;;
+
+        if (this.mWeeks != 0) {
+                result += this.mWeeks + &quot;W&quot;;
+        } else {
+                if (this.mDays != 0) {
+                        result += this.mDays + &quot;D&quot;;
+                }
+
+                if (this.mHours != 0 || this.mMinutes != 0 || this.mSeconds != 0) {
+                        result += &quot;T&quot;;
+
+                        if (this.mHours != 0) {
+                                result += this.mHours + &quot;H&quot;;
+                        }
+
+                        if ((this.mMinutes != 0) || ((this.mHours != 0) &amp;&amp; (this.mSeconds != 0))) {
+                                result += this.mMinutes + &quot;M&quot;;
+                        }
+
+                        if (this.mSeconds != 0) {
+                                result += this.mSeconds + &quot;S&quot;;
+                        }
+                } else if (this.mDays == 0) {
+                        result += &quot;T0S&quot;;
+                }
+        }
+        
+        return result;
+}
</ins></span></pre></div>
<a id="CalendarServertrunkcontribwebpollwebappjsutilsjs"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/contrib/webpoll/webapp/js/utils.js (0 => 11917)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/contrib/webpoll/webapp/js/utils.js                                (rev 0)
+++ CalendarServer/trunk/contrib/webpoll/webapp/js/utils.js        2013-11-08 21:59:31 UTC (rev 11917)
</span><span class="lines">@@ -0,0 +1,211 @@
</span><ins>+/**
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+*/
+
+/**
+ * Utility classes.
+ */
+
+// XML processing utilities
+// Make it easier to use namespaces by allowing the following &quot;NS:&quot; prefixes on
+// element names
+var gNamespaceShortcuts = {
+        &quot;D&quot; : &quot;DAV:&quot;,
+        &quot;C&quot; : &quot;urn:ietf:params:xml:ns:caldav&quot;,
+        &quot;CS&quot; : &quot;http://calendarserver.org/ns/&quot;
+};
+
+// Add a shortcut namespace for use in an xmlns map
+function addNamespace(shortcut, xmlnsmap) {
+        xmlnsmap[gNamespaceShortcuts[shortcut]] = shortcut;
+}
+
+function buildXMLNS(xmlnsmap) {
+        var xmlnstr = &quot;&quot;;
+        $.each(xmlnsmap, function(ns, nsprefix) {
+                xmlnstr += ' xmlns:' + nsprefix + '=&quot;' + ns + '&quot;';
+        });
+        return xmlnstr;
+}
+
+function addElements(elements, xmlnsmap) {
+        var propstr = &quot;&quot;;
+        $.each(elements, function(index, element) {
+                var segments = element.split(&quot;:&quot;);
+                addNamespace(segments[0], xmlnsmap);
+                propstr += '&lt;' + element + ' /&gt;';
+        });
+        return propstr;
+}
+
+// Find XML elements matching the specified xpath
+function findElementPath(node, path) {
+        return findElementPathSegments(node, path.split(&quot;/&quot;));
+}
+
+// Find XML elements matching the specified path segments
+function findElementPathSegments(root, segments) {
+        var elements = findElementNS(root, segments[0]);
+        if (segments.length == 1) {
+                return elements;
+        }
+        var results = [];
+        $.each(elements, function(index, name) {
+                var next = findElementPathSegments($(this), segments.slice(1));
+                $.each(next, function(index, item) {
+                        results.push(item);
+                });
+        });
+        return results;
+}
+
+// Find immediate children of node matching the XML {NS}name
+function findElementNS(node, nsname) {
+        var segments = nsname.split(&quot;:&quot;);
+        var namespace = gNamespaceShortcuts[segments[0]];
+        var name = segments[1];
+        var results = [];
+        node.children(name).each(function() {
+                if (this.namespaceURI == namespace) {
+                        results.push($(this));
+                }
+        });
+        return results;
+}
+
+// Get text of target element from an xpath
+function getElementText(node, path) {
+        var items = findElementPath(node, path);
+        return (items.length == 1) ? items[0].text() : null;
+}
+
+// Check for the existence of an element
+function hasElementPath(node, path) {
+        var elements = findElementPath(node, path);
+        return elements.length != 0;
+}
+
+function xmlEncode(text)
+{
+        return text.replace(/&amp;(?!\w+([;\s]|$))/g, &quot;&amp;amp;&quot;).replace(/&lt;/g, &quot;&amp;lt;&quot;).replace(/&gt;/g, &quot;&amp;gt;&quot;);
+}
+
+// URL helpers
+
+// Removing any trailing slash from a URL
+function removeTrailingSlash(url) {
+        return (url[url.length - 1] == &quot;/&quot;) ? url.substr(0, url.length - 1) : url;
+}
+
+// Removing any trailing slash from a URL
+function removeLeadingSlash(url) {
+        return (url[0] == &quot;/&quot;) ? url = url.substr(1) : url;
+}
+
+// Compare two URLs ignoring trailing slash
+function compareURLs(url1, url2) {
+        return removeTrailingSlash(url1) == removeTrailingSlash(url2);
+}
+
+// Join two URLs
+function joinURLs(url1, url2) {
+        return removeTrailingSlash(url1) + &quot;/&quot; + removeLeadingSlash(url2);
+}
+
+// Get last path segment
+function basenameURL(url) {
+        return removeTrailingSlash(url).split(&quot;/&quot;).pop();
+}
+
+// UUID
+
+function generateUUID() {
+        var result = &quot;&quot;;
+        for ( var i = 0; i &lt; 32; i++) {
+                if (i == 8 || i == 12 || i == 16 || i == 20)
+                        result = result + '-';
+                result += Math.floor(Math.random() * 16).toString(16).toUpperCase();
+        }
+        return result;
+}
+
+// Addresses
+
+function addressDescription(cn, addr) {
+        return addr ? (cn ? cn + &quot; &quot; : &quot;&quot;) + &quot;&lt;&quot; + addr + &quot;&gt;&quot; : &quot;&quot;;
+}
+
+function splitAddressDescription(desc) {
+        results = [&quot;&quot;, &quot;&quot;];
+        if (desc.indexOf(&quot;&lt;&quot;) == -1) {
+                results[1] = desc;
+        } else {
+                var splits = desc.split(&quot;&lt;&quot;);
+                results[0] = splits[0].substr(0, splits[0].length - 1);
+                results[1] = splits[1].substr(0, splits[1].length - 1);
+        }
+        
+        return results;
+}
+
+// JSString extensions
+
+if (typeof String.prototype.startsWith != 'function') {
+        String.prototype.startsWith = function(str) {
+                return this.slice(0, str.length) == str;
+        };
+}
+if (typeof String.prototype.endsWith != 'function') {
+        String.prototype.endsWith = function(str) {
+                return this.slice(-str.length) == str;
+        };
+}
+
+if (typeof String.prototype.strtoul != 'function') {
+
+        String.prototype.strtoul = function(offset) {
+                if (offset === undefined) {
+                        offset = 0;
+                }
+                var matches = this.substring(offset).match(/^[0-9]+/);
+                if (matches.length == 1) {
+                        return {
+                                num : parseInt(matches[0]),
+                                offset : offset + matches[0].length
+                        }
+                } else {
+                        throw &quot;ValueError&quot;;
+                }
+        }
+}
+
+// JSArray extensions
+
+if (typeof Array.prototype.average != 'function') {
+        Array.prototype.average = function() {
+                return this.sum() / this.length;
+        };
+}
+if (typeof Array.prototype.sum != 'function') {
+        Array.prototype.sum = function() {
+                var result = 0;
+                $.each(this, function(index, num) {
+                        result += num;
+                });
+                return result;
+        };
+}
</ins></span></pre></div>
<a id="CalendarServertrunkcontribwebpollwebappjswebpolljs"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/contrib/webpoll/webapp/js/webpoll.js (0 => 11917)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/contrib/webpoll/webapp/js/webpoll.js                                (rev 0)
+++ CalendarServer/trunk/contrib/webpoll/webapp/js/webpoll.js        2013-11-08 21:59:31 UTC (rev 11917)
</span><span class="lines">@@ -0,0 +1,956 @@
</span><ins>+/**
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+*/
+
+// Globals
+var gSession = null;
+var gViewController = null;
+
+// Page load
+$(function() {
+
+        $(&quot;#progressbar&quot;).progressbar({
+                value : false
+        });
+        showLoading(true);
+
+    var params = {};
+    var ps = window.location.search.split(/\?|&amp;/);
+    for (var i = 0; i &lt; ps.length; i++) {
+          if (ps[i]) {
+                var p = ps[i].split(/=/);
+                params[p[0]] = p[1];
+          }
+    }
+
+    // Setup CalDAV session
+        gSession = new CalDAVSession(params.user);
+        gSession.init(function() {
+                $(&quot;#title&quot;).text($(&quot;#title&quot;).text() + &quot; for User: &quot; + gSession.currentPrincipal.cn);
+                gViewController.refreshed();
+        });
+
+        gViewController = new ViewController(gSession);
+});
+
+function showLoading(visible) {
+        if (visible) {
+                $(&quot;#progressbar&quot;).progressbar(&quot;enable&quot;);
+                $(&quot;#loading&quot;).show();
+        } else {
+                $(&quot;#progressbar&quot;).progressbar(&quot;disable&quot;);
+                $(&quot;#loading&quot;).hide();
+        }
+}
+
+// Handles all the view interactions
+ViewController = function(session) {
+        this.session = session;
+        this.ownedPolls = new PollList($(&quot;#sidebar-owned&quot;), $(&quot;#sidebar-new-poll-count&quot;));
+        this.voterPolls = new PollList($(&quot;#sidebar-voter&quot;), $(&quot;#sidebar-vote-poll-count&quot;));
+        this.activePoll = null;
+        this.isNewPoll = null;
+
+        this.init();
+}
+
+// Setup all the parts of the view
+ViewController.prototype.init = function() {
+        // Setup title area
+
+        var view = this;
+
+        // Setup sidebar UI widgets
+        $(&quot;#sidebar&quot;).accordion({
+                heightStyle : &quot;content&quot;
+        });
+        $(&quot;#sidebar-owned&quot;).menu({
+                select : function(event, ui) {
+                        view.clickSelectPoll(event, ui, true);
+                }
+        });
+        $(&quot;#sidebar-new-poll&quot;).button({
+                icons : {
+                        primary : &quot;ui-icon-plusthick&quot;
+                }
+        }).click(function() {
+                view.clickAddPoll();
+        });
+
+        $(&quot;#sidebar-voter&quot;).menu({
+                select : function(event, ui) {
+                        view.clickSelectPoll(event, ui, false);
+                }
+        });
+
+        $(&quot;#refresh-btn&quot;).button({
+                icons : {
+                        primary : &quot;ui-icon-refresh&quot;
+                }
+        }).click(function() {
+                view.clickRefresh();
+        });
+
+
+        // Detail Panel
+        this.editSetVisible(false);
+        $(&quot;#editpoll-title-edit&quot;).focus(function() {
+                $(this).select();
+        });
+        $(&quot;#editpoll-tabs&quot;).tabs({
+                beforeActivate : function(event, ui) {
+                        view.showResults(event, ui);
+                }
+        });
+        $(&quot;#editpoll-save&quot;).button({
+                icons : {
+                        primary : &quot;ui-icon-check&quot;
+                }
+        }).click(function() {
+                view.clickPollSave();
+        });
+        $(&quot;#editpoll-cancel&quot;).button({
+                icons : {
+                        primary : &quot;ui-icon-close&quot;
+                }
+        }).click(function() {
+                view.clickPollCancel();
+        });
+        $(&quot;#editpoll-done&quot;).button({
+                icons : {
+                        primary : &quot;ui-icon-arrowreturnthick-1-w&quot;
+                }
+        }).click(function() {
+                view.clickPollCancel();
+        });
+        $(&quot;#editpoll-delete&quot;).button({
+                icons : {
+                        primary : &quot;ui-icon-trash&quot;
+                }
+        }).click(function() {
+                view.clickPollDelete();
+        });
+        $(&quot;#editpoll-autofill&quot;).button({
+                icons : {
+                        primary : &quot;ui-icon-gear&quot;
+                }
+        }).click(function() {
+                view.clickPollAutofill();
+        });
+        $(&quot;#editpoll-addevent&quot;).button({
+                icons : {
+                        primary : &quot;ui-icon-plus&quot;
+                }
+        }).click(function() {
+                view.clickAddEvent();
+        });
+        $(&quot;#editpoll-addvoter&quot;).button({
+                icons : {
+                        primary : &quot;ui-icon-plus&quot;
+                }
+        }).click(function() {
+                view.clickAddVoter();
+        });
+        
+        $(&quot;#editpoll-autofill&quot;).hide();
+        $(&quot;#response-key&quot;).hide();
+        $(&quot;#response-menu&quot;).menu();
+}
+
+// Add a poll to the UI
+ViewController.prototype.addPoll = function(poll) {
+        if (poll.owned) {
+                this.ownedPolls.addPoll(poll)                        
+        } else {
+                this.voterPolls.addPoll(poll)                        
+        }
+}
+
+// Switching away from active poll
+ViewController.prototype.aboutToClosePoll = function() {
+        if (this.activePoll &amp;&amp; this.activePoll.editing_poll.changed()) {
+                alert(&quot;Save or cancel the current poll changes first&quot;);
+                return false;
+        } else {
+                return true;
+        }
+}
+
+// Refresh the side bar - try to preserve currently selected item
+ViewController.prototype.clickRefresh = function() {
+        if (!this.aboutToClosePoll()) {
+                return;
+        }
+
+        var currentUID = this.activePoll ? this.activePoll.editing_poll.uid() : null;
+        var active_tab = $(&quot;#editpoll-tabs&quot;).tabs(&quot;option&quot;, &quot;active&quot;);
+        if (this.activePoll) {
+                this.clickPollCancel();
+        }
+        showLoading(true);
+        this.ownedPolls.clearPolls();
+        this.voterPolls.clearPolls();
+        var this_view = this;
+        this.session.currentPrincipal.refresh(function() {
+                this_view.refreshed();
+                if (currentUID) {
+                        this_view.selectPollByUID(currentUID);
+                        $(&quot;#editpoll-tabs&quot;).tabs(&quot;option&quot;, &quot;active&quot;, active_tab);
+                        if (active_tab == 2) {
+                                this.activePoll.buildResults();
+                        }
+                }
+        });
+}
+
+// Add poll button clicked
+ViewController.prototype.clickAddPoll = function() {
+        if (!this.aboutToClosePoll()) {
+                return;
+        }
+
+        // Make sure edit panel is visible
+        this.activatePoll(new Poll(CalendarResource.newPoll(&quot;New Poll&quot;)));
+        this.isNewPoll = true;
+        $(&quot;#editpoll-title-edit&quot;).focus();
+}
+
+// A poll was selected
+ViewController.prototype.clickSelectPoll = function(event, ui, owner) {
+        if (!this.aboutToClosePoll()) {
+                return;
+        }
+
+        this.selectPoll(ui.item.index(), owner);
+}
+
+// Select a poll from the list based on its UID
+ViewController.prototype.selectPollByUID = function(uid) {
+        var result = this.ownedPolls.indexOfPollUID(uid);
+        if (result !== null) {
+                this.selectPoll(result, true);
+                return;
+        }
+        result = this.voterPolls.indexOfPollUID(uid);
+        if (result !== null) {
+                this.selectPoll(result, false);
+                return;
+        }
+}
+
+//A poll was selected
+ViewController.prototype.selectPoll = function(index, owner) {
+
+        // Make sure edit panel is visible
+        this.activatePoll(owner ? this.ownedPolls.polls[index] : this.voterPolls.polls[index]);
+        if (owner) {
+                $(&quot;#editpoll-title-edit&quot;).focus();
+        }
+}
+
+// Activate specified poll
+ViewController.prototype.activatePoll = function(poll) {
+        this.activePoll = poll;
+        this.activePoll.setPanel();
+        this.isNewPoll = false;
+        this.editSetVisible(true);
+}
+
+// Save button clicked
+ViewController.prototype.clickPollSave = function() {
+
+        // TODO: Actually save it to the server
+
+        this.activePoll.getPanel();
+        if (this.isNewPoll) {
+                this.ownedPolls.newPoll(this.activePoll);
+        } else {
+                this.activePoll.list.changePoll(this.activePoll);
+        }
+}
+
+// Cancel button clicked
+ViewController.prototype.clickPollCancel = function() {
+
+        // Make sure edit panel is visible
+        this.activePoll.closed();
+        this.activePoll = null;
+        this.isNewPoll = null;
+        this.editSetVisible(false);
+}
+
+// Delete button clicked
+ViewController.prototype.clickPollDelete = function() {
+
+        // TODO: Actually delete it on the server
+
+        this.activePoll.list.removePoll(this.activePoll);
+
+        // Make sure edit panel is visible
+        this.activePoll = null;
+        this.isNewPoll = null;
+        this.editSetVisible(false);
+}
+
+// Autofill button clicked
+ViewController.prototype.clickPollAutofill = function() {
+        this.activePoll.autoFill();
+}
+
+// Add event button clicked
+ViewController.prototype.clickAddEvent = function() {
+        this.activePoll.addEvent();
+}
+
+// Add voter button clicked
+ViewController.prototype.clickAddVoter = function() {
+        var panel = this.activePoll.addVoter();
+        panel.find(&quot;.voter-address&quot;).focus();
+}
+
+// Toggle display of poll details
+ViewController.prototype.editSetVisible = function(visible) {
+
+        if (visible) {
+                
+                if (this.isNewPoll) {
+                        $(&quot;#editpoll-delete&quot;).hide();
+                        $(&quot;#editpoll-tabs&quot;).tabs(&quot;disable&quot;, 2);
+                } else {
+                        $(&quot;#editpoll-delete&quot;).show();
+                        $(&quot;#editpoll-tabs&quot;).tabs(&quot;enable&quot;, 2);
+                }
+                if (this.activePoll.owned &amp;&amp; this.activePoll.resource.object.mainComponent().editable()) {
+                        $(&quot;#editpoll-title-panel&quot;).hide();
+                        $(&quot;#editpoll-organizer-panel&quot;).hide();
+                        $(&quot;#editpoll-status-panel&quot;).hide();
+                        $(&quot;#editpoll-title-edit-panel&quot;).show();
+                        $(&quot;#editpoll-tabs&quot;).tabs(&quot;enable&quot;, 0);
+                        $(&quot;#editpoll-tabs&quot;).tabs(&quot;enable&quot;, 1);
+                        $(&quot;#editpoll-tabs&quot;).tabs(&quot;option&quot;, &quot;active&quot;, 0);
+                        $(&quot;#response-key&quot;).hide();
+                } else {
+                        $(&quot;#editpoll-title-edit-panel&quot;).hide();
+                        $(&quot;#editpoll-title-panel&quot;).show();
+                        $(&quot;#editpoll-organizer-panel&quot;).show();
+                        $(&quot;#editpoll-status-panel&quot;).show();
+                        $(&quot;#editpoll-tabs&quot;).tabs(&quot;option&quot;, &quot;active&quot;, 2);
+                        $(&quot;#editpoll-tabs&quot;).tabs(&quot;disable&quot;, 0);
+                        $(&quot;#editpoll-tabs&quot;).tabs(&quot;disable&quot;, 1);
+                        $(&quot;#response-key&quot;).toggle(this.activePoll.resource.object.mainComponent().editable());
+                        this.activePoll.buildResults();
+                }
+                
+                $(&quot;#editpoll-save&quot;).toggle(this.activePoll.resource.object.mainComponent().editable());
+                $(&quot;#editpoll-cancel&quot;).toggle(this.activePoll.resource.object.mainComponent().editable());
+                $(&quot;#editpoll-done&quot;).toggle(!this.activePoll.resource.object.mainComponent().editable());
+                $(&quot;#editpoll-autofill&quot;).toggle(this.activePoll.resource.object.mainComponent().editable());
+
+                $(&quot;#detail-nocontent&quot;).hide();
+                $(&quot;#editpoll&quot;).show();
+        } else {
+                $(&quot;#editpoll&quot;).hide();
+                $(&quot;#detail-nocontent&quot;).show();
+        }
+}
+
+ViewController.prototype.refreshed = function() {
+        showLoading(false);
+        if (this.ownedPolls.polls.length == 0 &amp;&amp; this.voterPolls.polls.length != 0) {
+                $(&quot;#sidebar&quot;).accordion(&quot;option&quot;, &quot;active&quot;, 1);
+        } else {
+                $(&quot;#sidebar&quot;).accordion(&quot;option&quot;, &quot;active&quot;, 0);
+        }
+}
+
+// Rebuild results panel each time it is selected
+ViewController.prototype.showResults = function(event, ui) {
+        if (ui.newPanel.selector == &quot;#editpoll-results&quot;) {
+                this.activePoll.buildResults();
+        }
+        $(&quot;#editpoll-autofill&quot;).toggle(ui.newPanel.selector == &quot;#editpoll-results&quot;);
+        $(&quot;#response-key&quot;).toggle(ui.newPanel.selector == &quot;#editpoll-results&quot;);
+}
+
+// Maintains the list of editable polls and manipulates the DOM as polls are
+// added
+// and removed.
+PollList = function(menu, counter) {
+        this.polls = [];
+        this.menu = menu;
+        this.counter = counter;
+}
+
+// Add a poll to the UI.
+PollList.prototype.addPoll = function(poll) {
+        this.polls.push(poll);
+        poll.list = this;
+        this.menu.append('&lt;li class=&quot;sidebar-list&quot;&gt;&lt;a href=&quot;#&quot;&gt;' + poll.title() + '&lt;/a&gt;&lt;/li&gt;');
+        this.menu.menu(&quot;refresh&quot;);
+        this.counter.text(this.polls.length);
+}
+
+// Add a poll to the UI and save its resource
+PollList.prototype.newPoll = function(poll) {
+        this.addPoll(poll);
+        poll.saveResource();
+        $(&quot;#editpoll-delete&quot;).show();
+}
+
+// Change a poll in the UI and save its resource
+PollList.prototype.changePoll = function(poll) {
+        var index = this.polls.indexOf(poll);
+        this.menu.find(&quot;a&quot;).eq(index).text(poll.title());
+        this.menu.menu(&quot;refresh&quot;);
+        poll.saveResource();
+}
+
+// Remove a poll resource and its UI
+PollList.prototype.removePoll = function(poll) {
+        var this_polllist = this;
+        poll.resource.removeResource(function() {
+                var index = this_polllist.polls.indexOf(poll);
+                this_polllist.polls.splice(index, 1);
+                this_polllist.menu.children(&quot;li&quot;).eq(index).remove();
+                this_polllist.menu.menu(&quot;refresh&quot;);
+                this_polllist.counter.text(this_polllist.polls.length);
+        });
+}
+
+PollList.prototype.indexOfPollUID = function(uid) {
+        var result = null;
+        $.each(this.polls, function(index, poll) {
+                if (poll.resource.object.mainComponent().uid() == uid) {
+                        result = index;
+                        return false;
+                }
+        });
+        return result;
+}
+
+// Remove all UI items
+PollList.prototype.clearPolls = function() {
+        this.menu.empty();
+        this.menu.menu(&quot;refresh&quot;);
+        this.polls = [];
+        this.counter.text(this.polls.length);
+}
+
+// An editable poll. It manipulates the DOM for editing a poll
+Poll = function(resource) {
+        this.resource = resource;
+        this.owned = this.resource.object.mainComponent().isOwned();
+        this.editing_object = null;
+        this.editing_poll = null;
+}
+
+Poll.prototype.title = function() {
+        return this.editing_poll ? this.editing_poll.summary() : this.resource.object.mainComponent().summary();
+}
+
+Poll.prototype.closed = function() {
+        this.editing_poll = null;
+}
+
+// Save the editable state
+Poll.prototype.saveResource = function(whenDone) {
+        // Only if it changed
+        if (this.editing_poll.changed()) {
+                this.resource.object = this.editing_object;
+                var this_poll = this;
+                this.resource.saveResource(function() {
+                        // Reload from the resource as it might change after write to server
+                        this_poll.editing_object = this_poll.resource.object.duplicate();
+                        this_poll.editing_poll = this_poll.editing_object.mainComponent();
+                        
+                        if (whenDone) {
+                                whenDone();
+                        }
+                });
+        }
+}
+
+// Fill the UI with details of the poll
+Poll.prototype.setPanel = function() {
+        
+        var this_poll = this;
+        this.editing_object = this.resource.object.duplicate();
+        this.editing_poll = this.editing_object.mainComponent();
+
+        // Setup the details panel with this poll
+        $(&quot;#editpoll-title-edit&quot;).val(this.editing_poll.summary());
+        $(&quot;#editpoll-title&quot;).text(this.editing_poll.summary());
+        $(&quot;#editpoll-organizer&quot;).text(this.editing_poll.organizerDisplayName());
+        $(&quot;#editpoll-status&quot;).text(this.editing_poll.status());
+        $(&quot;#editpoll-eventlist&quot;).empty();
+        $.each(this.editing_poll.events(), function(index, event) {
+                this_poll.setEventPanel(this_poll.addEventPanel(), event);
+        });
+        $(&quot;#editpoll-voterlist&quot;).empty();
+        $.each(this.editing_poll.voters(), function(index, voter) {
+                this_poll.setVoterPanel(this_poll.addVoterPanel(), voter);
+        });
+}
+
+// Get poll details from the UI
+Poll.prototype.getPanel = function() {
+        var this_poll = this;
+
+        // Get values from the details panel
+        if (this.owned) {
+                this.editing_poll.summary($(&quot;#editpoll-title-edit&quot;).val());
+                
+                var events = this.editing_poll.events();
+                $(&quot;#editpoll-eventlist&quot;).children().each(function(index) {
+                        this_poll.updateEventFromPanel($(this), events[index]);
+                });
+
+                var voters = this.editing_poll.voters();
+                $(&quot;#editpoll-voterlist&quot;).children().each(function(index) {
+                        this_poll.updateVoterFromPanel($(this), voters[index]);
+                });
+        }
+}
+
+//Add a new event item in the UI
+Poll.prototype.addEventPanel = function() {
+
+        var ctr = $(&quot;#editpoll-eventlist&quot;).children().length + 1;
+        var idstart = &quot;event-dtstart-&quot; + ctr;
+        var idend = &quot;event-dtend-&quot; + ctr;
+
+        // Add new list item
+        var evt = '&lt;div class=&quot;event&quot;&gt;';
+        evt += '&lt;div class=&quot;edit-datetime&quot;&gt;';
+        evt += '&lt;label for=&quot;' + idstart + '&quot;&gt;Start: &lt;/label&gt;';
+        evt += '&lt;input type=&quot;text&quot; id=&quot;' + idstart + '&quot; class=&quot;event-dtstart&quot;/&gt;';
+        evt += '&lt;/div&gt;';
+        evt += '&lt;div class=&quot;edit-datetime&quot;&gt;';
+        evt += '&lt;label for=&quot;' + idend + '&quot;&gt;End:   &lt;/label&gt;';
+        evt += '&lt;input type=&quot;text&quot; id=&quot;' + idend + '&quot; class=&quot;event-dtend&quot; /&gt;';
+        evt += '&lt;/div&gt;';
+        evt += '&lt;button class=&quot;input-remove&quot;&gt;Remove&lt;/button&gt;';
+        evt += '&lt;/div&gt;';
+        evt = $(evt).appendTo(&quot;#editpoll-eventlist&quot;);
+
+        evt.find(&quot;.event-dtstart&quot;).datetimepicker();
+        evt.find(&quot;.event-dtend&quot;).datetimepicker();
+        evt.find(&quot;.input-remove&quot;).button({
+                icons : {
+                        primary : &quot;ui-icon-close&quot;
+                }
+        });
+
+        return evt;
+}
+
+// Update the UI for this event
+Poll.prototype.setEventPanel = function(panel, event) {
+        panel.find(&quot;.event-dtstart&quot;).datetimepicker(&quot;setDate&quot;, event.dtstart());
+        panel.find(&quot;.event-dtend&quot;).datetimepicker(&quot;setDate&quot;, event.dtend());
+        return panel;
+}
+
+// Get details of the event from the UI
+Poll.prototype.updateEventFromPanel = function(panel, event) {
+        event.summary($(&quot;#editpoll-title-edit&quot;).val());
+        event.dtstart(panel.find(&quot;.event-dtstart&quot;).datetimepicker(&quot;getDate&quot;));
+        event.dtend(panel.find(&quot;.event-dtend&quot;).datetimepicker(&quot;getDate&quot;));
+}
+
+// Add event button clicked
+Poll.prototype.addEvent = function() {
+        
+        var ctr = $(&quot;#editpoll-eventlist&quot;).children().length;
+        var dtstart = new Date();
+        dtstart.setDate(dtstart.getDate() + ctr);
+        dtstart.setHours(12, 0, 0, 0);
+        var dtend = new Date();
+        dtend.setDate(dtend.getDate() + ctr);
+        dtend.setHours(13, 0, 0, 0);
+
+        // Add new list item
+        var vevent = this.editing_poll.addEvent(dtstart, dtend);
+        return this.setEventPanel(this.addEventPanel(), vevent);
+}
+
+//Add a new voter item in the UI
+Poll.prototype.addVoterPanel = function() {
+
+        var ctr = $(&quot;#editpoll-voterlist&quot;).children().length + 1;
+        var idvoter = &quot;voter-address-&quot; + ctr;
+
+        // Add new list item
+        var vtr = '&lt;div class=&quot;voter&quot;&gt;';
+        vtr += '&lt;div class=&quot;edit-voter&quot;&gt;';
+        vtr += '&lt;label for=&quot;' + idvoter + '&quot;&gt;Voter: &lt;/label&gt;';
+        vtr += '&lt;input type=&quot;text&quot; id=&quot;' + idvoter + '&quot; class=&quot;voter-address&quot;/&gt;';
+        vtr += '&lt;/div&gt;';
+        vtr += '&lt;button class=&quot;input-remove&quot;&gt;Remove&lt;/button&gt;';
+        vtr += '&lt;/div&gt;';
+        vtr = $(vtr).appendTo(&quot;#editpoll-voterlist&quot;);
+
+        vtr.find(&quot;.voter-address&quot;).autocomplete({
+                minLength : 3,
+                source : function(request, response) {
+                        gSession.calendarUserSearch(request.term, function(results) {
+                                response(results);
+                        });
+                }
+        }).focus(function() {
+                $(this).select();
+        });
+        
+        vtr.find(&quot;.input-remove&quot;).button({
+                icons : {
+                        primary : &quot;ui-icon-close&quot;
+                }
+        });
+
+        return vtr;
+}
+
+// Update UI for this voter
+Poll.prototype.setVoterPanel = function(panel, voter) {
+        panel.find(&quot;.voter-address&quot;).val(voter.addressDescription());
+        return panel;
+}
+
+// Get details of the voter from the UI
+Poll.prototype.updateVoterFromPanel = function(panel, voter) {
+        voter.addressDescription(panel.find(&quot;.voter-address&quot;).val());
+}
+
+// Add voter button clicked
+Poll.prototype.addVoter = function() {
+        // Add new list item
+        var voter = this.editing_poll.addVoter();
+        return this.setVoterPanel(this.addVoterPanel(), voter);
+}
+
+// Build the results UI based on the poll details
+Poll.prototype.buildResults = function() {
+        
+        var this_poll = this;
+        
+        // Sync with any changes from other panels
+        this.getPanel();
+
+        var event_details = this.editing_poll.events();
+        var voter_details = this.editing_poll.voters();
+
+        var thead = $(&quot;#editpoll-resulttable&quot;).children(&quot;thead&quot;).first();
+        var th_date = thead.children(&quot;tr&quot;).eq(0).empty();
+        var th_start = thead.children(&quot;tr&quot;).eq(1).empty();
+        var th_end = thead.children(&quot;tr&quot;).eq(2).empty();
+        var tf = $(&quot;#editpoll-resulttable&quot;).children(&quot;tfoot&quot;).first();
+        var tf_overall = tf.children(&quot;tr&quot;).first().empty();
+        var tf_commit = tf.children(&quot;tr&quot;).last().empty();
+        tf_commit.toggle(this.owned || !this_poll.editing_poll.editable());
+        var tbody = $(&quot;#editpoll-resulttable&quot;).children(&quot;tbody&quot;).first().empty();
+        $('&lt;td&gt;Date:&lt;/td&gt;').appendTo(th_date);
+        $('&lt;td&gt;Start:&lt;/td&gt;').appendTo(th_start);
+        $('&lt;td&gt;End:&lt;/td&gt;').appendTo(th_end);
+        $('&lt;td&gt;Overall:&lt;/td&gt;').appendTo(tf_overall);
+        $('&lt;td /&gt;').appendTo(tf_commit);
+        $.each(event_details, function(index, event) {
+                var td_date = $('&lt;td /&gt;').appendTo(th_date).text(event.dtstart().toDateString()).addClass(&quot;center-td&quot;);
+                var td_start = $('&lt;td /&gt;').appendTo(th_start).text(event.dtstart().toLocaleTimeString());
+                var td_end = $('&lt;td /&gt;').appendTo(th_end).text(event.dtend().toLocaleTimeString());
+                $('&lt;td /&gt;').appendTo(tf_overall).addClass(&quot;center-td&quot;);
+                $('&lt;td /&gt;').appendTo(tf_commit).addClass(&quot;center-td&quot;);
+                if (event.ispollwinner()) {
+                        td_date.addClass(&quot;poll-winner-td&quot;);
+                        td_start.addClass(&quot;poll-winner-td&quot;);
+                        td_end.addClass(&quot;poll-winner-td&quot;);
+                }
+                td_date.hover(
+                        function() {
+                                this_poll.hoverDialogOpen(td_date, thead, event);
+                        },
+                        this_poll.hoverDialogClose
+                );
+        });
+        $.each(voter_details, function(index, voter) {
+                var active = gSession.currentPrincipal.matchingAddress(voter.cuaddr());
+                var tr = $(&quot;&lt;tr/&gt;&quot;).appendTo(tbody);
+                $(&quot;&lt;td/&gt;&quot;).appendTo(tr).text(voter.nameOrAddress());
+                $.each(event_details, function(index, event) {
+                        var response = event.voter_responses()[voter.cuaddr()];
+                        var td = $(&quot;&lt;td /&gt;&quot;).appendTo(tr).addClass(&quot;center-td&quot;);
+                        if (event.ispollwinner()) {
+                                td.addClass(&quot;poll-winner-td&quot;);
+                        }
+                        if (active &amp;&amp; this_poll.editing_poll.editable()) {
+                                var radios = $('&lt;div id=&quot;response-' + index + '&quot; /&gt;').appendTo(td).addClass(&quot;response-btns&quot;);
+                                $('&lt;input type=&quot;radio&quot; id=&quot;respond_no-' + index + '&quot; name=&quot;response-' + index + '&quot;/&gt;').appendTo(radios);
+                                $('&lt;label for=&quot;respond_no-' + index + '&quot;/&gt;').appendTo(radios);
+                                $('&lt;input type=&quot;radio&quot; id=&quot;respond_maybe-' + index + '&quot; name=&quot;response-' + index + '&quot;/&gt;').appendTo(radios);
+                                $('&lt;label for=&quot;respond_maybe-' + index + '&quot;/&gt;').appendTo(radios);
+                                $('&lt;input type=&quot;radio&quot; id=&quot;respond_ok-' + index + '&quot; name=&quot;response-' + index + '&quot;/&gt;').appendTo(radios);
+                                $('&lt;label for=&quot;respond_ok-' + index + '&quot;/&gt;').appendTo(radios);
+                                $('&lt;input type=&quot;radio&quot; id=&quot;respond_best-' + index + '&quot; name=&quot;response-' + index + '&quot;/&gt;').appendTo(radios);
+                                $('&lt;label for=&quot;respond_best-' + index + '&quot;/&gt;').appendTo(radios);
+                                radios.buttonset();
+                                if (response !== undefined) {
+                                        if (response &lt; 40) {
+                                                $('#respond_no-' + index).click();
+                                        } else if (response &lt; 80) {
+                                                $('#respond_maybe-' + index).click();
+                                        } else if (response &lt; 90) {
+                                                $('#respond_ok-' + index).click();
+                                        } else {
+                                                $('#respond_best-' + index).click();
+                                        }
+                                }
+                                $('#respond_no-' + index).button({
+                                        icons : {
+                                                primary : &quot;ui-icon-close&quot;
+                                        },
+                                        text: false
+                                }).click(this_poll.clickResponse);
+                                $('#respond_maybe-' + index).button({
+                                        icons : {
+                                                primary : &quot;ui-icon-help&quot;
+                                        },
+                                        text: false
+                                }).click(this_poll.clickResponse);
+                                $('#respond_ok-' + index).button({
+                                        icons : {
+                                                primary : &quot;ui-icon-check&quot;
+                                        },
+                                        text: false
+                                }).click(this_poll.clickResponse);
+                                $('#respond_best-' + index).button({
+                                        icons : {
+                                                primary : &quot;ui-icon-circle-check&quot;
+                                        },
+                                        text: false
+                                }).click(this_poll.clickResponse);
+                        } else {
+                                td.text(this_poll.textForResponse(response)[0]);
+                        }
+                });
+                if (active) {
+                        tr.addClass(&quot;active-voter&quot;);
+                }
+        });
+
+        $.each(event_details, function(index, event) {
+                if (this_poll.editing_poll.editable()) {
+                        $('&lt;button id=&quot;winner-' + index + '&quot;&gt;Pick Winner&lt;/button&gt;').appendTo(tf_commit.children()[index + 1]).button({
+                                icons : {
+                                        primary : &quot;ui-icon-star&quot;
+                                },
+                        }).click(this_poll.clickWinner);
+                } else {
+                        if (event.ispollwinner()) {
+                                $(tf_commit.children()[index + 1]).addClass(&quot;poll-winner-td&quot;);
+                                $('&lt;div id=&quot;winner-text&quot;&gt;&lt;span id=&quot;winner-icon-left&quot; class=&quot;ui-icon ui-icon-star&quot; /&gt;Winner&lt;span id=&quot;winner-icon-right&quot; class=&quot;ui-icon ui-icon-star&quot; /&gt;&lt;/div&gt;').appendTo(tf_commit.children()[index + 1]);
+                        }
+                }
+        });
+
+        this.updateOverallResults();
+}
+
+Poll.prototype.textForResponse = function(response) {
+        var result = [];
+        if (response === undefined) {
+                result.push(&quot;No Response&quot;);
+                result.push(&quot;no-response-td&quot;);
+        } else if (response &lt; 40) {
+                result.push(&quot;No&quot;);
+                result.push(&quot;no-td&quot;);
+        } else if (response &lt; 80) {
+                result.push(&quot;Maybe&quot;);
+                result.push(&quot;maybe-td&quot;);
+        } else if (response &lt; 90) {
+                result.push(&quot;Ok&quot;);
+                result.push(&quot;ok-td&quot;);
+        } else {
+                result.push(&quot;Best&quot;);
+                result.push(&quot;best-td&quot;);
+        }
+        return result;
+}
+
+Poll.prototype.clickResponse = function() {
+        var splits = $(this).attr(&quot;id&quot;).split(&quot;-&quot;);
+        var response_type = splits[0];
+        var index = parseInt(splits[1]);
+        var response = 0;
+        if (response_type == &quot;respond_maybe&quot;) {
+                response = 50;
+        } else if (response_type == &quot;respond_ok&quot;) {
+                response = 85;
+        } else if (response_type == &quot;respond_best&quot;) {
+                response = 100;
+        }
+        
+        var event = gViewController.activePoll.editing_poll.events()[index];
+        event.changeVoterResponse(response);
+        gViewController.activePoll.updateOverallResults();
+}
+
+// A winner was chosen, make poll changes and create new event and save everything
+Poll.prototype.clickWinner = function() {
+        var splits = $(this).attr(&quot;id&quot;).split(&quot;-&quot;);
+        var event = gViewController.activePoll.editing_poll.events()[parseInt(splits[1])];
+        var new_resource = event.pickAsWinner();
+        new_resource.saveResource(function() {
+                gViewController.activePoll.saveResource(function() {
+                        gViewController.activatePoll(gViewController.activePoll);
+                });
+        })
+}
+
+// Open the event time-range hover dialog 
+Poll.prototype.hoverDialogOpenClassic = function(td_date, thead, event) {
+        var dialog_div = $('&lt;div id=&quot;hover-cal&quot; /&gt;').appendTo(td_date).dialog({
+                dialogClass: &quot;no-close&quot;,
+                position: { my: &quot;left top&quot;, at: &quot;right+20 top&quot;, of: thead },
+                show: &quot;fade&quot;,
+                title: &quot;Your Events for &quot; + event.dtstart().toDateString(),
+                width: 400,
+        });
+        var start = new Date(event.dtstart().getTime() - 6 * 60 * 60 * 1000);
+        var end = new Date(event.dtend().getTime() + 6 * 60 * 60 * 1000);
+        gSession.currentPrincipal.eventsForTimeRange(
+                start,
+                end,
+                function(results) {
+                        var text = &quot;&quot;;
+                        results = $.map(results, function(result) {
+                                return result.mainComponent();
+                        });
+                        results.push(event);
+                        results.sort(function(a, b) {
+                                return a.dtstart().getTime() - b.dtstart().getTime();
+                        });
+                        var relative_offset = 10;
+                        var last_end = null;
+                        $.each(results, function(index, result) {
+                                text = result.dtstart().toLocaleTimeString() + &quot; - &quot;;
+                                text += result.dtend().toLocaleTimeString() + &quot; : &quot;;
+                                text += result.summary();
+                                if (last_end !== null &amp;&amp; last_end.getTime() != result.dtstart().getTime()) {
+                                        relative_offset += 10;
+                                }
+                                last_end = result.dtend();
+                                $('&lt;div class=&quot;hover-event ui-corner-all&quot; style=&quot;top:' + relative_offset + 'px&quot;/&gt;').appendTo(dialog_div).addClass(result.pollitemid() !== null ? &quot;ui-state-active&quot; : &quot;ui-state-default&quot;).text(text);
+                        });
+                }
+        );
+}
+
+// Open the event time-range hover dialog 
+Poll.prototype.hoverDialogOpenFancy = function(td_date, thead, event) {
+        var dialog_div = $('&lt;div id=&quot;hover-cal&quot; /&gt;').appendTo(td_date).dialog({
+                dialogClass: &quot;no-close&quot;,
+                position: { my: &quot;left top&quot;, at: &quot;right+20 top&quot;, of: thead },
+                show: &quot;fade&quot;,
+                title: &quot;Your Events for &quot; + event.dtstart().toDateString(),
+                width: 400,
+        });
+        
+        var start = new Date(event.dtstart().getTime() - 6 * 60 * 60 * 1000);
+        start.setMinutes(0, 0, 0);
+        var startHour = start.getHours();
+        var end = new Date(event.dtend().getTime() + 6 * 60 * 60 * 1000);
+        end.setMinutes(0, 0, 0);
+        var endHour = end.getHours();
+        
+        var grid = $('&lt;table id=&quot;hover-grid&quot; /&gt;').appendTo(dialog_div);
+        for(var i = startHour; i &lt; endHour; i++) {
+                var text = i &gt; 12 ? i - 12 +&quot;:00 pm&quot; : i + &quot;:00 am&quot;;
+                $('&lt;tr&gt;&lt;td class=&quot;hover-grid-td-time&quot;&gt;' + text + '&lt;/td&gt;&lt;td class=&quot;hover-grid-td-slot&quot; /&gt;&lt;/tr&gt;').appendTo(grid);
+        }
+        gSession.currentPrincipal.eventsForTimeRange(
+                start,
+                end,
+                function(results) {
+                        results = $.map(results, function(result) {
+                                return result.mainComponent();
+                        });
+                        results.push(event);
+                        results.sort(function(a, b) {
+                                return a.dtstart().getTime() - b.dtstart().getTime();
+                        });
+                        var last_dtend = null;
+                        $.each(results, function(index, result) {
+                                var top_offset = (result.dtstart().getHours() - startHour) * 30;
+                                var height = ((result.dtend().getTime() - result.dtstart().getTime()) * 30) / (60 * 60 * 1000) - 6;
+                                var styles = &quot;top:&quot; + top_offset + &quot;px;height:&quot; + height + &quot;px&quot;;
+                                if (last_dtend !== null &amp;&amp; last_dtend &gt; result.dtstart()) {
+                                        styles += &quot;;left:206px;width:125px&quot;;
+                                }
+                                last_dtend = result.dtend();
+                                $('&lt;div class=&quot;hover-event ui-corner-all&quot; style=&quot;' + styles + '&quot; /&gt;').appendTo(grid).addClass(result.pollitemid() !== null ? &quot;ui-state-active&quot; : &quot;ui-state-default&quot;).text(result.summary());
+                        });
+                }
+        );
+}
+
+Poll.prototype.hoverDialogOpen = Poll.prototype.hoverDialogOpenFancy;
+
+// Close the event time-range hover dialog 
+Poll.prototype.hoverDialogClose = function() {
+        $(&quot;#hover-cal&quot;).dialog(&quot;close&quot;).remove();
+}
+
+Poll.prototype.updateOverallResults = function() {
+        var this_poll = this;
+        var event_details = this.editing_poll.events();
+        var voter_details = this.editing_poll.voters();
+        var tds = $(&quot;#editpoll-resulttable&quot;).children(&quot;tfoot&quot;).first().children(&quot;tr&quot;).first().children(&quot;td&quot;);
+
+        // Update overall items
+        $.each(event_details, function(index, event) {
+                var overall = [];
+                var responses = event.voter_responses();
+                $.each(voter_details, function(index, voter) {
+                        var response = responses[voter.cuaddr()];
+                        if (response !== undefined) {
+                                overall.push(response);
+                        }
+                });
+                var response_details = this_poll.textForResponse(overall.average());
+                var possible_classes = [&quot;best-td&quot;, &quot;ok-td&quot;, &quot;maybe-td&quot;, &quot;no-td&quot;, &quot;no-response-td&quot;];
+                possible_classes.splice(possible_classes.indexOf(response_details[1]), 1);
+                $(tds[index + 1]).text(response_details[0]);
+                $(tds[index + 1]).removeClass(possible_classes.join(&quot; &quot;));
+                $(tds[index + 1]).addClass(event.ispollwinner() ? &quot;poll-winner-td&quot; : response_details[1]);
+        });
+}
+
+Poll.prototype.autoFill = function() {
+        
+        var event_details = this.editing_poll.events();
+        $.each(event_details, function(index, event) {
+                // Freebusy
+                gSession.currentPrincipal.isBusy(
+                        gSession.currentPrincipal.defaultAddress(),
+                        event.dtstart(),
+                        event.dtend(),
+                        function(result) {
+                                $((result ? &quot;#respond_no-&quot; : &quot;#respond_ok-&quot;) + index).click();
+                        }
+                );
+        });
+}
+
</ins></span></pre>
</div>
</div>

</body>
</html>