You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@vcl.apache.org by jf...@apache.org on 2014/09/11 18:01:49 UTC

svn commit: r1624325 [7/13] - in /vcl/trunk/web: ./ .ht-inc/ .ht-inc/authmethods/ css/ js/ js/resources/

Added: vcl/trunk/web/.ht-inc/resource.php
URL: http://svn.apache.org/viewvc/vcl/trunk/web/.ht-inc/resource.php?rev=1624325&view=auto
==============================================================================
--- vcl/trunk/web/.ht-inc/resource.php (added)
+++ vcl/trunk/web/.ht-inc/resource.php Thu Sep 11 16:01:48 2014
@@ -0,0 +1,1640 @@
+<?php
+/*
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You 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.
+*/
+
+////////////////////////////////////////////////////////////////////////////////
+///
+/// \fn resource($type)
+///
+/// \param $type - type of resource
+///
+/// \brief wrapper to create appropriate object and call selectionText for that
+/// object
+///
+////////////////////////////////////////////////////////////////////////////////
+function resource($type) {
+	switch($type) {
+		case 'config':
+			$obj = new Config();
+			break;
+		case 'image':
+			$obj = new Image();
+			break;
+		case 'computer':
+			$obj = new Computer();
+			break;
+		case 'managementnode':
+			$obj = new ManagementNode();
+			break;
+		case 'schedule':
+			$obj = new Schedule();
+			break;
+	}
+
+	$html = $obj->selectionText();
+	print $html;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+///
+/// \class Resource
+///
+/// \brief base class for all resources; provides functionality useful to all
+/// types of resources
+///
+////////////////////////////////////////////////////////////////////////////////
+class Resource {
+	var $restype;
+	var $restypename;
+	var $hasmapping;
+	var $maptype;
+	var $maptypename;
+	var $basecdata;
+	var $defaultGetDataArgs;
+	var $deletable;
+	var $deletetoggled;
+	var $errmsg;
+	var $namefield;
+	var $noadd;
+	var $jsondata;
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn __construct()
+	///
+	/// \brief class construstor
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function __construct() {
+		# defines if resource type is mapped to another type
+		$this->hasmapping = 0;
+		# type of resource this resource maps to
+		$this->maptype = '';
+		# display name for $this->maptype
+		$this->maptypename = '';
+		# can this resource type be deleted
+		$this->deletable = 1;
+		# can this resource type be flagged as deleted
+		$this->deletetoggled = 1;
+		# array of arguments and default values that should be passed to getData function
+		$this->defaultGetDataArgs = array();
+		# base data for continuations
+		$this->basecdata = array('obj' => $this);
+		# field in database table used for the name of this resource type
+		$this->namefield = 'name';
+		# can this resource have new resources directly added
+		$this->addable = 1;
+		# base data for sending JSON response to AJAX call; this allows an
+		#   inheriting function to set some additional data before calling base
+		#   function
+		$this->jsondata = array();
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn getData($args)
+	///
+	/// \param $args - array of arguments that determine what data gets returned
+	///
+	/// \return empty array
+	///
+	/// \brief stub function; each inheriting class should implement this
+	/// function
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function getData($args) {
+		return array();
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn selectionText()
+	///
+	/// \brief generates HTML to select what management function to perform
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function selectionText() {
+		global $user;
+		# get a count of resources user can administer
+		$tmp = getUserResources(array("{$this->restype}Admin"), array("administer"));
+		$resAdminCnt = count($tmp[$this->restype]);
+
+		# get a count of resources user has access to
+		$tmp = getUserResources(array("{$this->restype}Admin"), array("available"));
+		$resCnt = count($tmp[$this->restype]);
+
+		# get a count of resource groups user can manage
+		$tmp = getUserResources(array("{$this->restype}Admin"), array("manageGroup"), 1);
+		$resGroupCnt = count($tmp[$this->restype]);
+
+		if($this->hasmapping) {
+			# get a count of $restype groups and $maptype groups user can map
+			$tmp = getUserResources(array("{$this->restype}Admin"), array("manageMapping"), 1);
+			$resMapCnt = count($tmp[$this->restype]);
+			$tmp = getUserResources(array("{$this->maptype}Admin"), array("manageMapping"), 1);
+			$maptypeMapCnt = count($tmp[$this->maptype]);
+		}
+		else {
+			$resMapCnt = 0;
+			$maptypeMapCnt = 0;
+		}
+
+		$h = '';
+
+		$h .= "<H2>Manage {$this->restypename}s</H2>\n";
+		$h .= "<FORM action=\"" . BASEURL . SCRIPT . "\" method=post>\n";
+		$showsubmit = 0;
+		if(in_array("{$this->restype}Admin", $user["privileges"]) &&
+		   ($this->addable == 1 || $resAdminCnt)) {
+			$cont = addContinuationsEntry("viewResources", $this->basecdata);
+			$h .= "<INPUT type=radio name=continuation value=\"$cont\" checked ";
+			$h .= "id=\"{$this->restype}edit\"><label for=\"{$this->restype}edit\">Edit ";
+			$h .= "{$this->restypename} Profiles</label><br>\n";
+			$showsubmit = 1;
+		}
+
+		if(($resAdminCnt && $resGroupCnt) || ($resMapCnt && $maptypeMapCnt)) {
+			if($this->hasmapping)
+				$label = "Edit Grouping &amp; Mapping";
+			else
+				$label = "Edit Grouping";
+			$cont = addContinuationsEntry("groupMapHTML", $this->basecdata);
+			$h .= "<INPUT type=radio name=continuation value=\"$cont\" id=\"";
+			$h .= "resgroupmap\"><label for=\"resgroupmap\">$label</label><br>\n";
+			$showsubmit = 1;
+		}
+
+		if($resAdminCnt)
+			$h .= $this->extraSelectAdminOptions();
+
+		if($showsubmit)
+			$h .= "<br><INPUT type=submit value=Submit>\n";
+		else
+			$h .= "You don't have access to manage any {$this->restype}s.<br>\n";
+		$h .= "</FORM>\n";
+
+		return $h;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn viewResources()
+	///
+	/// \brief prints a page to view resource information
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function viewResources() {
+		global $user, $mode;
+		$h = '';
+		$h .= "<h2>{$this->restypename} Profiles</h2>\n";
+
+		$resdata = $this->getData($this->defaultGetDataArgs);
+		if(! empty($resdata)) {
+			$tmp = array_keys($resdata);
+			$testid = $tmp[0];
+			$fields = array_keys($resdata[$testid]);
+		}
+
+		# hidden elements
+		$cdata = $this->basecdata;
+		$cdata['add'] = 1;
+		$cont = addContinuationsEntry('AJsaveResource', $cdata);
+		$h .= "<input type=\"hidden\" id=\"addresourcecont\" value=\"$cont\">\n";
+		if(! empty($resdata)) {
+			$h .= "<input type=\"hidden\" id=\"saveresourcecont\">\n";
+			$cont = addContinuationsEntry('AJeditResource', $this->basecdata);
+			$h .= "<input type=\"hidden\" id=\"editresourcecont\" value=\"$cont\">\n";
+			if($this->deletable) {
+				$cont = addContinuationsEntry('AJpromptToggleDeleteResource', $this->basecdata);
+				$h .= "<input type=\"hidden\" id=\"deleteresourcecont\" value=\"$cont\">\n";
+			}
+			$cont = addContinuationsEntry('jsonResourceStore', $this->basecdata);
+			$h .= "<div dojoType=\"dojo.data.ItemFileWriteStore\" url=\"" . BASEURL;
+			$h .= SCRIPT . "?continuation=$cont\" jsid=\"resourcestore\" ";
+			$h .= "comparatorMap=\"\{\}\"></div>\n";
+			/*$h .= "<div dojoType=\"dojo.data.ItemFileWriteStore\"\n";
+			$h .= "data=\"{'identifier':'id', 'label':'name', 'items':[]}\" \n";
+			$h .= "jsid=\"affiliationstore\"></div>\n";
+			$h .= "<div dojoType=\"dojo.data.ItemFileWriteStore\"\n";
+			$h .= "data=\"{'identifier':'id', 'label':'name', 'items':[]}\" \n";
+			$h .= "jsid=\"ownerstore\"></div>\n";
+			$h .= "<div dojoType=\"dojo.data.ItemFileWriteStore\"\n";
+			$h .= "data=\"{'identifier':'id', 'label':'name', 'items':[]}\" \n";
+			$h .= "jsid=\"editgroupstore\"></div>\n";*/
+		}
+
+		if($this->addable)
+			$h .= dijitButton('', "Add New {$this->restypename}", "addNewResource('Add {$this->restypename}');");
+
+		if(empty($resdata)) {
+			$h .= "<br><br>(No {$this->restypename}s found to which you have access.)\n";
+			$cont = addContinuationsEntry("viewResources", $this->basecdata);
+			$url = BASEURL . SCRIPT . "?continuation=$cont";
+			$h .= "<input type=\"hidden\" id=\"reloadpageurl\" value=\"$url\">\n";
+		}
+
+		$h .= $this->addEditDialogHTML(0);
+
+		if(empty($resdata)) {
+			print $h;
+			return;
+		}
+
+		# filters
+		$h .= "<div dojoType=\"dijit.TitlePane\" title=\"Filters (click to expand)\" ";
+		$h .= "open=\"false\">\n";
+		$h .= "<span id=\"namefilter\">\n";
+		$h .= "<strong>Name</strong>:\n";
+		$h .= "<div dojoType=\"dijit.form.TextBox\" id=\"namefilter\" length=\"80\">";
+		$h .= "  <script type=\"dojo/connect\" event=\"onKeyUp\" args=\"event\">\n";
+		$h .= "    if(event.keyCode == 13) resource.GridFilter();\n";
+		$h .= "  </script>\n";
+		$h .= "</div>\n";
+
+		$h .= dijitButton('', "Apply Name Filter", "resource.GridFilter();");
+		$h .= "<br>\n";
+
+		$h .= "</span>\n"; # namefilter
+		$h .= "<strong>Displayed Fields</strong>:<br>\n";
+		$h .= $this->addDisplayCheckboxes($fields, $resdata[$testid]);
+		if($this->deletetoggled) {
+			$h .= "<label for=\"showdeleted\"><strong>Include Deleted ";
+			$h .= "{$this->restypename}s:</strong>:</label>\n";
+			$h .= "<input type=\"checkbox\" dojoType=\"dijit.form.CheckBox\" ";
+			$h .= "id=\"showdeleted\" onChange=\"resource.GridFilter();\">\n";
+		}
+		$h .= "</div>\n";
+
+		$h .= $this->extraResourceFilters();
+
+		$h .= "<div id=\"gridcontainer\">\n";
+		$h .= "<table dojoType=\"dojox.grid.DataGrid\" jsId=\"resourcegrid\" ";
+		$h .= "sortInfo=3 store=\"resourcestore\" autoWidth=\"true\" style=\"";
+		#$h .= "height: 580px;\" query=\"{type: new RegExp('normal|federated|courseroll')}\">\n";
+		if($this->deletetoggled)
+			$h .= "height: 580px;\" query=\"{deleted: '0'}\">\n";
+		else
+			$h .= "height: 580px;\">\n";
+		$h .= "<thead>\n";
+		$h .= "<tr>\n";
+		if(preg_match('/MSIE/i', $_SERVER['HTTP_USER_AGENT']))
+			$w = array('64px', '38px', '200px', '142px', '65px', '142px', '59px', '58px', '63px', '73px');
+		else
+			$w = array('5em', '3em', '17em', '12em', '5em', '12em', '5em', '5em', '5.6em', '6.3em');
+		$h .= "<th field=\"id\" id=\"delcolth\" width=\"{$w[0]}\" formatter=\"resource.DeleteBtn\" styles=\"text-align: center;\">&nbsp;</th>\n";
+		$h .= "<th field=\"id\" width=\"{$w[1]}\" formatter=\"resource.EditBtn\" styles=\"text-align: center;\">&nbsp;</th>\n";
+		$h .= "<th field=\"name\" width=\"{$w[2]}\">Name</th>\n";
+		$h .= "<th field=\"owner\" width=\"{$w[3]}\">Owner</th>\n";
+		foreach($fields as $field) {
+			if($field == $this->namefield ||
+			   $field == 'name' ||
+			   $field == 'owner' ||
+			   is_array($resdata[$testid][$field]) ||
+			   preg_match('/id$/', $field))
+				continue;
+			$h .= "<th field=\"$field\" hidden=\"true\" formatter=\"resource.colformatter\">";
+			$h .= $this->fieldDisplayName($field);
+			$h .= "</th>\n";
+		}
+		$h .= "</tr>\n";
+		$h .= "</thead>\n";
+		$h .= "</table>\n";
+		$h .= "</div>\n";
+
+		if($this->deletable) {
+			# toggle delete dialog
+			$h .= "<div id=\"toggleDeleteDialog\" dojoType=\"dijit.Dialog\">\n";
+			$h .= "<h2 id=\"toggleDeleteHeading\"></h2>\n";
+			$h .= "<span id=\"toggleDeleteQuestion\"></span><br>\n";
+			$h .= "<div id=\"confdelrescontent\"></div>\n";
+			$h .= dijitButton('toggleDeleteBtn', "Delete {$this->restypename}", "submitToggleDeleteResource();");
+			$h .= dijitButton('', "Cancel", "clearHideConfirmDelete();");
+			$h .= "<input type=hidden id=\"submitdeletecont\">\n";
+			$h .= "</div>\n";
+		}
+
+		print $h;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn addDisplayCheckboxes($allfields, $sample)
+	///
+	/// \param $allfields - array of fields for which to generate checkboxes
+	/// \param $sample - sample data item that is used to determine what fields
+	/// to use for generating checkboxes
+	///
+	/// \return html
+	///
+	/// \brief generates checkboxes to be used as filters on the viewResources
+	/// page
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function addDisplayCheckboxes($allfields, $sample) {
+		$fields = array('owner');
+		foreach($allfields as $field) {
+			if($field == $this->namefield ||
+			   $field == 'name' ||
+			   $field == 'owner' ||
+			   is_array($sample[$field]) ||
+			   preg_match('/id$/', $field))
+				continue;
+			$fields[] = $field;
+		}
+		$h = '';
+		$fieldcnt = count($fields);
+		$cols = $fieldcnt / 4;
+		if($cols > 4)
+			$cols = 4;
+		if($fieldcnt < 6) {
+			foreach($fields as $field) {
+				if($field == 'name' || $field == 'owner')
+					$h .= "<input type=checkbox id=chk$field checked onClick=\"resource.toggleResFieldDisplay(this, '$field')\">";
+				else
+					$h .= "<input type=checkbox id=chk$field onClick=\"resource.toggleResFieldDisplay(this, '$field')\">";
+				$h .= "<label for=chk$field>";
+				$h .= $this->fieldDisplayName($field);
+				$h .= "</label><br>\n";
+			}
+		}
+		else {
+			$h .= "<table>\n";
+			$cnt = 0;
+			foreach($fields as $field) {
+				$mod = $cols;
+				if($cnt % $mod == 0)
+					$h .= "<tr>\n";
+				if($field == 'name' || $field == 'owner')
+					$h .= "  <td><input type=checkbox id=chk$field checked onClick=\"resource.toggleResFieldDisplay(this, '$field')\">";
+				else
+					$h .= "  <td><input type=checkbox id=chk$field onClick=\"resource.toggleResFieldDisplay(this, '$field')\">";
+				$h .= "<label for=chk$field>";
+				$h .= $this->fieldDisplayName($field);
+				$h .= "</label><br>\n";
+				$cnt++;
+				if($cnt % $mod == 0)
+					$h .= "</tr>\n";
+			}
+			if($cnt % $mod != 0)
+				$h .= "</tr>\n";
+			$h .= "</table>\n";
+		}
+		return $h;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn fieldDisplayName($field)
+	///
+	/// \param $field - name of a resource field
+	///
+	/// \return display value for $field
+	///
+	/// \brief generates the display value for $field; this base function just
+	/// uppercases the first letter; each inheriting class should create its own
+	/// function
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function fieldDisplayName($field) {
+		return ucfirst($field);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn extraResourceFilters()
+	///
+	/// \return empty string
+	///
+	/// \brief base function; allows inheriting classes to generate additional
+	/// filters to be displayed on the viewResources page
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function extraResourceFilters() {
+		return '';
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn jsonResourceStore()
+	///
+	/// \brief generates and sends a JSON formatted store of resource data
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function jsonResourceStore() {
+		global $user;
+		$args = $this->defaultGetDataArgs;
+		$args['includedeleted'] = 1;
+		$resdata = $this->getData($args);
+		$resources = getUserResources(array($this->restype . "Admin"), array("administer"), 0, 1);
+		foreach($resources as $type => $tmp) {
+			if($type != $this->restype)
+				unset($resources[$type]);
+		}
+
+
+		// this method may include fields for some records but not others
+		/*$items = array();
+		foreach($resdata as $id => $res) {
+			if(! array_key_exists($id, $resources[$this->restype]))
+				continue;
+			$g = array('id' => $id);
+			$g['name'] = $res[$this->namefield];
+			$g['owner'] = $res['owner'];
+			foreach($res as $key => $val) {
+				if($key == 'name' ||
+				   $key == 'owner' ||
+				   $key == $this->namefield ||
+				   is_array($val) ||
+				   preg_match('/id$/', $key))
+					continue;
+				$g[$key] = $val;
+			}
+			$items[] = $g;
+		}
+		return $items;*/
+
+
+
+		// this method only includes keys that exist in the first element
+		reset($resdata);
+		$id = key($resdata);
+		$fields = array_keys($resdata[$id]);
+		$items = array();
+		foreach($resdata as $id => $res) {
+			if(! array_key_exists($id, $resources[$this->restype]))
+				continue;
+			$item = array('id' => $id);
+			$item['name'] = $res[$this->namefield];
+			$item['owner'] = $res['owner'];
+			foreach($fields as $field) {
+				if($field == 'name' ||
+				   $field == 'owner' ||
+				   $field == $this->namefield ||
+				   ! array_key_exists($field, $res) ||
+				   is_array($res[$field]) ||
+				   preg_match('/id$/', $field))
+					continue;
+				$item[$field] = $res[$field];
+			}
+			$items[] = $item;
+			unset($resdata[$id]);
+		}
+		sendJSON($items, 'id');
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn toggleDeleteResource($rscid)
+	///
+	/// \param $rscid - id of a resource (from table specific to that resource,
+	/// not from the resource table)
+	///
+	/// \return 1 on success, 0 on failure
+	///
+	/// \brief if resource type allows resources to be flagged as deleted;
+	/// toggles the deleted flag; otherwise deletes the entry from that resources
+	/// table and the general resource table
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function toggleDeleteResource($rscid) {
+		if($this->deletetoggled) {
+			$query = "SELECT deleted "
+			       . "FROM `{$this->restype}` "
+			       . "WHERE id = $rscid";
+			$qh = doQuery($query);
+			if($row = mysql_fetch_assoc($qh)) {
+				$newval = (int)(! (int)$row['deleted']);
+				$query = "UPDATE {$this->restype} "
+				       . "SET deleted = $newval "
+				       . "WHERE id = $rscid";
+				doQuery($query);
+				$this->submitToggleDeleteResourceExtra($rscid, $row['deleted']);
+			}
+			else
+				return 0;
+		}
+		else {
+			$query = "DELETE r "
+			       . "FROM resource r, "
+			       .      "resourcetype rt "
+			       . "WHERE r.resourcetypeid = rt.id AND "
+			       .       "rt.name = '{$this->restype}' AND "
+			       .       "r.subid = $rscid";
+			doQuery($query);
+			$query = "DELETE FROM `{$this->restype}` "
+			       . "WHERE id = $rscid";
+			doQuery($query);
+
+			$this->submitToggleDeleteResourceExtra($rscid);
+		}
+
+		# clear user resource cache for this type
+		$key = getKey(array(array($this->restype . "Admin"), array("administer"), 0, 1, 0));
+		unset($_SESSION['userresources'][$key]);
+		$key = getKey(array(array($this->restype . "Admin"), array("administer"), 0, 0, 0));
+		unset($_SESSION['userresources'][$key]);
+
+		return 1;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn AJpromptToggleDeleteResource()
+	///
+	/// \brief generates and sends content prompting user to confirm deletion of
+	/// resource
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function AJpromptToggleDeleteResource() {
+		$rscid = processInputVar('rscid', ARG_NUMERIC);
+		# check access to $rscid
+		$resources = getUserResources(array("{$this->restype}Admin"), array("administer"), 0, 1);
+		if(! array_key_exists($rscid, $resources[$this->restype])) {
+			$type = strtolower($this->restypename);
+			$rt = array('status' => 'noaccess',
+			            'msg' => "You do not have access to delete the selected $type.",
+			            'rscid' => $rscid);
+			sendJSON($rt);
+			return;
+		}
+		$rt = array('html' => '',
+		            'title' => "Confirm Delete {$this->restypename}",
+		            'question' => "Delete the following {$this->restype}?",
+		            'btntxt' => "Delete {$this->restypename}",
+		            'status' => 'success');
+		$args = $this->defaultGetDataArgs;
+		if($this->deletetoggled)
+			$args['includedeleted'] = 1;
+		$args['rscid'] = $rscid;
+		$resdata = $this->getData($args);
+		if($this->deletetoggled && $resdata[$rscid]['deleted']) {
+			$rt['title'] = "Confirm Undelete {$this->restypename}";
+			$rt['question'] = "Undelete the following {$this->restype}?";
+			$rt['btntxt'] = "Undelete {$this->restypename}";
+		}
+		$fields = array_keys($resdata[$rscid]);
+		$rt['html'] .= "<table>";
+		$rt['html'] .= "<tr><th>Name:</th><td>{$resdata[$rscid][$this->namefield]}</td></tr>";
+		$rt['html'] .= "<tr><th>Owner:</th><td>{$resdata[$rscid]['owner']}</td></tr>";
+		foreach($fields as $field) {
+			if($field == $this->namefield ||
+			   $field == 'name' ||
+			   $field == 'owner' ||
+			   is_array($resdata[$rscid][$field]) ||
+			   preg_match('/id$/', $field))
+				continue;
+			$rt['html'] .= "<tr><th>";
+			$rt['html'] .= ucfirst($field);
+			$rt['html'] .= ":</th><td>{$resdata[$rscid][$field]}</td></tr>";
+		}
+		$rt['html'] .= "</table>";
+		$rt['html'] .= $this->toggleDeleteResourceExtra();
+
+		$cdata = getContinuationVar();
+		$cdata['rscid'] = $rscid;
+		$cont = addContinuationsEntry('AJsubmitToggleDeleteResource', $cdata);
+		$rt['cont'] = $cont;
+		sendJSON($rt);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn toggleDeleteResourceExtra()
+	///
+	/// \return empty string
+	///
+	/// \brief allows inheriting class to generate additional information to be
+	/// included on the confirm toggle delete resource page
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function toggleDeleteResourceExtra() {
+		return '';
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn AJsubmitToggleDeleteResource()
+	///
+	/// \brief AJAX callable wrapper for toggleDeleteResource
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function AJsubmitToggleDeleteResource() {
+		$rscid = getContinuationVar('rscid');
+		if($this->toggleDeleteResource($rscid))
+			$rt = array('status' => 'success', 'rscid' => $rscid);
+		else
+			$rt = array('status' => 'failed', 'rscid' => $rscid);
+		sendJSON($rt);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn submitToggleDeleteResourceExtra($rscid, $deleted)
+	///
+	/// \param $rscid - id of a resource (from table specific to that resource,
+	/// not from the resource table)
+	/// \param $deleted - (optional, default=0) 1 if resource was previously
+	/// deleted; 0 if not
+	///
+	/// \brief function to do any extra stuff specific to a resource type when
+	/// toggling delete for a resource; to be implemented by inheriting class if
+	/// needed
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function submitToggleDeleteResourceExtra($rscid, $deleted=0) {
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn groupMapHTML()
+	///
+	/// \brief prints HTML for groupping and mapping page
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function groupMapHTML() {
+		$h = '';
+		$h .= "<div dojoType=\"dojo.data.ItemFileWriteStore\" jsId=\"resourcetogroupsstore\" ";
+		$h .= "data=\"resourcetogroupsdata\"></div>\n";
+		$h .= "<div dojoType=\"dojo.data.ItemFileWriteStore\" jsId=\"grouptoresourcesstore\" ";
+		$h .= "data=\"grouptoresourcesdata\"></div>\n";
+		$h .= "<div dojoType=\"dojo.data.ItemFileWriteStore\" jsId=\"mapbyresgroupstore\" ";
+		$h .= "data=\"mapbyresgroupdata\"></div>\n";
+		$h .= "<div dojoType=\"dojo.data.ItemFileWriteStore\" jsId=\"mapbymaptogroupstore\" ";
+		$h .= "data=\"mapbymaptogroupdata\"></div>\n";
+		$h .= "<div id=\"mainTabContainer\" dojoType=\"dijit.layout.TabContainer\"\n";
+		$h .= "     style=\"width:600px;height:600px\">\n";
+		$h .= $this->groupByResourceHTML();
+		$h .= $this->groupByGroupHTML();
+		if($this->hasmapping) {
+			$h .= "<input type=\"hidden\" id=\"domapping\" value=\"1\">\n";
+			$h .= $this->mapByResGroupHTML();
+			$h .= $this->mapByMapToGroupHTML();
+		}
+		else
+			$h .= "<input type=\"hidden\" id=\"domapping\" value=\"0\">\n";
+		$h .= "</div>\n";
+		print $h;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn groupByResourceHTML()
+	///
+	/// \return html
+	///
+	/// \brief generates HTML for resource grouping by selecting a resource
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function groupByResourceHTML() {
+		# build list of resources
+		$tmp = getUserResources(array($this->restype . "Admin"), array('manageGroup'));
+		if(empty($tmp[$this->restype]))
+			return '';
+		$resources = $tmp[$this->restype];
+		uasort($resources, 'sortKeepIndex');
+		$h = '';
+		$h .= "<div id=\"groupbyresourcediv\" dojoType=\"dijit.layout.ContentPane\" title=\"Group By {$this->restypename}\">\n";
+		$h .= "<div id=\"groupbyresourcedesc\">\n";
+		$h .= "Select an item from the drop-down box and click \"Get Groups\" ";
+		$h .= "to see<br>all of the groups it is in. Then, select a group it ";
+		$h .= "is in and click the Remove<br>button to remove it from that group, ";
+		$h .= "or select a group it is not in and click<br>the Add button to ";
+		$h .= "add it to that group.<br><br>\n";
+		$h .= "</div>\n"; # groupbyresourcedesc
+		$h .= "<div id=\"groupbyresourcesel\">\n";
+		$h .= "{$this->restypename}:<select id=\"resources\">\n";
+		foreach($resources as $id => $res)
+			$h .= "<option value=$id>$res</option>\n";
+		$h .= "</select>\n";
+		$h .= dijitButton('fetchGrpsButton', "Get Groups",
+			               "populateLists('resources', 'ingroups', 'inresourcename', 'outresourcename', 'resgroupinggroupscont');");
+		$h .= "</div>\n"; # groupbyresourcesel
+		$h .= "<table><tbody><tr>\n";
+		# select for groups resource is in
+		$h .= "<td valign=top>\n";
+		$h .= "Groups <span style=\"font-weight: bold;\" id=\"inresourcename\"></span> is in:<br>\n";
+		$h .= "<table dojoType=\"dojox.grid.DataGrid\" jsId=\"ingroups\" ";
+		$h .= "store=\"resourcetogroupsstore\" style=\"width: 240px; height: 250px;\" query=\"{inout: 1}\" ";
+		$h .= "selectionMode=\"extended\">\n";
+		$h .= "<thead>\n";
+		$h .= "<tr>\n";
+		$h .= "<th field=\"name\" width=\"160px\"></th>\n";
+		$h .= "</tr>\n";
+		$h .= "</thead>\n";
+		$h .= "</table>\n";
+		$h .= "</td>\n";
+		# transfer buttons
+		$h .= "<td style=\"vertical-align: middle;\">\n";
+		$h .= dijitButton('', "<div style=\"width: 50px;\">&lt;-Add</div>", "resource.addRemItem('addgrpcont', 'resources', 'outgroups');");
+		$cdata = $this->basecdata;
+		$cdata['mode'] = 'add';
+		$cont = addContinuationsEntry('AJaddRemGroupResource', $cdata);
+		$h .= "<input type=\"hidden\" id=\"addgrpcont\" value=\"$cont\">\n";
+		$h .= "<br><br><br>\n";
+		$h .= dijitButton('', "<div style=\"width: 50px;\">Remove-&gt;</div>", "resource.addRemItem('remgrpcont', 'resources', 'ingroups');");
+		$cdata['mode'] = 'remove';
+		$cont = addContinuationsEntry('AJaddRemGroupResource', $cdata);
+		$h .= "<input type=\"hidden\" id=\"remgrpcont\" value=\"$cont\">\n";
+		$h .= "</td>\n";
+		# select for groups resource is not in
+		$h .= "<td valign=top>\n";
+		$h .= "Groups <span style=\"font-weight: bold;\" id=\"outresourcename\"></span> is not in:<br>\n";
+		$h .= "<table dojoType=\"dojox.grid.DataGrid\" jsId=\"outgroups\" ";
+		$h .= "store=\"resourcetogroupsstore\" style=\"width: 240px; height: 250px;\" query=\"{inout: 1}\" ";
+		$h .= "selectionMode=\"extended\">\n";
+		$h .= "<thead>\n";
+		$h .= "<tr>\n";
+		$h .= "<th field=\"name\" width=\"160px\"></th>\n";
+		$h .= "</tr>\n";
+		$h .= "</thead>\n";
+		$h .= "</table>\n";
+		$h .= "</td>\n";
+		$h .= "</tr></tbody></table>\n";
+		$cdata = $this->basecdata;
+		$cdata['store'] = 'resourcetogroupsstore';
+		$cdata['intitle'] = 'ingroups';
+		$cdata['outtitle'] = 'outgroups';
+		$cont = addContinuationsEntry('jsonResourceGroupingGroups', $cdata);
+		$h .= "<input type=hidden id=\"resgroupinggroupscont\" value=\"$cont\">\n";
+		$h .= "</div>\n";
+		return $h;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn jsonResourceGroupingGroups()
+	///
+	/// \brief sends JSON of resource groups for resource grouping by selecting a
+	/// resource page
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function jsonResourceGroupingGroups() {
+		$resid = processInputVar('id', ARG_NUMERIC);
+		$resources = getUserResources(array($this->restype . "Admin"), array("manageGroup"));
+		if(! array_key_exists($resid, $resources[$this->restype])) {
+			sendJSON(array('status' => 'noaccess'));
+			return;
+		}
+		$groups = getUserResources(array($this->restype . 'Admin'), array('manageGroup'), 1);
+		$memberships = getResourceGroupMemberships($this->restype);
+		$all = array();
+		foreach($groups[$this->restype] as $id => $group) {
+			if(array_key_exists($resid, $memberships[$this->restype]) &&
+				in_array($id, $memberships[$this->restype][$resid]))
+				$all[] = array('id' => $id, 'name' => $group, 'inout' => 1);
+			else
+				$all[] = array('id' => $id, 'name' => $group, 'inout' => 0);
+		}
+		$arr = array('items' => $all,
+		             'intitle' => getContinuationVar('intitle'),
+		             'outtitle' => getContinuationVar('outtitle'));
+		sendJSON($arr);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn AJaddRemGroupResource()
+	///
+	/// \brief adds or removes groups for a resource and sends JSON response
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function AJaddRemGroupResource() {
+		$rscid = processInputVar('id', ARG_NUMERIC);
+		$resources = getUserResources(array($this->restype . "Admin"), array("manageGroup"));
+		if(! array_key_exists($rscid, $resources[$this->restype])) {
+			$arr = array('status' => 'noaccess');
+			sendJSON($arr);
+			return;
+		}
+
+		$groups = getUserResources(array($this->restype . "Admin"), array("manageGroup"), 1);
+		$tmp = processInputVar('listids', ARG_STRING);
+		$tmp = explode(',', $tmp);
+		$groupids = array();
+		foreach($tmp as $id) {
+			if(! is_numeric($id))
+				continue;
+			if(! array_key_exists($id, $groups[$this->restype])) {
+				$arr = array('status' => 'noaccess');
+				sendJSON($arr);
+				return;
+			}
+			$groupids[] = $id;
+		}
+
+		$mode = getContinuationVar('mode');
+
+		$args = $this->defaultGetDataArgs;
+		$args['rscid'] = $rscid;
+		$resdata = $this->getData($args);
+
+		if($mode == 'add') {
+			$adds = array();
+			foreach($groupids as $id)
+				$adds[] = "({$resdata[$rscid]['resourceid']}, $id)";
+			$query = "INSERT IGNORE INTO resourcegroupmembers "
+					 . "(resourceid, resourcegroupid) VALUES ";
+			$query .= implode(',', $adds);
+			doQuery($query);
+		}
+		else {
+			$rems = implode(',', $groupids);
+			$query = "DELETE FROM resourcegroupmembers "
+					 . "WHERE resourceid = {$resdata[$rscid]['resourceid']} AND "
+					 .       "resourcegroupid IN ($rems)";
+			doQuery($query);
+		}
+
+		$_SESSION['userresources'] = array();
+		$regids = "^" . implode('$|^', $groupids) . "$";
+		$arr = array('status' => 'success',
+		             'regids' => $regids,
+		             'inselobj' => 'ingroups',
+		             'outselobj' => 'outgroups');
+		sendJSON($arr);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn groupByGroupHTML()
+	///
+	/// \return html
+	///
+	/// \brief generates HTML for resource grouping by selecting a resource group
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function groupByGroupHTML() {
+		$resources = getUserResources(array($this->restype . 'Admin'), array('manageGroup'));
+		if(empty($resources[$this->restype]))
+			return '';
+		$h = '';
+		$h .= "<div id=\"groupbygroupdiv\" dojoType=\"dijit.layout.ContentPane\" title=\"Group By Group\">\n";
+		$h .= "Select a group from the drop-down box and click \"Get {$this->restypename}s\"<br>";
+		$h .= "to see all of the resources in it. Then, select a resource in it and click the<br>";
+		$h .= "Remove button to remove it from that group, or select a resource that is not ";
+		$h .= "<br>in it and click the Add button to add it to that group.<br><br>\n";
+		$h .= "Group:<select id=\"resgroups\">\n";
+		# build list of groups
+		$tmp = getUserResources(array($this->restype . "Admin"), array('manageGroup'), 1);
+		$groups = $tmp[$this->restype];
+		uasort($groups, 'sortKeepIndex');
+		foreach($groups as $id => $group)
+			$h .= "<option value=$id>$group</option>\n";
+		$h .= "</select>\n";
+		$h .= dijitButton('fetchResourcesButton', "Get {$this->restypename}s",
+		                  "populateLists('resgroups', 'inresources', 'ingroupname', 'outgroupname', 'resgroupingresourcescont');");
+		$h .= "<table><tbody><tr>\n";
+		# select for resources in group
+		$h .= "<td valign=top>\n";
+		$h .= "{$this->restypename}s in <span style=\"font-weight: bold;\" id=\"ingroupname\"></span>:<br>\n";
+		$h .= "<table dojoType=\"dojox.grid.DataGrid\" jsId=\"inresources\" ";
+		$h .= "store=\"grouptoresourcesstore\" style=\"width: 240px; height: 250px;\" query=\"{inout: 1}\" ";
+		$h .= "selectionMode=\"extended\" sortInfo=\"1\">\n";
+		$h .= "<thead>\n";
+		$h .= "<tr>\n";
+		$h .= "<th field=\"name\" width=\"160px\"></th>\n";
+		$h .= "</tr>\n";
+		$h .= "</thead>\n";
+		$h .= "</table>\n";
+		$h .= "</td>\n";
+		# transfer buttons
+		$h .= "<td style=\"vertical-align: middle;\">\n";
+		$h .= dijitButton('', "<div style=\"width: 50px;\">&lt;-Add</div>", "resource.addRemItem('additemcont', 'resgroups', 'outresources');");
+		$cdata = $this->basecdata;
+		$cdata['mode'] = 'add';
+		$cont = addContinuationsEntry('AJaddRemResourceGroup', $cdata);
+		$h .= "<input type=\"hidden\" id=\"additemcont\" value=\"$cont\">\n";
+		$h .= "<br><br><br>\n";
+		$h .= dijitButton('', "<div style=\"width: 50px;\">Remove-&gt;</div>", "resource.addRemItem('remitemcont', 'resgroups', 'inresources');");
+		$cdata['mode'] = 'remove';
+		$cont = addContinuationsEntry('AJaddRemResourceGroup', $cdata);
+		$h .= "<input type=\"hidden\" id=\"remitemcont\" value=\"$cont\">\n";
+		$h .= "</td>\n";
+		# select for groups resource is not in
+		$h .= "<td valign=top>\n";
+		$h .= "{$this->restypename}s not in <span style=\"font-weight: bold;\" id=\"outgroupname\"></span>:<br>\n";
+		$h .= "<table dojoType=\"dojox.grid.DataGrid\" jsId=\"outresources\" ";
+		$h .= "store=\"grouptoresourcesstore\" style=\"width: 240px; height: 250px;\" query=\"{inout: 1}\" ";
+		$h .= "selectionMode=\"extended\" sortInfo=\"1\">\n";
+		$h .= "<thead>\n";
+		$h .= "<tr>\n";
+		$h .= "<th field=\"name\" width=\"160px\"></th>\n";
+		$h .= "</tr>\n";
+		$h .= "</thead>\n";
+		$h .= "</table>\n";
+		$h .= "</td>\n";
+		$h .= "</tr></tbody></table>\n";
+		$cdata = $this->basecdata;
+		$cdata['store'] = 'grouptoresourcesstore';
+		$cdata['intitle'] = 'inresources';
+		$cdata['outtitle'] = 'outresources';
+		$cont = addContinuationsEntry('jsonResourceGroupingResources', $cdata);
+		$h .= "<input type=hidden id=\"resgroupingresourcescont\" value=\"$cont\">\n";
+		$h .= "</div>\n";
+		return $h;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn jsonResourceGroupingResources()
+	///
+	/// \brief sends JSON of resources for resource grouping by selecting a
+	/// resource group
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function jsonResourceGroupingResources() {
+		$groupid = processInputVar('id', ARG_NUMERIC);
+		$groups = getUserResources(array($this->restype . "Admin"), array("manageGroup"), 1);
+		if(! array_key_exists($groupid, $groups[$this->restype])) {
+			sendJSON(array('status' => 'noaccess'));
+			return;
+		}
+		$resources = getUserResources(array($this->restype . 'Admin'), array('manageGroup'));
+		#uasort($resources[$this->restype], 'sortKeepIndex');
+		$memberships = getResourceGroupMemberships($this->restype);
+		$all = array();
+		foreach($resources[$this->restype] as $id => $res) {
+			if(array_key_exists($id, $memberships[$this->restype]) &&
+				in_array($groupid, $memberships[$this->restype][$id]))
+				$all[] = array('id' => $id, 'name' => $res, 'inout' => 1);
+			else
+				$all[] = array('id' => $id, 'name' => $res, 'inout' => 0);
+		}
+		$arr = array('items' => $all,
+		             'intitle' => getContinuationVar('intitle'),
+		             'outtitle' => getContinuationVar('outtitle'));
+		sendJSON($arr);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn AJaddRemResourceGroup()
+	///
+	/// \brief adds or removes resources for a resource group and sends JSON
+	/// response
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function AJaddRemResourceGroup() {
+		$groupid = processInputVar('id', ARG_NUMERIC);
+		$groups = getUserResources(array($this->restype . "Admin"), array("manageGroup"), 1);
+		if(! array_key_exists($groupid, $groups[$this->restype])) {
+			$arr = array('status' => 'noaccess');
+			sendJSON($arr);
+			return;
+		}
+
+		$resources = getUserResources(array($this->restype . "Admin"), array("manageGroup"));
+		$tmp = processInputVar('listids', ARG_STRING);
+		$tmp = explode(',', $tmp);
+		$rscids = array();
+		foreach($tmp as $id) {
+			if(! is_numeric($id))
+				continue;
+			if(! array_key_exists($id, $resources[$this->restype])) {
+				$arr = array('status' => 'noaccess');
+				sendJSON($arr);
+				return;
+			}
+			$rscids[] = $id;
+		}
+
+		$mode = getContinuationVar('mode');
+
+		$resdata = $this->getData($this->defaultGetDataArgs);
+
+		if($mode == 'add') {
+			$adds = array();
+			foreach($rscids as $id)
+				$adds[] = "({$resdata[$id]['resourceid']}, $groupid)";
+			$query = "INSERT IGNORE INTO resourcegroupmembers "
+					 . "(resourceid, resourcegroupid) VALUES ";
+			$query .= implode(',', $adds);
+		}
+		else {
+			$delids = array();
+			foreach($rscids as $id)
+				$delids[] = $resdata[$id]['resourceid'];
+			$inlist = implode(',', $delids);
+			$query = "DELETE FROM resourcegroupmembers "
+					 . "WHERE resourcegroupid = $groupid AND "
+					 .       "resourceid IN ($inlist)";
+			doQuery($query);
+		}
+
+		doQuery($query);
+		$_SESSION['userresources'] = array();
+		$regids = "^" . implode('$|^', $rscids) . "$";
+		$arr = array('status' => 'success',
+		             'regids' => $regids,
+		             'inselobj' => 'inresources',
+		             'outselobj' => 'outresources');
+		sendJSON($arr);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn 
+	///
+	/// \brief Resource
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function groupByGridHTML() {
+		# TODO
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn mapByResGroupHTML()
+	///
+	/// \return html
+	///
+	/// \brief generates HTML for resource mapping by selecting a resource group
+	/// of this type of resource
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function mapByResGroupHTML() {
+		$tmp = getUserResources(array($this->restype . "Admin"),
+		                        array("manageMapping"), 1);
+		$groups = $tmp[$this->restype];
+		uasort($groups, "sortKeepIndex");
+		$tmp = getUserResources(array($this->maptype . "Admin"),
+		                        array("manageMapping"), 1);
+		$mapgroups = $tmp[$this->maptype];
+		uasort($mapgroups, "sortKeepIndex");
+		$h = '';
+
+		if(! count($groups) || ! count($mapgroups)) {
+			$h .= "You don't have access to manage any mappings for this resource ";
+			$h .= "type.<br>\n";
+			return $h;
+		}
+
+		$h .= "<div id=\"mapbyresgroupdiv\" dojoType=\"dijit.layout.ContentPane\" title=\"Map By {$this->restypename} Group\">\n";
+		$h .= "Select an item from the drop-down box and click \"Get {$this->maptypename} Groups\"<br>";
+		$h .= "to see all of the groups it maps to. Then, select a group ";
+		$h .= "it does not map<br>to and click the Add button to map it to that group, ";
+		$h .= "or select a group it<br>maps to and click the Remove ";
+		$h .= "button to unmap it from that group.<br><br>\n";
+		$h .= "{$this->restypename} Group:<select id=\"groups\">\n";
+		foreach($groups as $id => $group)
+			$h .= "<option value=$id>$group</option>\n";
+		$h .= "</select>\n";
+		$h .= dijitButton('', "Get {$this->maptypename} Groups",
+		                  "populateLists('groups', 'inmapgroups', 'inmapgroupname', 'outmapgroupname', 'mapbyresgroupcont');");
+		$h .= "<table><tbody><tr>\n";
+		# select for groups mapped to
+		$h .= "<td valign=top>\n";
+		$h .= "{$this->maptypename} Groups <span style=\"font-weight: bold;\" id=\"inmapgroupname\"></span> maps to:<br>\n";
+		$h .= "<table dojoType=\"dojox.grid.DataGrid\" jsId=\"inmapgroups\" ";
+		$h .= "store=\"mapbyresgroupstore\" style=\"width: 240px; height: 250px;\" query=\"{inout: 1}\" ";
+		$h .= "selectionMode=\"extended\">\n";
+		$h .= "<thead>\n";
+		$h .= "<tr>\n";
+		$h .= "<th field=\"name\" width=\"160px\"></th>\n";
+		$h .= "</tr>\n";
+		$h .= "</thead>\n";
+		$h .= "</table>\n";
+		$h .= "</td>\n";
+		# transfer buttons
+		$h .= "<td style=\"vertical-align: middle;\">\n";
+		$h .= dijitButton('', "<div style=\"width: 50px;\">&lt;-Add</div>", "resource.addRemItem('addmapgrpcont', 'groups', 'outmapgroups');");
+		$cdata = $this->basecdata;
+		$cdata['mode'] = 'add';
+		$cont = addContinuationsEntry('AJaddRemMapToGroup', $cdata);
+		$h .= "<input type=\"hidden\" id=\"addmapgrpcont\" value=\"$cont\">\n";
+		$h .= "<br>\n";
+		$h .= "<br>\n";
+		$h .= "<br>\n";
+		$h .= dijitButton('', "<div style=\"width: 50px;\">Remove-&gt;</div>", "resource.addRemItem('remmapgrpcont', 'groups', 'inmapgroups');");
+		$cdata['mode'] = 'remove';
+		$cont = addContinuationsEntry('AJaddRemMapToGroup', $cdata);
+		$h .= "<input type=\"hidden\" id=\"remmapgrpcont\" value=\"$cont\">\n";
+		$h .= "</td>\n";
+		# select for groups resource is not in
+		$h .= "<td valign=top>\n";
+		$h .= "{$this->maptypename} Groups <span style=\"font-weight: bold;\" id=\"outmapgroupname\"></span> does not map to:<br>\n";
+		$h .= "<table dojoType=\"dojox.grid.DataGrid\" jsId=\"outmapgroups\" ";
+		$h .= "store=\"mapbyresgroupstore\" style=\"width: 240px; height: 250px;\" query=\"{inout: 1}\" ";
+		$h .= "selectionMode=\"extended\">\n";
+		$h .= "<thead>\n";
+		$h .= "<tr>\n";
+		$h .= "<th field=\"name\" width=\"160px\"></th>\n";
+		$h .= "</tr>\n";
+		$h .= "</thead>\n";
+		$h .= "</table>\n";
+		$h .= "</td>\n";
+		$h .= "</tr></tbody></table>\n";
+		$cdata = $this->basecdata;
+		$cdata['store'] = 'mapbyresgroupstore';
+		$cdata['intitle'] = 'inmapgroups';
+		$cdata['outtitle'] = 'outmapgroups';
+		$cont = addContinuationsEntry('jsonResourceMappingMapToGroups', $cdata);
+		$h .= "<input type=hidden id=\"mapbyresgroupcont\" value=\"$cont\">\n";
+		$h .= "</div>\n";
+		return $h;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn jsonResourceMappingMapToGroups()
+	///
+	/// \brief sends JSON of resource groups for resource mapping by selecting a
+	/// resource group of this resource type
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function jsonResourceMappingMapToGroups() {
+		$resgrpid = processInputVar('id', ARG_NUMERIC);
+		$resources = getUserResources(array($this->restype . "Admin"), array("manageMapping"), 1);
+		if(! array_key_exists($resgrpid, $resources[$this->restype])) {
+			sendJSON(array('status' => 'noaccess'));
+			return;
+		}
+		$mapgroups = getUserResources(array($this->maptype . 'Admin'), array('manageMapping'), 1);
+		$mapping = getResourceMapping($this->restype, $this->maptype);
+		$all = array();
+
+		foreach($mapgroups[$this->maptype] as $id => $group) {
+			if(array_key_exists($resgrpid, $mapping) &&
+				in_array($id, $mapping[$resgrpid]))
+				$all[] = array('id' => $id, 'name' => $group, 'inout' => 1);
+			else
+				$all[] = array('id' => $id, 'name' => $group, 'inout' => 0);
+		}
+		$arr = array('items' => $all,
+		             'intitle' => getContinuationVar('intitle'),
+		             'outtitle' => getContinuationVar('outtitle'));
+		sendJSON($arr);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn AJaddRemMapToGroup()
+	///
+	/// \brief adds or removes groups that map to a group of this resource type
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function AJaddRemMapToGroup() {
+		$groupid = processInputVar('id', ARG_NUMERIC);
+		$groups = getUserResources(array($this->restype . "Admin"),
+		                           array("manageMapping"), 1);
+		if(! array_key_exists($groupid, $groups[$this->restype])) {
+			$arr = array('status' => 'noaccess');
+			sendJSON($arr);
+			return;
+		}
+
+		$mapgroups = getUserResources(array($this->maptype . "Admin"),
+		                              array("manageMapping"), 1);
+		$tmp = processInputVar('listids', ARG_STRING);
+		$tmp = explode(',', $tmp);
+		$mapids = array();
+		foreach($tmp as $id) {
+			if(! is_numeric($id))
+				continue;
+			if(! array_key_exists($id, $mapgroups[$this->maptype])) {
+				$arr = array('status' => 'noaccess');
+				sendJSON($arr);
+				return;
+			}
+			$mapids[] = $id;
+		}
+
+		$mytypeid = getResourceTypeID($this->restype);
+		$maptypeid = getResourceTypeID($this->maptype);
+
+		$mode = getContinuationVar('mode');
+
+		if($mode == 'add') {
+			$adds = array();
+			foreach($mapids as $id)
+				$adds[] = "($groupid, $mytypeid, $id, $maptypeid)";
+			$query = "INSERT IGNORE INTO resourcemap "
+					 .        "(resourcegroupid1, resourcetypeid1, "
+					 .         "resourcegroupid2, resourcetypeid2) "
+					 . "VALUES ";
+			$query .= implode(',', $adds);
+			doQuery($query);
+		}
+		else {
+			foreach($mapids as $id) {
+				$query = "DELETE FROM resourcemap "
+						 . "WHERE resourcegroupid1 = $groupid AND "
+						 .       "resourcetypeid1 = $mytypeid AND "
+						 .       "resourcegroupid2 = $id AND "
+						 .       "resourcetypeid2 = $maptypeid";
+				doQuery($query);
+			}
+		}
+		$regids = "^" . implode('$|^', $mapids) . "$";
+		$arr = array('status' => 'success',
+		             'regids' => $regids,
+		             'inselobj' => 'inmapgroups',
+		             'outselobj' => 'outmapgroups');
+		sendJSON($arr);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn mapByMapToGroupHTML()
+	///
+	/// \return html
+	///
+	/// \brief generates HTML for resource mapping by selecting a resource group
+	/// of the type that this type maps to
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function mapByMapToGroupHTML() {
+		$tmp = getUserResources(array($this->maptype . "Admin"),
+		                        array("manageMapping"), 1);
+		$mapgroups = $tmp[$this->maptype];
+		uasort($mapgroups, "sortKeepIndex");
+		$tmp = getUserResources(array($this->restype . "Admin"),
+		                        array("manageMapping"), 1);
+		$groups = $tmp[$this->restype];
+		uasort($groups, "sortKeepIndex");
+		$h = '';
+
+		if(! count($mapgroups) || ! count($groups)) {
+			$h .= "You don't have access to manage any mappings for this resource ";
+			$h .= "type.<br>\n";
+			return $h;
+		}
+
+		$h .= "<div id=\"mapbymaptogroupdiv\" dojoType=\"dijit.layout.ContentPane\" title=\"Map By {$this->maptypename} Group\">\n";
+		$h .= "Select an item from the drop-down box and click \"Get {$this->restypename} Groups\"<br>";
+		$h .= "to see all of the groups it maps to. Then, select a group ";
+		$h .= "it does not map<br>to and click the Add button to map it to that group, ";
+		$h .= "or select a group it<br>maps to and click the Remove ";
+		$h .= "button to unmap it from that group.<br><br>\n";
+		$h .= "{$this->maptypename} Group:<select id=\"maptogroups\">\n";
+		foreach($mapgroups as $id => $group)
+			$h .= "<option value=$id>$group</option>\n";
+		$h .= "</select>\n";
+		$h .= dijitButton('', "Get {$this->restypename} Groups",
+		                  "populateLists('maptogroups', 'inmaptogroups', 'inmaptogroupname', 'outmaptogroupname', 'mapbymaptogroupcont');");
+		$h .= "<table><tbody><tr>\n";
+		# select for groups mapped to
+		$h .= "<td valign=top>\n";
+		$h .= "{$this->restypename} Groups <span style=\"font-weight: bold;\" id=\"inmaptogroupname\"></span> maps to:<br>\n";
+		$h .= "<table dojoType=\"dojox.grid.DataGrid\" jsId=\"inmaptogroups\" ";
+		$h .= "store=\"mapbymaptogroupstore\" style=\"width: 240px; height: 250px;\" query=\"{inout: 1}\" ";
+		$h .= "selectionMode=\"extended\">\n";
+		$h .= "<thead>\n";
+		$h .= "<tr>\n";
+		$h .= "<th field=\"name\" width=\"160px\"></th>\n";
+		$h .= "</tr>\n";
+		$h .= "</thead>\n";
+		$h .= "</table>\n";
+		$h .= "</td>\n";
+		# transfer buttons
+		$h .= "<td style=\"vertical-align: middle;\">\n";
+		$h .= dijitButton('', "<div style=\"width: 50px;\">&lt;-Add</div>",
+		                  "resource.addRemItem('addmaptogrpcont', 'maptogroups', 'outmaptogroups');");
+		$cdata = $this->basecdata;
+		$cdata['mode'] = 'add';
+		$cont = addContinuationsEntry('AJaddRemGroupMapTo', $cdata);
+		$h .= "<input type=\"hidden\" id=\"addmaptogrpcont\" value=\"$cont\">\n";
+		$h .= "<br><br><br>\n";
+		$h .= dijitButton('', "<div style=\"width: 50px;\">Remove-&gt;</div>",
+		                  "resource.addRemItem('remmaptogrpcont', 'maptogroups', 'inmaptogroups');");
+		$cdata['mode'] = 'remove';
+		$cont = addContinuationsEntry('AJaddRemGroupMapTo', $cdata);
+		$h .= "<input type=\"hidden\" id=\"remmaptogrpcont\" value=\"$cont\">\n";
+		$h .= "</td>\n";
+		# select for groups resource is not in
+		$h .= "<td valign=top>\n";
+		$h .= "{$this->restypename} Groups <span style=\"font-weight: bold;\" id=\"outmaptogroupname\"></span> does not map to:<br>\n";
+		$h .= "<table dojoType=\"dojox.grid.DataGrid\" jsId=\"outmaptogroups\" ";
+		$h .= "store=\"mapbymaptogroupstore\" style=\"width: 240px; height: 250px;\" query=\"{inout: 1}\" ";
+		$h .= "selectionMode=\"extended\">\n";
+		$h .= "<thead>\n";
+		$h .= "<tr>\n";
+		$h .= "<th field=\"name\" width=\"160px\"></th>\n";
+		$h .= "</tr>\n";
+		$h .= "</thead>\n";
+		$h .= "</table>\n";
+		$h .= "</td>\n";
+		$h .= "</tr></tbody></table>\n";
+		$cdata = $this->basecdata;
+		$cdata['store'] = 'mapbymaptogroupstore';
+		$cdata['intitle'] = 'inmaptogroups';
+		$cdata['outtitle'] = 'outmaptogroups';
+		$cont = addContinuationsEntry('jsonResourceMappingGroups', $cdata);
+		$h .= "<input type=hidden id=\"mapbymaptogroupcont\" value=\"$cont\">\n";
+		$h .= "</div>\n";
+		return $h;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn jsonResourceMappingGroups()
+	///
+	/// \brief sends JSON of resource groups for resource mapping by selecting a
+	/// resource group of the type this type maps to
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function jsonResourceMappingGroups() {
+		$resmaptogrpid = processInputVar('id', ARG_NUMERIC);
+		$resources = getUserResources(array($this->maptype . "Admin"), array("manageMapping"), 1);
+		if(! array_key_exists($resmaptogrpid, $resources[$this->maptype])) {
+			sendJSON(array('status' => 'noaccess'));
+			return;
+		}
+		$groups = getUserResources(array($this->restype . 'Admin'), array('manageMapping'), 1);
+		$mapping = getResourceMapping($this->maptype, $this->restype);
+		$all = array();
+
+		foreach($groups[$this->restype] as $id => $group) {
+			if(array_key_exists($resmaptogrpid, $mapping) &&
+				in_array($id, $mapping[$resmaptogrpid]))
+				$all[] = array('id' => $id, 'name' => $group, 'inout' => 1);
+			else
+				$all[] = array('id' => $id, 'name' => $group, 'inout' => 0);
+		}
+		$arr = array('items' => $all,
+		             'intitle' => getContinuationVar('intitle'),
+		             'outtitle' => getContinuationVar('outtitle'));
+		sendJSON($arr);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn AJaddRemGroupMapTo()
+	///
+	/// \brief adds or removes groups that map to a group of the type this
+	/// resource maps to
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function AJaddRemGroupMapTo() {
+		$mapgroupid = processInputVar('id', ARG_NUMERIC);
+		$mapgroups = getUserResources(array($this->maptype . "Admin"),
+		                              array("manageMapping"), 1);
+		if(! array_key_exists($mapgroupid, $mapgroups[$this->maptype])) {
+			$arr = array('status' => 'noaccess');
+			sendJSON($arr);
+			return;
+		}
+
+		$groups = getUserResources(array($this->restype . "Admin"),
+		                           array("manageMapping"), 1);
+		$tmp = processInputVar('listids', ARG_STRING);
+		$tmp = explode(',', $tmp);
+		$groupids = array();
+		foreach($tmp as $id) {
+			if(! is_numeric($id))
+				continue;
+			if(! array_key_exists($id, $groups[$this->restype])) {
+				$arr = array('status' => 'noaccess');
+				sendJSON($arr);
+				return;
+			}
+			$groupids[] = $id;
+		}
+
+		$mytypeid = getResourceTypeID($this->restype);
+		$maptypeid = getResourceTypeID($this->maptype);
+
+		$mode = getContinuationVar('mode');
+
+		if($mode == 'add') {
+			$adds = array();
+			foreach($groupids as $id)
+				$adds[] = "($id, $mytypeid, $mapgroupid, $maptypeid)";
+			$query = "INSERT IGNORE INTO resourcemap "
+					 .        "(resourcegroupid1, resourcetypeid1, "
+					 .         "resourcegroupid2, resourcetypeid2) "
+					 . "VALUES ";
+			$query .= implode(',', $adds);
+			doQuery($query);
+		}
+		else {
+			foreach($groupids as $id) {
+				$query = "DELETE FROM resourcemap "
+						 . "WHERE resourcegroupid1 = $id AND "
+						 .       "resourcetypeid1 = $mytypeid AND "
+						 .       "resourcegroupid2 = $mapgroupid AND "
+						 .       "resourcetypeid2 = $maptypeid";
+				doQuery($query);
+			}
+		}
+		$regids = "^" . implode('$|^', $groupids) . "$";
+		$arr = array('status' => 'success',
+		             'regids' => $regids,
+		             'inselobj' => 'inmaptogroups',
+		             'outselobj' => 'outmaptogroups');
+		sendJSON($arr);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn mapByGridHTML()
+	///
+	/// \brief
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function mapByGridHTML() {
+		# TODO
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn AJsaveResource()
+	///
+	/// \brief saves changes to a resource; must be implemented by inheriting
+	/// class
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function AJsaveResource() {
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn AJeditResource()
+	///
+	/// \brief sends data for editing a resource
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function AJeditResource() {
+		$rscid = processInputVar('rscid', ARG_NUMERIC);
+		$resources = getUserResources(array($this->restype . 'Admin'), array('administer'), 0, 1);
+		if(! array_key_exists($rscid, $resources[$this->restype])) {
+			$ret = array('status' => 'noaccess');
+			sendJSON($ret);
+			return;
+		}
+		$args = $this->defaultGetDataArgs;
+		$args['rscid'] = $rscid;
+		$tmp = $this->getData($args);
+		$data = $tmp[$rscid];
+		$cdata = $this->basecdata;
+		$cdata['rscid'] = $rscid;
+		$cdata['olddata'] = $data;
+
+		# save continuation
+		$cont = addContinuationsEntry('AJsaveResource', $cdata);
+
+		$ret = $this->jsondata;
+		$ret['title'] = "Edit {$this->restypename}";
+		$ret['cont'] = $cont;
+		$ret['resid'] = $rscid;
+		$ret['data'] = $data;
+		$ret['status'] = 'success';
+		sendJSON($ret);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn addResource($data)
+	///
+	/// \param $data - array of needed data for adding a new resource
+	///
+	/// \return id of new resource
+	///
+	/// \brief handles all parts of adding a new resource to the database; should
+	/// be implemented by inheriting class, but not required since it is only
+	/// called by functions in the inheriting class (nothing in this base class
+	/// calls it directly)
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function addResource($data) {
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn addEditDialogHTML($add)
+	///
+	/// \param $add (optional, defaul=0) - 0 for edit, 1 for add
+	///
+	/// \brief handles generating HTML for dialog used to edit resource; must be
+	/// implemented by inheriting class
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function addEditDialogHTML($add) {
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn extraSelectAdminOptions()
+	///
+	/// \return html
+	///
+	/// \brief generates any HTML for additional options that should be shown on
+	/// selectionText page
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function extraSelectAdminOptions() {
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////////
+///
+/// \fn AJstartImage()
+///
+/// \brief starts the imaging process for a reservation
+///
+////////////////////////////////////////////////////////////////////////////////
+function AJstartImage() {
+	global $user;
+	$requestid = getContinuationVar("requestid");
+	$checkpoint = getContinuationVar("checkpoint", 0);
+
+	$data = getRequestInfo($requestid);
+	$disableUpdate = 1;
+	$imageid = '';
+	if(count($data['reservations']) == 1) {
+		$imageid = $data['reservations'][0]['imageid'];
+		$revid = $data['reservations'][0]['imagerevisionid'];
+	}
+	else {
+		foreach($data["reservations"] as $res) {
+			if($res["forcheckout"]) {
+				$imageid = $res["imageid"];
+				$revid = $res['imagerevisionid'];
+				break;
+			}
+		}
+	}
+	if(! empty($imageid)) {
+		$imageData = getImages(0, $imageid);
+		if($imageData[$imageid]['ownerid'] == $user['id'])
+			$disableUpdate = 0;
+		if($imageData[$imageid]['installtype'] == 'none' ||
+		   $imageData[$imageid]['installtype'] == 'kickstart')
+			$disableUpdate = 1;
+	}
+	else {
+		$data['status'] = 'error';
+		$data['errmsg'] = _("There was an error in starting the imaging process. Please contact a system administrator.");
+		sendJSON($data);
+		return;
+	}
+	$obj = new Image();
+	$cdata = array('obj' => $obj,
+	               'requestid' => $requestid,
+	               'imageid' => $imageid,
+	               'baserevisionid' => $data['reservations'][0]['imagerevisionid'],
+	               'checkpoint' => $checkpoint,
+	               'add' => 1);
+	$cont = addContinuationsEntry('AJsaveResource', $cdata, SECINDAY, 0);
+	$arr = array('newcont' => $cont,
+	             'enableupdate' => 0,
+	             'connectmethods' => $imageData[$imageid]['connectmethods'],
+	             'owner' => "{$user['unityid']}@{$user['affiliation']}",
+	             'checkpoint' => $checkpoint);
+
+	$cdata = array('obj' => $obj,
+	               'imageid' => $imageid,
+	               'newimage' => 1,
+	               'curmethods' => $imageData[$imageid]['connectmethods']);
+	$cont = addContinuationsEntry('connectmethodDialogContent', $cdata);
+	$arr['connectmethodurl'] = BASEURL . SCRIPT . "?continuation=$cont";
+
+	if(! $disableUpdate) {
+		$revisions = getImageRevisions($imageid);
+		if(array_key_exists($revid, $revisions))
+			$comments = $revisions[$revid]['comments'];
+		else {
+			$keys = array_keys($revisions);
+			if(count($keys)) {
+				$key = array_pop($keys);
+				$comments = $revisions[$key]['comments'];
+			}
+			else
+				$comments = '';
+		}
+		if(preg_match('/\w/', $comments)) {
+			$cmt  = "These are the comments from the previous revision ";
+			$cmt .= "({$revisions[$revid]['revision']}):<br>";
+			$cmt .= "{$revisions[$revid]['comments']}<br><br>";
+		}
+		else
+			$cmt = "The previous revision did not have any comments.<br><br>";
+		$arr['comments'] = $cmt;
+		$cdata = array('obj' => $obj,
+		               'requestid' => $requestid,
+		               'imageid' => $imageid,
+		               'checkpoint' => $checkpoint,
+		               'revisionid' => $revid);
+		$cont = addContinuationsEntry('AJupdateImage', $cdata, SECINDAY, 0);
+		$arr['updatecont'] = $cont;
+		$arr['enableupdate'] = 1;
+	}
+	$arr['status'] = 'success';
+	sendJSON($arr);
+}
+
+?>

Added: vcl/trunk/web/.ht-inc/schedule.php
URL: http://svn.apache.org/viewvc/vcl/trunk/web/.ht-inc/schedule.php?rev=1624325&view=auto
==============================================================================
--- vcl/trunk/web/.ht-inc/schedule.php (added)
+++ vcl/trunk/web/.ht-inc/schedule.php Thu Sep 11 16:01:48 2014
@@ -0,0 +1,464 @@
+<?php
+/*
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You 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.
+*/
+
+////////////////////////////////////////////////////////////////////////////////
+///
+/// \class Schedule
+///
+/// \brief extends Resource class to add things specific to resources of the
+/// schedule type
+///
+////////////////////////////////////////////////////////////////////////////////
+class Schedule extends Resource {
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn __construct()
+	///
+	/// \brief calls parent constructor; initializes things for Schedule class
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function __construct() {
+		parent::__construct();
+		$this->restype = 'schedule';
+		$this->restypename = 'Schedule';
+		$this->namefield = 'name';
+		$this->basecdata['obj'] = $this;
+		$this->deletable = 1;
+		$this->deletetoggled = 0;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn getData($args)
+	///
+	/// \param $args - unused in this class
+	///
+	/// \return array of data as returned from getSchedules
+	///
+	/// \brief wrapper for calling getSchedules
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function getData($args) {
+		return getSchedules();
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn addEditDialogHTML()
+	///
+	/// \param $add - unused for this class
+	///
+	/// \brief generates HTML for dialog used to edit resource
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function addEditDialogHTML($add=0) {
+		global $user, $days;
+		# dialog for on page editing
+		$h = '';
+		$h .= "<div dojoType=dijit.Dialog\n";
+		$h .= "      id=\"addeditdlg\"\n";
+		$h .= "      title=\"Edit {$this->restypename}\"\n";
+		$h .= "      duration=250\n";
+		$h .= "      style=\"width: 70%;\"\n";
+		$h .= "      draggable=true>\n";
+		$h .= "<div id=\"addeditdlgcontent\">\n";
+		# id
+		$h .= "<input type=\"hidden\" id=\"editresid\">\n";
+
+		$h .= "<div style=\"text-align: center;\">\n";
+		# name
+		$errmsg = "Name cannot contain single (') or double (&quot;) quotes, "
+		        . "less than (&lt;), or greater than (&gt;) and can be from 2 to 30 "
+		        . "characters long";
+		$h .= labeledFormItem('name', 'Name', 'text', '^([A-Za-z0-9-!@#$%^&\*\(\)_=\+\[\]{}\\\|:;,\./\?~` ]){2,30}$',
+		                      1, '', $errmsg, '', '', '200px'); 
+		# owner
+		$extra = array('onKeyPress' => 'setOwnerChecking');
+		$h .= labeledFormItem('owner', 'Owner', 'text', '', 1,
+		                      "{$user['unityid']}@{$user['affiliation']}", 'Unknown user',
+		                      'checkOwner', $extra, '200px');
+		#$h .= labeledFormItem('owner', 'Owner', 'text', '{$user['unityid']}@{$user['affiliation']}',
+		#                      1, '', 'Unknown user', 'checkOwner', 'onKeyPress', 'setOwnerChecking');
+		$cont = addContinuationsEntry('AJvalidateUserid');
+		$h .= "<input type=\"hidden\" id=\"valuseridcont\" value=\"$cont\">\n";
+
+		# table of times
+		$h .= "<br>";
+		$h .= "<span style=\"text-align: center;\"><h3>Schedule Times</h3></span>\n";
+		$h .= "The start and end day/times are based on a week's time period with ";
+		$h .= "the start/end point being 'Sunday 12:00&nbsp;am'. i.e. The earliest ";
+		$h .= "start day/time is 'Sunday 12:00&nbsp;am' and the latest end day/";
+		$h .= "time is 'Sunday 12:00&nbsp;am'<br><br>\n";
+		$h .= "Start:";
+		$h .= selectInputAutoDijitHTML('startday', $days, 'startday');
+		$h .= "<input type=\"text\" id=\"starttime\" dojoType=\"dijit.form.TimeTextBox\" ";
+		$h .= "required=\"true\" value=\"T00:00:00\"/>\n";
+		$h .= "End:";
+		$h .= selectInputAutoDijitHTML('endday', $days, 'endday');
+		$h .= "<input type=\"text\" id=\"endtime\" dojoType=\"dijit.form.TimeTextBox\" ";
+		$h .= "required=\"true\" value=\"T00:00:00\" />\n";
+		$h .= dijitButton('addTimeBtn', "Add", "addTime();");
+		$h .= "</div>\n"; # text-align: center
+		$h .= "<div dojoType=\"dojo.data.ItemFileWriteStore\" jsId=\"scheduleStore\" ";
+		$h .= "data=\"scheduleTimeData\"></div>\n";
+		$h .= "<table dojoType=\"dojox.grid.DataGrid\" jsId=\"scheduleGrid\" sortInfo=1 ";
+		$h .= "store=\"scheduleStore\" style=\"width: 520px; height: 165px;\">\n";
+		$h .= "<thead>\n";
+		$h .= "<tr>\n";
+		$h .= "<th field=\"startday\" width=\"94px\" formatter=\"formatDay\">Start Day</th>\n";
+		$h .= "<th field=\"startday\" width=\"94px\" formatter=\"formatTime\">Start Time</th>\n";
+		$h .= "<th field=\"endday\" width=\"94px\" formatter=\"formatDay\">End Day</th>\n";
+		$h .= "<th field=\"endday\" width=\"94px\" formatter=\"formatTime\">End Time</th>\n";
+		$h .= "<th field=\"remove\" width=\"80px\">Remove</th>\n";
+		$h .= "</tr>\n";
+		$h .= "</thead>\n";
+		$h .= "</table>\n";
+		$h .= "</div>\n"; # addeditdlgcontent
+
+		$h .= "<div id=\"addeditdlgerrmsg\" class=\"nperrormsg\"></div>\n";
+		$h .= "<div id=\"editdlgbtns\" align=\"center\">\n";
+		$h .= dijitButton('addeditbtn', "Confirm", "saveResource();");
+		$h .= dijitButton('', "Cancel", "addEditDlgHide();");
+		$h .= "</div>\n"; # editdlgbtns
+		$h .= "</div>\n"; # addeditdlg
+
+		$h .= "<div dojoType=dijit.Dialog\n";
+		$h .= "      id=\"groupingnote\"\n";
+		$h .= "      title=\"Schedule Grouping\"\n";
+		$h .= "      duration=250\n";
+		$h .= "      draggable=true>\n";
+		$h .= "Each schedule should be a member of a schedule<br>resource group. The following dialog will allow you<br>to add the new schedule to a group.<br><br>\n";
+		$h .= "<div align=\"center\">\n";
+		$h .= dijitButton('', "Close", "dijit.byId('groupingnote').hide();");
+		$h .= "</div>\n"; # btn div
+		$h .= "</div>\n"; # groupingnote
+
+		$h .= "<div dojoType=dijit.Dialog\n";
+		$h .= "      id=\"groupdlg\"\n";
+		$h .= "      title=\"Schedule Grouping\"\n";
+		$h .= "      duration=250\n";
+		$h .= "      draggable=true>\n";
+		$h .= "<div id=\"groupdlgcontent\"></div>\n";
+		$h .= "<div align=\"center\">\n";
+		$script  = "    dijit.byId('groupdlg').hide();\n";
+		$script .= "    checkFirstAdd();\n";
+		$h .= dijitButton('', "Close", $script);
+		$h .= "</div>\n"; # btn div
+		$h .= "</div>\n"; # groupdlg
+
+		return $h;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn AJsaveResource()
+	///
+	/// \brief saves changes to resource
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function AJsaveResource() {
+		$add = getContinuationVar('add', 0);
+		$data = $this->validateResourceData();
+		if($data['error']) {
+			$ret = array('status' => 'error', 'msg' => $data['errormsg']);
+			sendJSON($ret);
+			return;
+		}
+
+		if($add) {
+			if(! $data['rscid'] = $this->addResource($data)) {
+				sendJSON(array('status' => 'adderror',
+				               'errormsg' => 'Error encountered while trying to create new schedule.<br>Please contact an admin for assistance.'));
+				return;
+			}
+		}
+		else {
+			$ownerid = getUserlistID($data['owner']);
+			$query = "UPDATE schedule "
+			       . "SET name = '{$data['name']}', "
+			       .     "ownerid = $ownerid "
+			       . "WHERE id = {$data['rscid']}";
+			doQuery($query);
+		}
+
+		if(! $add) {
+			$query = "DELETE FROM scheduletimes WHERE scheduleid = {$data['rscid']}";
+			doQuery($query, 101);
+		}
+		$qvals = array();
+		foreach($data['times'] as $time)
+			$qvals[] = "({$data['rscid']}, {$time['start']}, {$time['end']})";
+		$allvals = implode(',', $qvals);
+		$query = "INSERT INTO scheduletimes "
+		       .        "(scheduleid, start, end) "
+				 . "VALUES $allvals";
+		doQuery($query, 101);
+
+		# clear user resource cache for this type
+		$key = getKey(array(array($this->restype . "Admin"), array("administer"), 0, 1, 0));
+		unset($_SESSION['userresources'][$key]);
+		$key = getKey(array(array($this->restype . "Admin"), array("administer"), 0, 0, 0));
+		unset($_SESSION['userresources'][$key]);
+		$key = getKey(array(array($this->restype . "Admin"), array("manageGroup"), 0, 1, 0));
+		unset($_SESSION['userresources'][$key]);
+		$key = getKey(array(array($this->restype . "Admin"), array("manageGroup"), 0, 0, 0));
+		unset($_SESSION['userresources'][$key]);
+
+		$tmp = $this->getData(array('includedeleted' => 0, 'rscid' => $data['rscid']));
+		$data = $tmp[$data['rscid']];
+		$arr = array('status' => 'success');
+		if($add) {
+			$arr['action'] = 'add';
+			$arr['nogroups'] = 0;
+			$groups = getUserResources(array($this->restype . 'Admin'), array('manageGroup'), 1);
+			if(count($groups[$this->restype]))
+				$arr['groupingHTML'] = $this->groupByResourceHTML();
+			else
+				$arr['nogroups'] = 1;
+		}
+		else
+			$arr['action'] = 'edit';
+		$arr['data'] = $data;
+		sendJSON($arr);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn addResource($data)
+	///
+	/// \param $data - array of needed data for adding a new resource
+	///
+	/// \return id of new resource
+	///
+	/// \brief handles adding a new schedule and other associated data to the
+	/// database
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function addResource($data) {
+		global $user;
+
+		$ownerid = getUserlistID($data['owner']);
+		$query = "INSERT INTO schedule "
+		       .         "(name, "
+		       .         "ownerid) "
+		       . "VALUES ('{$data['name']}', "
+		       .         "$ownerid)";
+		doQuery($query);
+
+		$rscid = dbLastInsertID();
+		if($rscid == 0) {
+			return 0;
+		}
+
+		$query = "INSERT INTO resource "
+				 .        "(resourcetypeid, "
+				 .        "subid) "
+				 . "VALUES (15, "
+				 .         "$rscid)";
+		doQuery($query, 223);
+	
+		return $rscid;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn validateResourceData()
+	///
+	/// \return array with these fields:\n
+	/// \b rscid - id of resource (from schedule table)\n
+	/// \b name\n
+	/// \b owner\n
+	/// \b times - array of arrays, each having 2 keys: start and end, each in
+	///    unix timestamp format\n
+	/// \b error - 0 if submitted data validates; 1 if anything is invalid\n
+	/// \b errormsg - if error = 1; string of error messages separated by html
+	///    break tags
+	///
+	/// \brief validates form input from editing or adding a schedule
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function validateResourceData() {
+		global $user;
+
+		$return = array('error' => 0);
+		$errormsg = array();
+
+		$return['rscid'] = getContinuationVar('rscid', 0);
+		$return["name"] = processInputVar("name", ARG_STRING);
+		$return["owner"] = processInputVar("owner", ARG_STRING, "{$user["unityid"]}@{$user['affiliation']}");
+		$times = processInputVar('times', ARG_STRING);
+
+		if(! preg_match("/^([A-Za-z0-9-!@#$%^&\*\(\)_=\+\[\]{}\\\|:;,\.\/\?~` ]){2,30}$/", $return['name'])) {
+			$return['error'] = 1;
+			$errormsg[] = "Name cannot contain single (') or double (&quot;) quotes, "
+			            . "less than (&lt;), or greater than (&gt;) and can be from 2 to 30 "
+			            . "characters long";
+		}
+		elseif($this->checkForScheduleName($return['name'], $return['rscid'])) {
+			$return['error'] = 1;
+			$errormsg[] = "A schedule already exists with this name.";
+		}
+		if(! validateUserid($return['owner'])) {
+			$return['error'] = 1;
+			$errormsg[] = "Submitted owner is not valid";
+		}
+		if(! preg_match('/^([0-9]+:[0-9]+,)*([0-9]+:[0-9]+){1}$/', $times)) {
+			$return['error'] = 1;
+			$errormsg[] = "Invalid time data submitted";
+		}
+
+		if(! $return['error']) {
+			$times = explode(',', $times);
+			$return['times'] = array();
+			foreach($times as $pair) {
+				list($start, $end) = explode(':', $pair);
+				foreach($return['times'] as $check) {
+					if($start < $check['end'] && $end > $check['start']) {
+						$return['error'] = 1;
+						$errormsg[] = "Two sets of times are overlapping - please correct and save again";
+						break(2);
+					}
+				}
+				$return['times'][] = array('start' => $start, 'end' => $end);
+			}
+		}
+
+		if($return['error'])
+			$return['errormsg'] = implode('<br>', $errormsg);
+
+		return $return;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn toggleDeleteResourceExtra()
+	///
+	/// \return html
+	///
+	/// \brief generates additional HTML to be included at the end of the confirm
+	/// toggle delete resource page
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function toggleDeleteResourceExtra() {
+		$rscid = processInputVar('rscid', ARG_NUMERIC);
+		$data = $this->getData('');
+		$h = '';
+		if(count($data[$rscid]["times"])) {
+			$h .= "<br><TABLE>\n";
+			$h .= "  <TR>\n";
+			$h .= "    <TH>Start</TH>\n";
+			$h .= "    <TD>&nbsp;</TD>\n";
+			$h .= "    <TH>End</TH>\n";
+			$h .= "  <TR>\n";
+			foreach($data[$rscid]["times"] as $time) {
+				$h .= "  <TR>\n";
+				$h .= "    <TD>" . $this->minToDaytime($time["start"]) . "</TD>\n";
+				$h .= "    <TD>&nbsp;</TD>\n";
+				$h .= "    <TD>" . $this->minToDaytime($time["end"]) . "</TD>\n";
+				$h .= "  </TR>\n";
+			}
+			$h .= "</TABLE><br>\n";
+		}
+		return $h;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn submitToggleDeleteResourceExtra($rscid, $deleted)
+	///
+	/// \param $rscid - id of a resource (from schedule table)
+	/// \param $deleted - (optional, default=0) 1 if resource was previously
+	/// deleted; 0 if not
+	///
+	/// \brief handles deleting associated entries from scheduletimes table
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function submitToggleDeleteResourceExtra($rscid, $deleted=0) {
+		$query = "DELETE FROM scheduletimes "
+		       . "WHERE scheduleid = $rscid";
+		doQuery($query);
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn checkForScheduleName($name, $id)
+	///
+	/// \param $name - the name of a schedule
+	/// \param $id - id of a schedule to ignore
+	///
+	/// \return 1 if $name is already in the schedule table, 0 if not
+	///
+	/// \brief checks for $name being in the schedule table except for $id
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function checkForScheduleName($name, $id) {
+		$query = "SELECT id FROM schedule "
+				 . "WHERE name = '$name'";
+		
+		if(! empty($id))
+			$query .= " AND id != $id";
+		$qh = doQuery($query, 101);
+		if(mysql_num_rows($qh))
+			return 1;
+		return 0;
+	}
+
+	/////////////////////////////////////////////////////////////////////////////
+	///
+	/// \fn minToDaytime($min)
+	///
+	/// \param $min - minute of the week
+	///
+	/// \return day of week and time of day
+	///
+	/// \brief calculates day of week and time of day from $min and returns a
+	/// nicely formatted string of "Weekday&nbsp;HH:MM am/pm"
+	///
+	/////////////////////////////////////////////////////////////////////////////
+	function minToDaytime($min) {
+		$days = array("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday",
+						  "Friday", "Saturday");
+		$time = minuteToTime($min % 1440);
+		if((int)($min / 1440) == 0) {
+			$day = 0;
+		}
+		elseif((int)($min / 1440) == 1) {
+			$day = 1;
+		}
+		elseif((int)($min / 1440) == 2) {
+			$day = 2;
+		}
+		elseif((int)($min / 1440) == 3) {
+			$day = 3;
+		}
+		elseif((int)($min / 1440) == 4) {
+			$day = 4;
+		}
+		elseif((int)($min / 1440) == 5) {
+			$day = 5;
+		}
+		elseif((int)($min / 1440) == 6) {
+			$day = 6;
+		}
+		elseif((int)($min / 1440) > 6) {
+			$day = 0;
+		}
+		return $days[$day] . "&nbsp;$time";
+	}
+}
+?>