<?php /** * EGroupware - Calendar's Listview and Search * * @link http://www.egroupware.org * @package calendar * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> * @copyright (c) 2005-11 by RalfBecker-At-outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ /** * Class to generate the calendar listview and the search * * The new UI, BO and SO classes have a strikt definition, in which time-zone they operate: * UI only operates in user-time, so there have to be no conversation at all !!! * BO's functions take and return user-time only (!), they convert internaly everything to servertime, because * SO operates only on server-time * * The state of the UI elements is managed in the uical class, which all UI classes extend. * * All permanent debug messages of the calendar-code should done via the debug-message method of the bocal class !!! */ class calendar_uilist extends calendar_ui { var $public_functions = array( 'listview' => True, ); /** * integer level or string function- or widget-name * * @var mixed */ var $debug=false; /** * Filternames * * @var array */ var $date_filters = array( 'after' => 'After current date', 'before' => 'Before current date', 'all' => 'All events', 'custom' => 'Selected range', ); /** * Constructor * * @param array $set_states=null to manualy set / change one of the states, default NULL = use $_REQUEST */ function __construct($set_states=null) { parent::__construct(true,$set_states); // call the parent's constructor $GLOBALS['egw_info']['flags']['app_header'] = $GLOBALS['egw_info']['apps']['calendar']['title'].' - '.lang('Listview'). // for a single owner we add it's name to the app-header (count(explode(',',$this->owner)) == 1 ? ': '.$this->bo->participant_name($this->owner) : ''); foreach($this->date_filters as $name => $label) { $this->date_filters[$name] = lang($label); } $this->check_owners_access(); } /** * Show the calendar on the home page * * @return string with content */ function &home() { // set the defaults for the home-page $this->__construct(array( 'date' => $this->bo->date2string($this->bo->now_su), 'cat_id' => 0, 'filter' => 'all', 'owner' => $this->user, 'multiple' => 0, 'view' => $this->bo->cal_prefs['mainscreen_showevents'], )); egw_session::appsession('calendar_list','calendar',''); // in case there's already something set return $this->listview(null,'',true); } /** * Show the listview */ function listview($content=null,$msg='',$home=false) { if ($_GET['msg']) $msg .= $_GET['msg']; if ($this->group_warning) $msg .= $this->group_warning; $etpl = new etemplate('calendar.list'); if (is_array($content)) { // handle a single button like actions foreach(array('delete','timesheet','document') as $button) { if ($content['nm']['rows'][$button]) { list($id) = each($content['nm']['rows'][$button]); $content['nm']['action'] = $button; $content['nm']['selected'] = array($id); } } // Handle actions if ($content['nm']['action']) { // Allow merge using the date range filter if(strpos($content['nm']['action'],'document') !== false && !count($content['nm']['selected']) && !$content['nm']['select_all']) { $content['nm']['selected'][] = $this->get_merge_range($content['nm']); } if (!count($content['nm']['selected']) && !$content['nm']['select_all']) { $msg = lang('You need to select some events first'); } else { if ($this->action($content['nm']['action'],$content['nm']['selected'],$content['nm']['select_all'], $success,$failed,$action_msg,'calendar_list',$msg, $content['nm']['checkboxes']['no_notifications'])) { $msg .= lang('%1 event(s) %2',$success,$action_msg); } elseif(is_null($msg)) { $msg .= lang('%1 event(s) %2, %3 failed because of insufficent rights !!!',$success,$action_msg,$failed); } } } } $content = array( 'nm' => egw_session::appsession('calendar_list','calendar'), 'msg' => $msg, ); if (!is_array($content['nm'])) { $content['nm'] = array( 'get_rows' => 'calendar.calendar_uilist.get_rows', 'filter_no_lang' => True, // I set no_lang for filter (=dont translate the options) 'no_filter2' => True, // I disable the 2. filter (params are the same as for filter) 'no_cat' => True, // I disable the cat-selectbox 'filter' => 'after', 'order' => 'cal_start',// IO name of the column to sort after (optional for the sortheaders) 'sort' => 'ASC',// IO direction of the sort: 'ASC' or 'DESC' 'default_cols' => '!week,weekday,cal_title,cal_description,recure,cal_location,cal_owner,cat_id,pm_id', 'filter_onchange' => "set_style_by_class('table','custom_hide','visibility',this.value == 'custom' ? 'visible' : 'hidden'); if (this.value != 'custom') this.form.submit();", 'header_left' => 'calendar.list.dates', 'row_id' => 'row_id', // set in get rows "$event[id]:$event[recur_date]" 'actions' => $this->get_actions(), ); } if ($_GET['search']) { $content['nm']['search'] = $_GET['search']; } if (isset($_REQUEST['keywords'])) // new search => set filters so every match is shown { $this->adjust_for_search($_REQUEST['keywords'],$content['nm']); } $sel_options['filter'] = &$this->date_filters; // add scrollbar to long describtion, if user choose so in his prefs if ($this->prefs['limit_des_lines'] > 0 || (string)$this->prefs['limit_des_lines'] == '') { $content['css'] .= '<style type="text/css">@media screen { .listDescription { max-height: '. (($this->prefs['limit_des_lines'] ? $this->prefs['limit_des_lines'] : 5) * 1.35). // dono why em is not real lines 'em; overflow: auto; }}</style>'; } $html = $etpl->exec('calendar.calendar_uilist.listview',$content,$sel_options,$readonlys,'',$home ? -1 : 0); // Not sure why this has to be echoed instead of appended, but that's what works. echo calendar_uiviews::edit_series(); return $html; } /** * set filter for search, so that everything is shown */ function adjust_for_search($keywords,&$params) { $params['search'] = $keywords; $params['start'] = 0; $params['order'] = 'cal_start'; if ($keywords) { $params['filter'] = 'all'; $params['sort'] = 'DESC'; unset($params['col_filter']['participant']); } else { $params['filter'] = 'after'; $params['sort'] = 'ASC'; } } /** * query calendar for nextmatch in the listview * * @internal * @param array &$params parameters * @param array &$rows returned rows/events * @param array &$readonlys eg. to disable buttons based on acl */ function get_rows(&$params,&$rows,&$readonlys) { //echo "uilist::get_rows() params="; _debug_array($params); if (isset($_GET['listview_days']) && is_numeric($_GET['listview_days'])) { $params['filter'] = 'fixed'; } if ($params['filter'] == 'custom') { if (!$params['startdate'] && !$params['enddate']) { $params['filter'] = 'all'; } elseif (!$params['startdate']) { $params['filter'] = 'before'; $this->manage_states(array('date' => $this->bo->date2string($params['enddate']))); } elseif (!$params['enddate']) { $params['filter'] = 'after'; $this->manage_states(array('date' => $this->bo->date2string($params['startdate']))); } } $old_params = egw_session::appsession('calendar_list','calendar'); if ($old_params['filter'] && $old_params['filter'] != $params['filter']) // filter changed => order accordingly { $params['order'] = 'cal_start'; $params['sort'] = $params['filter'] == 'after' ? 'ASC' : 'DESC'; } if ($old_params['search'] != $params['search']) { $this->adjust_for_search($params['search'],$params); } if (!$params['csv_export']) egw_session::appsession('calendar_list','calendar',$params); // do we need to query custom fields and which $select_cols = explode(',',$params['selectcols']); if (in_array('cfs',$select_cols)) { $cfs = array(); foreach($select_cols as $col) { if ($col[0] == '#') $cfs[] = substr($col,1); } } $search_params = array( 'cat_id' => $this->cat_id, 'filter' => $this->filter, 'query' => $params['search'], 'offset' => (int) $params['start'], 'num_rows'=> $params['num_rows'], 'order' => $params['order'] ? $params['order'].' '.$params['sort'] : 'cal_start', 'cfs' => $params['csv_export'] ? array() : $cfs, ); switch($params['filter']) { case 'all': break; case 'before': $search_params['end'] = $this->date; $label = lang('Before %1',$this->bo->long_date($this->date)); break; case 'custom': $GLOBALS['egw']->js->set_onload("set_style_by_class('table','custom_hide','visibility','visible');"); $this->first = $search_params['start'] = $params['startdate']; $this->last = $search_params['end'] = $params['enddate']; $label = $this->bo->long_date($this->first,$this->last); break; case 'fixed': if ($this->listview_days == 5 || $this->listview_days == 7) // weekview { $this->first = $this->datetime->get_weekday_start($this->year,$this->month,$this->day); $this->last = $this->bo->date2array($this->first); $this->last['day'] += (int) $this->listview_days - 1; $this->last['hour'] = 23; $this->last['minute'] = $this->last['sec'] = 59; unset($this->last['raw']); $this->last = $this->bo->date2ts($this->last); $this->date_filters['fixed'] = $label = lang('Week').' '.adodb_date('W',$this->first).': '.$this->bo->long_date($this->first,$this->last); $params['startdate'] = $search_params['start'] = $this->first; $params['enddate'] = $search_params['end'] = $this->last; break; } elseif ((string)$this->listview_days === '0') // monthview { $this->first = $this->bo->date2array($this->date); $this->first['day'] = 1; unset($this->first['raw']); $this->last = $this->first; $this->last['month'] += 1; $this->first = $this->bo->date2ts($this->first); $this->last = $this->bo->date2ts($this->last); $this->last--; $this->date_filters['fixed'] = $label = lang(adodb_date('F',$this->bo->date2ts($this->date))).' '.$this->year; $params['startdate'] = $search_params['start'] = $this->first; $params['enddate'] = $search_params['end'] = $this->last; break; } // fall through to after given date case 'after': default: $label = lang('After %1',$this->bo->long_date($this->date)); $search_params['start'] = $this->date; break; } if ($label) { $GLOBALS['egw_info']['flags']['app_header'] .= ': '.$label; $params['options-filter'][$params['filter']] = $label; // Add it in, or it will be cleared } if ((int) $params['col_filter']['participant']) { $search_params['users'] = (int) $params['col_filter']['participant']; } elseif(empty($params['search'])) // active search displays entries from all users { $search_params['users'] = explode(',',$this->owner); } $rows = $js_integration_data = array(); foreach((array) $this->bo->search($search_params) as $event) { if (($readonlys['edit['.$event['id'].']'] = !$this->bo->check_perms(EGW_ACL_EDIT,$event))) { $event['class'] .= 'rowNoEdit '; } // show only edit or view icon, not both $readonlys['view['.$event['id'].']'] = !$readonlys['edit['.$event['id'].']']; // Delete disabled for other applications if (($readonlys['delete['.$event['id'].']'] = !$this->bo->check_perms(EGW_ACL_DELETE,$event) || !is_numeric($event['id']))) { $event['class'] .= 'rowNoDelete '; } // mark deleted events if ($event['deleted']) { $event['class'] .= 'rowDeleted '; } // Filemanager disabled for other applications $readonlys['filemanager['.$event['id'].']'] = !is_numeric($event['id']); $event['recure'] = $this->bo->recure2string($event); if ($params['csv_export']) { $event['participants'] = implode(",\n",$this->bo->participants($event,true)); } else { $event['parts'] = implode(",\n",$this->bo->participants($event,true)); $event['date'] = $this->bo->date2string($event['start']); } if (empty($event['description'])) $event['description'] = ' '; // no description screws the titles horz. alignment if (empty($event['location'])) $event['location'] = ' '; // no location screws the owner horz. alignment // respect category permissions if(!empty($event['category'])) { $event['category'] = $this->categories->check_list(EGW_ACL_READ, $event['category']); } if(!(int)$event['id'] && preg_match('/^([a-z_-]+)([0-9]+)$/i',$event['id'],$matches)) { $app = $matches[1]; $app_id = $matches[2]; $icons = array(); if (($is_private = calendar_bo::integration_get_private($app,$app_id,$event))) { $icons[] = html::image('calendar','private'); } else { $icons = calendar_uiviews::integration_get_icons($app,$app_id,$event); } } else { $is_private = !$this->bo->check_perms(EGW_ACL_READ,$event); } if ($is_private) { $readonlys['filemanager['.$event['id'].']'] = $readonlys['view['.$event['id'].']'] = true; // no view icon $event['class'] .= 'rowNoView '; } $event['app'] = 'calendar'; $event['app_id'] = $event['id']; // Edit link if($app && $app_id) { $popup = calendar_uiviews::integration_get_popup($app,$app_id); // Need to strip off 'onclick' $event['edit_link'] = preg_replace('/ ?onclick="(.+)"/i', '$1', $popup); $event['app'] = $app; $event['app_id'] = $app_id; // populate js_integration_data, if not already set if (!isset($js_integration_data[$app])) { $js_integration_data[$app] = calendar_bo::integration_get_data($app,'edit_link'); if (!is_array($js_integration_data[$app]) || !isset($js_integration_data[$app]['edit'])) { $js_integration_data[$app]['edit'] = egw_link::get_registry($app, 'edit'); $js_integration_data[$app]['edit_id'] = egw_link::get_registry($app, 'edit_id'); $js_integration_data[$app]['edit_popup'] = egw_link::get_registry($app, 'edit_popup'); } } } elseif ($event['recur_type'] != MCAL_RECUR_NONE) { $event['edit_link'] = "edit_series({$event['id']}, {$event['start']});return false;"; $event['app_id'] .= ':'.$event['recur_date']; } else { $view_link = egw::link('/index.php',array('menuaction'=>'calendar.calendar_uiforms.edit','cal_id'=>$event['id'],'date'=>$this->bo->date2string($event['start']))); $event['edit_link'] = $this->popup($view_link).'; return false;'; } // set id for grid $event['row_id'] = $event['id'].($event['recur_type'] ? ':'.$event['recur_date'] : ''); $rows[] = $event; unset($app); unset($app_id); } // set js_calendar_integration object, for app.js cal_open() function $GLOBALS['egw_info']['flags']['java_script'] = '<script type="text/javascript"> var js_integration_data='.json_encode($js_integration_data).'; </script>'; $wv=0; $dv=0; $params['options-selectcols']['week'] = lang('Week'); $params['options-selectcols']['weekday'] = lang('Weekday'); if ((substr($this->cal_prefs['nextmatch-calendar.list.rows'],0,4) == 'week' && strlen($this->cal_prefs['nextmatch-calendar.list.rows'])==4) || substr($this->cal_prefs['nextmatch-calendar.list.rows'],0,5) == 'week,') { $rows['format'] = '32'; // prefix date with week-number $wv=1; } if (!(strpos($this->cal_prefs['nextmatch-calendar.list.rows'],'weekday')===FALSE)) { $rows['format'] = '16'; $dv=1; } if ($wv && $dv) { $rows['format'] = '64'; } if ($this->cat_id) $rows['no_cat_id'] = true; if (!$GLOBALS['egw_info']['user']['apps']['projectmanager']) { $params['options-selectcols']['pm_id'] = false; } //_debug_array($rows); return $this->bo->total; } /** * apply an action to multiple events * * @param string/int $action 'delete', 'ical', 'print', 'email' * @param array $checked event id's to use if !$use_all * @param boolean $use_all if true use all events of the current selection (in the session) * @param int &$success number of succeded actions * @param int &$failed number of failed actions (not enought permissions) * @param string &$action_msg translated verb for the actions, to be used in a message like %1 events 'deleted' * @param string/array $session_name 'calendar_list' * @return boolean true if all actions succeded, false otherwise */ function action($action,$checked,$use_all,&$success,&$failed,&$action_msg,$session_name,&$msg,$skip_notification=false) { //echo '<p>' . __METHOD__ . "('$action',".print_r($checked,true).','.(int)$use_all.",...)</p>\n"; $success = $failed = 0; // Split out combined values if(strpos($action, 'status') !== false) { list($action, $status) = explode('-', $action); } elseif (strpos($action, '_') !== false) { list($action, $settings) = explode('_', $action,2); } if ($use_all) { // get the whole selection $query = is_array($session_name) ? $session_name : egw_session::appsession($session_name,'calendar'); @set_time_limit(0); // switch off the execution time limit, as for big selections it's too small $query['num_rows'] = -1; // all $this->get_rows($query,$checked,$readonlys,($action != 'ical')); // true = only return the id's } else { // Pull the date for recurring events $split = array(); foreach($checked as $key) { if(!is_array($key) && strpos($key,':') !== false) { list($id, $recur_date) = explode(':', $key); $split[] = array( 'id' => $id, 'recur_date' => $recur_date ); } else { $split[] = $key; } } $checked = $split; } // Actions where one action is done to the group switch($action) { case 'ical': $boical = new calendar_ical(); $ical =& $boical->exportVCal($checked,'2.0','PUBLISH',false); html::content_header($content['file'] ? $content['file'] : 'event.ics','text/calendar',bytes($ical)); echo $ical; common::egw_exit(); break; case 'document': $msg = $this->download_document($checked,$settings); $failed = count($checked); return false; } // Actions where the action is applied to each entry if(strpos($action, 'timesheet') !== false) { $timesheet_bo = new timesheet_bo(); } foreach($checked as $event) { if(!(int)$event['id'] && preg_match('/^([a-z_-]+)([0-9]+)$/i',$event['id'],$matches)) { $app = $matches[1]; $app_id = $matches[2]; } else { $id = $event['id']; $recur_date = $event['recur_date']; } switch($action) { case 'delete': $action_msg = lang('deleted'); if ($id && $this->bo->delete($id, $recur_date,false,$skip_notification)) { $success++; } else { $failure++; } break; case 'undelete': $action_msg = lang('recovered'); if ($id && ($event = $this->bo->read($id, $recur_date)) && $this->bo->check_perms(EGW_ACL_EDIT,$id) && is_array($event) && $event['deleted']) { $event['deleted'] = false; if($this->bo->save($event)) { $success++; break; } } $failure++; break; case 'status': if($id && ($event = $this->bo->read($id, $recur_date))) { $old_status = $event['participants'][$GLOBALS['egw_info']['user']['account_id']]; calendar_so::split_status($old_status, $quantity, $role); if ($old_status != $status) { //echo "<p>$uid: status changed '$data[old_status]' --> '$status<'/p>\n"; $new_status = calendar_so::combine_status($status, $quantity, $role); if ($this->bo->set_status($id,$GLOBALS['egw_info']['user']['account_id'],$new_status,$recur_date, false,true,$skip_notification)) { $success++; $msg = lang('Status changed'); } } } else { $failure++; } break; case 'timesheet-add': if($id && !$app) { $event = $this->bo->read($id, $recur_date); } elseif ($app) { $query = egw_session::appsession('calendar_list','calendar'); $query['query'] = $app_id; $query['search'] = $app_id; $result = $this->bo->search($query); $event = $result[$app.$app_id]; } if(!$event) { $failure++; continue; } $timesheet = array( 'ts_title' => $event['title'], 'ts_description' => $event['description'], 'ts_start' => $event['start'], 'ts_duration' => ($event['end'] - $event['start']) / 60, 'ts_quantity' => ($event['end'] - $event['start']) / 3600, 'ts_owner' => $GLOBALS['egw_info']['user']['account_id'], 'cat_id' => null, 'pl_id' => null ); // Add global categories $categories = explode(',',$event['category']); $global_categories = array(); foreach($categories as $cat_id) { if($GLOBALS['egw']->categories->is_global($cat_id)) { $global_categories[] = $cat_id; } } if(count($global_categories)) { $timesheet['cat_id'] = implode(',', $global_categories); } $timesheet_bo->data = array(); $err = $timesheet_bo->save($timesheet); if(!$err) { $success++; // Can't link to just one of a recurring series of events if(!$recur_date || $app) { // Create link $link_id = $app ? $app_id : $id; egw_link::link($app ? $app : 'calendar', $link_id, 'timesheet', $timesheet_bo->data['ts_id']); } } else { $failure++; } $msg = lang('Timesheet entries created for '); break; } unset($app); unset($app_id); } return ($failure == 0); } /** * Get date ranges to select for merging instead of individual events * * @param $nm nextmatch array from submit * * @return array of ranges */ protected function get_merge_range($nm) { $checked = array(); if($nm['filter'] == 'fixed') { $checked['start'] = $nm['startdate']; $last = $this->bo->date2array($nm['enddate']); $last['hour'] = '23'; $last['minute'] = $last['sec'] = '59'; $checked['end'] = $this->bo->date2ts($last); } else { switch($nm['filter']) { case 'after': $checked['start'] = $nm['startdate'] ? $nm['startdate'] : strtotime('today'); break; case 'before': $checked['end'] = $nm['enddate'] ? $nm['enddate'] : strtotime('tomorrow'); break; case 'custom': $checked['start'] = $nm['startdate']; $checked['end'] = $nm['enddate']; break; } } return $checked; } /** * Download a document with inserted entries * * @param array $ids timesheet-ids * @param string $document vfs-path of document * @return string error-message or error, otherwise the function does NOT return! */ function download_document($ids,$document='') { if (!$document) { $document = $GLOBALS['egw_info']['user']['preferences']['calendar']['default_document']; } elseif ($document[0] != '/') { $document = $GLOBALS['egw_info']['user']['preferences']['calendar']['document_dir'].'/'.$document; } if (!@egw_vfs::stat($document)) { return lang("Document '%1' does not exist or is not readable for you!",$document); } $document_merge = new calendar_merge(); return $document_merge->download($document,$ids); } /** * Get actions / context menu items * * @return array see nextmatch_widget::get_actions() */ private function get_actions() { $actions = array( 'open' => array( 'caption' => 'Open', 'default' => true, 'allowOnMultiple' => false, 'url' => 'menuaction=calendar.calendar_uiforms.edit&cal_id=$id', 'popup' => egw_link::get_registry('calendar', 'view_popup'), 'group' => $group=1, 'onExecute' => 'javaScript:cal_open', 'disableClass' => 'rowNoView', ), 'select_all' => array( 'caption' => 'Whole query', 'checkbox' => true, 'hint' => 'Apply the action on the whole query, NOT only the shown events', 'group' => ++$group, ), 'no_notifications' => array( 'caption' => 'Do not notify', 'checkbox' => true, 'hint' => 'Do not notify of these changes', 'group' => $group, ), ); $status = $this->bo->verbose_status; unset($status['G']); $actions['status'] = array( 'caption' => 'Change your status', 'icon' => 'check', 'prefix' => 'status-', 'children' => $status, 'group' => ++$group, ); ++$group; // integration with other apps: infolog, calendar, filemanager if ($GLOBALS['egw_info']['user']['apps']['filemanager']) { $actions['filemanager'] = array( 'icon' => 'filemanager/navbar', 'caption' => 'Filemanager', 'url' => 'menuaction=filemanager.filemanager_ui.index&path=/apps/$app/$id', 'group' => $group, 'allowOnMultiple' => false, 'onExecute' => 'javaScript:cal_fix_app_id', 'disableClass' => 'rowNoView', ); } if ($GLOBALS['egw_info']['user']['apps']['timesheet']) { $actions['timesheet'] = array( // interactive add for a single event 'icon' => 'timesheet/navbar', 'caption' => 'Timesheet', 'url' => 'menuaction=timesheet.timesheet_ui.edit&link_app[]=$app&link_id[]=$id', 'group' => $group, 'allowOnMultiple' => false, 'hideOnDisabled' => true, // show only one timesheet action in context menu 'onExecute' => 'javaScript:cal_fix_app_id', 'popup' => egw_link::get_registry('timesheet', 'add_popup'), ); $actions['timesheet-add'] = array( // automatic add for multiple events 'icon' => 'timesheet/navbar', 'caption' => 'Timesheet', 'group' => $group, 'allowOnMultiple' => 'only', 'hideOnDisabled' => true, // show only one timesheet action in context menu ); } $actions['ical'] = array( 'icon' => 'filesave', 'caption' => 'Export (iCal)', 'group' => ++$group, 'hint' => 'Download this event as iCal', 'disableClass' => 'rowNoView', ); $actions['documents'] = addressbook_merge::document_action( $this->bo->cal_prefs['document_dir'], ++$group, 'Insert in document', 'document_', $this->bo->cal_prefs['default_document'] ); ++$group; $actions['delete'] = array( 'caption' => 'Delete', 'confirm' => 'Delete this event', 'confirm_multiple' => 'Delete these entries', 'group' => $group, 'disableClass' => 'rowNoDelete', ); // Add in deleted for admins if($GLOBALS['egw_info']['server']['calendar_delete_history']) { $actions['undelete'] = array( 'caption' => 'Un-delete', 'hint' => 'Recover this event', 'group' => $group, 'enabled' => 'javaScript:nm_enableClass', 'enableClass' => 'rowDeleted', 'hideOnDisabled' => true, ); } //_debug_array($actions); return $actions; } }