Merge branch '1.0.x' into testing

This commit is contained in:
Evan Prodromou 2011-08-02 15:20:00 -04:00
commit 575ecd9f4c
22 changed files with 751 additions and 243 deletions

86
README
View File

@ -2,19 +2,19 @@
README
------
StatusNet 0.9.7 "World Leader Pretend"
17 March 2011
StatusNet 1.0.0beta2
2 August 2011
This is the README file for StatusNet, the Open Source microblogging
platform. It includes installation instructions, descriptions of
options you can set, warnings, tips, and general info for
administrators. Information on using StatusNet can be found in the
This is the README file for StatusNet, the Open Source social
networking platform. It includes installation instructions,
descriptions of options you can set, warnings, tips, and general info
for administrators. Information on using StatusNet can be found in the
"doc" subdirectory or in the "help" section on-line.
About
=====
StatusNet is a Free and Open Source microblogging platform. It helps
StatusNet is a Free and Open Source social networking platform. It helps
people in a community, company or group to exchange short (140
characters, by default) messages over the Web. Users can choose which
people to "follow" and receive only their friends' or colleagues'
@ -96,47 +96,27 @@ for additional terms.
New this version
================
This is a security, bug and feature release since version 0.9.6 released on
23 October 2010.
For best compatibility with client software and site federation, and a
lot of bug fixes, it is highly recommended that all public sites
upgrade to the new version. Upgrades require new database indexes for
best performance; see Upgrade below.
This is a security release since version 0.9.7 released on 11 March
2011. It fixes security bug #3260. All sites running version 0.9.7 or
below are recommended to upgrade to 0.9.9 immediately.
Notable changes this version:
- GroupPrivateMessage plugin lets users send private messages
to a group. (Similar to "private groups" on Yammer.)
- Support for Twitter streaming API in Twitter bridge plugin
- Support for a new Activity Streams-based API using AtomPub, allowing
richer API data. See http://status.net/wiki/AtomPub for details.
- Unified Facebook plugin, replacing previous Facebook application
and Facebook Connect plugin.
- A plugin to send out a daily summary email to network users.
- In-line thumbnails of some attachments (video, images) and oEmbed objects.
- Local copies of remote profiles to let moderators manage OStatus users.
- Upgrade upstream JS, minify everything.
- Allow pushing plugin JS, CSS, and static files to a CDN.
- Configurable nickname rules.
- Better support for bit.ly URL shortener.
- InProcessCache plugin for additional caching on top of memcached.
- Support for Activity Streams JSON feeds on many streams.
- User-initiated backup and restore of account data in Activity Streams
format.
- Bookmark plugin for making del.icio.us-like social bookmarking sites,
including del.icio.us backup file import. Supports OStatus.
- SQLProfile plugin to tune SQL queries.
- Better sorting on timelines to support restored or imported data.
- Hundreds of translations from http://translatewiki.net/
- Hundreds of performance tunings, bug fixes, and UI improvements.
- Remove deprecated data from Activity Streams Atom output, to the
extent possible.
- NewMenu plugin for new layout of menu items.
- Experimental support for moving an account from one server to
another, using new AtomPub API.
- Fix bug #3260, a cross-site scripting (XSS) bug that allows an
attacker to inject JavaScript into a page with a carefully structured URL.
- Updated code for Google Analytics to reflect new API.
- Various fixes for Bookmark plugin.
- Updates to reCAPTCHA plugin based on changes to API.
- New plugin to move the site notice to the sidebar.
- Add rss.me to notice source list.
- Updates to data backup/restore.
- Correct use of "likes" in Facebook plugin.
- Ignore failures in Twitter plugin.
A full changelog is available at http://status.net/wiki/StatusNet_0.9.7.
A full changelog is available at http://status.net/wiki/StatusNet_0.9.9.
NOTE: The short-lived StatusNet 0.9.8 ("Letter Never Sent") did not
adequately fix bug #3260 as originally thought; thus this new release.
Prerequisites
=============
@ -246,9 +226,9 @@ especially if you've previously installed PHP/MySQL packages.
1. Unpack the tarball you downloaded on your Web server. Usually a
command like this will work:
tar zxf statusnet-0.9.7.tar.gz
tar zxf statusnet-0.9.9.tar.gz
...which will make a statusnet-0.9.7 subdirectory in your current
...which will make a statusnet-0.9.9 subdirectory in your current
directory. (If you don't have shell access on your Web server, you
may have to unpack the tarball on your local computer and FTP the
files to the server.)
@ -256,7 +236,7 @@ especially if you've previously installed PHP/MySQL packages.
2. Move the tarball to a directory of your choosing in your Web root
directory. Usually something like this will work:
mv statusnet-0.9.7 /var/www/statusnet
mv statusnet-0.9.9 /var/www/statusnet
This will make your StatusNet instance available in the statusnet path of
your server, like "http://example.net/statusnet". "microblog" or
@ -494,7 +474,7 @@ off of amd64 to another server.
Public feed
-----------
You can send *all* messages from your microblogging site to a
You can send *all* messages from your social networking site to a
third-party service using XMPP. This can be useful for providing
search, indexing, bridging, or other cool services.
@ -634,7 +614,7 @@ Private
The administrator can set the "private" flag for a site so that it's
not visible to non-logged-in users. This might be useful for
workgroups who want to share a microblogging site for project
workgroups who want to share a social networking site for project
management, but host it on a public server.
Total privacy is not guaranteed or ensured. Also, privacy is
@ -671,7 +651,7 @@ with this situation.
If you've been using StatusNet 0.7, 0.6, 0.5 or lower, or if you've
been tracking the "git" version of the software, you will probably
want to upgrade and keep your existing data. There is no automated
upgrade procedure in StatusNet 0.9.7. Try these step-by-step
upgrade procedure in StatusNet 0.9.9. Try these step-by-step
instructions; read to the end first before trying them.
0. Download StatusNet and set up all the prerequisites as if you were
@ -692,7 +672,7 @@ instructions; read to the end first before trying them.
5. Once all writing processes to your site are turned off, make a
final backup of the Web directory and database.
6. Move your StatusNet directory to a backup spot, like "statusnet.bak".
7. Unpack your StatusNet 0.9.7 tarball and move it to "statusnet" or
7. Unpack your StatusNet 0.9.9 tarball and move it to "statusnet" or
wherever your code used to be.
8. Copy the config.php file and the contents of the avatar/, background/,
file/, and local/ subdirectories from your old directory to your new
@ -1753,8 +1733,8 @@ There are several ways to get more information about StatusNet.
Feedback
========
* Microblogging messages to http://support.status.net/ are very welcome.
* The microblogging group http://identi.ca/group/statusnet is a good
* Messages to http://support.status.net/ are very welcome.
* The group http://identi.ca/group/statusnet is a good
place to discuss the software.
* StatusNet has a bug tracker for any defects you may find, or ideas for
making things better. http://status.net/bugs

View File

@ -27,6 +27,11 @@ class Avatar extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
static function pivotGet($keyCol, $keyVals, $otherCols)
{
return Memcached_DataObject::pivotGet('Avatar', $keyCol, $keyVals, $otherCols);
}
// We clean up the file, too
function delete()

View File

@ -76,46 +76,7 @@ class Memcached_DataObject extends Safe_DataObject
*/
function multiGet($cls, $keyCol, $keyVals, $skipNulls=true)
{
$result = array_fill_keys($keyVals, null);
$toFetch = array();
foreach ($keyVals as $keyVal) {
$i = self::getcached($cls, $keyCol, $keyVal);
if ($i !== false) {
$result[$keyVal] = $i;
} else if (!empty($keyVal)) {
$toFetch[] = $keyVal;
}
}
if (count($toFetch) > 0) {
$i = DB_DataObject::factory($cls);
if (empty($i)) {
throw new Exception(_('Cannot instantiate class ' . $cls));
}
$i->whereAddIn($keyCol, $toFetch, $i->columnType($keyCol));
if ($i->find()) {
while ($i->fetch()) {
$copy = clone($i);
$copy->encache();
$result[$i->$keyCol] = $copy;
}
}
// Save state of DB misses
foreach ($toFetch as $keyVal) {
if (empty($result[$keyVal])) {
// save the fact that no such row exists
$c = self::memcache();
if (!empty($c)) {
$ck = self::cachekey($cls, $keyCol, $keyVal);
$c->set($ck, null);
}
}
}
}
$result = self::pivotGet($cls, $keyCol, $keyVals);
$values = array_values($result);
@ -131,6 +92,70 @@ class Memcached_DataObject extends Safe_DataObject
return new ArrayWrapper($values);
}
/**
* Get multiple items from the database by key
*
* @param string $cls Class to fetch
* @param string $keyCol name of column for key
* @param array $keyVals key values to fetch
* @param boolean $otherCols Other columns to hold fixed
*
* @return array Array mapping $keyVals to objects, or null if not found
*/
static function pivotGet($cls, $keyCol, $keyVals, $otherCols = array())
{
$result = array_fill_keys($keyVals, null);
$toFetch = array();
foreach ($keyVals as $keyVal) {
$kv = array_merge($otherCols, array($keyCol => $keyVal));
$i = self::multicache($cls, $kv);
if ($i !== false) {
$result[$keyVal] = $i;
} else if (!empty($keyVal)) {
$toFetch[] = $keyVal;
}
}
if (count($toFetch) > 0) {
$i = DB_DataObject::factory($cls);
if (empty($i)) {
throw new Exception(_('Cannot instantiate class ' . $cls));
}
foreach ($otherCols as $otherKeyCol => $otherKeyVal) {
$i->$otherKeyCol = $otherKeyVal;
}
$i->whereAddIn($keyCol, $toFetch, $i->columnType($keyCol));
if ($i->find()) {
while ($i->fetch()) {
$copy = clone($i);
$copy->encache();
$result[$i->$keyCol] = $copy;
}
}
// Save state of DB misses
foreach ($toFetch as $keyVal) {
if (empty($result[$keyVal])) {
$kv = array_merge($otherCols, array($keyCol => $keyVal));
// save the fact that no such row exists
$c = self::memcache();
if (!empty($c)) {
$ck = self::multicacheKey($cls, $kv);
$c->set($ck, null);
}
}
}
}
return $result;
}
function columnType($columnName)
{

View File

@ -106,7 +106,7 @@ class Notice extends Memcached_DataObject
function getProfile()
{
if (is_int($this->_profile) && $this->_profile == -1) {
$this->_profile = Profile::staticGet('id', $this->profile_id);
$this->_setProfile(Profile::staticGet('id', $this->profile_id));
if (empty($this->_profile)) {
// TRANS: Server exception thrown when a user profile for a notice cannot be found.
@ -117,6 +117,11 @@ class Notice extends Memcached_DataObject
return $this->_profile;
}
function _setProfile($profile)
{
$this->_profile = $profile;
}
function delete()
{
@ -1366,17 +1371,11 @@ class Notice extends Memcached_DataObject
*/
function getReplyProfiles()
{
$ids = $this->getReplies();
$profiles = array();
foreach ($ids as $id) {
$profile = Profile::staticGet('id', $id);
if (!empty($profile)) {
$profiles[] = $profile;
}
}
$ids = $this->getReplies();
return $profiles;
$profiles = Profile::multiGet('id', $ids);
return $profiles->fetchAll();
}
/**
@ -1433,25 +1432,14 @@ class Notice extends Memcached_DataObject
$gi->notice_id = $this->id;
if ($gi->find()) {
while ($gi->fetch()) {
$ids[] = $gi->group_id;
}
}
$ids = $gi->fetchAll('group_id');
self::cacheSet($keypart, implode(',', $ids));
}
$groups = array();
foreach ($ids as $id) {
$group = User_group::staticGet('id', $id);
if ($group) {
$groups[] = $group;
}
}
return $groups;
$groups = User_group::multiGet('id', $ids);
return $groups->fetchAll();
}
/**
@ -2382,11 +2370,10 @@ class Notice extends Memcached_DataObject
if ($this->scope & Notice::ADDRESSEE_SCOPE) {
// XXX: just query for the single reply
$replies = $this->getReplies();
if (!in_array($profile->id, $replies)) {
$repl = Reply::pkeyGet(array('notice_id' => $this->id,
'profile_id' => $profile->id));
if (empty($repl)) {
return false;
}
}
@ -2492,4 +2479,28 @@ class Notice extends Memcached_DataObject
return $scope;
}
static function fillProfiles($notices)
{
$map = self::getProfiles($notices);
foreach ($notices as $notice) {
if (array_key_exists($notice->profile_id, $map)) {
$notice->_setProfile($map[$notice->profile_id]);
}
}
return array_values($map);
}
static function getProfiles(&$notices)
{
$ids = array();
foreach ($notices as $notice) {
$ids[] = $notice->profile_id;
}
$ids = array_unique($ids);
return Memcached_DataObject::pivotGet('Profile', 'id', $ids);
}
}

View File

@ -49,6 +49,11 @@ class Profile extends Memcached_DataObject
return Memcached_DataObject::staticGet('Profile',$k,$v);
}
function multiGet($keyCol, $keyVals, $skipNulls=true)
{
return parent::multiGet('Profile', $keyCol, $keyVals, $skipNulls);
}
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
@ -63,12 +68,18 @@ class Profile extends Memcached_DataObject
return $this->_user;
}
protected $_avatars = array();
function getAvatar($width, $height=null)
{
if (is_null($height)) {
$height = $width;
}
if (array_key_exists($width, $this->_avatars)) {
return $this->_avatars[$width];
}
$avatar = null;
if (Event::handle('StartProfileGetAvatar', array($this, $width, &$avatar))) {
@ -78,9 +89,16 @@ class Profile extends Memcached_DataObject
Event::handle('EndProfileGetAvatar', array($this, $width, &$avatar));
}
$this->_avatars[$width] = $avatar;
return $avatar;
}
function _fillAvatar($width, $avatar)
{
$this->_avatars[$width] = $avatar;
}
function getOriginalAvatar()
{
$avatar = DB_DataObject::factory('avatar');
@ -225,9 +243,14 @@ class Profile extends Memcached_DataObject
function isMember($group)
{
$gm = Group_member::pkeyGet(array('profile_id' => $this->id,
'group_id' => $group->id));
return (!empty($gm));
$groups = $this->getGroups(0, null);
$gs = $groups->fetchAll();
foreach ($gs as $g) {
if ($group->id == $g->id) {
return true;
}
}
return false;
}
function isAdmin($group)
@ -268,16 +291,7 @@ class Profile extends Memcached_DataObject
self::cacheSet($keypart, implode(',', $ids));
}
$groups = array();
foreach ($ids as $id) {
$group = User_group::staticGet('id', $id);
if (!empty($group)) {
$groups[] = $group;
}
}
return new ArrayWrapper($groups);
return User_group::multiGet('id', $ids);
}
function isTagged($peopletag)
@ -1357,7 +1371,22 @@ class Profile extends Memcached_DataObject
function __sleep()
{
$vars = parent::__sleep();
$skip = array('_user');
$skip = array('_user', '_avatars');
return array_diff($vars, $skip);
}
static function fillAvatars(&$profiles, $width)
{
$ids = array();
foreach ($profiles as $profile) {
$ids[] = $profile->id;
}
$avatars = Avatar::pivotGet('profile_id', $ids, array('width' => $width,
'height' => $width));
foreach ($profiles as $profile) {
$profile->_fillAvatar($width, $avatars[$profile->id]);
}
}
}

View File

@ -22,6 +22,11 @@ class Reply extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
function pkeyGet($kv)
{
return Memcached_DataObject::pkeyGet('Reply',$kv);
}
/**
* Wrapper for record insertion to update related caches
*/

View File

@ -33,6 +33,11 @@ class User_group extends Memcached_DataObject
function staticGet($k,$v=NULL) {
return Memcached_DataObject::staticGet('User_group',$k,$v);
}
function multiGet($keyCol, $keyVals, $skipNulls=true)
{
return parent::multiGet('User_group', $keyCol, $keyVals, $skipNulls);
}
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE

View File

@ -20,7 +20,7 @@
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
define('STATUSNET_BASE_VERSION', '1.0.0');
define('STATUSNET_LIFECYCLE', 'beta1'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
define('STATUSNET_LIFECYCLE', 'beta2'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
define('STATUSNET_VERSION', STATUSNET_BASE_VERSION . STATUSNET_LIFECYCLE);
define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
@ -156,4 +156,5 @@ function PEAR_ErrorToPEAR_Exception($err)
}
throw new PEAR_Exception($err->getMessage());
}
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'PEAR_ErrorToPEAR_Exception');

View File

@ -83,17 +83,16 @@ class NoticeList extends Widget
$this->out->elementStart('div', array('id' =>'notices_primary'));
$this->out->elementStart('ol', array('class' => 'notices xoxo'));
$cnt = 0;
while ($this->notice->fetch() && $cnt <= NOTICES_PER_PAGE) {
$cnt++;
if ($cnt > NOTICES_PER_PAGE) {
break;
}
$notices = $this->notice->fetchAll();
$total = count($notices);
$notices = array_slice($notices, 0, NOTICES_PER_PAGE);
self::prefill($notices);
foreach ($notices as $notice) {
try {
$item = $this->newListItem($this->notice);
$item = $this->newListItem($notice);
$item->show();
} catch (Exception $e) {
// we log exceptions and continue
@ -105,7 +104,7 @@ class NoticeList extends Widget
$this->out->elementEnd('ol');
$this->out->elementEnd('div');
return $cnt;
return $total;
}
/**
@ -122,4 +121,24 @@ class NoticeList extends Widget
{
return new NoticeListItem($notice, $this->out);
}
static function prefill(&$notices, $avatarSize=AVATAR_STREAM_SIZE)
{
// Prefill the profiles
$profiles = Notice::fillProfiles($notices);
// Prefill the avatars
Profile::fillAvatars($profiles, $avatarSize);
$p = Profile::current();
$ids = array();
foreach ($notices as $notice) {
$ids[] = $notice->id;
}
if (!empty($p)) {
Memcached_DataObject::pivotGet('Fave', 'notice_id', $ids, array('user_id' => $p->id));
}
}
}

View File

@ -76,17 +76,18 @@ class ThreadedNoticeList extends NoticeList
$this->out->element('h2', null, _m('HEADER','Notices'));
$this->out->elementStart('ol', array('class' => 'notices threaded-notices xoxo'));
$cnt = 0;
$notices = $this->notice->fetchAll();
$total = count($notices);
$notices = array_slice($notices, 0, NOTICES_PER_PAGE);
self::prefill($notices);
$conversations = array();
while ($this->notice->fetch() && $cnt <= NOTICES_PER_PAGE) {
$cnt++;
if ($cnt > NOTICES_PER_PAGE) {
break;
}
foreach ($notices as $notice) {
// Collapse repeats into their originals...
$notice = $this->notice;
if ($notice->repeat_of) {
$orig = Notice::staticGet('id', $notice->repeat_of);
if ($orig) {
@ -119,7 +120,7 @@ class ThreadedNoticeList extends NoticeList
$this->out->elementEnd('ol');
$this->out->elementEnd('div');
return $cnt;
return $total;
}
/**
@ -223,6 +224,7 @@ class ThreadedNoticeListItem extends NoticeListItem
$item = new ThreadedNoticeListMoreItem($moreCutoff, $this->out, count($notices));
$item->show();
}
NoticeList::prefill($notices, AVATAR_MINI_SIZE);
foreach (array_reverse($notices) as $notice) {
if (Event::handle('StartShowThreadedNoticeSub', array($this, $this->notice, $notice))) {
$item = new ThreadedNoticeListSubItem($notice, $this->notice, $this->out);

View File

@ -1127,8 +1127,11 @@ function common_tag_link($tag)
function common_canonical_tag($tag)
{
// only alphanum
$tag = preg_replace('/[^\pL\pN]/u', '', $tag);
$tag = mb_convert_case($tag, MB_CASE_LOWER, "UTF-8");
return str_replace(array('-', '_', '.'), '', $tag);
$tag = substr($tag, 0, 64);
return $tag;
}
function common_valid_profile_tag($str)
@ -1501,16 +1504,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,
* this is run through the background queues if possible.
*/
function common_broadcast_profile(Profile $profile)
{
$qm = QueueManager::get();
$qm->enqueue($profile, "profile");
return true;
Event::handle('BroadcastProfile', array($profile));
}
function common_profile_url($nickname)

View File

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

View File

@ -6,3 +6,11 @@
.event-title { margin-left: 0px; }
#content .event .entry-title { 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()
{
$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->li();
@ -97,49 +108,71 @@ class EventForm extends Form
$this->unli();
$this->li();
$today = new DateTime('today');
$today->setTimezone(new DateTimeZone(common_timezone()));
$this->out->input('event-startdate',
// TRANS: Field label on event form.
_m('LABEL','Start date'),
null,
$today->format('m/d/Y'),
// TRANS: Field title on event form.
_m('Date the event starts.'),
'startdate');
$this->unli();
$this->li();
$this->out->input('event-starttime',
// TRANS: Field label on event form.
_m('LABEL','Start time'),
null,
// TRANS: Field title on event form.
_m('Time the event starts.'),
'starttime');
$times = EventTimeList::getTimes();
$this->out->dropdown(
'event-starttime',
// TRANS: Field label on event form.
_m('LABEL','Start time'),
$times,
// TRANS: Field title on event form.
_m('Time the event starts.'),
false,
null
);
$this->unli();
$this->li();
$this->out->input('event-enddate',
// TRANS: Field label on event form.
_m('LABEL','End date'),
null,
$today->format('m/d/Y'),
// TRANS: Field title on event form.
_m('Date the event ends.'),
'enddate');
$this->unli();
$this->li();
$this->out->input('event-endtime',
// TRANS: Field label on event form.
_m('LABEL','End time'),
null,
// TRANS: Field title on event form.
_m('Time the event ends.'),
'endtime');
// XXX: Initial end time should be at least 30 mins out? We could do
// every 15 minute instead -z
$keys = array_keys($times);
$endStr = date('m/d/y', strtotime('now')) . " {$keys[0]}";
$end = new DateTime($endStr);
$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->li();
$this->out->input('event-location',
// TRANS: Field label on event form.
_m('LABEL','Location'),
_m('LABEL','Where?'),
null,
// TRANS: Field title on event form.
_m('Event location.'),

View File

@ -83,13 +83,33 @@ class EventListItem extends NoticeListItemAdapter
$out->elementEnd('h3'); // VEVENT/H3 OUT
$startDate = strftime("%x", strtotime($event->start_time));
$startTime = strftime("%R", strtotime($event->start_time));
$now = new DateTime();
$startDate = new DateTime($event->start_time);
$endDate = new DateTime($event->end_time);
$userTz = new DateTimeZone(common_timezone());
$endDate = strftime("%x", strtotime($event->end_time));
$endTime = strftime("%R", strtotime($event->end_time));
// Localize the time for the observer
$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
@ -98,16 +118,16 @@ class EventListItem extends NoticeListItemAdapter
$out->element('abbr', array('class' => 'dtstart',
'title' => common_date_iso8601($event->start_time)),
$startDate . ' ' . $startTime);
$out->text(' - ');
if ($startDate == $endDate) {
$startDateStr . ' ' . $startTimeStr);
$out->text(' ');
if ($startDateStr == $endDateStr) {
$out->element('span', array('class' => 'dtend',
'title' => common_date_iso8601($event->end_time)),
$endTime);
$endTimeStr);
} else {
$out->element('span', array('class' => 'dtend',
'title' => common_date_iso8601($event->end_time)),
$endDate . ' ' . $endTime);
$endDateStr . ' ' . $endTimeStr);
}
$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 $location = null;
protected $description = null;
protected $startTime = null;
protected $endTime = null;
protected $startTime = null;
protected $endTime = null;
/**
* Returns the title of the action
@ -89,67 +89,78 @@ class NeweventAction extends Action
$this->checkSessionToken();
}
$this->title = $this->trimmed('title');
try {
if (empty($this->title)) {
// TRANS: Client exception thrown when trying to post an event without providing a title.
throw new ClientException(_m('Title required.'));
}
$this->title = $this->trimmed('title');
$this->location = $this->trimmed('location');
$this->url = $this->trimmed('url');
$this->description = $this->trimmed('description');
if (empty($this->title)) {
// TRANS: Client exception thrown when trying to post an event without providing a title.
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)) {
// TRANS: Client exception thrown when trying to post an event without providing a start date.
throw new ClientException(_m('Start date required.'));
}
$startDate = $this->trimmed('startdate');
$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 = '00:00';
}
$startTime = $this->trimmed('event-starttime');
$endDate = $this->trimmed('enddate');
if (empty($startTime)) {
$startTime = '00:00';
}
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.'));
}
$endDate = $this->trimmed('enddate');
$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 = '00:00';
}
$endTime = $this->trimmed('event-endtime');
$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);
$this->endTime = strtotime($end);
common_debug("Event start: '$end'");
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 Exception(sprintf(_m('Could not parse date "%s".'),
$start));
}
$this->startTime = strtotime($start);
$this->endTime = strtotime($end);
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) {
// 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 Exception(sprintf(_m('Could not parse date "%s".'),
$end));
if ($this->endTime == 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".'),
$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;
@ -220,9 +231,13 @@ class NeweventAction extends Action
RSVP::saveNew($profile, $event, RSVP::POSITIVE);
} catch (ClientException $ce) {
$this->error = $ce->getMessage();
$this->showPage();
return;
if ($this->boolean('ajax')) {
$this->outputAjaxError($ce->getMessage());
} else {
$this->error = $ce->getMessage();
$this->showPage();
return;
}
}
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
*

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

@ -103,7 +103,11 @@ class MeteorPlugin extends RealtimePlugin
function _updateInitialize($timeline, $user_id)
{
$script = parent::_updateInitialize($timeline, $user_id);
return $script." MeteorUpdater.init(\"$this->webserver\", $this->webport, \"{$timeline}\");";
$ours = sprintf("MeteorUpdater.init(%s, %s, %s);",
json_encode($this->webserver),
json_encode($this->webport),
json_encode($timeline));
return $script." ".$ours;
}
function _connect()

View File

@ -369,6 +369,18 @@ class OMBPlugin extends Plugin
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
*

View File

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