[CalendarServer-changes] [11917] CalendarServer/trunk/contrib
source_changes at macosforge.org
source_changes at macosforge.org
Wed Mar 12 11:17:23 PDT 2014
Revision: 11917
http://trac.calendarserver.org//changeset/11917
Author: cdaboo at apple.com
Date: 2013-11-08 13:59:31 -0800 (Fri, 08 Nov 2013)
Log Message:
-----------
webpoll prototype/demo webapp.
Added Paths:
-----------
CalendarServer/trunk/contrib/webpoll/
CalendarServer/trunk/contrib/webpoll/Makefile
CalendarServer/trunk/contrib/webpoll/README.txt
CalendarServer/trunk/contrib/webpoll/caldavd-test-webpoll.plist
CalendarServer/trunk/contrib/webpoll/webapp/
CalendarServer/trunk/contrib/webpoll/webapp/css/
CalendarServer/trunk/contrib/webpoll/webapp/css/webpoll.css
CalendarServer/trunk/contrib/webpoll/webapp/index.html
CalendarServer/trunk/contrib/webpoll/webapp/js/
CalendarServer/trunk/contrib/webpoll/webapp/js/caldav.js
CalendarServer/trunk/contrib/webpoll/webapp/js/jcal.js
CalendarServer/trunk/contrib/webpoll/webapp/js/utils.js
CalendarServer/trunk/contrib/webpoll/webapp/js/webpoll.js
Added: CalendarServer/trunk/contrib/webpoll/Makefile
===================================================================
--- CalendarServer/trunk/contrib/webpoll/Makefile (rev 0)
+++ CalendarServer/trunk/contrib/webpoll/Makefile 2013-11-08 21:59:31 UTC (rev 11917)
@@ -0,0 +1,36 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# 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 "AS IS" 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
Added: CalendarServer/trunk/contrib/webpoll/README.txt
===================================================================
--- CalendarServer/trunk/contrib/webpoll/README.txt (rev 0)
+++ CalendarServer/trunk/contrib/webpoll/README.txt 2013-11-08 21:59:31 UTC (rev 11917)
@@ -0,0 +1,29 @@
+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 "consensus" 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 "make webpoll" in this directory to download all the relevant dependencies.
+
+In the CalendarServer directory do "./run -f ./contrib/webpoll/caldavd-test-webpoll.plist" to run the server.
+
+In a browser navigate to "/webpoll".
+
+Using the webapp
+================
+
+TBD
Added: CalendarServer/trunk/contrib/webpoll/caldavd-test-webpoll.plist
===================================================================
--- 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)
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ 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 "AS IS" 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.
+ -->
+
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+
+ <!-- Import a parent config before this one -->
+ <key>ImportConfig</key>
+ <string>./conf/caldavd-test.plist</string>
+
+ <!-- Child aliases -->
+ <key>Aliases</key>
+ <array>
+ <dict>
+ <key>url</key>
+ <string>/webpoll</string>
+ <key>path</key>
+ <string>./contrib/webpoll/webapp</string>
+ </dict>
+ </array>
+
+ </dict>
+</plist>
Added: CalendarServer/trunk/contrib/webpoll/webapp/css/webpoll.css
===================================================================
--- 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)
@@ -0,0 +1,310 @@
+ at CHARSET "UTF-8";
+
+html,body {
+ font: 80% "Trebuchet MS", sans-serif;
+}
+
+/* Main Panel settings */
+#main-panel {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ right: 0px;
+ bottom: 0px;
+}
+
+/* Title Text */
+#title {
+ font: 14pt "Trebuchet MS", sans-serif;
+ position: absolute;
+ margin-top: 5px;
+ margin-left: 20px;
+ height: 32px;
+ left: 0px;
+ top: 0px;
+}
+
+#loading {
+ font: 14pt "Trebuchet MS", 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 "Trebuchet MS", sans-serif;
+}
+
+#sidebar-new-poll-count, #sidebar-vote-poll-count {
+ position: absolute;
+ background-color:#3366ff;
+ font: 8pt "Trebuchet MS", 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 "Trebuchet MS", 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 "Trebuchet MS", 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;
+}
+
Added: CalendarServer/trunk/contrib/webpoll/webapp/index.html
===================================================================
--- 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)
@@ -0,0 +1,110 @@
+<!doctype html>
+<html lang="us">
+<head>
+ <meta charset="utf-8">
+ <title>WebPoll</title>
+ <link href="css/cupertino/jquery-ui-1.10.3.css" rel="stylesheet">
+ <link href="css/datetimepicker.css" rel="stylesheet">
+ <link href="css/webpoll.css" rel="stylesheet">
+ <script src="js/jquery-2.0.3.js"></script>
+ <script src="js/jquery-ui-1.10.3.js"></script>
+ <script src="js/datetimepicker.js"></script>
+ <script src="js/json2.js"></script>
+ <script src="js/utils.js"></script>
+ <script src="js/jcal.js"></script>
+ <script src="js/caldav.js"></script>
+ <script src="js/webpoll.js"></script>
+</head>
+<body>
+
+<div id="main-panel">
+
+<div id="title">WebPoll</div>
+
+<div id="loading">
+Loading
+<div id="progressbar"></div>
+</div>
+
+<!-- Sidebar Panel-->
+<div id="sidepanel">
+ <div id="sidebar">
+ <h3 class="sidebar-title"><span>Your Polls</span><span id="sidebar-new-poll-count">0</span></h3>
+ <div>
+ <ul id="sidebar-owned"></ul>
+ <button id="sidebar-new-poll">New Poll</button>
+ </div>
+ <h3 class="sidebar-title"><span>Polls to Vote On<span id="sidebar-vote-poll-count">0</span></span></h3>
+ <div>
+ <ul id="sidebar-voter"></ul>
+ </div>
+ </div>
+ <div id="refresh"><button id="refresh-btn">Refresh</button></div>
+</div>
+
+<!-- Detail Panel -->
+<div id="detail">
+ <div id="detail-nocontent" class="ui-widget">Select a poll in the side-bar to view its details.</div>
+ <div id="editpoll">
+ <div id="editpoll-description">
+ <div id="editpoll-title-edit-panel" class="ui-widget">
+ <label for="editpoll-title-edit">Title: </label>
+ <input id="editpoll-title-edit" />
+ </div>
+ <div id="editpoll-title-panel" class="ui-widget">Title: <span id="editpoll-title"></span></div>
+ <div id="editpoll-organizer-panel" class="ui-widget">Organizer: <span id="editpoll-organizer"></span></div>
+ <div id="editpoll-status-panel" class="ui-widget">Status: <span id="editpoll-status"></span></div>
+ </div>
+ <div id="editpoll-details">
+ <div id="editpoll-tabs">
+ <ul>
+ <li id="editpoll-events-tab"><a href="#editpoll-events">Events</a></li>
+ <li id="editpoll-voters-tab"><a href="#editpoll-voters">Voters</a></li>
+ <li><a href="#editpoll-results">Results</a></li>
+ </ul>
+ <div id="editpoll-events">
+ <div id="editpoll-eventlist"></div>
+ <button id="editpoll-addevent">Add Event</button>
+ </div>
+ <div id="editpoll-voters">
+ <div id="editpoll-voterlist"></div>
+ <button id="editpoll-addvoter">Add Voter</button>
+ </div>
+ <div id="editpoll-results">
+ <table id="editpoll-resulttable">
+ <thead>
+ <tr></tr>
+ <tr></tr>
+ <tr></tr>
+ </thead>
+ <tbody>
+ </tbody>
+ <tfoot>
+ <tr></tr>
+ <tr></tr>
+ </tfoot>
+ </table>
+ </div>
+ </div>
+ <button id="editpoll-save">Save</button>
+ <button id="editpoll-cancel">Cancel</button>
+ <button id="editpoll-done">Done</button>
+ <button id="editpoll-delete">Delete Poll</button>
+ <button id="editpoll-autofill">Auto Fill</button>
+ <div id="response-key" class="ui-widget">
+ Possible Responses:
+ <ul id="response-menu">
+ <li><a href="#"><span class="ui-icon ui-icon-close"></span>No</a></li>
+ <li><a href="#"><span class="ui-icon ui-icon-help"></span>Maybe</a></li>
+ <li><a href="#"><span class="ui-icon ui-icon-check"></span>Ok</a></li>
+ <li><a href="#"><span class="ui-icon ui-icon-circle-check"></span>Best</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</div>
+
+</div>
+
+</body>
+</html>
Property changes on: CalendarServer/trunk/contrib/webpoll/webapp/index.html
___________________________________________________________________
Added: svn:executable
+ *
Added: CalendarServer/trunk/contrib/webpoll/webapp/js/caldav.js
===================================================================
--- 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)
@@ -0,0 +1,1028 @@
+/**
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# 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 "AS IS" 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("D", nsmap);
+
+ var propstr = addElements(props, nsmap);
+
+ return Ajax({
+ url : url,
+ type : "PROPFIND",
+ contentType : "application/xml; charset=utf-8",
+ headers : {
+ "Prefer" : "return=minimal",
+ "Depth" : depth
+ },
+ data : '<?xml version="1.0" encoding="utf-8" ?>' + '<D:propfind' + buildXMLNS(nsmap) + '>' + '<D:prop>' + propstr + '</D:prop>' + '</D:propfind>',
+ });
+}
+
+// A calendar-query REPORT request for VPOLLs only
+function PollQueryReport(url, props) {
+ var nsmap = {};
+ addNamespace("D", nsmap);
+ addNamespace("C", nsmap);
+
+ var propstr = addElements(props, nsmap);
+ propstr += '<C:calendar-data content-type="application/calendar+json" />';
+
+ return Ajax({
+ url : url,
+ type : "REPORT",
+ contentType : "application/xml; charset=utf-8",
+ headers : {
+ "Prefer" : "return=minimal",
+ "Depth" : "0"
+ },
+ data : '<?xml version="1.0" encoding="utf-8" ?>' +
+ '<C:calendar-query' + buildXMLNS(nsmap) + '>' +
+ '<D:prop>' + propstr + '</D:prop>' +
+ '<C:filter>' +
+ '<C:comp-filter name="VCALENDAR">' +
+ '<C:comp-filter name="VPOLL" />' +
+ '</C:comp-filter>' +
+ '</C:filter>' +
+ '</C:calendar-query>',
+ });
+}
+
+// A calendar-query REPORT request for VEVENTs in time-range, expanded
+function TimeRangeExpandedSummaryQueryReport(url, start, end) {
+ var nsmap = {};
+ addNamespace("D", nsmap);
+ addNamespace("C", nsmap);
+
+ return Ajax({
+ url : url,
+ type : "REPORT",
+ contentType : "application/xml; charset=utf-8",
+ headers : {
+ "Prefer" : "return=minimal",
+ "Depth" : "0"
+ },
+ data : '<?xml version="1.0" encoding="utf-8" ?>' +
+ '<C:calendar-query' + buildXMLNS(nsmap) + '>' +
+ '<D:prop>' +
+ '<C:calendar-data content-type="application/calendar+json">' +
+ '<C:expand start="' + jcaldate.jsDateToiCal(start) + '" end="' + jcaldate.jsDateToiCal(end) + '"/>' +
+ '<C:comp name="VCALENDAR">' +
+ '<C:allprop/>' +
+ '<C:comp name="VEVENT">' +
+ '<C:prop name="UID"/><C:prop name="DTSTART"/><C:prop name="DTEND"/><C:prop name="DURATION"/><C:prop name="SUMMARY"/>' +
+ '</C:comp>' +
+ '</C:comp>' +
+ '</C:calendar-data>' +
+ '</D:prop>' +
+ '<C:filter>' +
+ '<C:comp-filter name="VCALENDAR">' +
+ '<C:comp-filter name="VEVENT">' +
+ '<C:time-range start="' + jcaldate.jsDateToiCal(start) + '" end="' + jcaldate.jsDateToiCal(end) + '"/>' +
+ '</C:comp-filter>' +
+ '</C:comp-filter>' +
+ '</C:filter>' +
+ '</C:calendar-query>'
+ });
+}
+
+// A freebusy POST request
+function Freebusy(url, fbrequest) {
+ return Ajax({
+ url : url,
+ type : "POST",
+ contentType : "application/calendar+json; charset=utf-8",
+ data : fbrequest.toString(),
+ });
+}
+
+// A calendar-user-search REPORT request
+function UserSearchReport(url, text) {
+ var nsmap = {};
+ addNamespace("D", nsmap);
+ addNamespace("C", nsmap);
+ addNamespace("CS", nsmap);
+
+ return Ajax({
+ url : url,
+ type : "REPORT",
+ contentType : "application/xml; charset=utf-8",
+ headers : {
+ "Depth" : "0"
+ },
+ data : '<?xml version="1.0" encoding="utf-8" ?>'
+ + '<CS:calendarserver-principal-search context="user"' + buildXMLNS(nsmap) + '>' + '<CS:search-token>' + xmlEncode(text) + '</CS:search-token>'
+ + '<CS:limit><CS:nresults>20</CS:nresults></CS:limit>' + '<D:prop>' + '<D:displayname />' + '<C:calendar-user-address-set />' + '</D:prop>'
+ + '</CS:calendarserver-principal-search>',
+ });
+}
+
+// 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, "D:multistatus/D:response/D:propstat/D:prop/");
+}
+
+// Get property href text value from the overall multistatus
+MultiStatusResponse.prototype.getPropertyHrefTextList = function(prop) {
+ return this._getPropertyHrefTextList($(this.response), prop, "D:multistatus/D:response/D:propstat/D:prop/");
+}
+
+// Get property text value from the specified response node
+MultiStatusResponse.prototype.getResourcePropertyText = function(response_node, prop) {
+ return this._getPropertyText(response_node, prop, "D:propstat/D:prop/");
+}
+
+// Get property href text value from the specified response node
+MultiStatusResponse.prototype.getResourcePropertyHrefTextList = function(response_node, prop) {
+ return this._getPropertyHrefTextList(response_node, prop, "D:propstat/D:prop/");
+}
+
+// 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 + "/D:href");
+ 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), "D:multistatus/D:response");
+ var msr = this;
+ $.each(items, function(index, item) {
+ var href = getElementText(item, "D:href");
+ 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), "C:schedule-response/C:response");
+ $.each(items, function(index, item) {
+ doIt(getElementText(item, "C:recipient/D:href"), item);
+ });
+}
+
+// A CalDAV session for a specific principal
+CalDAVSession = function(user) {
+ this.currentPrincipal = null;
+ //this.host = "http://172.16.105.104:8080/ucaldav";
+ //this.host = "https://cyrus.local:8543";
+ this.host = "";
+
+ if (user === undefined) {
+ this.auth = null;
+ } else {
+ this.auth = user;
+ }
+}
+
+// Setup session
+CalDAVSession.prototype.init = function(whenDone) {
+ this.currentUserPropfind(whenDone);
+}
+
+gWellKnown = "/.well-known/caldav";
+//gWellKnown = "/";
+
+// 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), "0", [
+ "D:current-user-principal"
+ ]).done(function(response) {
+ var msr = new MultiStatusResponse(response, gWellKnown);
+ var href = msr.getPropertyText("D:current-user-principal/D:href");
+ if (href == null) {
+ alert("Could not determine current user.");
+ } 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, "/principals/"), item).done(function(response) {
+ var msr = new MultiStatusResponse(response, "/principals/");
+ var results = [];
+ msr.doToEachChildResource(function(url, response_node) {
+ var cn = msr.getResourcePropertyText(response_node, "D:displayname");
+ var cuaddr = CalDAVPrincipal.bestCUAddress(msr.getResourcePropertyHrefTextList(response_node, "C:calendar-user-address-set"));
+ if (cuaddr) {
+ results.push((cn ? cn + " " : "") + "<" + cuaddr + ">");
+ }
+ });
+
+ 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("mailto:");
+ });
+ if (results.length == 0) {
+ results = $.grep(cuaddress_set, function(cuaddr, index) {
+ return cuaddr.startsWith("urn:uuid:");
+ });
+ }
+
+ 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), "0", [
+ "D:displayname", "C:calendar-home-set", "C:schedule-inbox-URL", "C:schedule-outbox-URL", "C:calendar-user-address-set"
+ ]).done(function(response) {
+ var msr = new MultiStatusResponse(response, principal.url);
+ principal.cn = msr.getPropertyText("D:displayname");
+ principal.home_url = msr.getPropertyText("C:calendar-home-set/D:href");
+ principal.inbox_url = msr.getPropertyText("C:schedule-inbox-URL/D:href");
+ principal.outbox_url = msr.getPropertyText("C:schedule-outbox-URL/D:href");
+ principal.calendar_user_addresses = msr.getPropertyHrefTextList("C:calendar-user-address-set");
+ // 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), "1", [
+ "D:resourcetype", "D:displayname", "D:add-member", "C:supported-calendar-component-set"
+ ]).done(function(response) {
+ var msr = new MultiStatusResponse(response, this_principal.home_url);
+ msr.doToEachChildResource(function(url, response_node) {
+ if (!hasElementPath(response_node, "D:propstat/D:prop/D:resourcetype/D:collection")
+ || !hasElementPath(response_node, "D:propstat/D:prop/D:resourcetype/C:calendar"))
+ return;
+
+ // Separate out support for VPOLL and VEVENT
+ var comps = findElementPath(response_node, "D:propstat/D:prop/C:supported-calendar-component-set/C:comp");
+ 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("name") == "VPOLL") {
+ has_vpoll = true;
+ }
+ if (comp.attr("name") == "VEVENT") {
+ has_vevent = true;
+ }
+ });
+ }
+
+ // Build the calendar and assign to appropriate arrays
+ var cal = new CalendarCollection(url);
+ cal.displayname = getElementText(response_node, "D:propstat/D:prop/D:resourcetype/D:displayname")
+ if (!cal.displayname) {
+ cal.displayname = basenameURL(url);
+ }
+ cal.addmember = getElementText(response_node, "D:propstat/D:prop/D:add-member/D:href")
+ 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("method", "REQUEST");
+ var fb = fbrequest.newComponent("vfreebusy", true);
+ fb.newProperty(
+ "organizer",
+ this.defaultAddress(),
+ { "cn" : this.cn },
+ "cal-address"
+ );
+ fb.newProperty(
+ "attendee",
+ user,
+ {},
+ "cal-address"
+ );
+ fb.newProperty(
+ "dtstart",
+ jcaldate.jsDateTojCal(start),
+ {},
+ "date-time"
+ );
+ fb.newProperty(
+ "dtend",
+ jcaldate.jsDateTojCal(end),
+ {},
+ "date-time"
+ );
+
+ 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, "C:calendar-data");
+ if (caldata) {
+ caldata = jcal.fromString(caldata);
+ if (caldata.mainComponent().name() == "vfreebusy") {
+ // Any FREEBUSY property means busy sometime during the requested period
+ result = caldata.mainComponent().hasProperty("freebusy")
+ }
+ }
+ });
+
+ 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, "C:calendar-data"));
+ 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), [
+ "D:getetag"
+ ]).done(function(response) {
+ var msr = new MultiStatusResponse(response, calendar.url);
+ msr.doToEachChildResource(function(url, response_node) {
+ var etag = msr.getResourcePropertyText(response_node, "D:getetag");
+ var caldata = jcal.fromString(msr.getResourcePropertyText(response_node, "C:calendar-data"));
+ 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() == "vpoll") {
+ 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 : "POST",
+ contentType : "application/calendar+json; charset=utf-8",
+ headers : {
+ "Prefer" : "return=representation",
+ "Accept" : "application/calendar+json"
+ },
+ data : this.object.toString(),
+ }).done(function(response, textStatus, jqXHR) {
+ // Get Content-Location header as new url
+ this.url = jqXHR.getResponseHeader("Content-Location");
+
+ // Check for returned data and ETag
+ this.etag = jqXHR.getResponseHeader("Etag");
+ 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("vpoll").getPropertyText("uid") + ".ics");
+ }
+
+ // Do conditional PUT
+ Ajax({
+ context : this,
+ url : joinURLs(gSession.host, this.url),
+ type : "PUT",
+ contentType : "application/calendar+json; charset=utf-8",
+ headers : {
+ "Prefer" : "return=representation",
+ "Accept" : "application/calendar+json"
+ },
+ data : this.object.toString(),
+ }).done(function(response, textStatus, jqXHR) {
+ // Check for returned data and ETag
+ this.etag = jqXHR.getResponseHeader("Etag");
+ 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 : "DELETE",
+ }).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("uid");
+}
+
+// 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("organizer");
+}
+
+CalendarComponent.prototype.organizerDisplayName = function() {
+ return new CalendarUser(this.data.getProperty("organizer"), this).nameOrAddress();
+}
+
+CalendarComponent.prototype.status = function() {
+ return this.data.getPropertyValue("status");
+}
+
+CalendarComponent.prototype.summary = function(value) {
+ if (value === undefined) {
+ return this.data.getPropertyValue("summary");
+ } else {
+ if (this.summary() != value) {
+ this.data.updateProperty("summary", value);
+ this.changed(true);
+ }
+ }
+}
+
+CalendarComponent.prototype.dtstart = function(value) {
+ if (value === undefined) {
+ return jcaldate.jCalTojsDate(this.data.getPropertyValue("dtstart"));
+ } else {
+ if (this.dtstart() - value !== 0) {
+ this.data.updateProperty("dtstart", jcaldate.jsDateTojCal(value), {}, "date-time");
+ this.changed(true);
+ }
+ }
+}
+
+CalendarComponent.prototype.dtend = function(value) {
+ if (value === undefined) {
+ var dtend = this.data.getPropertyValue("dtend");
+ if (dtend === null) {
+ var offset = 0;
+ var duration = this.data.getPropertyValue("duration");
+ 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("dtend", jcaldate.jsDateTojCal(value), {}, "date-time");
+ this.data.removeProperties("duration");
+ this.changed(true);
+ }
+ }
+}
+
+CalendarComponent.prototype.pollitemid = function(value) {
+ if (value === undefined) {
+ return this.data.getPropertyValue("poll-item-id");
+ } else {
+ if (this.politemid() != value) {
+ this.data.updateProperty("poll-item-id", value);
+ this.changed(true);
+ }
+ }
+}
+
+CalendarComponent.prototype.voter_responses = function() {
+ var voter_results = {}
+ $.each(this.data.properties("voter"), function(index, voter) {
+ voter_results[voter[3]] = parseInt(voter[1]["response"]);
+ });
+ 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("voter"), 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(
+ "voter",
+ gSession.currentPrincipal.defaultAddress(),
+ { "response" : response.toString() },
+ "cal-address"
+ );
+ this.changed(true);
+ }
+ } else {
+ this.data.removePropertiesMatchingValue(function(propdata) {
+ return propdata[0] == "voter" && 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("vcalendar", 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("vpoll", CalendarPoll);
+
+// Create a brand new poll, defaulting various properties
+CalendarPoll.newPoll = function(title) {
+ var calendar = jcal.newCalendar();
+ var vpoll = calendar.newComponent("vpoll", true);
+ vpoll.newProperty("summary", title);
+ vpoll.newProperty("poll-mode", "BASIC");
+ vpoll.newProperty("poll-properties", ["DTSTART","DTEND"]);
+ vpoll.newProperty(
+ "organizer",
+ gSession.currentPrincipal.defaultAddress(),
+ { "cn" : gSession.currentPrincipal.cn },
+ "cal-address"
+ );
+ vpoll.newProperty(
+ "voter",
+ gSession.currentPrincipal.defaultAddress(),
+ {
+ "cn" : gSession.currentPrincipal.cn,
+ "partstat" : "ACCEPTED"
+ },
+ "cal-address"
+ );
+
+ 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 == "IN-PROCESS" : true;
+}
+
+CalendarComponent.prototype.pollwinner = function(value) {
+ if (value === undefined) {
+ return this.data.getPropertyValue("poll-winner");
+ } else {
+ if (this.pollwinner() != value) {
+ this.data.updateProperty("poll-winner", 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("vevent"), 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("vevent", true);
+ vevent.newProperty("dtstart", jcaldate.jsDateTojCal(dtstart), {}, "date-time");
+ vevent.newProperty("dtend", jcaldate.jsDateTojCal(dtend), {}, "date-time");
+ vevent.newProperty("summary", this.summary());
+ vevent.newProperty("poll-item-id", (this.data.components("vevent").length).toString());
+ vevent.newProperty(
+ "voter",
+ this.organizer(),
+ {"response" : "80"},
+ "cal-address"
+ );
+ 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("voter"), 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("voter", "", {}, "cal-address"), this);
+}
+
+// Mark current user as accepted
+CalendarPoll.prototype.acceptInvite = function() {
+ if (!this.isOwned()) {
+ var voters = $.grep(this.data.properties("voter"), function(voter) {
+ return gSession.currentPrincipal.matchingAddress(voter[3]);
+ });
+ $.each(voters, function(index, voter) {
+ voter[1]["partstat"] = "ACCEPTED";
+ delete voter[1]["rsvp"];
+ })
+ }
+}
+
+// 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("vevent", 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("status", "CONFIRMED");
+ vpoll.data.newProperty("poll-winner", 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("vevent", true);
+ vevent.updateProperty("uid", this.uid());
+ vevent.copyProperty("summary", this.data);
+ vevent.copyProperty("dtstart", this.data);
+ vevent.copyProperty("dtend", this.data);
+ vevent.copyProperty("organizer", vpoll.data);
+ $.each(vpoll.data.properties("voter"), function(index, voter) {
+ var attendee = vevent.newProperty(
+ "attendee",
+ voter[3],
+ {},
+ "cal-address"
+ );
+ $.each(voter[1], function(key, value) {
+ if (key == "partstat") {
+ value = gSession.currentPrincipal.matchingAddress(voter[3]) ? "ACCEPTED" : "NEEDS-ACTION";
+ }
+ 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]["cn"] ? this.data[1]["cn"] + " " : "";
+ return addressDescription(cn, this.data[3]);
+ } else {
+ if (this.addressDescription() != value) {
+ var splits = splitAddressDescription(value);
+ if (splits[0]) {
+ this.data[1]["cn"] = splits[0];
+ } else {
+ delete this.data[1]["cn"];
+ }
+ 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]["cn"] ? this.data[1]["cn"] : 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]["response"];
+ } else {
+ if (this.response != value) {
+ this.data[1]["response"] = value;
+ this.parent.changed(true);
+ }
+ }
+}
Added: CalendarServer/trunk/contrib/webpoll/webapp/js/jcal.js
===================================================================
--- 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)
@@ -0,0 +1,478 @@
+/**
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# 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 "AS IS" 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 = "-//calendarserver.org//jcal v1//EN";
+
+jcalparser = {
+ PARSER_ALLOW : 0, // Pass the "suspect" data through to the object model
+ PARSER_IGNORE : 1, // Ignore the "suspect" data
+ PARSER_FIX: 2, // Fix (or if not possible ignore) the "suspect" data
+ PARSER_RAISE: 3 // Raise an exception
+}
+
+// Some clients escape ":" - 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(["vcalendar", [], []]);
+ calendar.newProperty("version", "2.0");
+ calendar.newProperty("prodid", 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] != "vtimezone");
+ });
+ 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("uid", generateUUID());
+ jcomp.newProperty("dtstamp", new jcaldate().toString(), {}, "date-time");
+ }
+ 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 ? "text" : 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 = "text";
+ }
+ 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 = "utc";
+}
+
+jcaldate.jsDateTojCal = function(date) {
+ return date.toISOString().substr(0, 19) + "Z";
+}
+
+jcaldate.jsDateToiCal = function(date) {
+ return jcaldate.jsDateTojCal(date).replace(/\-/g, "").replace(/\:/g, "");
+}
+
+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) + "Z";
+}
+
+// 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 >= 0;
+
+ var remainder = seconds;
+ if (remainder < 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 ([+]/-) "P" (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] != "P")
+ throw "Invalid duration";
+ offset += 1;
+
+ // Look for time
+ if (data[offset] != "T") {
+ // Must have a number
+ var strnum = data.strtoul(offset);
+ var num = strnum.num;
+ offset = strnum.offset;
+
+ // Now look at character
+ if (data[offset] == "W") {
+ // 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 "Invalid duration";
+ }
+ return;
+ } else if (data[offset] == "D") {
+ // 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] != "T")
+ throw "Invalid duration";
+ } else {
+ // Error in format
+ throw "Invalid duration";
+ }
+ }
+
+ // 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 "Invalid duration";
+ else
+ return;
+ }
+ var strnum = data.strtoul(offset);
+ var num = strnum.num;
+ offset = strnum.offset;
+
+ // Look for hour
+ if (data[offset] == "H") {
+ // 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] == "M") {
+ // 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] == "S") {
+ // Get hours
+ this.mSeconds = num;
+ offset += 1;
+
+ // No more data - exit
+ if (offset == maxoffset)
+ return;
+ }
+
+ throw "Invalid duration";
+ } catch(err) {
+ throw "Invalid duration";
+ }
+}
+
+jcalduration.prototype.generate = function(self, os) {
+ var result = "";
+
+ if (!this.mForward && (this.mWeeks || this.mDays || this.mHours || this.mMinutes || this.mSeconds)) {
+ result += "-";
+ }
+ result += "P";
+
+ if (this.mWeeks != 0) {
+ result += this.mWeeks + "W";
+ } else {
+ if (this.mDays != 0) {
+ result += this.mDays + "D";
+ }
+
+ if (this.mHours != 0 || this.mMinutes != 0 || this.mSeconds != 0) {
+ result += "T";
+
+ if (this.mHours != 0) {
+ result += this.mHours + "H";
+ }
+
+ if ((this.mMinutes != 0) || ((this.mHours != 0) && (this.mSeconds != 0))) {
+ result += this.mMinutes + "M";
+ }
+
+ if (this.mSeconds != 0) {
+ result += this.mSeconds + "S";
+ }
+ } else if (this.mDays == 0) {
+ result += "T0S";
+ }
+ }
+
+ return result;
+}
Added: CalendarServer/trunk/contrib/webpoll/webapp/js/utils.js
===================================================================
--- 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)
@@ -0,0 +1,211 @@
+/**
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# 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 "AS IS" 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 "NS:" prefixes on
+// element names
+var gNamespaceShortcuts = {
+ "D" : "DAV:",
+ "C" : "urn:ietf:params:xml:ns:caldav",
+ "CS" : "http://calendarserver.org/ns/"
+};
+
+// Add a shortcut namespace for use in an xmlns map
+function addNamespace(shortcut, xmlnsmap) {
+ xmlnsmap[gNamespaceShortcuts[shortcut]] = shortcut;
+}
+
+function buildXMLNS(xmlnsmap) {
+ var xmlnstr = "";
+ $.each(xmlnsmap, function(ns, nsprefix) {
+ xmlnstr += ' xmlns:' + nsprefix + '="' + ns + '"';
+ });
+ return xmlnstr;
+}
+
+function addElements(elements, xmlnsmap) {
+ var propstr = "";
+ $.each(elements, function(index, element) {
+ var segments = element.split(":");
+ addNamespace(segments[0], xmlnsmap);
+ propstr += '<' + element + ' />';
+ });
+ return propstr;
+}
+
+// Find XML elements matching the specified xpath
+function findElementPath(node, path) {
+ return findElementPathSegments(node, path.split("/"));
+}
+
+// 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(":");
+ 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(/&(?!\w+([;\s]|$))/g, "&").replace(/</g, "<").replace(/>/g, ">");
+}
+
+// URL helpers
+
+// Removing any trailing slash from a URL
+function removeTrailingSlash(url) {
+ return (url[url.length - 1] == "/") ? url.substr(0, url.length - 1) : url;
+}
+
+// Removing any trailing slash from a URL
+function removeLeadingSlash(url) {
+ return (url[0] == "/") ? 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) + "/" + removeLeadingSlash(url2);
+}
+
+// Get last path segment
+function basenameURL(url) {
+ return removeTrailingSlash(url).split("/").pop();
+}
+
+// UUID
+
+function generateUUID() {
+ var result = "";
+ for ( var i = 0; i < 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 + " " : "") + "<" + addr + ">" : "";
+}
+
+function splitAddressDescription(desc) {
+ results = ["", ""];
+ if (desc.indexOf("<") == -1) {
+ results[1] = desc;
+ } else {
+ var splits = desc.split("<");
+ 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 "ValueError";
+ }
+ }
+}
+
+// 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;
+ };
+}
Added: CalendarServer/trunk/contrib/webpoll/webapp/js/webpoll.js
===================================================================
--- 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)
@@ -0,0 +1,956 @@
+/**
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# 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 "AS IS" 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() {
+
+ $("#progressbar").progressbar({
+ value : false
+ });
+ showLoading(true);
+
+ var params = {};
+ var ps = window.location.search.split(/\?|&/);
+ for (var i = 0; i < 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() {
+ $("#title").text($("#title").text() + " for User: " + gSession.currentPrincipal.cn);
+ gViewController.refreshed();
+ });
+
+ gViewController = new ViewController(gSession);
+});
+
+function showLoading(visible) {
+ if (visible) {
+ $("#progressbar").progressbar("enable");
+ $("#loading").show();
+ } else {
+ $("#progressbar").progressbar("disable");
+ $("#loading").hide();
+ }
+}
+
+// Handles all the view interactions
+ViewController = function(session) {
+ this.session = session;
+ this.ownedPolls = new PollList($("#sidebar-owned"), $("#sidebar-new-poll-count"));
+ this.voterPolls = new PollList($("#sidebar-voter"), $("#sidebar-vote-poll-count"));
+ 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
+ $("#sidebar").accordion({
+ heightStyle : "content"
+ });
+ $("#sidebar-owned").menu({
+ select : function(event, ui) {
+ view.clickSelectPoll(event, ui, true);
+ }
+ });
+ $("#sidebar-new-poll").button({
+ icons : {
+ primary : "ui-icon-plusthick"
+ }
+ }).click(function() {
+ view.clickAddPoll();
+ });
+
+ $("#sidebar-voter").menu({
+ select : function(event, ui) {
+ view.clickSelectPoll(event, ui, false);
+ }
+ });
+
+ $("#refresh-btn").button({
+ icons : {
+ primary : "ui-icon-refresh"
+ }
+ }).click(function() {
+ view.clickRefresh();
+ });
+
+
+ // Detail Panel
+ this.editSetVisible(false);
+ $("#editpoll-title-edit").focus(function() {
+ $(this).select();
+ });
+ $("#editpoll-tabs").tabs({
+ beforeActivate : function(event, ui) {
+ view.showResults(event, ui);
+ }
+ });
+ $("#editpoll-save").button({
+ icons : {
+ primary : "ui-icon-check"
+ }
+ }).click(function() {
+ view.clickPollSave();
+ });
+ $("#editpoll-cancel").button({
+ icons : {
+ primary : "ui-icon-close"
+ }
+ }).click(function() {
+ view.clickPollCancel();
+ });
+ $("#editpoll-done").button({
+ icons : {
+ primary : "ui-icon-arrowreturnthick-1-w"
+ }
+ }).click(function() {
+ view.clickPollCancel();
+ });
+ $("#editpoll-delete").button({
+ icons : {
+ primary : "ui-icon-trash"
+ }
+ }).click(function() {
+ view.clickPollDelete();
+ });
+ $("#editpoll-autofill").button({
+ icons : {
+ primary : "ui-icon-gear"
+ }
+ }).click(function() {
+ view.clickPollAutofill();
+ });
+ $("#editpoll-addevent").button({
+ icons : {
+ primary : "ui-icon-plus"
+ }
+ }).click(function() {
+ view.clickAddEvent();
+ });
+ $("#editpoll-addvoter").button({
+ icons : {
+ primary : "ui-icon-plus"
+ }
+ }).click(function() {
+ view.clickAddVoter();
+ });
+
+ $("#editpoll-autofill").hide();
+ $("#response-key").hide();
+ $("#response-menu").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 && this.activePoll.editing_poll.changed()) {
+ alert("Save or cancel the current poll changes first");
+ 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 = $("#editpoll-tabs").tabs("option", "active");
+ 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);
+ $("#editpoll-tabs").tabs("option", "active", 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("New Poll")));
+ this.isNewPoll = true;
+ $("#editpoll-title-edit").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) {
+ $("#editpoll-title-edit").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(".voter-address").focus();
+}
+
+// Toggle display of poll details
+ViewController.prototype.editSetVisible = function(visible) {
+
+ if (visible) {
+
+ if (this.isNewPoll) {
+ $("#editpoll-delete").hide();
+ $("#editpoll-tabs").tabs("disable", 2);
+ } else {
+ $("#editpoll-delete").show();
+ $("#editpoll-tabs").tabs("enable", 2);
+ }
+ if (this.activePoll.owned && this.activePoll.resource.object.mainComponent().editable()) {
+ $("#editpoll-title-panel").hide();
+ $("#editpoll-organizer-panel").hide();
+ $("#editpoll-status-panel").hide();
+ $("#editpoll-title-edit-panel").show();
+ $("#editpoll-tabs").tabs("enable", 0);
+ $("#editpoll-tabs").tabs("enable", 1);
+ $("#editpoll-tabs").tabs("option", "active", 0);
+ $("#response-key").hide();
+ } else {
+ $("#editpoll-title-edit-panel").hide();
+ $("#editpoll-title-panel").show();
+ $("#editpoll-organizer-panel").show();
+ $("#editpoll-status-panel").show();
+ $("#editpoll-tabs").tabs("option", "active", 2);
+ $("#editpoll-tabs").tabs("disable", 0);
+ $("#editpoll-tabs").tabs("disable", 1);
+ $("#response-key").toggle(this.activePoll.resource.object.mainComponent().editable());
+ this.activePoll.buildResults();
+ }
+
+ $("#editpoll-save").toggle(this.activePoll.resource.object.mainComponent().editable());
+ $("#editpoll-cancel").toggle(this.activePoll.resource.object.mainComponent().editable());
+ $("#editpoll-done").toggle(!this.activePoll.resource.object.mainComponent().editable());
+ $("#editpoll-autofill").toggle(this.activePoll.resource.object.mainComponent().editable());
+
+ $("#detail-nocontent").hide();
+ $("#editpoll").show();
+ } else {
+ $("#editpoll").hide();
+ $("#detail-nocontent").show();
+ }
+}
+
+ViewController.prototype.refreshed = function() {
+ showLoading(false);
+ if (this.ownedPolls.polls.length == 0 && this.voterPolls.polls.length != 0) {
+ $("#sidebar").accordion("option", "active", 1);
+ } else {
+ $("#sidebar").accordion("option", "active", 0);
+ }
+}
+
+// Rebuild results panel each time it is selected
+ViewController.prototype.showResults = function(event, ui) {
+ if (ui.newPanel.selector == "#editpoll-results") {
+ this.activePoll.buildResults();
+ }
+ $("#editpoll-autofill").toggle(ui.newPanel.selector == "#editpoll-results");
+ $("#response-key").toggle(ui.newPanel.selector == "#editpoll-results");
+}
+
+// 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('<li class="sidebar-list"><a href="#">' + poll.title() + '</a></li>');
+ this.menu.menu("refresh");
+ 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();
+ $("#editpoll-delete").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("a").eq(index).text(poll.title());
+ this.menu.menu("refresh");
+ 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("li").eq(index).remove();
+ this_polllist.menu.menu("refresh");
+ 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("refresh");
+ 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
+ $("#editpoll-title-edit").val(this.editing_poll.summary());
+ $("#editpoll-title").text(this.editing_poll.summary());
+ $("#editpoll-organizer").text(this.editing_poll.organizerDisplayName());
+ $("#editpoll-status").text(this.editing_poll.status());
+ $("#editpoll-eventlist").empty();
+ $.each(this.editing_poll.events(), function(index, event) {
+ this_poll.setEventPanel(this_poll.addEventPanel(), event);
+ });
+ $("#editpoll-voterlist").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($("#editpoll-title-edit").val());
+
+ var events = this.editing_poll.events();
+ $("#editpoll-eventlist").children().each(function(index) {
+ this_poll.updateEventFromPanel($(this), events[index]);
+ });
+
+ var voters = this.editing_poll.voters();
+ $("#editpoll-voterlist").children().each(function(index) {
+ this_poll.updateVoterFromPanel($(this), voters[index]);
+ });
+ }
+}
+
+//Add a new event item in the UI
+Poll.prototype.addEventPanel = function() {
+
+ var ctr = $("#editpoll-eventlist").children().length + 1;
+ var idstart = "event-dtstart-" + ctr;
+ var idend = "event-dtend-" + ctr;
+
+ // Add new list item
+ var evt = '<div class="event">';
+ evt += '<div class="edit-datetime">';
+ evt += '<label for="' + idstart + '">Start: </label>';
+ evt += '<input type="text" id="' + idstart + '" class="event-dtstart"/>';
+ evt += '</div>';
+ evt += '<div class="edit-datetime">';
+ evt += '<label for="' + idend + '">End: </label>';
+ evt += '<input type="text" id="' + idend + '" class="event-dtend" />';
+ evt += '</div>';
+ evt += '<button class="input-remove">Remove</button>';
+ evt += '</div>';
+ evt = $(evt).appendTo("#editpoll-eventlist");
+
+ evt.find(".event-dtstart").datetimepicker();
+ evt.find(".event-dtend").datetimepicker();
+ evt.find(".input-remove").button({
+ icons : {
+ primary : "ui-icon-close"
+ }
+ });
+
+ return evt;
+}
+
+// Update the UI for this event
+Poll.prototype.setEventPanel = function(panel, event) {
+ panel.find(".event-dtstart").datetimepicker("setDate", event.dtstart());
+ panel.find(".event-dtend").datetimepicker("setDate", event.dtend());
+ return panel;
+}
+
+// Get details of the event from the UI
+Poll.prototype.updateEventFromPanel = function(panel, event) {
+ event.summary($("#editpoll-title-edit").val());
+ event.dtstart(panel.find(".event-dtstart").datetimepicker("getDate"));
+ event.dtend(panel.find(".event-dtend").datetimepicker("getDate"));
+}
+
+// Add event button clicked
+Poll.prototype.addEvent = function() {
+
+ var ctr = $("#editpoll-eventlist").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 = $("#editpoll-voterlist").children().length + 1;
+ var idvoter = "voter-address-" + ctr;
+
+ // Add new list item
+ var vtr = '<div class="voter">';
+ vtr += '<div class="edit-voter">';
+ vtr += '<label for="' + idvoter + '">Voter: </label>';
+ vtr += '<input type="text" id="' + idvoter + '" class="voter-address"/>';
+ vtr += '</div>';
+ vtr += '<button class="input-remove">Remove</button>';
+ vtr += '</div>';
+ vtr = $(vtr).appendTo("#editpoll-voterlist");
+
+ vtr.find(".voter-address").autocomplete({
+ minLength : 3,
+ source : function(request, response) {
+ gSession.calendarUserSearch(request.term, function(results) {
+ response(results);
+ });
+ }
+ }).focus(function() {
+ $(this).select();
+ });
+
+ vtr.find(".input-remove").button({
+ icons : {
+ primary : "ui-icon-close"
+ }
+ });
+
+ return vtr;
+}
+
+// Update UI for this voter
+Poll.prototype.setVoterPanel = function(panel, voter) {
+ panel.find(".voter-address").val(voter.addressDescription());
+ return panel;
+}
+
+// Get details of the voter from the UI
+Poll.prototype.updateVoterFromPanel = function(panel, voter) {
+ voter.addressDescription(panel.find(".voter-address").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 = $("#editpoll-resulttable").children("thead").first();
+ var th_date = thead.children("tr").eq(0).empty();
+ var th_start = thead.children("tr").eq(1).empty();
+ var th_end = thead.children("tr").eq(2).empty();
+ var tf = $("#editpoll-resulttable").children("tfoot").first();
+ var tf_overall = tf.children("tr").first().empty();
+ var tf_commit = tf.children("tr").last().empty();
+ tf_commit.toggle(this.owned || !this_poll.editing_poll.editable());
+ var tbody = $("#editpoll-resulttable").children("tbody").first().empty();
+ $('<td>Date:</td>').appendTo(th_date);
+ $('<td>Start:</td>').appendTo(th_start);
+ $('<td>End:</td>').appendTo(th_end);
+ $('<td>Overall:</td>').appendTo(tf_overall);
+ $('<td />').appendTo(tf_commit);
+ $.each(event_details, function(index, event) {
+ var td_date = $('<td />').appendTo(th_date).text(event.dtstart().toDateString()).addClass("center-td");
+ var td_start = $('<td />').appendTo(th_start).text(event.dtstart().toLocaleTimeString());
+ var td_end = $('<td />').appendTo(th_end).text(event.dtend().toLocaleTimeString());
+ $('<td />').appendTo(tf_overall).addClass("center-td");
+ $('<td />').appendTo(tf_commit).addClass("center-td");
+ if (event.ispollwinner()) {
+ td_date.addClass("poll-winner-td");
+ td_start.addClass("poll-winner-td");
+ td_end.addClass("poll-winner-td");
+ }
+ 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 = $("<tr/>").appendTo(tbody);
+ $("<td/>").appendTo(tr).text(voter.nameOrAddress());
+ $.each(event_details, function(index, event) {
+ var response = event.voter_responses()[voter.cuaddr()];
+ var td = $("<td />").appendTo(tr).addClass("center-td");
+ if (event.ispollwinner()) {
+ td.addClass("poll-winner-td");
+ }
+ if (active && this_poll.editing_poll.editable()) {
+ var radios = $('<div id="response-' + index + '" />').appendTo(td).addClass("response-btns");
+ $('<input type="radio" id="respond_no-' + index + '" name="response-' + index + '"/>').appendTo(radios);
+ $('<label for="respond_no-' + index + '"/>').appendTo(radios);
+ $('<input type="radio" id="respond_maybe-' + index + '" name="response-' + index + '"/>').appendTo(radios);
+ $('<label for="respond_maybe-' + index + '"/>').appendTo(radios);
+ $('<input type="radio" id="respond_ok-' + index + '" name="response-' + index + '"/>').appendTo(radios);
+ $('<label for="respond_ok-' + index + '"/>').appendTo(radios);
+ $('<input type="radio" id="respond_best-' + index + '" name="response-' + index + '"/>').appendTo(radios);
+ $('<label for="respond_best-' + index + '"/>').appendTo(radios);
+ radios.buttonset();
+ if (response !== undefined) {
+ if (response < 40) {
+ $('#respond_no-' + index).click();
+ } else if (response < 80) {
+ $('#respond_maybe-' + index).click();
+ } else if (response < 90) {
+ $('#respond_ok-' + index).click();
+ } else {
+ $('#respond_best-' + index).click();
+ }
+ }
+ $('#respond_no-' + index).button({
+ icons : {
+ primary : "ui-icon-close"
+ },
+ text: false
+ }).click(this_poll.clickResponse);
+ $('#respond_maybe-' + index).button({
+ icons : {
+ primary : "ui-icon-help"
+ },
+ text: false
+ }).click(this_poll.clickResponse);
+ $('#respond_ok-' + index).button({
+ icons : {
+ primary : "ui-icon-check"
+ },
+ text: false
+ }).click(this_poll.clickResponse);
+ $('#respond_best-' + index).button({
+ icons : {
+ primary : "ui-icon-circle-check"
+ },
+ text: false
+ }).click(this_poll.clickResponse);
+ } else {
+ td.text(this_poll.textForResponse(response)[0]);
+ }
+ });
+ if (active) {
+ tr.addClass("active-voter");
+ }
+ });
+
+ $.each(event_details, function(index, event) {
+ if (this_poll.editing_poll.editable()) {
+ $('<button id="winner-' + index + '">Pick Winner</button>').appendTo(tf_commit.children()[index + 1]).button({
+ icons : {
+ primary : "ui-icon-star"
+ },
+ }).click(this_poll.clickWinner);
+ } else {
+ if (event.ispollwinner()) {
+ $(tf_commit.children()[index + 1]).addClass("poll-winner-td");
+ $('<div id="winner-text"><span id="winner-icon-left" class="ui-icon ui-icon-star" />Winner<span id="winner-icon-right" class="ui-icon ui-icon-star" /></div>').appendTo(tf_commit.children()[index + 1]);
+ }
+ }
+ });
+
+ this.updateOverallResults();
+}
+
+Poll.prototype.textForResponse = function(response) {
+ var result = [];
+ if (response === undefined) {
+ result.push("No Response");
+ result.push("no-response-td");
+ } else if (response < 40) {
+ result.push("No");
+ result.push("no-td");
+ } else if (response < 80) {
+ result.push("Maybe");
+ result.push("maybe-td");
+ } else if (response < 90) {
+ result.push("Ok");
+ result.push("ok-td");
+ } else {
+ result.push("Best");
+ result.push("best-td");
+ }
+ return result;
+}
+
+Poll.prototype.clickResponse = function() {
+ var splits = $(this).attr("id").split("-");
+ var response_type = splits[0];
+ var index = parseInt(splits[1]);
+ var response = 0;
+ if (response_type == "respond_maybe") {
+ response = 50;
+ } else if (response_type == "respond_ok") {
+ response = 85;
+ } else if (response_type == "respond_best") {
+ 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("id").split("-");
+ 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 = $('<div id="hover-cal" />').appendTo(td_date).dialog({
+ dialogClass: "no-close",
+ position: { my: "left top", at: "right+20 top", of: thead },
+ show: "fade",
+ title: "Your Events for " + 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 = "";
+ 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() + " - ";
+ text += result.dtend().toLocaleTimeString() + " : ";
+ text += result.summary();
+ if (last_end !== null && last_end.getTime() != result.dtstart().getTime()) {
+ relative_offset += 10;
+ }
+ last_end = result.dtend();
+ $('<div class="hover-event ui-corner-all" style="top:' + relative_offset + 'px"/>').appendTo(dialog_div).addClass(result.pollitemid() !== null ? "ui-state-active" : "ui-state-default").text(text);
+ });
+ }
+ );
+}
+
+// Open the event time-range hover dialog
+Poll.prototype.hoverDialogOpenFancy = function(td_date, thead, event) {
+ var dialog_div = $('<div id="hover-cal" />').appendTo(td_date).dialog({
+ dialogClass: "no-close",
+ position: { my: "left top", at: "right+20 top", of: thead },
+ show: "fade",
+ title: "Your Events for " + 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 = $('<table id="hover-grid" />').appendTo(dialog_div);
+ for(var i = startHour; i < endHour; i++) {
+ var text = i > 12 ? i - 12 +":00 pm" : i + ":00 am";
+ $('<tr><td class="hover-grid-td-time">' + text + '</td><td class="hover-grid-td-slot" /></tr>').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 = "top:" + top_offset + "px;height:" + height + "px";
+ if (last_dtend !== null && last_dtend > result.dtstart()) {
+ styles += ";left:206px;width:125px";
+ }
+ last_dtend = result.dtend();
+ $('<div class="hover-event ui-corner-all" style="' + styles + '" />').appendTo(grid).addClass(result.pollitemid() !== null ? "ui-state-active" : "ui-state-default").text(result.summary());
+ });
+ }
+ );
+}
+
+Poll.prototype.hoverDialogOpen = Poll.prototype.hoverDialogOpenFancy;
+
+// Close the event time-range hover dialog
+Poll.prototype.hoverDialogClose = function() {
+ $("#hover-cal").dialog("close").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 = $("#editpoll-resulttable").children("tfoot").first().children("tr").first().children("td");
+
+ // 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 = ["best-td", "ok-td", "maybe-td", "no-td", "no-response-td"];
+ possible_classes.splice(possible_classes.indexOf(response_details[1]), 1);
+ $(tds[index + 1]).text(response_details[0]);
+ $(tds[index + 1]).removeClass(possible_classes.join(" "));
+ $(tds[index + 1]).addClass(event.ispollwinner() ? "poll-winner-td" : 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 ? "#respond_no-" : "#respond_ok-") + index).click();
+ }
+ );
+ });
+}
+
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/3ae74e8d/attachment.html>
More information about the calendarserver-changes
mailing list