Commit c538c745 authored by Nathan Lewis's avatar Nathan Lewis
Browse files

Add likes to activity streams (Bug #1321480)



Change-Id: I689fb61b4f75c56dca0e1e400e74c14580afb133
Signed-off-by: default avatarNathan Lewis <nathan.lewis@totaralms.com>
parent 7a7673c0
/**
* Javascript for the Activity Stream
* @source: http://gitorious.org/mahara/mahara
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
//self executing function for namespacing code
(function( ActivityStreamManager, $, undefined ) {
function init() {
$('.as-actionlike').click(function(event) {
var link = $(this);
sendjsonrequest(
config.wwwroot + 'blocktype/activitystream/likes/like.json.php',
{'activityid': link.attr('activityid'), 'action': link.attr('action')},
'GET',
function(reply) {
linkid = link.attr('id').replace('actionlike', '');
$('#totallikes' + linkid + '.as-totallikes').replaceWith(reply.totallikes);
$('#actionlike' + linkid + '.as-actionlike').html(reply.newactiontext);
$('#actionlike' + linkid + '.as-actionlike').attr('action', reply.newaction);
}
);
});
}
$(document).ready(function() {
init();
});
}( window.ActivityStreamManager = window.ActivityStreamManager || {}, jQuery ));
\ No newline at end of file
......@@ -12,10 +12,19 @@
defined('INTERNAL') || die();
$string['activity'] = 'activity';
$string['activitystream'] = 'Activity stream';
$string['description'] = 'Show history of activities';
$string['like'] = 'Like';
$string['likethisobjecttypename'] = 'Like this %s';
$string['noactivities'] = 'There are no activities to display in this stream. This activity stream will be filled with
relevant activities as they occur.';
$string['noactivitieshomestream'] = 'There are no activities to display in your home stream. You may need to change
some notifications to "Home stream" so they will be displayed here. <a href="%s">Edit your notification settings</a>.';
$string['numberoflikes'] = array(
0 => '%d person likes this',
1 => '%d people like this'
);
$string['title'] = 'Activity stream';
$string['unlike'] = 'Unlike';
$string['unlikethisobjecttypename'] = 'Unlike this %s';
......@@ -13,6 +13,10 @@ defined('INTERNAL') || die();
require_once(get_config('libroot') . 'access.php');
require_once(get_config('libroot') . 'activity.php');
// Needed for formatting the date.
require_once(get_config('docroot') . 'interaction/lib.php');
require_once(get_config('docroot') . 'interaction/forum/lib.php');
require_once('likes/lib.php');
class PluginBlocktypeActivitystream extends SystemBlocktype {
......@@ -32,6 +36,10 @@ class PluginBlocktypeActivitystream extends SystemBlocktype {
return array('general');
}
public static function get_instance_javascript() {
return array('js/activitystream.js');
}
public static function render_instance(BlockInstance $instance, $editing = false) {
global $USER;
safe_require('interaction', 'forum'); // Used for date formatting.
......@@ -71,13 +79,20 @@ class PluginBlocktypeActivitystream extends SystemBlocktype {
foreach ($activities as $activity) {
$classname = get_activity_type_classname($activity->activitytype);
$result = new stdClass();
// Activity details.
$result->body = $classname::get_activity_body($activity);
$result->ctime = relative_date(get_string('strftimerecentfullrelative', 'interaction.forum'),
get_string('strftimerecentfull'), $activity->subactivity[0]->ctime);
// Primary user (icon and name link).
$primaryuser = get_user($activity->subactivity[0]->usr);
$result->primaryuserurl = profile_url($primaryuser);
$result->primaryusername = display_name($primaryuser, null, true);
$result->primaryuser = $primaryuser;
// Actions.
$result->actions = static::get_actions($activity);
// Likes.
$result->totallikes = Likes::total_likes($activity);
// Final result.
$formattedactivities[] = $result;
}
......@@ -99,6 +114,21 @@ class PluginBlocktypeActivitystream extends SystemBlocktype {
return $smarty->fetch('blocktype:activitystream:activitystream.tpl');
}
/**
* Gets a list of actions that are relevant for the given activity.
*
* @param object $activity
* @return array of html strings
*/
private static function get_actions($activity) {
$actions = array();
// Like.
$actions[] = Likes::action_link($activity);
return $actions;
}
public static function has_instance_config() {
return true;
}
......@@ -167,7 +197,7 @@ class PluginBlocktypeActivitystream extends SystemBlocktype {
* @param int $institution id of the institution that owns this stream (e.g. institution that owns the view)
* @param mixed $paginationid either return activities with activityid lower than the specified paginationid, or false
* @return array of activity data objects which can be used to display activities in an activity stream:
* activity->activityset the activityid of the primary activity used for grouping (== subactivity[0]->id)
* activity->id the activityid of the primary activity used for grouping (== subactivity[0]->id)
* ->activitytype
* ->activitysubtype (optional)
* ->objecttype the type of object that the activity was performed on
......@@ -846,7 +876,7 @@ class PluginBlocktypeActivitystream extends SystemBlocktype {
*
* @param array of records $rawactivities
* @return array of activity data objects which can be used to display activities in an activity stream:
* activity->activityset the activityid of the primary activity used for grouping (== subactivity[0]->id)
* activity->id the activityid of the primary activity used for grouping (== subactivity[0]->id)
* ->activitytype
* ->activitysubtype (optional)
* ->objecttype the type of object that the activity was performed on
......@@ -870,7 +900,7 @@ class PluginBlocktypeActivitystream extends SystemBlocktype {
// If the activityset of this activity is different from the previous record then start a new 'set'.
$previousactivityset = $activity->activityset;
$set = new stdClass();
$set->activityset = $activity->activityset;
$set->id = $activity->activityset;
$set->activitytype = $activity->activitytype;
$set->activitysubtype = $activity->activitysubtype;
$set->objecttype = $activity->objecttype;
......
<?php
/**
*
* @package mahara
* @subpackage blocktype-activitystream
* @author Nathan Lewis <nathan.lewis@totaralms.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
defined('INTERNAL') || die();
require_once(get_config('libroot') . 'activity.php');
class Likes {
/**
* Get the information required to uniquely identify the activity or object it is based on.
*
* Likes on views and artefacts need to go on the view or artefact itself, not the activity.
*
* System activities and subactivities may have different meanings for 'objectid'. Because of this, we
* put the like on the activity itself.
*
* TODO: Likes on group, institution and interaction activities are being treated the same as system
* activities. This means that likes on these activities will be put on the activities. Someone may
* want to change this behaviour in the future, particularly relating to forums (interactions).
*
* @param object $activity
* @return object
*/
private static function get_related_object($activity) {
$object = new stdClass();
if ($activity->objecttype == ActivityType::OBJECTTYPE_VIEW ||
$activity->objecttype == ActivityType::OBJECTTYPE_ARTEFACT) {
$object->objecttype = $activity->objecttype;
$object->objectid = $activity->objectid;
}
else {
$object->objecttype = ActivityType::OBJECTTYPE_ACTIVITY;
$object->objectid = $activity->id;
}
return $activity;
}
/**
* Creates an html string stating how many likes the activity or object it is based on has.
*
* @param object $activity
* @return html string
*/
public static function total_likes($activity) {
$object = self::get_related_object($activity);
$totallikes = count_records('likes',
'objecttype', $object->objecttype,
'objectid', $object->objectid);
$totallikesstring = get_string("numberoflikes", 'blocktype.activitystream', $totallikes);
return "<div class='as-totallikes' id='totallikes{$activity->objecttype}_{$activity->objectid}'>{$totallikesstring}</div>";
}
/**
* Creates an html link which can be clicked to add or remove a like from the activity or object it is based on.
*
* @param object $activity
* @return html string
*/
public static function action_link($activity) {
global $USER;
$baseobject = self::get_related_object($activity);
$isliked = record_exists('likes',
'objecttype', $baseobject->objecttype,
'objectid', $baseobject->objectid,
'usr', $USER->get('id'));
$action = $isliked ? 'unlike' : 'like';
$label = self::action_label($action, $activity);
return "<a class='as-actionlike' activityid='{$activity->id}' " .
"id='actionlike{$activity->objecttype}_{$activity->objectid}' action='{$action}'>$label</a>";
}
/**
* Get the string indicating the action that can be performed on the base activity or object.
*
* @param string $action 'like' or 'unlike'
* @param object $activity
* @return string
*/
public static function action_label($action, $activity) {
$baseobject = self::get_related_object($activity);
$objecttypename = ActivityType::get_object_type_name($baseobject->objecttype, $baseobject->objectid);
if ($objecttypename) {
return get_string($action . 'thisobjecttypename', 'blocktype.activitystream', $objecttypename);
}
else {
return get_string($action, 'blocktype.activitystream');
}
}
/**
* Add a like to the activity or object it is based on, for the given user.
*
* @param object $activity
* @param int $userid
*/
public static function add($activity, $userid) {
$object = self::get_related_object($activity);
// Check if it already exists.
$recordexists = record_exists('likes',
'objecttype', $object->objecttype,
'objectid', $object->objectid,
'usr', $userid);
if (!$recordexists) {
$like = new stdClass();
$like->objecttype = $object->objecttype;
$like->objectid = $object->objectid;
$like->usr = $userid;
$like->ctime = db_format_timestamp(time());
insert_record('likes', $like);
}
}
/**
* Remove a like from the activity or object it is based on, for the given user.
*
* @param object $activity
* @param int $userid
*/
public static function remove($activity, $userid) {
$object = self::get_related_object($activity);
// Remove a like record (doesn't matter if it doesn't exist).
delete_records('likes',
'objecttype', $object->objecttype,
'objectid', $object->objectid,
'usr', $userid);
}
}
<?php
/**
*
* @package mahara
* @subpackage blocktype-activitystream
* @author Nathan Lewis <nathan.lewis@totaralms.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
define('INTERNAL', 1);
define('JSON', 1);
require(dirname(dirname(dirname(dirname(__FILE__)))) . '/init.php');
safe_require('blocktype', 'activitystream');
$activityid = param_integer('activityid');
$action = param_alpha('action');
$activity = get_record('activity', 'id', $activityid);
if ($action == 'like') {
Likes::add($activity, $USER->get('id'));
$newaction = 'unlike';
}
else {
Likes::remove($activity, $USER->get('id'));
$newaction = 'like';
}
$totallikes = Likes::total_likes($activity);
$newactiontext = Likes::action_label($newaction, $activity);
// Send a reply with the new action and likes total.
json_reply(false, array('newaction' => $newaction, 'newactiontext' => $newactiontext, 'totallikes' => $totallikes));
<div class="as-activity">
<div class="as-topsection">
<a href="{$activity->primaryuserurl}" title="{$activity->primaryusername}">
<img class="usericon" src="{profile_icon_url user=$activity->primaryuser maxheight=50 maxwidth=50}"
alt="{str tag="editprofileicon" section="artefact.file"}" />
</a>
<div class="as-toprightsection">
{if $activity->body}
<div>{$activity->body|safe}</div>
{/if}
<a class="as-usericon" href="{$activity->primaryuserurl}" title="{$activity->primaryusername}">
<img class="usericon" src="{profile_icon_url user=$activity->primaryuser maxheight=50 maxwidth=50}"
alt="{str tag="editprofileicon" section="artefact.file"}" />
</a>
<div class="as-rightside">
<div class="as-body">{$activity->body|safe}</div>
<div class="as-middle">
<ul class="as-controls">
{foreach from=$activity->actions item=action}
<li class="{if $dwoo.foreach.default.index > 0}bar-before{/if}">{$action|safe}</li>
{/foreach}
</ul>
<div class="as-ctime">{$activity->ctime}</div>
</div>
{if $activity->totallikes || $activity->comments}
<div class="as-bottom">
{if $activity->totallikes}{$activity->totallikes|safe}{/if}
</div>
{/if}
</div>
</div>
......@@ -15,23 +15,39 @@
border: 3px solid #F3F3F3;
padding: 5px;
margin: 5px;
min-height: 50px;
}
.activitystream .as-topsection {
.activitystream .as-rightside {
margin-left: 55px;
width: calc(100% - 55px);
}
.activitystream .as-middle {
display: inline-block;
width: 100%;
}
.activitystream .usericon {
float: left;
margin: 0;
.activitystream .as-bottom {
display: inline-block;
width: 100%;
}
.activitystream .as-toprightsection {
margin-left: 55px;
.activitystream .as-usericon {
float: left;
}
.activitystream .as-ctime {
float: right;
}
.activitystream .as-controls {
float: left;
margin: 0;
}
.activitystream .as-controls li {
display: inline;
margin: 0;
padding: 0 5px;
cursor: pointer;
}
......@@ -570,6 +570,7 @@ abstract class ActivityType {
const OBJECTTYPE_SYSTEM = 5;
const OBJECTTYPE_INTERACTION = 6;
const OBJECTTYPE_FORUMTOPIC = 7;
const OBJECTTYPE_ACTIVITY = 8;
/**
* NOTE: Child classes MUST call the parent constructor, AND populate
......@@ -585,6 +586,67 @@ abstract class ActivityType {
$this->activityname = strtolower(substr(get_class($this), strlen('ActivityType')));
}
/**
* Get the name of the type of the specified objecttype/object.
*
* @param int $objecttype as defined by the OBJECTTYPE_XXX consts above
* @param int $objectid optional id of the object
* @return string name of the type of the objecttype/object, or an empty string
*/
public static function get_object_type_name($objecttype, $objectid = 0) {
switch ($objecttype) {
case ActivityType::OBJECTTYPE_VIEW:
$objecttypename = get_string('view', 'view');
break;
case ActivityType::OBJECTTYPE_ARTEFACT:
if ($objectid) {
$sql = "SELECT type.name, type.plugin
FROM {artefact_installed_type} type
JOIN {artefact} artefact
ON artefact.id = ?
AND artefact.artefacttype = type.name";
$artefacttype = get_record_sql($sql, $objectid);
$objecttypename = strtolower(get_string($artefacttype->name, 'artefact.' . $artefacttype->plugin));
}
else {
$objecttypename = get_string('artefact', 'mahara');
}
break;
case ActivityType::OBJECTTYPE_GROUP:
$objecttypename = get_string('group', 'group');
break;
case ActivityType::OBJECTTYPE_INSTITUTION:
$objecttypename = get_string('institution', 'mahara');
break;
case ActivityType::OBJECTTYPE_FORUMTOPIC:
$objecttypename = get_string('topic', 'interaction.forum');
break;
case ActivityType::OBJECTTYPE_ACTIVITY:
$objecttypename = get_string('activity', 'blocktype.activitystream');
break;
case ActivityType::OBJECTTYPE_INTERACTION:
if ($objectid) {
$plugin = get_field('interaction_instance', 'plugin', 'id', $objectid);
// Assumes each interaction implements the string 'name'.
$objecttypename = strtolower(get_string('name', 'interaction.' . $plugin));
}
else {
// It doesn't really make sense to ask the object type name of an interaction without
// also specifying the objectid.
$objecttypename = "";
}
break;
case ActivityType::OBJECTTYPE_SYSTEM:
// Technically, this shouldn't happen, as there is no such thing as a "system object" outside of
// the context of an activity type.
default:
$objecttypename = "";
break;
}
return $objecttypename;
}
/**
* This method should return an array which names the fields that must be present in the
* $data that was passed to the class's constructor. It should include all necessary data
......
......@@ -474,6 +474,22 @@
<KEY NAME="memberfk" TYPE="foreign" FIELDS="member" REFTABLE="usr" REFFIELDS="id" />
</KEYS>
</TABLE>
<TABLE NAME="likes">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" />
<FIELD NAME="objecttype" TYPE="int" LENGTH="10" NOTNULL="true" />
<FIELD NAME="objectid" TYPE="int" LENGTH="10" NOTNULL="true" />
<FIELD NAME="usr" TYPE="int" LENGTH="10" NOTNULL="true" />
<FIELD NAME="ctime" TYPE="datetime" NOTNULL="true" />
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" />
<KEY NAME="usrfk" TYPE="foreign" FIELDS="usr" REFTABLE="usr" REFFIELDS="id" />
</KEYS>
<INDEXES>
<INDEX NAME="likeuniqueidx" UNIQUE="true" FIELDS="objecttype, objectid, usr"/>
</INDEXES>
</TABLE>
<TABLE NAME="artefact">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" />
......
......@@ -3456,5 +3456,25 @@ function xmldb_core_upgrade($oldversion=0) {
}
}
// Activity like table.
if ($oldversion < 2014060600) {
// Add activity_like table.
$table = new XMLDBTable('likes');
$table->addFieldInfo('id', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, XMLDB_SEQUENCE);
$table->addFieldInfo('objecttype', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL);
$table->addFieldInfo('objectid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL);
$table->addFieldInfo('usr', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL);
$table->addFieldInfo('ctime', XMLDB_TYPE_DATETIME, null, null, XMLDB_NOTNULL);
$table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->addKeyInfo('usrfk', XMLDB_KEY_FOREIGN, array('usr'), 'usr', array('id'));
$table->addIndexInfo('likeuniqueidx', XMLDB_INDEX_UNIQUE, array('objecttype, objectid, usr'));
if (!table_exists($table)) {
create_table($table);
}
}
return $status;
}
......@@ -16,7 +16,7 @@ $config = new stdClass();
// See https://wiki.mahara.org/index.php/Developer_Area/Version_Numbering_Policy
// For upgrades on stable branches, increment the version by one. On master, use the date.
$config->version = 2014060500;
$config->version = 2014060600;
$config->release = '1.10.0dev';
$config->minupgradefrom = 2009022600;
$config->minupgraderelease = '1.1.0 (release tag 1.1.0_RELEASE)';
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment