Commit 72818951 authored by Richard Mansfield's avatar Richard Mansfield
Browse files

Moodle outcomes integration: view submission/release to/from mnet hosts;...

Moodle outcomes integration: view submission/release to/from mnet hosts; hidden view access keys; export of additional artefacts with views
parent b8a3d600
......@@ -52,6 +52,9 @@ class Dispatcher {
'auth/mnet/auth.php/keepalive_server' => 'xmlrpc_not_implemented',
'auth/mnet/auth.php/kill_children' => 'kill_children',
'auth/mnet/auth.php/kill_child' => 'xmlrpc_not_implemented',
'mod/assignment/type/mahara/rpclib.php/get_views_for_user' => 'get_views_for_user',
'mod/assignment/type/mahara/rpclib.php/submit_view_for_assessment' => 'submit_view_for_assessment',
'mod/assignment/type/mahara/rpclib.php/release_submitted_view' => 'release_submitted_view',
),
'portfolio_in' => array(
'portfolio/mahara/lib.php/send_content_intent' => 'send_content_intent',
......
......@@ -444,6 +444,90 @@ function xmlrpc_not_implemented() {
return true;
}
function get_views_for_user($username, $query=null) {
global $REMOTEWWWROOT, $USER;
list ($user, $authinstance) = find_remote_user($username, $REMOTEWWWROOT);
if (!$user) {
return false;
}
$USER->reanimate($user->id, $authinstance->id);
require_once('view.php');
$data = View::view_search($query, null, (object) array('owner' => $USER->get('id')));
$data->displayname = display_name($user);
if ($data->count) {
foreach ($data->data as &$v) {
$v['url'] = '/view/view.php?id=' . $v['id'];
$v['fullurl'] = get_config('wwwroot') . 'view/view.php?id=' . $v['id'];
}
}
return $data;
}
function submit_view_for_assessment($username, $viewid) {
global $REMOTEWWWROOT;
list ($user, $authinstance) = find_remote_user($username, $REMOTEWWWROOT);
if (!$user) {
return false;
}
$viewid = (int) $viewid;
if (!$viewid) {
return false;
}
require_once('view.php');
$view = new View($viewid);
$view->set('submittedhost', $authinstance->config['wwwroot']);
// Create secret key
$access = View::new_token($view->get('id'), false);
$data = array(
'id' => $view->get('id'),
'title' => $view->get('title'),
'description' => $view->get('description'),
'fullurl' => get_config('wwwroot') . 'view/view.php?id=' . $view->get('id') . '&mt=' . $access->token,
'url' => '/view/view.php?id=' . $view->get('id') . '&mt=' . $access->token,
'accesskey' => $access->token,
);
foreach (plugins_installed('artefact') as $plugin) {
safe_require('artefact', $plugin->name);
$classname = generate_class_name('artefact', $plugin->name);
if (is_callable($classname . '::view_submit_external_data')) {
$data[$plugin->name] = call_static_method($classname, 'view_submit_external_data', $view->get('id'));
}
}
return $data;
}
function release_submitted_view($viewid, $assessmentdata, $teacherusername) {
global $REMOTEWWWROOT, $USER;
require_once('view.php');
$view = new View($viewid);
list ($teacher, $authinstance) = find_remote_user($teacherusername, $REMOTEWWWROOT);
db_begin();
foreach (plugins_installed('artefact') as $plugin) {
safe_require('artefact', $plugin->name);
$classname = generate_class_name('artefact', $plugin->name);
if (is_callable($classname . '::view_release_external_data')) {
call_static_method($classname, 'view_release_external_data', $view, $assessmentdata, $teacher ? $teacher->id : 0);
}
}
// Release the view for editing
$view->set('submittedhost', null);
$view->commit();
db_commit();
}
/**
* Given a USER, get all Service Providers for that User, based on child auth
* instances of its canonical auth instance
......@@ -639,6 +723,18 @@ function get_peer($wwwroot, $cache=true) {
return $peers[$wwwroot];
}
function get_peer_from_instanceid($authinstanceid) {
$sql = 'SELECT
h.*
FROM
{auth_instance} ai,
{host} h
WHERE
ai.institution = h.institution AND
ai.id = ?';
return get_record_sql($sql, array($authinstanceid));
}
/**
* Check that the signature has been signed by the remote host.
*/
......
......@@ -83,6 +83,17 @@ abstract class PluginArtefact extends Plugin {
public static function group_tabs($groupid) {
return array();
}
/**
* Returns any artefacts that are not inside a view
* but which need to be exported along with it.
* @param array $viewids
* @return array of artefact ids
*/
public static function view_export_extra_artefacts($viewids) {
return array();
}
}
/**
......
......@@ -362,6 +362,7 @@ class AuthXmlrpc extends Auth {
// the session object.
$SESSION->set('mnetuser', $user->id);
$SESSION->set('authinstance', $this->instanceid);
$SESSION->set('mnetuserfrom', $_SERVER['HTTP_REFERER']);
return true;
}
......
......@@ -227,9 +227,13 @@ class PluginExportLeap extends PluginExport {
private function get_links_for_view($viewid) {
static $viewartefactdata = null;
static $vaextra = null;
if (is_null($viewartefactdata)) {
$viewartefactdata = get_records_select_array('view_artefact', 'view IN (' . join(', ', array_keys($this->views)) . ')');
}
if (is_null($vaextra)) {
$vaextra = $this->get_view_extra_artefacts(true);
}
$links = array();
foreach ($viewartefactdata as $va) {
......@@ -240,6 +244,15 @@ class PluginExportLeap extends PluginExport {
);
}
}
if (isset($vaextra[$viewid])) {
foreach ($vaextra[$viewid] as $artefactid) {
$links[] = (object)array(
'type' => 'is_evidence_of', // Fix this
'id' => 'portfolio:artefact' . $artefactid,
);
}
}
return $links;
}
......@@ -398,6 +411,7 @@ class LeapExportElement {
$this->smarty->assign('content', $this->get_content());
$this->smarty->assign('contenttype', $this->get_content_type());
$this->smarty->assign('leaptype', $this->get_leap_type());
$this->smarty->assign('author', $this->get_entry_author());
if ($tags = $this->artefact->get('tags')) {
$tags = array_map(create_function('$a',
......@@ -542,6 +556,16 @@ class LeapExportElement {
}
}
/**
* The id of the entry's author
* Override this if the author is different from the portfolio holder
*
* @return int
*/
public function get_entry_author() {
return;
}
/**
* The relationship this artefact has to a view.
* Almost always is_part_of, but could also be supports or anything else.
......
{include file="export:leap:entryheader.tpl"}
<title>{$title|escape}</title>
<id>{$id}</id>
{if $author} <author>
<name>{$author|display_name|escape}</name>
</author>
{/if}
{if $updated} <updated>{$updated}</updated>
{/if}
{if $created} <published>{$created}</published>
......
......@@ -219,6 +219,9 @@ abstract class PluginExport extends Plugin {
WHERE v.owner = ?
$vaextra";
$tmpartefacts = (array)get_column_sql($sql, array($userid));
// Some artefacts are not inside the view, but still need to be exported with it
$tmpartefacts = array_unique(array_merge($tmpartefacts, $this->get_view_extra_artefacts()));
}
else {
$tmpartefacts = array();
......@@ -298,6 +301,43 @@ abstract class PluginExport extends Plugin {
}
}
/**
* Artefact plugins can specify additional artefacts required for view export
*/
protected function get_view_extra_artefacts($indexed=false) {
static $data = null;
if (is_null($data)) {
$plugins = plugins_installed('artefact');
$viewids = array_keys($this->views);
$data = array('byview' => array(), 'artefacts' => array());
foreach ($plugins as &$plugin) {
safe_require('artefact', $plugin->name);
$classname = generate_class_name('artefact', $plugin->name);
// @todo: work out why PluginArtefactResume::view_export_extra_artefacts()
// cannot be called while all the other PluginArtefactFoobar versions
// happily call the PluginArtefact parent version
if (is_callable($classname . '::view_export_extra_artefacts')) {
$viewartefact = call_static_method($classname, 'view_export_extra_artefacts', $viewids);
foreach ($viewartefact as &$va) {
if (!isset($data['byview'][$va->view])) {
$data['byview'][$va->view] = array();
}
$data['byview'][$va->view][$va->artefact] = $va->artefact;
if (!isset($data['artefacts'][$va->artefact])) {
$data['artefacts'][$va->artefact] = $va->artefact;
}
}
}
}
}
if ($indexed) {
return $data['byview'];
}
return array_values($data['artefacts']);
}
}
?>
......@@ -84,7 +84,7 @@ $string['updatemembership'] = 'Update membership';
$string['memberchangefailed'] = 'Failed to update some membership information';
$string['memberchangesuccess'] = 'Membership status changed successfully';
$string['viewreleasedsubject'] = 'Your view has been released';
$string['viewreleasedmessage'] = 'The view that you submitted to group %s has been released back to you by %s';
$string['viewreleasedmessage'] = 'The view that you submitted to %s has been released back to you by %s';
$string['viewreleasedsuccess'] = 'View was released successfully';
$string['groupmembershipchangesubject'] = 'Group membership: %s';
$string['groupmembershipchangedmessagetutor'] = 'You have been promoted to a tutor in this group';
......
......@@ -831,6 +831,7 @@ $string['country.zw'] = 'Zimbabwe';
$string['system'] = 'System';
$string['done'] = 'Done';
$string['back'] = 'Back';
$string['backto'] = 'Back to %s';
$string['alphabet'] = 'A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z';
$string['formatpostbbcode'] = 'You can format your post using BBCode. %sLearn more%s';
......
......@@ -68,7 +68,7 @@ $string['view'] = 'view';
$string['views'] = 'views';
$string['View'] = 'View';
$string['Views'] = 'Views';
$string['viewsubmittedtogroup'] = 'This View has been submitted to <a href="%sgroup/view.php?id=%s">%s</a>';
$string['viewsubmittedtogroup'] = 'This View has been submitted to <a href="%s">%s</a>';
$string['nobodycanseethisview2'] = 'Only you can see this View';
$string['noviews'] = 'No Views.';
$string['youhavenoviews'] = 'You have no Views.';
......@@ -162,7 +162,7 @@ $string['viewinformationsaved'] = 'View information saved successfully';
$string['canteditdontown'] = 'You can\'t edit this View because you don\'t own it';
$string['canteditdontownfeedback'] = 'You can\'t edit this feedback because you don\'t own it';
$string['canteditsubmitted'] = 'You can\'t edit this View because it has been submitted for assessment to group "%s". You will have to wait until a tutor releases your view.';
$string['canteditsubmitted'] = 'You can\'t edit this View because it has been submitted for assessment to "%s". You will have to wait until a tutor releases your view.';
$string['feedbackchangedtoprivate'] = 'Feedback changed to private';
$string['addtutors'] = 'Add Tutors';
......
......@@ -575,7 +575,8 @@
<FIELD NAME="ctime" TYPE="datetime" NOTNULL="true" />
<FIELD NAME="mtime" TYPE="datetime" NOTNULL="true" />
<FIELD NAME="atime" TYPE="datetime" NOTNULL="true" />
<FIELD NAME="submittedto" TYPE="int" LENGTH="10" NOTNULL="false" />
<FIELD NAME="submittedgroup" TYPE="int" LENGTH="10" NOTNULL="false" />
<FIELD NAME="submittedhost" TYPE="char" LENGTH="255" NOTNULL="false" />
<FIELD NAME="numcolumns" TYPE="int" LENGTH="2" NOTNULL="true" />
<FIELD NAME="layout" TYPE="int" LENGTH="10"/>
<FIELD NAME="template" TYPE="int" LENGTH="1" DEFAULT="0" NOTNULL="true" />
......@@ -585,7 +586,8 @@
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" />
<KEY NAME="ownerfk" TYPE="foreign" FIELDS="owner" REFTABLE="usr" REFFIELDS="id" />
<KEY NAME="submittedtofk" TYPE="foreign" FIELDS="submittedto" REFTABLE="group" REFFIELDS="id" />
<KEY NAME="submittedgroupfk" TYPE="foreign" FIELDS="submittedgroup" REFTABLE="group" REFFIELDS="id" />
<KEY NAME="submittedhostfk" TYPE="foreign" FIELDS="submittedhost" REFTABLE="host" REFFIELDS="wwwroot" />
<KEY NAME="layoutfk" TYPE="foreign" FIELDS="layout" REFTABLE="view_layout" REFFIELDS="id"/>
<KEY NAME="groupfk" TYPE="foreign" FIELDS="group" REFTABLE="group" REFFIELDS="id" />
<KEY NAME="institutionfk" TYPE="foreign" FIELDS="institution" REFTABLE="institution" REFFIELDS="name" />
......@@ -722,6 +724,7 @@
<FIELD NAME="token" TYPE="char" LENGTH="100" NOTNULL="true" />
<FIELD NAME="startdate" TYPE="datetime" NOTNULL="false" />
<FIELD NAME="stopdate" TYPE="datetime" NOTNULL="false" />
<FIELD NAME="visible" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" />
</FIELDS>
<KEYS>
<KEY NAME="viewfk" TYPE="foreign" FIELDS="view" REFTABLE="view" REFFIELDS="id" />
......
......@@ -1050,6 +1050,42 @@ function xmldb_core_upgrade($oldversion=0) {
}
}
if ($oldversion < 2009051200) {
// Rename submittedto column to submittedgroup
if (is_postgres()) {
execute_sql("ALTER TABLE {view} RENAME submittedto TO submittedgroup");
}
else if (is_mysql()) {
execute_sql("ALTER TABLE {view} DROP FOREIGN KEY {view_sub_fk}");
execute_sql("ALTER TABLE {view} DROP INDEX {view_sub_ix}");
execute_sql("ALTER TABLE {view} CHANGE submittedto submittedgroup BIGINT(10) DEFAULT NULL");
execute_sql("ALTER TABLE {view} ADD CONSTRAINT {view_sub_fk} FOREIGN KEY {view_sub_ix} (submittedgroup) REFERENCES {group}(id)");
}
// Add submittedhost column for views submitted to remote moodle hosts
$table = new XMLDBTable('view');
$field = new XMLDBField('submittedhost');
$field->setAttributes(XMLDB_TYPE_CHAR, 255, XMLDB_UNSIGNED, null);
add_field($table, $field);
// Do this manually because xmldb tries to create a key with the same name (view_sub_vk) as an existing one, and fails.
if (is_postgres()) {
execute_sql("ALTER TABLE {view} ADD CONSTRAINT {view_subh_fk} FOREIGN KEY (submittedhost) REFERENCES {host}(wwwroot)");
execute_sql("CREATE INDEX {view_subh_ix} ON {view} (submittedhost)");
}
else if (is_mysql()) {
execute_sql("ALTER TABLE {view} ADD CONSTRAINT {view_subh_fk} FOREIGN KEY {view_subh_ix} (submittedhost) REFERENCES {host}(wwwroot)");
}
}
if ($oldversion < 2009051201) {
// Invisible view access keys for roaming moodle teachers
$table = new XMLDBTable('view_access_token');
$field = new XMLDBField('visible');
$field->setAttributes(XMLDB_TYPE_INTEGER, 1, null, XMLDB_NOTNULL, null, null, null, 1);
add_field($table, $field);
}
return $status;
}
......
......@@ -1373,11 +1373,13 @@ function pieform_template_dir($file, $pluginlocation='') {
* @param integer $view_id View ID to check
* @param integer $user_id User trying to look at the view (defaults to
* currently logged in user, or null if user isn't logged in)
* @param string $usertoken Key created by view owner for logged-out user access
* @param string $mnettoken Key created by mahara for teachers roaming from moodle
*
* @returns boolean Wether the specified user can look at the specified view.
*/
function can_view_view($view_id, $user_id=null, $token=null) {
global $USER;
function can_view_view($view_id, $user_id=null, $usertoken=null, $mnettoken=null) {
global $USER, $SESSION;
$now = time();
$dbnow = db_format_timestamp($now);
......@@ -1387,10 +1389,10 @@ function can_view_view($view_id, $user_id=null, $token=null) {
$publicviews = get_config('allowpublicviews');
if ($publicviews) {
if (!$token) {
$token = get_cookie('viewaccess:'.$view_id);
if (!$usertoken) {
$usertoken = get_cookie('viewaccess:'.$view_id);
}
if ($token && (!$user_id || $user_id == $USER->get('id')) && $view_id == get_view_from_token($token)) {
if ($usertoken && (!$user_id || $user_id == $USER->get('id')) && $view_id == get_view_from_token($usertoken)) {
return true;
}
}
......@@ -1425,6 +1427,21 @@ function can_view_view($view_id, $user_id=null, $token=null) {
// - it has been submitted to them for assessment, or
// - they have been granted access via the edit view access page.
if ($SESSION->get('mnetuser')) {
if (!$mnettoken) {
$mnettoken = get_cookie('mviewaccess:'.$view_id);
}
if ($mnettoken && $view_id == get_view_from_token($mnettoken, false)) {
$mnetviewlist = $SESSION->get('mnetviewaccess');
if (empty($mnetviewlist)) {
$mnetviewlist = array();
}
$mnetviewlist[$view_id] = true;
$SESSION->set('mnetviewaccess', $mnetviewlist);
return true;
}
}
require_once(get_config('docroot') . 'lib/view.php');
$view = new View($view_id);
......@@ -1432,7 +1449,7 @@ function can_view_view($view_id, $user_id=null, $token=null) {
return true;
}
if ($submitgroup = $view->get('submittedto')) {
if ($submitgroup = $view->get('submittedgroup')) {
require_once(get_config('docroot') . 'lib/group.php');
if (group_user_can_assess_submitted_views($submitgroup, $user_id)) {
return true;
......@@ -1481,17 +1498,17 @@ function can_view_view($view_id, $user_id=null, $token=null) {
/* return the view associated with a given token */
function get_view_from_token($token) {
function get_view_from_token($token, $visible=true) {
if (!$token) {
return false;
}
return get_field_sql('
SELECT view
FROM {view_access_token}
WHERE token = ?
WHERE token = ? AND visible = ?
AND (startdate IS NULL OR startdate < current_timestamp)
AND (stopdate IS NULL OR stopdate > current_timestamp)
', array($token));
', array($token, (int)$visible));
}
......
......@@ -27,7 +27,7 @@
defined('INTERNAL') || die();
$config = new StdClass;
$config->version = 2009050901;
$config->version = 2009051201;
$config->release = '1.2.0alpha3dev';
$config->minupgradefrom = 2008040200;
$config->minupgraderelease = '1.0.0 (release tag 1.0.0_RELEASE)';
......
......@@ -40,7 +40,8 @@ class View {
private $atime;
private $startdate;
private $stopdate;
private $submittedto;
private $submittedgroup;
private $submittedhost;
private $title;
private $description;
private $loggedin;
......@@ -446,6 +447,7 @@ class View {
$bi->delete();
}
}
handle_event('deleteview', $this->id);
delete_records('view','id',$this->id);
$this->deleted = true;
db_commit();
......@@ -477,7 +479,7 @@ class View {
UNION
SELECT 'token', token, NULL AS role, NULL AS grouptype, startdate, stopdate
FROM {view_access_token}
WHERE view = ?
WHERE view = ? AND visible = 1
", array($this->id, $this->id, 0, $this->id, $this->id));
if ($data) {
foreach ($data as &$item) {
......@@ -561,7 +563,7 @@ class View {
delete_records('view_access', 'view', $this->get('id'));
delete_records('view_access_usr', 'view', $this->get('id'));
delete_records('view_access_group', 'view', $this->get('id'));
delete_records('view_access_token', 'view', $this->get('id'));
delete_records('view_access_token', 'view', $this->get('id'), 'visible', 1);
$time = db_format_timestamp(time());
// View access
......@@ -623,21 +625,39 @@ class View {
return $this->copynewgroups;
}
public function release($groupid, $releaseuser=null) {
if ($this->get('submittedto') != $groupid) {
throw new ParameterException("View with id " . $this->get('id') .
" has not been submitted to group $groupid");
public function is_submitted() {
return $this->get('submittedgroup') || $this->get('submittedhost');
}
public function submitted_to() {
if ($group = $this->get('submittedgroup')) {
return array('type' => 'group', 'id' => $group, 'name' => get_field('group', 'name', 'id', $group));
}
if ($host = $this->get('submittedhost')) {
return array('type' => 'host', 'wwwroot' => $host, 'name' => get_field('host', 'name', 'wwwroot', $host));
}
return null;
}
public function release($releaseuser=null) {
$submitinfo = $this->submitted_to();
if (is_null($submitinfo)) {
throw new ParameterException("View with id " . $this->get('id') . " has not been submitted");
}
$releaseuser = optional_userobj($releaseuser);
$this->set('submittedto', null);
if ($submitinfo['type'] == 'group') {
$this->set('submittedgroup', null);
}
else if ($submitinfo['type'] == 'host') {
$this->set('submittedhost', null);
}
$this->commit();
$ownerlang = get_user_language($this->get('owner'));
require_once('activity.php');
activity_occurred('maharamessage',
array('users' => array($this->get('owner')),
'subject' => get_string_from_language($ownerlang, 'viewreleasedsubject', 'group'),
'message' => get_string_from_language($ownerlang, 'viewreleasedmessage', 'group',
get_field('group', 'name', 'id', $groupid),
'message' => get_string_from_language($ownerlang, 'viewreleasedmessage', 'group', $submitinfo['name'],
display_name($releaseuser, $this->get_owner_object()))));
}
......@@ -1670,9 +1690,11 @@ class View {
}
else {
$count = count_records('view', 'owner', $userid, 'type', 'portfolio');
$viewdata = get_records_sql_array('SELECT v.id,v.title,v.startdate,v.stopdate,v.description, v.template, g.id AS groupid, g.name
$viewdata = get_records_sql_array('SELECT v.id,v.title,v.startdate,v.stopdate,v.description, v.template,
g.id AS submitgroupid, g.name AS submitgroupname, h.wwwroot AS submithostwwwroot, h.name AS submithostname
FROM {view} v
LEFT OUTER JOIN {group} g ON (v.submittedto = g.id AND g.deleted = 0)
LEFT OUTER JOIN {group} g ON (v.submittedgroup = g.id AND g.deleted = 0)
LEFT OUTER JOIN {host} h ON (v.submittedhost = h.wwwroot)
WHERE v.owner = ' . $userid . '
AND v.type = \'portfolio\'
ORDER BY v.title, v.id', '', $offset, $limit);
......@@ -1709,8 +1731,13 @@ class View {
$data[$i]['id'] = $viewdata[$i]->id;
$data[$i]['title'] = $viewdata[$i]->title;
$data[$i]['description'] = $viewdata[$i]->description;
if (!empty($viewdata[$i]->name)) {
$data[$i]['submittedto'] = array('name' => $viewdata[$i]->name, 'id' => $viewdata[$i]->groupid);
if (!empty($viewdata[$i]->submitgroupid)) {
$data[$i]['submittedto'] = get_string('viewsubmittedtogroup', 'view',
get_config('wwwroot') . 'group/view.php?id=' . $viewdata[$i]->submitgroupid,
$viewdata[$i]->submitgroupname);
}
else if (!empty($viewdata[$i]->submithostwwwroot)) {
$data[$i]['submittedto'] = get_string('viewsubmittedtogroup', 'view', $viewdata[$i]->submithostwwwroot, $viewdata[$i]->submithostname);
}
$data[$i]['artefacts'] = array();
$data[$i]['accessgroups'] = array();
......@@ -2077,7 +2104,7 @@ class View {
$viewdata = get_records_sql_assoc('
SELECT id, title, description, owner, ownerformat
FROM {view}
WHERE submittedto = ?
WHERE submittedgroup = ?
ORDER BY title, id',
array($groupid)
);
......@@ -2277,6 +2304,27 @@ class View {
));
}
public static function new_token($viewid, $visible=1) {
if (!$visible) {
// Currently it only makes sense to have one invisible key per view.
// They are only used during view submission, and a view can only be
// submitted to one group or remote host at any one time.
delete_records('view_access_token', 'view', $viewid, 'visible', 0);
}
$data = new StdClass;
$data->view = $viewid;
$data->visible = $visible;
$data->token = random_string(20);
while (record_exists('view_access_token', 'token', $data->token)) {
$data->token = random_string(20);