Merge branch '1.0.x' of gitorious.org:statusnet/mainline into 1.0.x

This commit is contained in:
Evan Prodromou 2011-08-02 13:13:10 -04:00
commit f6a7335ccd
11 changed files with 501 additions and 82 deletions

View File

@ -1501,16 +1501,18 @@ function common_enqueue_notice($notice)
} }
/** /**
* Broadcast profile updates to remote subscribers. * Legacy function to broadcast profile updates to OMB remote subscribers.
*
* XXX: This probably needs killing, but there are several bits of code
* that broadcast profile changes that need to be dealt with. AFAIK
* this function is only used for OMB. -z
* *
* Since this may be slow with a lot of subscribers or bad remote sites, * Since this may be slow with a lot of subscribers or bad remote sites,
* this is run through the background queues if possible. * this is run through the background queues if possible.
*/ */
function common_broadcast_profile(Profile $profile) function common_broadcast_profile(Profile $profile)
{ {
$qm = QueueManager::get(); Event::handle('BroadcastProfile', array($profile));
$qm->enqueue($profile, "profile");
return true;
} }
function common_profile_url($nickname) function common_profile_url($nickname)

View File

@ -82,6 +82,7 @@ class EventPlugin extends MicroappPlugin
case 'CancelrsvpAction': case 'CancelrsvpAction':
case 'ShoweventAction': case 'ShoweventAction':
case 'ShowrsvpAction': case 'ShowrsvpAction':
case 'TimelistAction':
include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
return false; return false;
case 'EventListItem': case 'EventListItem':
@ -89,6 +90,7 @@ class EventPlugin extends MicroappPlugin
case 'EventForm': case 'EventForm':
case 'RSVPForm': case 'RSVPForm':
case 'CancelRSVPForm': case 'CancelRSVPForm':
case 'EventTimeList':
include_once $dir . '/'.strtolower($cls).'.php'; include_once $dir . '/'.strtolower($cls).'.php';
break; break;
case 'Happening': case 'Happening':
@ -121,6 +123,8 @@ class EventPlugin extends MicroappPlugin
$m->connect('rsvp/:id', $m->connect('rsvp/:id',
array('action' => 'showrsvp'), array('action' => 'showrsvp'),
array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')); array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
$m->connect('main/event/updatetimes',
array('action' => 'timelist'));
return true; return true;
} }
@ -345,7 +349,7 @@ class EventPlugin extends MicroappPlugin
function onEndShowScripts($action) function onEndShowScripts($action)
{ {
$action->inlineScript('$(document).ready(function() { $("#event-startdate").datepicker(); $("#event-enddate").datepicker(); });'); $action->script($this->path('event.js'));
} }
function onEndShowStyles($action) function onEndShowStyles($action)

View File

@ -6,3 +6,11 @@
.event-title { margin-left: 0px; } .event-title { margin-left: 0px; }
#content .event .entry-title { margin-left: 0px; } #content .event .entry-title { margin-left: 0px; }
#content .event .entry-content { margin-left: 0px; } #content .event .entry-content { margin-left: 0px; }
.ui-autocomplete {
max-height: 100px;
overflow-y: auto;
/* prevent horizontal scrollbar */
overflow-x: hidden;
/* add padding to account for vertical scrollbar */
padding-right: 20px;
}

73
plugins/Event/event.js Normal file
View File

@ -0,0 +1,73 @@
$(document).ready(function() {
var today = new Date();
$("#event-startdate").datepicker({
// Don't let the user set a crazy start date
minDate: today,
onClose: function(dateText, picker) {
// Don't let the user set a crazy end date
var newStartDate = new Date(dateText);
var endDate = new Date($("#event-startdate").val());
if (endDate < newStartDate) {
$("#event-enddate").val(dateText);
}
if (dateText !== null) {
$("#event-enddate").datepicker('option', 'minDate', new Date(dateText));
}
},
onSelect: function() {
var startd = $("#event-startdate").val();
var endd = $("#event-enddate").val();
var sdate = new Date(startd);
var edate = new Date(endd);
if (sdate !== edate) {
updateTimes();
}
}
});
$("#event-enddate").datepicker({
minDate: today,
onSelect: function() {
var startd = $("#event-startdate").val();
var endd = $("#event-enddate").val();
var sdate = new Date(startd);
var edate = new Date(endd);
if (sdate !== edate) {
updateTimes();
}
}
});
function updateTimes() {
var startd = $("#event-startdate").val();
var endd = $("#event-enddate").val();
var startt = $("#event-starttime option:selected").val();
var endt = $("#event-endtime option:selected").val();
var sdate = new Date(startd + " " + startt);
var edate = new Date(endd + " " + endt);
var duration = (startd === endd);
$.getJSON($('#timelist_action_url').val(),
{ start: startt, ajax: true, duration: duration },
function(data) {
var times = [];
$.each(data, function(key, val) {
times.push('<option value="' + key + '">' + val + '</option>');
});
$("#event-endtime").html(times.join(''));
if (startt < endt) {
$("#event-endtime").val(endt).attr("selected", "selected");
}
})
}
$("#event-starttime").change(function(e) {
updateTimes();
});
});

View File

@ -84,6 +84,17 @@ class EventForm extends Form
function formData() function formData()
{ {
$this->out->elementStart('fieldset', array('id' => 'new_event_data')); $this->out->elementStart('fieldset', array('id' => 'new_event_data'));
// Passing in the URL of the Ajax action that the .js for this form hits
// when selecting event start and end times. JavaScript will try to
// use a relative path, unless explicitely told where an action is,
// and that's a bit difficult to calculate since the event form is on
// so many pages with different paths. It might be worth solving this
// globally by putting the base site path in the Identifier-URL meta tag
// or something similar, so it would be easy to calculate the exact path
// for actions and other things in JavaScripts. -z
$this->out->hidden('timelist_action_url', common_local_url('timelist'));
$this->out->elementStart('ul', 'form_data'); $this->out->elementStart('ul', 'form_data');
$this->li(); $this->li();
@ -97,49 +108,71 @@ class EventForm extends Form
$this->unli(); $this->unli();
$this->li(); $this->li();
$today = new DateTime('today');
$today->setTimezone(new DateTimeZone(common_timezone()));
$this->out->input('event-startdate', $this->out->input('event-startdate',
// TRANS: Field label on event form. // TRANS: Field label on event form.
_m('LABEL','Start date'), _m('LABEL','Start date'),
null, $today->format('m/d/Y'),
// TRANS: Field title on event form. // TRANS: Field title on event form.
_m('Date the event starts.'), _m('Date the event starts.'),
'startdate'); 'startdate');
$this->unli(); $this->unli();
$this->li(); $this->li();
$this->out->input('event-starttime',
// TRANS: Field label on event form. $times = EventTimeList::getTimes();
_m('LABEL','Start time'),
null, $this->out->dropdown(
// TRANS: Field title on event form. 'event-starttime',
_m('Time the event starts.'), // TRANS: Field label on event form.
'starttime'); _m('LABEL','Start time'),
$times,
// TRANS: Field title on event form.
_m('Time the event starts.'),
false,
null
);
$this->unli(); $this->unli();
$this->li(); $this->li();
$this->out->input('event-enddate', $this->out->input('event-enddate',
// TRANS: Field label on event form. // TRANS: Field label on event form.
_m('LABEL','End date'), _m('LABEL','End date'),
null, $today->format('m/d/Y'),
// TRANS: Field title on event form. // TRANS: Field title on event form.
_m('Date the event ends.'), _m('Date the event ends.'),
'enddate'); 'enddate');
$this->unli(); $this->unli();
$this->li(); $this->li();
$this->out->input('event-endtime',
// TRANS: Field label on event form. // XXX: Initial end time should be at least 30 mins out? We could do
_m('LABEL','End time'), // every 15 minute instead -z
null, $keys = array_keys($times);
// TRANS: Field title on event form. $endStr = date('m/d/y', strtotime('now')) . " {$keys[0]}";
_m('Time the event ends.'), $end = new DateTime($endStr);
'endtime'); $end->modify('+30');
$this->out->dropdown(
'event-endtime',
// TRANS: Field label on event form.
_m('LABEL','End time'),
EventTimeList::getTimes($end->format('c'), true),
// TRANS: Field title on event form.
_m('Time the event ends.'),
false,
null
);
$this->unli(); $this->unli();
$this->li(); $this->li();
$this->out->input('event-location', $this->out->input('event-location',
// TRANS: Field label on event form. // TRANS: Field label on event form.
_m('LABEL','Location'), _m('LABEL','Where?'),
null, null,
// TRANS: Field title on event form. // TRANS: Field title on event form.
_m('Event location.'), _m('Event location.'),

View File

@ -83,13 +83,33 @@ class EventListItem extends NoticeListItemAdapter
$out->elementEnd('h3'); // VEVENT/H3 OUT $out->elementEnd('h3'); // VEVENT/H3 OUT
$startDate = strftime("%x", strtotime($event->start_time)); $now = new DateTime();
$startTime = strftime("%R", strtotime($event->start_time)); $startDate = new DateTime($event->start_time);
$endDate = new DateTime($event->end_time);
$userTz = new DateTimeZone(common_timezone());
$endDate = strftime("%x", strtotime($event->end_time)); // Localize the time for the observer
$endTime = strftime("%R", strtotime($event->end_time)); $now->setTimeZone($userTz);
$startDate->setTimezone($userTz);
$endDate->setTimezone($userTz);
// FIXME: better dates $thisYear = $now->format('Y');
$startYear = $startDate->format('Y');
$endYear = $endDate->format('Y');
$dateFmt = 'D, F j, '; // e.g.: Mon, Aug 31
if ($startYear != $thisYear || $endYear != $thisYear) {
$dateFmt .= 'Y,'; // append year if we need to think about years
}
$startDateStr = $startDate->format($dateFmt);
$endDateStr = $endDate->format($dateFmt);
$timeFmt = 'g:ia';
$startTimeStr = $startDate->format($timeFmt);
$endTimeStr = $endDate->format("{$timeFmt} (T)");
$out->elementStart('div', 'event-times'); // VEVENT/EVENT-TIMES IN $out->elementStart('div', 'event-times'); // VEVENT/EVENT-TIMES IN
@ -98,16 +118,16 @@ class EventListItem extends NoticeListItemAdapter
$out->element('abbr', array('class' => 'dtstart', $out->element('abbr', array('class' => 'dtstart',
'title' => common_date_iso8601($event->start_time)), 'title' => common_date_iso8601($event->start_time)),
$startDate . ' ' . $startTime); $startDateStr . ' ' . $startTimeStr);
$out->text(' - '); $out->text(' ');
if ($startDate == $endDate) { if ($startDateStr == $endDateStr) {
$out->element('span', array('class' => 'dtend', $out->element('span', array('class' => 'dtend',
'title' => common_date_iso8601($event->end_time)), 'title' => common_date_iso8601($event->end_time)),
$endTime); $endTimeStr);
} else { } else {
$out->element('span', array('class' => 'dtend', $out->element('span', array('class' => 'dtend',
'title' => common_date_iso8601($event->end_time)), 'title' => common_date_iso8601($event->end_time)),
$endDate . ' ' . $endTime); $endDateStr . ' ' . $endTimeStr);
} }
$out->elementEnd('div'); // VEVENT/EVENT-TIMES OUT $out->elementEnd('div'); // VEVENT/EVENT-TIMES OUT

View File

@ -0,0 +1,119 @@
<?php
/**
* Helper class for calculating and displaying event times
*
* PHP version 5
*
* @category Data
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2011, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Class to get fancy times for the dropdowns on the new event form
*/
class EventTimeList {
/**
* Round up to the nearest half hour
*
* @param string $time the time to round (date/time string)
* @return DateTime the rounded time
*/
public static function nearestHalfHour($time)
{
$start = strtotime($time);
$minutes = date('i', $start);
$hour = date('H', $start);
if ($minutes >= 30) {
$minutes = '00';
$hour++;
} else {
$minutes = '30';
}
$newTimeStr = date('m/d/y', $start) . " {$hour}:{$minutes}:00";
return new DateTime($newTimeStr);
}
/**
* Output a list of times in half-hour intervals
*
* @param string $start Time to start with (date/time string)
* @param boolean $duration Whether to include the duration of the event
* (from the start)
* @return array $times (UTC time string => localized time string)
*/
public static function getTimes($start = 'now', $duration = false)
{
$newTime = self::nearestHalfHour($start);
$newTime->setTimezone(new DateTimeZone(common_timezone()));
$times = array();
$len = 0;
for ($i = 0; $i < 48; $i++) {
// make sure we store the time as UTC
$newTime->setTimezone(new DateTimeZone('UTC'));
$utcTime = $newTime->format('H:i:s');
// localize time for user
$newTime->setTimezone(new DateTimeZone(common_timezone()));
$localTime = $newTime->format('g:ia');
// pretty up the end-time option list a bit
if ($duration) {
$len += 30;
$hours = $len / 60;
// for i18n
$hourStr = _m('hour');
$hoursStr = _m('hrs');
$minStr = _m('mins');
switch ($hours) {
case 0:
$total = " (0 {$minStr})";
break;
case .5:
$total = " (30 {$minStr})";
break;
case 1:
$total = " (1 {$hourStr})";
break;
default:
$total = " ({$hours} " . $hoursStr . ')';
break;
}
$localTime .= $total;
}
$times[$utcTime] = $localTime;
$newTime->modify('+30min'); // 30 min intervals
}
return $times;
}
}

View File

@ -52,8 +52,8 @@ class NeweventAction extends Action
protected $title = null; protected $title = null;
protected $location = null; protected $location = null;
protected $description = null; protected $description = null;
protected $startTime = null; protected $startTime = null;
protected $endTime = null; protected $endTime = null;
/** /**
* Returns the title of the action * Returns the title of the action
@ -89,67 +89,78 @@ class NeweventAction extends Action
$this->checkSessionToken(); $this->checkSessionToken();
} }
$this->title = $this->trimmed('title'); try {
if (empty($this->title)) { $this->title = $this->trimmed('title');
// TRANS: Client exception thrown when trying to post an event without providing a title.
throw new ClientException(_m('Title required.'));
}
$this->location = $this->trimmed('location'); if (empty($this->title)) {
$this->url = $this->trimmed('url'); // TRANS: Client exception thrown when trying to post an event without providing a title.
$this->description = $this->trimmed('description'); throw new ClientException(_m('Title required.'));
}
$startDate = $this->trimmed('startdate'); $this->location = $this->trimmed('location');
$this->url = $this->trimmed('url');
$this->description = $this->trimmed('description');
if (empty($startDate)) { $startDate = $this->trimmed('startdate');
// TRANS: Client exception thrown when trying to post an event without providing a start date.
throw new ClientException(_m('Start date required.'));
}
$startTime = $this->trimmed('starttime'); if (empty($startDate)) {
// TRANS: Client exception thrown when trying to post an event without providing a start date.
throw new ClientException(_m('Start date required.'));
}
if (empty($startTime)) { $startTime = $this->trimmed('event-starttime');
$startTime = '00:00';
}
$endDate = $this->trimmed('enddate'); if (empty($startTime)) {
$startTime = '00:00';
}
if (empty($endDate)) { $endDate = $this->trimmed('enddate');
// TRANS: Client exception thrown when trying to post an event without providing an end date.
throw new ClientException(_m('End date required.'));
}
$endTime = $this->trimmed('endtime'); if (empty($endDate)) {
// TRANS: Client exception thrown when trying to post an event without providing an end date.
throw new ClientException(_m('End date required.'));
}
if (empty($endTime)) { $endTime = $this->trimmed('event-endtime');
$endTime = '00:00';
}
$start = $startDate . ' ' . $startTime; if (empty($endTime)) {
$endTime = '00:00';
}
common_debug("Event start: '$start'"); $start = $startDate . ' ' . $startTime;
$end = $endDate . ' ' . $endTime; common_debug("Event start: '$start'");
common_debug("Event start: '$end'"); $end = $endDate . ' ' . $endTime;
$this->startTime = strtotime($start); common_debug("Event start: '$end'");
$this->endTime = strtotime($end);
if ($this->startTime == 0) { $this->startTime = strtotime($start);
// TRANS: Client exception thrown when trying to post an event with a date that cannot be processed. $this->endTime = strtotime($end);
// TRANS: %s is the data that could not be processed.
throw new Exception(sprintf(_m('Could not parse date "%s".'),
$start));
}
if ($this->startTime == 0) {
// TRANS: Client exception thrown when trying to post an event with a date that cannot be processed.
// TRANS: %s is the data that could not be processed.
throw new ClientException(sprintf(_m('Could not parse date "%s".'),
$start));
}
if ($this->endTime == 0) { if ($this->endTime == 0) {
// TRANS: Client exception thrown when trying to post an event with a date that cannot be processed. // TRANS: Client exception thrown when trying to post an event with a date that cannot be processed.
// TRANS: %s is the data that could not be processed. // TRANS: %s is the data that could not be processed.
throw new Exception(sprintf(_m('Could not parse date "%s".'), throw new ClientException(sprintf(_m('Could not parse date "%s".'),
$end)); $end));
}
} catch (ClientException $ce) {
if ($this->boolean('ajax')) {
$this->outputAjaxError($ce->getMessage());
return false;
} else {
$this->error = $ce->getMessage();
$this->showPage();
return false;
}
} }
return true; return true;
@ -220,9 +231,13 @@ class NeweventAction extends Action
RSVP::saveNew($profile, $event, RSVP::POSITIVE); RSVP::saveNew($profile, $event, RSVP::POSITIVE);
} catch (ClientException $ce) { } catch (ClientException $ce) {
$this->error = $ce->getMessage(); if ($this->boolean('ajax')) {
$this->showPage(); $this->outputAjaxError($ce->getMessage());
return; } else {
$this->error = $ce->getMessage();
$this->showPage();
return;
}
} }
if ($this->boolean('ajax')) { if ($this->boolean('ajax')) {
@ -242,6 +257,23 @@ class NeweventAction extends Action
} }
} }
// @todo factor this out into a base class
function outputAjaxError($msg)
{
header('Content-Type: text/xml;charset=utf-8');
$this->xw->startDocument('1.0', 'UTF-8');
$this->elementStart('html');
$this->elementStart('head');
// TRANS: Page title after an AJAX error occurs
$this->element('title', null, _('Ajax Error'));
$this->elementEnd('head');
$this->elementStart('body');
$this->element('p', array('id' => 'error'), $msg);
$this->elementEnd('body');
$this->elementEnd('html');
return;
}
/** /**
* Show the event form * Show the event form
* *

106
plugins/Event/timelist.php Normal file
View File

@ -0,0 +1,106 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2011, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Event
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @copyright 2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Callback handler to populate end time dropdown
*/
class TimelistAction extends Action {
private $start;
private $duration;
/**
* Get ready
*
* @param array $args misc. arguments
*
* @return boolean true
*/
function prepare($args) {
parent::prepare($args);
$this->start = $this->arg('start');
$this->duration = $this->boolean('duration', false);
return true;
}
/**
* Handle input and ouput something
*
* @param array $args $_REQUEST arguments
*
* @return void
*/
function handle($args)
{
parent::handle($args);
if (!common_logged_in()) {
// TRANS: Error message displayed when trying to perform an action that requires a logged in user.
$this->clientError(_('Not logged in.'));
return;
}
if (!empty($this->start)) {
$times = EventTimeList::getTimes($this->start, $this->duration);
} else {
$this->clientError(_m('Unexpected form submission.'));
return;
}
if ($this->boolean('ajax')) {
header('Content-Type: application/json; charset=utf-8');
print json_encode($times);
} else {
$this->clientError(_m('This action is AJAX only.'));
}
}
/**
* Override the regular error handler to show something more
* ajaxy
*
* @param string $msg error message
* @param int $code error code
*/
function clientError($msg, $code = 400) {
if ($this->boolean('ajax')) {
header('Content-Type: application/json; charset=utf-8');
print json_encode(
array(
'success' => false,
'code' => $code,
'message' => $msg
)
);
} else {
parent::clientError($msg, $code);
}
}
}

View File

@ -369,6 +369,18 @@ class OMBPlugin extends Plugin
return true; return true;
} }
/**
* Broadcast a profile over OMB
*
* @param Profile $profile to broadcast
* @return false
*/
function onBroadcastProfile($profile) {
$qm = QueueManager::get();
$qm->enqueue($profile, "profile");
return true;
}
/** /**
* Plugin version info * Plugin version info
* *

View File

@ -1171,9 +1171,19 @@ td.entity_profile {
width: auto; width: auto;
} }
#event-startdate, #event-starttime, #event-enddate, #event-endtime { label[for=event-starttime], label[for=event-endtime] {
width: 120px; display: none;
}
#event-starttime, #event-endtime {
margin-top: -1px;
margin-bottom: -1px;
height: 2em;
}
#event-startdate, #event-enddate {
margin-right: 20px; margin-right: 20px;
width: 120px;
} }
/* Limited-scope specific styles */ /* Limited-scope specific styles */