Commit 389df353 authored by Ghada El-Zoghbi's avatar Ghada El-Zoghbi Committed by Gerrit Code Review
Browse files

Annotation artefact: Bug 1397759

A new artefact similar to the comment artefact but with less
functionality (i.e. no attached files, etc).
It's an explenation of why a particular evidence meets a
particular standard.

If an annotation is created and added to a page, when the user
deletes it from the page, the instance is deleted along with the
annotation and its feedback.

TODO:

1. Imports seem to be working.
Can get all comments to import and display.
Needs some serious testing.

2. Made changes for broken images but another bug was reported and is
currently being worked on. So, may not need the fixes in here. Changes in:
- htdocs/artefact/file/download.php

To completely fix the broken images for all artefacts, changes are also required
in htdocs/lib/embeddedimage.php to delete based on resourceid instead of fileid.

Change-Id: Ibdb2e1c6500862645bac741bf61cff37e5a5b35c
parent 586fdcd6
<?php
/**
*
* @package mahara
* @subpackage artefact-annotation
* @author Catalyst IT Ltd
* @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(__FILE__))) . '/init.php');
require_once(get_config('libroot') . 'view.php');
require_once(get_config('libroot') . 'pieforms/pieform.php');
safe_require('artefact', 'annotation');
// Pagination is not really working here so extradata won't
// really be a parameter.
$extradata = json_decode(param_variable('extradata', null));
$ispagination = false;
if (param_exists('offset')) {
$ispagination = true;
$limit = param_integer('limit', 10);
$offset = param_integer('offset');
}
if (!isset($extradata)) {
$viewid = json_decode(param_variable('viewid'));
$annotationid = json_decode(param_variable('annotationid'));
$artefactid = json_decode(param_variable('artefactid', ''));
$blockid = json_decode(param_variable('blockid'));
$extradata = new stdClass();
$extradata->view = $viewid;
$extradata->artefact = $artefactid;
$extradata->annotation = $annotationid;
$extradata->blockid = $blockid;
}
if (empty($extradata->view) || empty($extradata->annotation) || empty($extradata->blockid)) {
json_reply('local', get_string('annotationinformationerror', 'artefact.annotation'));
}
if (!can_view_view($extradata->view)) {
json_reply('local', get_string('noaccesstoview', 'view'));
}
if (!artefact_in_view($extradata->annotation, $extradata->view)) {
json_reply('local', get_string('accessdenied', 'error'));
}
if (!empty($extradata->artefact) && !artefact_in_view($extradata->artefact, $extradata->view)) {
json_reply('local', get_string('accessdenied', 'error'));
}
if ($ispagination) {
// This is not really working yet. Need to do more work on artefact/artefact.php
$options = ArtefactTypeAnnotationfeedback::get_annotation_feedback_options();
$options->limit = $limit;
$options->offset = $offset;
$options->view = $extradata->view;
$options->annotation = $extradata->annotation;
$options->artefact = $extradata->artefact;
$options->block = $extradata->blockid;
$annotationfeedback = ArtefactTypeAnnotationfeedback::get_annotation_feedback($options);
json_reply(false, array('data' => $annotationfeedback));
}
else {
$view = new View($extradata->view);
$annotationartefact = artefact_instance_from_id($extradata->annotation);
list($feedbackcount, $annotationfeedback) = ArtefactTypeAnnotationfeedback::get_annotation_feedback_for_view($annotationartefact, $view, $extradata->blockid);
json_reply(false, array('data' => $annotationfeedback));
}
<?php
/**
*
* @package mahara
* @subpackage blocktype-annotation
* @author Catalyst IT Ltd
* @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();
$string['title'] = 'Annotation';
$string['description'] = 'A block to display your annotations of why your evidence meets a standard.';
$string['annotationreadonlymessage'] = 'The annotation is no longer editable now that feedback has been given.';
\ No newline at end of file
<!-- @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. -->
<h3>Read-only annotation content</h3>
<p>This annotation is in a read-only state due to other users leaving feedback.
If you want to change the annotation you will need to delete this annotation
block and start again.</p>
<?php
/**
*
* @package mahara
* @subpackage blocktype-annotation
* @author Catalyst IT Ltd
* @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();
class PluginBlocktypeAnnotation extends SystemBlocktype {
public static function single_only() {
return false;
}
public static function get_title() {
return get_string('title', 'blocktype.annotation/annotation');
}
public static function get_description() {
return get_string('description', 'blocktype.annotation/annotation');
}
public static function get_categories() {
return array('general' => 14500);
}
public static function get_viewtypes() {
return array('portfolio');
}
public static function has_title_link() {
return false; // true; // need to do more work on aretfact/artefact.php before this can be switched on.
}
public static function override_instance_title() {
return get_string('Annotation', 'artefact.annotation');
}
public static function allowed_in_view(View $view) {
// Annotations don't make sense in groups?
return $view->get('group') == null;
}
/**
* defines if the title should be shown if there is no content in the block
*
* If the title of the block should be hidden when there is no content,
* override the the function in the blocktype class.
*
* @return boolean whether the title of the block should be shown or not
*/
public static function hide_title_on_empty_content() {
return true;
}
/**
* Returns a list of artefact IDs that are in this blockinstance.
*
* People may embed artefacts as images etc. They show up as links to the
* download script, which isn't much to go on, but should be enough for us
* to detect that the artefacts are therefore 'in' this blocktype.
*/
public static function get_artefacts(BlockInstance $instance) {
$configdata = $instance->get('configdata');
$artefacts = array();
if (isset($configdata['artefactid'])) {
$artefacts[] = $configdata['artefactid'];
// Add all artefacts found in the text
$text = $instance->get_artefact_instance($configdata['artefactid'])->get('description');
$artefacts = array_unique(array_merge($artefacts, artefact_get_references_in_html($text)));
// Get all the feedback on this annotation
// to retrieve all the artefacts found in their text
// and to include the feedback as part of the view_artefact.
// Please note that images owned by other users that are place on feedback
// will not be part of the view_artefact because the owner of the
// annotation does not own the image being placed on the feedback.
// Therefore, when exported as Leap2A, these images will not come through.
$sql = "SELECT a.id, a.description
FROM {artefact} a
INNER JOIN {artefact_annotation_feedback} af ON a.id = af.artefact
WHERE af.onannotation = ?";
// Keep a list of the feedback ids.
$artefactfeedback = array();
if ($feedback = get_records_sql_array($sql, array($configdata['artefactid']))) {
foreach ($feedback as $f) {
// Include the feedback artefact.
$artefactfeedback[] = $f->id;
// Include any artefacts found in its text.
// The BlockInstance::rebuild_artefact_list() will sort out the ownership.
$artefacts = array_unique(array_merge($artefacts, artefact_get_references_in_html($f->description)));
}
// Now merge the feedback artefacts as well.
$artefacts = array_unique(array_merge($artefacts, $artefactfeedback));
}
}
return $artefacts;
}
/**
* Indicates whether this block can be loaded by Ajax after the page is done. This
* improves page-load times by allowing blocks to be rendered in parallel instead
* of in serial.
*
* You might want to disable this for:
* - Blocks with particularly finicky Javascript contents
* - Blocks that need to write to the session (the Ajax loader uses the session in read-only)
* - Blocks that won't take long to render (static content, external content)
*
* @return boolean
*/
public static function should_ajaxify() {
// No, don't ajaxify this block. TinyMCE has issues.
return false;
}
public static function render_instance(BlockInstance $instance, $editing=false) {
$smarty = smarty_core();
$artefactid = '';
$text = '';
$feedbackcount = 0;
$instance->set('artefactplugin', 'annotation');
$configdata = $instance->get('configdata');
if (!empty($configdata['artefactid'])) {
safe_require('artefact', 'file');
$artefactid = $configdata['artefactid'];
$artefact = $instance->get_artefact_instance($artefactid);
$viewid = $instance->get('view');
$text = $artefact->get('description');
require_once(get_config('docroot') . 'lib/view.php');
$view = new View($viewid);
list($feedbackcount, $annotationfeedback) = ArtefactTypeAnnotationfeedback::get_annotation_feedback_for_view($artefact, $view, $instance->get('id'), true, $editing);
$smarty->assign('annotationfeedback', $annotationfeedback);
if (!$editing) {
$smarty->assign('addannotationscript', get_config('wwwroot') . 'artefact/annotation/js/annotation.js');
}
}
$smarty->assign('text', $text);
$smarty->assign('artefactid', $artefactid);
$smarty->assign('annotationfeedbackcount', $feedbackcount);
$html = $smarty->fetch('blocktype:annotation:annotation.tpl');
return $html;
}
public static function has_instance_config() {
return true;
}
public static function instance_config_form(BlockInstance $instance) {
global $USER;
$instance->set('artefactplugin', 'annotation');
// Get the saved configs in the artefact
$configdata = $instance->get('configdata');
if (!$height = get_config('blockeditorheight')) {
$cfheight = param_integer('cfheight', 0);
$height = $cfheight ? $cfheight * 0.7 : 150;
}
// Default annotation text.
$text = '';
$tags = '';
$artefactid = '';
$readonly = false;
$textreadonly = false;
$view = $instance->get_view();
if (!empty($configdata['artefactid'])) {
$artefactid = $configdata['artefactid'];
try {
$artefact = $instance->get_artefact_instance($artefactid);
// Get the annotation record -> to get the artefact it's linked to.
$annotation = new ArtefactTypeAnnotation($artefactid);
// Get the total annotation feedback inserted so far by anyone.
$totalannotationfeedback = ArtefactTypeAnnotationfeedback::count_annotation_feedback($artefactid, array($view->get('id')), array($annotation->get('artefact')));
$readonly = $artefact->get('owner') !== $view->get('owner')
|| $artefact->get('group') !== $view->get('group')
|| $artefact->get('institution') !== $view->get('institution')
|| $artefact->get('locked')
|| !$USER->can_edit_artefact($artefact);
if (isset($totalannotationfeedback[$view->get('id')])) {
$textreadonly = $totalannotationfeedback[$view->get('id')]->total > 0;
}
$text = $artefact->get('description');
$tags = $artefact->get('tags');
}
catch (ArtefactNotFoundException $e) {
unset($artefactid);
}
}
$elements = array(
'text' => array(
'type' => ($textreadonly ? 'html' : 'wysiwyg'),
'class' => '',
'title' => get_string('Annotation', 'artefact.annotation'),
'width' => '100%',
'height' => $height . 'px',
'defaultvalue' => $text,
'rules' => array('maxlength' => 65536),
),
'annotationreadonlymsg' => array(
'type' => 'html',
'class' => 'message info' . ($textreadonly ? '' : ' hidden'),
'value' => get_string('annotationreadonlymessage', 'blocktype.annotation/annotation'),
'help' => true,
),
'allowfeedback' => array(
'type' => 'checkbox',
'title' => get_string('allowannotationfeedback', 'artefact.annotation'),
'defaultvalue' => (!empty($artefact) ? $artefact->get('allowcomments') : 1),
),
'tags' => array(
'type' => 'tags',
'class' => $readonly ? 'hidden' : '',
'width' => '100%',
'title' => get_string('tags'),
'description' => get_string('tagsdescprofile'),
'defaultvalue' => $tags,
),
'tagsreadonly' => array(
'type' => 'html',
'class' => $readonly ? '' : 'hidden',
'width' => '100%',
'title' => get_string('tags'),
'value' => '<div id="instconf_tagsreadonly_display">' . (is_array($tags) ? hsc(join(', ', $tags)) : '') . '</div>',
),
);
if ($textreadonly) {
// The annotation is displayed as html, need to populate its value.
$elements['text']['value'] = $text;
}
return $elements;
}
public static function delete_instance($instance) {
$configdata = $instance->get('configdata');
if (!empty($configdata)) {
$artefactid = $configdata['artefactid'];
if (!empty($artefactid) && $artefactid) {
// Delete the annotation and all its feedback.
safe_require('artefact', 'annotation');
$annotation = new ArtefactTypeAnnotation($artefactid);
$annotation->delete();
}
}
}
public static function instance_config_save($values, $instance) {
require_once('embeddedimage.php');
safe_require('artefact', 'annotation');
$data = array();
$view = $instance->get_view();
$configdata = $instance->get('configdata');
foreach (array('owner', 'group', 'institution') as $f) {
$data[$f] = $view->get($f);
}
// The title will always be Annotation.
$title = get_string('Annotation', 'artefact.annotation');
$data['title'] = $title;
$values['title'] = $title;
if (empty($configdata['artefactid'])) {
// This is a new annotation.
$artefact = new ArtefactTypeAnnotation(0, $data);
}
else {
// The user is editing the annotation.
$artefact = new ArtefactTypeAnnotation($configdata['artefactid']);
}
$artefact->set('title', $title);
$artefact->set('description', $values['text']);
$artefact->set('allowcomments', (!empty($values['allowfeedback']) ? $values['allowfeedback'] : 0));
$artefact->set('tags', $values['tags']);
$artefact->set('view', $view->get('id'));
$artefact->commit();
// Now fix up the text in case there were any embedded images.
// Do this after saving because we may not have an artefactid yet.
$newdescription = EmbeddedImage::prepare_embedded_images($values['text'], 'annotation', $artefact->get('id'), $view->get('group'));
if ($newdescription !== false && $newdescription !== $values['text']) {
$updatedartefact = new stdClass();
$updatedartefact->id = $artefact->get('id');
$updatedartefact->description = $newdescription;
update_record('artefact', $updatedartefact, 'id');
}
$values['artefactid'] = $artefact->get('id');
$instance->save_artefact_instance($artefact);
unset($values['text']);
unset($values['allowfeedback']);
unset($values['annotationreadonlymsg']);
// Pass back a list of any other blocks that need to be rendered
// due to this change.
$values['_redrawblocks'] = array_unique(get_column(
'view_artefact', 'block',
'artefact', $values['artefactid'],
'view', $instance->get('view')
));
return $values;
}
public static function default_copy_type() {
return 'full';
}
}
<?php
/**
*
* @package mahara
* @subpackage artefact-annotation
* @author Catalyst IT Ltd
* @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();
$config = new StdClass;
$config->version = 2014122100;
$config->release = '1.0.0';
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20100319" COMMENT="XMLDB file for Mahara's annotation-related tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<!--
This table contains static data relevant to the types of users that can delete
an annotationfeedback in the artefact_annotation_feedback table. Currently, there
are only 3 types: 'author', 'owner', 'admin'.
-->
<TABLE NAME="artefact_annotation_deletedby">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="name" TYPE="char" LENGTH="50" NOTNULL="true"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="nameuk" UNIQUE="true" FIELDS="name"/>
</INDEXES>
</TABLE>
<!--
This table has a 1-1 relation with every row in the artefact table with
artefacttype = 'annotation'.
This table links an annotation to an artefact and/or a view.
- artefact_annotation.annotation -> artefact.id (where artefact.artefacttype = 'annotation').
- artefact_annotation.artefact -> artefact.id (where artefact.artefacttype = 'xxxx').
It designates that this annotation is linked to this artefact.
- artefact_annotation.view -> view.id
It designates that this annotation is linked to this view.
- If artefact_annotation.artefact and artefact.view are populated -> this annotation and its
feedback are related to this linked artefact when on this view.
- artefact_annotation.artefact is populated -> the linked artefact and its feedback do not
relate to any view at the movment.
-->
<TABLE NAME="artefact_annotation">
<FIELDS>
<FIELD NAME="annotation" TYPE="int" LENGTH="10" NOTNULL="true"/>
<FIELD NAME="artefact" TYPE="int" LENGTH="10" NOTNULL="false"/>
<FIELD NAME="view" TYPE="int" LENGTH="10" NOTNULL="false"/>
</FIELDS>
<KEYS>
<KEY NAME="annotationpk" TYPE="primary" FIELDS="annotation"/>
<KEY NAME="annotationfk" TYPE="foreign" FIELDS="annotation" REFTABLE="artefact" REFFIELDS="id"/>
<KEY NAME="artefactfk" TYPE="foreign" FIELDS="artefact" REFTABLE="artefact" REFFIELDS="id"/>
<KEY NAME="viewfk" TYPE="foreign" FIELDS="view" REFTABLE="view" REFFIELDS="id"/>
</KEYS>
</TABLE>
<!--
This table has 1-1 relation with every row in the artefact table with
artefacttype = 'annotationfeedback'. The artefact table holds the actual
feedback (artefact.description) input by the user.
The artefact_annotation_feedback table holds extra data relevant to
the annotationfeedback.
- artefact_annotation_feedback.artefact -> artefact.id (where artefact.artefacttype = 'annotationfeedback').
- artefact_annotation_feedback.onannotation -> artefact_annotation.annotation
It designates that this feedback is related to this particular annotation.
-->
<TABLE NAME="artefact_annotation_feedback">
<FIELDS>
<FIELD NAME="artefact" TYPE="int" LENGTH="10" NOTNULL="true"/>
<FIELD NAME="onannotation" TYPE="int" LENGTH="10" NOTNULL="true"/>
<FIELD NAME="private" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" UNSIGNED="true"/>
<FIELD NAME="deletedby" TYPE="char" LENGTH="50" NOTNULL="false"/>
<FIELD NAME="requestpublic" TYPE="char" LENGTH="50" NOTNULL="false"/>
<FIELD NAME="lastcontentupdate" TYPE="datetime" NOTNULL="false"/>
</FIELDS>
<KEYS>
<KEY NAME="artefactpk" TYPE="primary" FIELDS="artefact"/>
<KEY NAME="artefactfk" TYPE="foreign" FIELDS="artefact" REFTABLE="artefact" REFFIELDS="id"/>
<KEY NAME="onannotationfk" TYPE="foreign" FIELDS="onannotation" REFTABLE="artefact" REFFIELDS="id"/>
<KEY NAME="deletedbyfk" TYPE="foreign" FIELDS="deletedby" REFTABLE="artefact_annotation_deletedby" REFFIELDS="name"/>
<KEY NAME="requestpublicfk" TYPE="foreign" FIELDS="requestpublic" REFTABLE="artefact_annotation_deletedby" REFFIELDS="name"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
<?php
/**
*
* @package mahara
* @subpackage artefact-annotation
* @author Catalyst IT Ltd
* @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('MENUITEM', 'myportfolio');
require(dirname(dirname(dirname(__FILE__))) . '/init.php');
define('TITLE', get_string('editannotationfeedback', 'artefact.annotation'));
safe_require('artefact', 'annotation');
$annotationfeedbackid = param_integer('id');
$viewid = param_integer('viewid');
$annotationfeedback = new ArtefactTypeAnnotationFeedback((int) $annotationfeedbackid);
if ($USER->get('id') != $annotationfeedback->get('author')) {
throw new AccessDeniedException(get_string('canteditnotauthor', 'artefact.annotation'));
}
$annotationid = $annotationfeedback->get('onannotation');
$annotation = new ArtefactTypeAnnotation($annotationid);
$onview = $annotation->get('view');
if ($onview && $onview != $viewid) {
throw new NotFoundException(get_string('annotationfeedbacknotinview', 'artefact.annotation', $annotationfeedbackid, $viewid));
}
$maxage = (int) get_config_plugin('artefact', 'annotation', 'commenteditabletime');
$editableafter = time() - 60 * $maxage;
$goto = $annotation->get_view_url($viewid, false);
if ($annotationfeedback->get('ctime') < $editableafter) {
$SESSION->add_error_msg(get_string('cantedittooold', 'artefact.annotation', $maxage));
redirect($goto);
}
$lastcomment = ArtefactTypeAnnotationfeedback::last_public_annotation_feedback($annotationid, $viewid, $annotation->get('artefact'));
if (!$annotationfeedback->get('private') && $annotationfeedbackid != $lastcomment->id) {
$SESSION->add_error_msg(get_string('cantedithasreplies', 'artefact.annotation'));
redirect($goto);
}
$elements = array();
$elements['message'] = array(
'type' => 'wysiwyg',
'title' => get_string('Annotationfeedback', 'artefact.annotation'),
'rows' => 5,
'cols' => 80,
'defaultvalue' => $annotationfeedback->get('description'),
'rules' => array('maxlength' => 8192),
);
$elements['ispublic'] = array(
'type' => 'checkbox',
'title' => get_string('makepublic', 'artefact.annotation'),