diff --git a/lang/en_utf8/data.php b/lang/en_utf8/data.php index e59622fc8e6..55aa7880f16 100644 --- a/lang/en_utf8/data.php +++ b/lang/en_utf8/data.php @@ -216,7 +216,7 @@ $string['pagesize'] = 'Entries per page'; $string['participants'] = 'Participants'; $string['picture'] = 'Picture'; $string['pleaseaddsome'] = 'Please create some below or choose a predefined set to get started.'; -$string['portfolionotfile'] = 'Export to a portfolio rather than a file (csv only)'; +$string['portfolionotfile'] = 'Export to a portfolio rather than a file (csv and leap2a only)'; $string['presetinfo'] = 'Saving as a preset will publish this template. Other users may be able to use it in their databases.'; $string['presets'] = 'Presets'; $string['radiobutton'] = 'Radio buttons'; diff --git a/lang/en_utf8/portfolio.php b/lang/en_utf8/portfolio.php index 0e93ccf4ed2..ed8e471dcf6 100644 --- a/lang/en_utf8/portfolio.php +++ b/lang/en_utf8/portfolio.php @@ -51,6 +51,7 @@ $string['format_image'] = 'Image'; $string['format_mbkp'] = 'Moodle Backup Format'; $string['format_video'] = 'Video'; $string['format_text'] = 'Plain Text'; +$string['format_leap2a'] = 'LEAP2A portfolio format'; $string['hidden'] = 'Hidden'; $string['highfilesizethreshold'] = 'High transfer filesize'; $string['highfilesizethresholddesc'] = 'Filesizes over this threshold will be considered to take a high amount of time to transfer'; diff --git a/lang/en_utf8/portfolio_format_leap2a.php b/lang/en_utf8/portfolio_format_leap2a.php new file mode 100644 index 00000000000..be4b6101e6e --- /dev/null +++ b/lang/en_utf8/portfolio_format_leap2a.php @@ -0,0 +1,10 @@ +from) tried to link to a non existing entry ($a->to) with rel $a->rel'; +$string['overwritingselection'] = 'Overwriting the original type of an entry ($a) to selection in make_selection'; +$string['selflink'] = 'A LEAP2A entry ($a->id) tried to link to itself with rel $a->rel'; diff --git a/lib/portfolio/caller.php b/lib/portfolio/caller.php index 0c852eceb17..ae600aad4b2 100644 --- a/lib/portfolio/caller.php +++ b/lib/portfolio/caller.php @@ -314,18 +314,22 @@ abstract class portfolio_caller_base { * as this function must be called statically. * * @return array list of formats + * */ - public static function supported_formats($caller=null) { - if ($caller && $formats = $caller->get('supportedformats')) { - if (is_array($formats)) { - return $formats; - } + public final function supported_formats() { + $basic = $this->base_supported_formats(); + if (empty($this->supportedformats)) { + $specific = array(); + } else if (!is_array($this->supportedformats)) { debugging(get_class($caller) . ' has set a non array value of member variable supported formats - working around but should be fixed in code'); - return array($formats); + $specific = array($formats); + } else { + $specific = $this->supportedformats; } - return array(PORTFOLIO_FORMAT_FILE); + return portfolio_most_specific_formats($specific, $basic); } + public abstract static function base_supported_formats(); /** * this is the "return to where you were" url @@ -392,7 +396,7 @@ abstract class portfolio_caller_base { } } } else if (count($args) != 0) { - if (count($args) != 3) { + if (count($args) < 3) { throw new portfolio_caller_exception('invalidfileareaargs', 'portfolio'); } $files = array_values(call_user_func_array(array($fs, 'get_area_files'), $args)); @@ -401,7 +405,6 @@ abstract class portfolio_caller_base { case 0: return; case 1: { $this->singlefile = $files[0]; - $this->supportedformats = array(portfolio_format_from_file($this->singlefile)); return; } default: { @@ -410,6 +413,66 @@ abstract class portfolio_caller_base { } } + /** + * the button-location always knows best + * what the formats are... so it should be trusted. + * + * @param array $formats array of PORTFOLIO_FORMAT_XX + */ + public function set_formats_from_button($formats) { + $base = $this->base_supported_formats(); + if (count($base) != count($formats) + || count($base) != count(array_intersect($base, $formats))) { + return $this->supportedformats = portfolio_most_specific_formats($formats, $base); + } + // in the case where the button hasn't actually set anything, + // we need to run through again and resolve conflicts + $removed = array(); + foreach ($formats as $f1key => $f1) { + if (in_array($f1, $removed)) { + continue; + } + $f1obj = portfolio_format_object($f1); + foreach ($formats as $f2key => $f2) { + if (in_array($f2, $removed)) { + continue; + } + if ($f1obj->conflicts($f2)) { + unset($formats[$f2key]); + $removed[] = $f2; + } + } + } + $this->supportedformats = $formats; + } + + /** + * adds a new format to the list of supported formats. + * handles removing conflicting and less specific + * formats at the same time. + * + * @param string $format one of PORTFOLIO_FORMAT_XX + * + * @return void + */ + protected function add_format($format) { + if (in_array($format, $this->supportedformats)) { + return; + } + $formatobj = portfolio_format_object($format); + foreach ($this->supportedformats as $key => $f) { + $f2obj = portfolio_format_object($f); + if ($formatobj->conflicts($f)) { + unset($this->supportedformats[$key]); + } + $class = get_class($f2obj); + if ($formatobj instanceof $class) { + unset($this->supportedformats[$key]); + } + } + $this->supportedformats[] = $format; + } + /** * array of arguments the caller expects to be passed through to it * this must be keyed on the argument name, and the array value is a boolean, diff --git a/lib/portfolio/constants.php b/lib/portfolio/constants.php index f21dfebb576..1492cea6516 100644 --- a/lib/portfolio/constants.php +++ b/lib/portfolio/constants.php @@ -142,6 +142,17 @@ define('PORTFOLIO_FORMAT_SPREADSHEET', 'spreadsheet'); */ define('PORTFOLIO_FORMAT_PRESENTATION', 'presentation'); +/** + * abstract - just used to say, "we support all these" + */ +define('PORTFOLIO_FORMAT_RICH', 'rich'); + +/** + * leap2a http://wiki.cetis.ac.uk/LEAP_2.0 + * supported by mahara and and others + */ +define('PORTFOLIO_FORMAT_LEAP2A', 'leap2a'); + // ************************************************** // // EXPORT TIME LEVELS // these should correspond to a string diff --git a/lib/portfolio/exceptions.php b/lib/portfolio/exceptions.php index dee46e5020c..28ab057a049 100644 --- a/lib/portfolio/exceptions.php +++ b/lib/portfolio/exceptions.php @@ -80,3 +80,8 @@ class portfolio_plugin_exception extends portfolio_exception {} * exception for interacting with the button class */ class portfolio_button_exception extends portfolio_exception {} + +/** + * leap2a exception - for invalid api calls + */ +class portfolio_format_leap2a_exception extends portfolio_exception {} diff --git a/lib/portfolio/exporter.php b/lib/portfolio/exporter.php index 8935dfedfe2..4934324e113 100644 --- a/lib/portfolio/exporter.php +++ b/lib/portfolio/exporter.php @@ -152,6 +152,8 @@ class portfolio_exporter { public function get($field) { if ($field == 'format') { return portfolio_format_object($this->format); + } else if ($field == 'formatclass') { + return $this->format; } if (property_exists($this, $field)) { return $this->{$field}; @@ -287,7 +289,7 @@ class portfolio_exporter { if ($this->caller->has_export_config()) { $callerobj = $this->caller; } - $formats = portfolio_supported_formats_intersect($this->caller->supported_formats($this->caller), $this->instance->supported_formats()); + $formats = portfolio_supported_formats_intersect($this->caller->supported_formats(), $this->instance->supported_formats()); $expectedtime = $this->instance->expected_time($this->caller->expected_time()); if (count($formats) == 0) { // something went wrong, we should not have gotten this far. diff --git a/lib/portfolio/formats.php b/lib/portfolio/formats.php index e97b823ed3c..fdf86b80ebc 100644 --- a/lib/portfolio/formats.php +++ b/lib/portfolio/formats.php @@ -53,9 +53,34 @@ class portfolio_format_file { * given a file, return a snippet of markup in whatever format * to link to that file. * usually involves the path given by {@link get_file_directory} + * this is not supported in subclasses of portfolio_format_file + * since they're all just single files. */ public static function file_output($file) { - return ''; + throw new portfolio_exception('fileoutputnotsupported', 'portfolio'); + } + + /** + * whether this format conflicts with the given format + * this is used for the case where an export location + * "generally" supports something like FORMAT_PLAINHTML + * but then in a specific export case, must add attachments + * which means that FORMAT_RICHHTML is supported in that case + * which implies removing support for FORMAT_PLAINHTML. + * Note that conflicts don't have to be bi-directional + * (eg FORMAT_PLAINHTML conflicts with FORMAT_RICHHTML + * but not the other way around) and things within the class hierarchy + * are resolved automatically anyway. + * + * This is really just between subclasses of format_rich + * and subclasses of format_file. + * + * @param string $format one of the FORMAT_XX constants + * + * @return boolean + */ + public static function conflicts($format) { + return false; } } @@ -77,9 +102,14 @@ class portfolio_format_image extends portfolio_format_file { * in case we want to be really specific. */ class portfolio_format_plainhtml extends portfolio_format_file { + public static function mimetypes() { return array('text/html'); } + + public static function conflicts($format) { + return ($format == PORTFOLIO_FORMAT_RICHHTML); + } } /** @@ -104,16 +134,38 @@ class portfolio_format_text extends portfolio_format_file { public static function mimetypes() { return array('text/plain'); } + + public static function conflicts($format ) { + return ($format == PORTFOLIO_FORMAT_PLAINHTML + || $format == PORTFOLIO_FORMAT_RICHHTML); + } } /** * base class for rich formats. * these are multipart - eg things with attachments */ -class portfolio_format_rich { +abstract class portfolio_format_rich { public static function mimetypes() { return array(null); } + + public static function conflicts($format) { + return false; + } + + /** + * given a file, return a snippet of markup in whatever format + * to link to that file. + * usually involves the path given by {@link get_file_directory} + * this is not supported in subclasses of portfolio_format_file + * since they're all just single files. + * + * @param stored_file $file the file to link to + * @param mixed $extras any extra arguments + */ + public static abstract function file_output($file, $extras=null); + } /** @@ -124,23 +176,62 @@ class portfolio_format_richhtml extends portfolio_format_rich { public static function get_file_directory() { return 'site_files'; } - public static function file_output($file) { + public static function file_output($file, $extras=null) { $path = self::get_file_directory() . '/' . $file->get_filename(); if (in_array($file->get_mimetype(), portfolio_format_image::mimetypes())) { return '' . $file->get_filename() . ''; } return '' . $file->get_filename() . ''; } + public static function conflicts($format) { // TODO revisit the conflict with file, since we zip here + return ($format == PORTFOLIO_FORMAT_PLAINHTML || $format == PORTFOLIO_FORMAT_FILE); + } + } -class portfolio_format_leap extends portfolio_format_rich { } +class portfolio_format_leap2a extends portfolio_format_rich { + + public static function get_file_directory() { + return 'files/'; + } + + /** + * return the link to a file + * + * @param stored_file $file + * @param boolean $entry whether the file is a LEAP2A entry or just a bundled file (default false) + */ + public static function file_output($file, $entry=false) { + $id = ''; + if ($entry) { + $id = 'portfolio:file' . $file->get_id; + } else { + $id = self::get_file_directory() . '/' . $file->get_filename(); + } + return '' . $file->get_filename() . ''; + } + + public static function leap2a_writer(stdclass $user=null) { + global $CFG; + if (empty($user)) { + global $USER; + $user = $USER; + } + require_once($CFG->libdir . '/portfolio/formats/leap2a/lib.php'); + return new portfolio_format_leap2a_writer($user); + } + + public static function manifest_name() { + return 'leap2a.xml'; + } +} /** * later.... a moodle plugin might support this. * it's commented out in portfolio_supported_formats so cannot currently be used. */ -class portfolio_format_mbkp extends portfolio_format_rich {} +//class portfolio_format_mbkp extends portfolio_format_rich {} /** * 'PDF format', subtype of file. diff --git a/lib/portfolio/formats/leap2a/lib.php b/lib/portfolio/formats/leap2a/lib.php new file mode 100644 index 00000000000..2912ccd9954 --- /dev/null +++ b/lib/portfolio/formats/leap2a/lib.php @@ -0,0 +1,477 @@ +. + * + * @package moodle + * @subpackage portfolio + * @author Penny Leach + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com + * + * This file contains the LEAP2a writer used by portfolio_format_leap2a + */ + +/** + * object to encapsulate the writing of leap2a. + * should be used like: + * + * $writer = portfolio_format_leap2a::leap2a_writer($USER); + * $entry = new portfolio_format_leap2a_entry('forumpost6', $title, 'leaptype', 'somecontent') + * $entry->add_link('something', 'has_part')->add_link('somethingelse', 'has_part'); + * .. etc + * $writer->add_entry($entry); + * $xmlstr = $writer->to_xml(); + * + * @TODO find a way to ensure that all referenced files are included + */ +class portfolio_format_leap2a_writer { + + /** the domdocument object used to create elements */ + private $dom; + /** the top level feed element */ + private $feed; + /** the user exporting data */ + private $user; + /** the id of the feed - this is unique to the user and date and used for portfolio ns as well as feed id */ + private $id; + /** the entries for the feed - keyed on id */ + private $entries = array(); + + /** + * constructor - usually generated from portfolio_format_leap2a::leap2a_writer($USER); + * + * @param stdclass $user the user exporting (almost always $USER) + * + */ + public function __construct(stdclass $user) { // todo something else - exporter, format, etc + global $CFG; + $this->user = $user; + $this->exporttime = time(); + $this->id = $CFG->wwwroot . '/portfolio/export/leap2a/' . $this->user->id . '/' . $this->exporttime; + + $this->dom = new DomDocument('1.0', 'utf-8'); + + $this->feed = $this->dom->createElement('feed'); + $this->feed->setAttribute('xmlns', 'http://www.w3.org/2005/Atom'); + $this->feed->setAttribute('xmlns:rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); + $this->feed->setAttribute('xmlns:leap', 'http://wiki.cetis.ac.uk/2009-03/LEAP2A_predicates#'); + $this->feed->setAttribute('xmlns:leaptype', 'http://wiki.cetis.ac.uk/2009-03/LEAP2A_types#'); + $this->feed->setAttribute('xmlns:categories', 'http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories/'); + $this->feed->setAttribute('xmlns:portfolio', $this->id); // this is just a ns for ids of elements for convenience + + $this->dom->appendChild($this->feed); + + $this->feed->appendChild($this->dom->createElement('id', $this->id)); + $this->feed->appendChild($this->dom->createElement('title', get_string('feedtitle', 'portfolio_format_leap2a', fullname($this->user)))); + + $generator = $this->dom->createElement('generator', 'Moodle'); + $generator->setAttribute('uri', $CFG->wwwroot); + $generator->setAttribute('version', $CFG->version); + + $this->feed->appendChild($generator); + + $author = $this->dom->createElement('author'); + $author->appendChild($this->dom->createElement('name', fullname($this->user))); + $author->appendChild($this->dom->createElement('email', $this->user->email)); + $author->appendChild($this->dom->CreateElement('uri', $CFG->wwwroot . '/user/view.php?id=' . $this->user->id)); + + $this->feed->appendChild($author); + // header done, we can start appending entry elements now + } + + /** + * adds a entry to the feed ready to be exported + * + * @param portfolio_format_leap2a_entry $entry the entry to add + */ + public function add_entry(portfolio_format_leap2a_entry $entry) { + if (array_key_exists($entry->id, $this->entries)) { + throw new portfolio_format_leap2a_exception('entryalreadyexists', 'portfolio_format_leap2a', '', $entry->id); + } + $this->entries[$entry->id] = $entry; + return $entry; + } + + /** + * make an entry that has previously been added into the feed into a selection. + * + * @param mixed $selectionentry the entry to make a selection (id or entry object) + * @param array $ids array of ids this selection includes + * @param string $selectiontype http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories/selection_type + */ + public function make_selection($selectionentry, $ids, $selectiontype) { + $selectionid = null; + if ($selectionentry instanceof portfolio_format_leap2a_entry) { + $selectionid = $selectionentry->id; + } else if (is_string($selectionentry)) { + $selectionid = $selectionentry; + } + if (!array_key_exists($selectionid, $this->entries)) { + throw new portfolio_format_leap2a_exception('invalidentryid', 'portfolio_format_leap2a', '', $selectionid); + } + foreach ($ids as $entryid) { + if (!array_key_exists($entryid, $this->entries)) { + throw new portfolio_format_leap2a_exception('invalidentryid', 'portfolio_format_leap2a', '', $entryid); + } + $this->entries[$selectionid]->add_link($entryid, 'has_part'); + $this->entries[$entryid]->add_link($selectionid, 'is_part_of'); + } + $this->entries[$selectionid]->add_category($selectiontype, 'selection_type'); + if ($this->entries[$selectionid]->type != 'selection') { + debugging(get_string('overwritingselection', 'portfolio_format_leap2a', $this->entries[$selectionid]->type)); + $this->entries[$selectionid]->type = 'selection'; + } + } + + /** + * validate the feed and all entries + */ + private function validate() { + foreach ($this->entries as $entry) { + // first call the entry's own validation method + // which will throw an exception if there's anything wrong + $entry->validate(); + // now make sure that all links are in place + foreach ($entry->links as $linkedid => $rel) { + // the linked to entry exists + if (!array_key_exists($linkedid, $this->entries)) { + $a = (object)array('rel' => $rel->type, 'to' => $linkedid, 'from' => $entry->id); + throw new portfolio_format_leap2a_exception('nonexistantlink', 'portfolio_format_leap2a', '', $a); + } + // and contains a link back to us + if (!array_key_exists($entry->id, $this->entries[$linkedid]->links)) { + + } + // we could later check that the reltypes were properly inverse, but nevermind for now. + } + } + } + + /** + * return the entire feed as a string + * calls validate() first on everything + * + * @return string + */ + public function to_xml() { + $this->validate(); + foreach ($this->entries as $entry) { + $this->feed->appendChild($entry->to_dom($this->dom, $this->user)); + } + return $this->dom->saveXML(); + } +} + +/** + * this class represents a single leap2a entry. + * you can create these directly and then add them to the main leap feed object + */ +class portfolio_format_leap2a_entry { + + /** entry id - something like forumpost6, must be unique to the feed **/ + public $id; + /** title of the entry **/ + public $title; + /** leap2a entry type **/ + public $type; + /** optional author (only if different to feed author) **/ + public $author; + /** summary - for split long content **/ + public $summary; + /** main content of the entry. can be html,text,xhtml or a stored_file **/ + public $content; + /** updated date - unix timestamp */ + public $updated; + /** published date (ctime) - unix timestamp */ + public $published; + + /** used internally for file content **/ + private $contentsrc; + /** used internally for file content **/ + private $referencedfile; + + /** the required fields for a leap2a entry */ + private $requiredfields = array( 'id', 'title', 'type'); + + /** extra fields which usually should be set (except author) but are not required */ + private $optionalfields = array('author', 'updated', 'published', 'content', 'summary'); + + /** links from this entry to other entries */ + public $links = array(); + + /** attachments to this entry */ + public $attachments = array(); + + /** categories for this entry */ + private $categories = array(); + + /** + * constructor. All arguments are required (and will be validated) + * http://wiki.cetis.ac.uk/2009-03/LEAP2A_types + * + * @param string $id unique id of this entry. + * could be something like forumpost6 for example. + * This must be unique to the entire feed. + * @param string $title title of the entry. This is pure atom. + * @param string $type the leap type of this entry. + * @param mixed $content the content of the entry. string (xhtml/html/text) or stored_file + */ + public function __construct($id, $title, $type, $content=null) { + $this->id = $id; + $this->title = $title; + $this->type = $type; + $this->content = $this->__set('content', $content); + + } + + /** + * override __set to do proper dispatching for different things + * only allows the optional and required leap2a entry fields to be set + */ + public function __set($field, $value) { + // detect the case where content is being set to be a file directly + if ($field == 'content' && $value instanceof stored_file) { + return $this->set_content_file($value); + } + if (in_array($field, $this->requiredfields) || in_array($field, $this->optionalfields)) { + return $this->{$field} = $value; + } + throw new portfolio_format_leap2a_exception('invalidentryfield', 'portfolio_format_leap2a', '', $field); + } + + /** + * sets the content of this entry to have a source + * this will take care of namespacing the filepath + * to the final path in the resulting zip file. + * + * @param stored_file $file the file to link to + * @param boolean $overridetype (default true) will set the entry rdf type to resource, + * overriding what was previously set. + * will be ignored if type is empty already + */ + public function set_content_file(stored_file $file, $overridetype=true) { + $this->contentsrc = portfolio_format_leap2a::get_file_directory() . $file->get_filename(); + if (empty($overridetype) || empty($this->type)) { + $this->type = 'resource'; + } + $this->referencedfile = $file; + } + + /** + * validate this entry. + * at the moment this just makes sure required fields exist + * but it could also check things against a list, for example + */ + public function validate() { + foreach ($this->requiredfields as $key) { + if (empty($this->{$key})) { + throw new portfolio_format_leap2a_exception('missingfield', 'portfolio_format_leap2a', '', $key); + } + } + if ($this->type == 'selection') { + if (count($this->links) == 0) { + throw new portfolio_format_leap2a_exception('emptyselection', 'portfolio_format_leap2a'); + } + //TODO make sure we have a category with a scheme 'selection_type' + } + } + + /** + * add a link from this entry to another one + * these will be collated at the end of the export (during to_xml) + * and validated at that point. This function does no validation + * http://wiki.cetis.ac.uk/2009-03/LEAP2A_relationships + * + * @param mixed $otherentry portfolio_format_leap2a_entry or its id + * @param string $reltype (no leap: ns required) + * + * @return the current entry object. This is so that these calls can be chained + * eg $entry->add_link('something6', 'has_part')->add_link('something7', 'has_part'); + * + */ + public function add_link($otherentry, $reltype, $displayorder=null) { + if ($otherentry instanceof portfolio_format_leap2a_entry) { + $otherentry = $otherentry->id; + } + if ($otherentry == $this->id) { + throw new portfolio_format_leap2a_exception('selflink', 'portfolio_format_leap2a', '', (object)array('rel' => $reltype, 'id' => $this->id)); + } + // add on the leap: ns if required + if (!in_array($reltype, array('related', 'alternate', 'enclosure'))) { + $rel = 'leap:' . $reltype; + } + + $this->links[$otherentry] = (object)array('rel' => $reltype, 'order' => $displayorder); + + return $this; + } + + /** + * add an attachment to the feed. + * adding the file to the files area has to be handled outside this class separately. + * + * @param stored_file $file the file to add + */ + public function add_attachment(stored_file $file) { + $this->attachments[$file->get_id()] = $file; + } + + /** + * add a category to this entry + * http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories + * + * @param string $term eg 'Offline' + * @param string $scheme (optional) eg resource_type + * @param string $label (optional) eg File + * + * "tags" should just pass a term here and no scheme or label. + * they will be automatically normalised if they have spaces. + */ + public function add_category($term, $scheme=null, $label=null) { + // "normalise" terms and set their label if they have spaces + // see http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories#Plain_tags for more information + if (empty($scheme) && strpos($term, ' ') !== false) { + $label = $term; + $term = str_replace(' ', '-', $term); + } + $this->categories[] = (object)array( + 'term' => $term, + 'scheme' => $scheme, + 'label' => $label, + ); + } + + /** + * Create an entry element and append all the children + * And return it rather than adding it to the dom. + * This is handled by the main writer object. + * + * @param DomDocument $dom use this to create elements + * + * @return DomElement + */ + public function to_dom(DomDocument $dom, $feedauthor) { + $entry = $dom->createElement('entry'); + $entry->appendChild($dom->createElement('id', $this->id)); + $entry->appendChild($dom->createElement('title', $this->title)); + if ($this->author && $this->author->id != $feedauthor->id) { + $author = $dom->createElement('author'); + $author->appendChild($dom->createElement('name', fullname($this->author))); + $entry->appendChild($author); + } + // selectively add uncomplicated optional elements + foreach (array('updated', 'published') as $field) { + if ($this->{$field}) { + // TODO get the proper date format + $entry->appendChild($dom->createElement($field, $this->{$field})); + } + } + // deal with referenced files first since it's simple + if ($this->contentsrc) { + $content = $dom->createElement('content'); + $content->setAttribute('src', $this->contentsrc); + $content->setAttribute('type', $this->referencedfile->get_mimetype()); + $entry->appendChild($content); + } else if (empty($this->content)) { + $entry->appendChild($dom->createElement('content')); + } else { + $content = $this->create_xhtmlish_element($dom, 'content', $this->content); + $entry->appendChild($content); + } + + if (!empty($this->summary)) { + $summary = $this->create_xhtmlish_element($dom, 'summary', $this->summary); + $entry->appendChild($summary); + } + + $type = $dom->createElement('rdf:type'); + $type->setAttribute('rdf:resource', 'leaptype:' . $this->type); + $entry->appendChild($type); + + foreach ($this->links as $otherentry => $l) { + $link = $dom->createElement('link'); + $link->setAttribute('rel', $l->rel); + $link->setAttribute('href', $otherentry); + if ($l->order) { + $link->setAttribute('leap:display_order', $l->order); + } + $entry->appendChild($link); + } + foreach ($this->attachments as $id => $file) { + $link = $dom->createElement('link'); + $link->setAttribute('rel', 'enclosure'); + $link->setAttribute('href', portfolio_format_leap2a::get_file_directory() . $file->get_filename()); + $link->setAttribute('length', $file->get_filesize()); + $entry->appendChild($link); + } + foreach ($this->categories as $category) { + $cat = $dom->createElement('category'); + $cat->setAttribute('term', $category->term); + if ($category->scheme) { + $cat->setAttribute('scheme', $category->scheme); + } + if ($category->label && $category->label != $category->term) { + $cat->setAttribute('label', $category->label); + } + $entry->appendChild($cat); + } + return $entry; + } + + /** + * try to load whatever is in $content into xhtml and add it to the dom. + * failing that, load the html, escape it, and set it as the body of the tag + * either way it sets the type attribute of the top level element + * moodle should always provide xhtml content, but user-defined content can't be trusted + * + * @param DomDocument $dom the dom doc to use + * @param string $tagname usually 'content' or 'summary' + * @param string $content the content to use, either xhtml or html. + * + * @return DomElement + */ + private function create_xhtmlish_element(DomDocument $dom, $tagname, $content) { + $topel = $dom->createElement($tagname); + $maybexml = true; + if (strpos($content, '<') === false && strpos($content, '>') === false) { + $maybexml = false; + } + // try to load content as xml + $tmp = new DomDocument(); + if ($maybexml && @$tmp->loadXML('
' . $content . '
')) { + $topel->setAttribute('type', 'xhtml'); + $content = $dom->importNode($tmp->documentElement, true); + $content->setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + $topel->appendChild($content); + // if that fails, it could still be html + } else if ($maybexml && @$tmp->loadHTML($content)) { + $topel->setAttribute('type', 'html'); + $topel->nodeValue = $content; + // TODO figure out how to convert this to xml + // TODO because we end up with wrapped around it + // which is annoying + // either we already know it's text from the first check + // or nothing else has worked anyway + } else { + $topel->nodeValue = $content; + $topel->setAttribute('type', 'text'); + return $topel; + } + return $topel; + } +} diff --git a/lib/portfolio/forms.php b/lib/portfolio/forms.php index 50928129e53..b5566745cd1 100644 --- a/lib/portfolio/forms.php +++ b/lib/portfolio/forms.php @@ -254,22 +254,7 @@ class portfolio_instance_select extends moodleform { function definition() { $this->caller = $this->_customdata['caller']; - $options = portfolio_instance_select( - portfolio_instances(), - $this->caller->supported_formats($this->caller), - get_class($this->caller), - $this->caller->get('singlefile'), - 'instance', - true, - true - ); - // TODO maybe add on some information to the user if they're already exporting - // and some of the options were skipped because they are for plugins that don't support - // multiple exports per session - if (empty($options)) { - debugging('noavailableplugins', 'portfolio'); - return false; - } + $options = $this->_customdata['options']; $mform =& $this->_form; $mform->addElement('select', 'instance', get_string('selectplugin', 'portfolio'), $options); $mform->addElement('hidden', 'id', $this->_customdata['id']); diff --git a/lib/portfolio/plugin.php b/lib/portfolio/plugin.php index f9bb5f67186..ca2b6893bf9 100644 --- a/lib/portfolio/plugin.php +++ b/lib/portfolio/plugin.php @@ -109,7 +109,7 @@ abstract class portfolio_plugin_base { * @return array list of formats */ public static function supported_formats() { - return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICHHTML); + return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICH); } /** diff --git a/lib/portfoliolib.php b/lib/portfoliolib.php index 6e5aa6821d2..352bc72e144 100644 --- a/lib/portfoliolib.php +++ b/lib/portfoliolib.php @@ -145,10 +145,10 @@ class portfolio_add_button { * sets the available export formats for this content * this function will also poll the static function in the caller class * and make sure we're not overriding a format that has nothing to do with mimetypes - * eg if you pass IMAGE here but the caller can export LEAP it will keep LEAP as well. + * eg if you pass IMAGE here but the caller can export LEAP2A it will keep LEAP2A as well. * see portfolio_most_specific_formats for more information * - * @param array $formats if the calling code knows better than the static method on the calling class (supported_formats) + * @param array $formats if the calling code knows better than the static method on the calling class (base_supported_formats) * eg, if it's going to be a single file, or if you know it's HTML, you can pass it here instead * this is almost always the case so you should always use this. * {@see portfolio_format_from_file} for how to get the appropriate formats to pass here for uploaded files. @@ -163,20 +163,33 @@ class portfolio_add_button { if (empty($this->callbackclass)) { throw new portfolio_button_exception('noclassbeforeformats', 'portfolio'); } - $callerformats = call_user_func(array($this->callbackclass, 'supported_formats')); + $callerformats = call_user_func(array($this->callbackclass, 'base_supported_formats')); $this->formats = portfolio_most_specific_formats($formats, $callerformats); } + public function reset_formats() { + $this->set_formats(); + } + + /** * if we already know we have exactly one file, * bypass set_formats and just pass the file * so we can detect the formats by mimetype. * - * @param stored_file $file + * @param stored_file $file file to set the format from + * @param mixed $extraformats any additional formats other than by mimetype + * eg leap2a etc */ - public function set_format_by_file(stored_file $file) { + public function set_format_by_file(stored_file $file, $extraformats=null) { $this->file = $file; - $this->formats = array(portfolio_format_from_file($file)); + if (is_string($extraformats)) { + $this->set_formats(array(portfolio_format_from_file($file), $extraformats)); + } else if (is_array($extraformats)) { + $this->set_formats(array_merge(array(portfolio_format_from_file($file)), $extraformats)); + } else { + $this->set_formats(portfolio_format_from_file($file)); + } } /* @@ -230,8 +243,10 @@ class portfolio_add_button { $formoutput .= "\n" . ''; $formoutput .= "\n" . ''; $formoutput .= "\n" . ''; + $formoutput .= "\n" . ''; $linkoutput .= 'callbackfile=' . $this->callbackfile . '&callbackclass=' - . $this->callbackclass . '&course=' . (!empty($COURSE) ? $COURSE->id : 0); + . $this->callbackclass . '&course=' . (!empty($COURSE) ? $COURSE->id : 0) + . '&callerformats=' . implode(',', $this->formats); $selectoutput = ''; if (count($this->instances) == 1) { $tmp = array_values($this->instances); @@ -478,7 +493,8 @@ function portfolio_supported_formats() { PORTFOLIO_FORMAT_SPREADSHEET => 'portfolio_format_spreadsheet', PORTFOLIO_FORMAT_PRESENTATION => 'portfolio_format_presentation', /*PORTFOLIO_FORMAT_MBKP, */ // later - /*PORTFOLIO_FORMAT_LEAP, */ // also later + PORTFOLIO_FORMAT_LEAP2A => 'portfolio_format_leap2a', + PORTFOLIO_FORMAT_RICH => 'portfolio_format_rich', ); } @@ -538,13 +554,17 @@ function portfolio_supported_formats_intersect($callerformats, $pluginformats) { $intersection = array(); foreach ($callerformats as $cf) { if (!array_key_exists($cf, $allformats)) { - debugging(get_string('invalidformat', 'portfolio', $cf)); + if (!portfolio_format_is_abstract($cf)) { + debugging(get_string('invalidformat', 'portfolio', $cf)); + } continue; } $cfobj = new $allformats[$cf](); foreach ($pluginformats as $p => $pf) { if (!array_key_exists($pf, $allformats)) { - debugging(get_string('invalidformat', 'portfolio', $pf)); + if (!portfolio_format_is_abstract($pf)) { + debugging(get_string('invalidformat', 'portfolio', $pf)); + } unset($pluginformats[$p]); // to avoid the same warning over and over continue; } @@ -556,8 +576,34 @@ function portfolio_supported_formats_intersect($callerformats, $pluginformats) { return $intersection; } +/** + * tiny helper to figure out whether a portfolio format is abstract + * + * @param string $format the format to test + * + * @retun bool + */ +function portfolio_format_is_abstract($format) { + if (class_exists($format)) { + $class = $format; + } else if (class_exists('portfolio_format_' . $format)) { + $class = 'portfolio_format_' . $format; + } else { + $allformats = portfolio_supported_formats(); + if (array_key_exists($format, $allformats)) { + $class = $allformats[$format]; + } + } + if (empty($class)) { + return true; // it may as well be, we can't instantiate it :) + } + $rc = new ReflectionClass($class); + return $rc->isAbstract(); +} + /** * return the combination of the two arrays of formats with duplicates in terms of specificity removed +* and also removes conflicting formats * use case: a module is exporting a single file, so the general formats would be FILE and MBKP * while the specific formats would be the specific subclass of FILE based on mime (say IMAGE) * and this function would return IMAGE and MBKP @@ -569,20 +615,39 @@ function portfolio_supported_formats_intersect($callerformats, $pluginformats) { */ function portfolio_most_specific_formats($specificformats, $generalformats) { $allformats = portfolio_supported_formats(); + if (empty($specificformats)) { + return $generalformats; + } else if (empty($generalformats)) { + return $specificformats; + } foreach ($specificformats as $f) { // look for something less specific and remove it, ie outside of the inheritance tree of the current formats. if (!array_key_exists($f, $allformats)) { - throw new portfolio_button_exception('invalidformat', 'portfolio', $f); + if (!portfolio_format_is_abstract($pf)) { + throw new portfolio_button_exception('invalidformat', 'portfolio', $f); + } } $fobj = new $allformats[$f]; foreach ($generalformats as $key => $cf) { $cfclass = $allformats[$cf]; - if ($fobj instanceof $cfclass) { - unset($generalformats[$cf]); + if ($fobj instanceof $cfclass && $cfclass != get_class($fobj)) { + debugging("unsetting $key $cf because it's not specific enough ($f is better)"); + unset($generalformats[$key]); + } + // check for conflicts + if ($fobj->conflicts($cf)) { + debugging("unsetting $key $cf because it conflicts with $f"); + unset($generalformats[$key]); } } + //debugging('inside loop'); + //print_object($generalformats); } - return array_merge(array_values($specificformats), array_values($generalformats)); + + //debugging('final formats'); + $finalformats = array_unique(array_merge(array_values($specificformats), array_values($generalformats))); + //print_object($finalformats); + return $finalformats; } /** @@ -784,9 +849,9 @@ function portfolio_report_insane($insane, $instances=false, $return=false) { * @param string $classfile file containing the callback class definition * @param array $callbackargs arguments to pass to the callback class */ -function portfolio_fake_add_url($instanceid, $classname, $classfile, $callbackargs) { +function portfolio_fake_add_url($instanceid, $classname, $classfile, $callbackargs, $formats) { global $CFG; - $url = $CFG->wwwroot . '/portfolio/add.php?instance=' . $instanceid . '&callbackclass=' . $classname . '&callbackfile=' . $classfile . '&sesskey=' . sesskey(); + $url = $CFG->wwwroot . '/portfolio/add.php?instance=' . $instanceid . '&callbackclass=' . $classname . '&callbackfile=' . $classfile . '&sesskey=' . sesskey(); if (is_object($callbackargs)) { $callbackargs = (array)$callbackargs; @@ -795,8 +860,9 @@ function portfolio_fake_add_url($instanceid, $classname, $classfile, $callbackar return $url; } foreach ($callbackargs as $key => $value) { - $url .= '&ca_' . $key . '=' . urlencode($value); + $url .= '&ca_' . $key . '=' . urlencode($value); } + $url .= '&callerformats=' . implode(',', $formats); return $url; } diff --git a/lib/simpletest/portfolio_testclass.php b/lib/simpletest/portfolio_testclass.php index 7cb77adcb52..d9185b01d88 100644 --- a/lib/simpletest/portfolio_testclass.php +++ b/lib/simpletest/portfolio_testclass.php @@ -103,6 +103,9 @@ class portfolio_caller_test extends portfolio_caller_base { public static function expected_callbackargs() { return array(); } + public static function base_supported_formats() { + return array(PORTFOLIO_FORMAT_RICH, PORTFOLIO_FORMAT_FILE); + } } class portfolio_exporter_test extends portfolio_exporter { diff --git a/mod/assignment/lib.php b/mod/assignment/lib.php index a7e3c28d231..d7547b4df48 100644 --- a/mod/assignment/lib.php +++ b/mod/assignment/lib.php @@ -1821,7 +1821,6 @@ class assignment_base { } if (count($files) > 1 && $this->portfolio_exportable() && has_capability('mod/assignment:exportownsubmission', $this->context)) { $button->set_callback_options('assignment_portfolio_caller', array('id' => $this->cm->id)); - $button->set_formats(PORTFOLIO_PORMAT_FILE); $output .= '
' . $button->to_html(); } } @@ -3395,10 +3394,7 @@ class assignment_portfolio_caller extends portfolio_module_caller_base { throw new portfolio_caller_exception('notexportable', 'portfolio', $this->get_return_url()); } - $this->set_file_and_format_data($this->fileid, $this->assignment->context->id, 'assignment_submission', $this->user->id); - if (empty($this->supportedformats) && is_callable(array($this->assignment, 'portfolio_supported_formats'))) { - $this->supportedformats = $this->assignment->portfolio_supported_formats(); - } + $this->set_file_and_format_data($this->fileid, $this->assignment->context->id, 'assignment_submission', $this->user->id, 'timemodified', false); } public function prepare_package() { @@ -3406,6 +3402,26 @@ class assignment_portfolio_caller extends portfolio_module_caller_base { if (is_callable(array($this->assignment, 'portfolio_prepare_package'))) { return $this->assignment->portfolio_prepare_package($this->exporter, $this->user->id); } + if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { + $leapwriter = $this->exporter->get('format')->leap2a_writer(); + $files = array(); + if ($this->singlefile) { + $files[] = $this->singlefile; + } elseif ($this->multifiles) { + $files = $this->multifiles; + } else { + throw new portfolio_caller_exception('invalidpreparepackagefile', 'portfolio', $this->get_return_url()); + } + $baseid = 'assignment' . $this->assignment->assignment->assignmenttype . $this->assignment->assignment->id . 'submission'; + foreach ($files as $file) { + $id = $baseid . $file->get_id(); + $entry = new portfolio_format_leap2a_entry($id, $file->get_filename(), 'resource', $file); + $entry->add_category('offline', 'resource_type'); + $leapwriter->add_entry($entry); + $this->exporter->copy_existing_file($file); + } + return $this->exporter->write_new_file($leapwriter->to_xml(), $this->exporter->get('format')->manifest_name(), true); + } return $this->prepare_package_file(); } @@ -3441,6 +3457,10 @@ class assignment_portfolio_caller extends portfolio_module_caller_base { public static function display_name() { return get_string('modulename', 'assignment'); } + + public static function base_supported_formats() { + return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A); + } } /** diff --git a/mod/assignment/type/online/assignment.class.php b/mod/assignment/type/online/assignment.class.php index f358694d00a..bd2b68c6d13 100644 --- a/mod/assignment/type/online/assignment.class.php +++ b/mod/assignment/type/online/assignment.class.php @@ -110,6 +110,7 @@ class assignment_online extends assignment_base { echo format_text($text, $submission->data2); $button = new portfolio_add_button(); $button->set_callback_options('assignment_portfolio_caller', array('id' => $this->cm->id), '/mod/assignment/lib.php'); + $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML); //TODO this might have files? $button->render(); } else if (!has_capability('mod/assignment:submit', $context)) { //fix for #4604 echo '
'. get_string('guestnosubmit', 'assignment').'
'; @@ -285,11 +286,19 @@ class assignment_online extends assignment_base { function portfolio_prepare_package($exporter, $userid=0) { $submission = $this->get_submission($userid); - $exporter->write_new_file(format_text($submission->data1, $submission->data2), 'assignment.html', false); - } - - function portfolio_supported_formats() { - return array(PORTFOLIO_FORMAT_PLAINHTML); + $html = format_text($submission->data1, $submission->data2); + if ($exporter->get('formatclass') == PORTFOLIO_FORMAT_PLAINHTML) { + return $exporter->write_new_file($html, 'assignment.html', false); + } else if ($exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { + $leapwriter = $exporter->get('format')->leap2a_writer(); + $entry = new portfolio_format_leap2a_entry('assignmentonline' . $this->assignment->id, $this->assignment->name, 'resource', $html); // TODO entry? + $entry->add_category('web', 'resource_type'); + $leapwriter->add_entry($entry); + return $exporter->write_new_file($leapwriter->to_xml(), $exporter->get('format')->manifest_name(), true); + //TODO attached files?! + } else { + die('wtf ;' . $exporter->get('formatclass')); + } } function extend_settings_navigation($node) { diff --git a/mod/assignment/type/upload/assignment.class.php b/mod/assignment/type/upload/assignment.class.php index c6d59850147..3d00d6ffd3b 100644 --- a/mod/assignment/type/upload/assignment.class.php +++ b/mod/assignment/type/upload/assignment.class.php @@ -368,7 +368,7 @@ class assignment_upload extends assignment_base { } if (count($files) > 1 && has_capability('mod/assignment:exportownsubmission', $this->context)) { $button->set_callback_options('assignment_portfolio_caller', array('id' => $this->cm->id), '/mod/assignment/lib.php'); - $button->set_formats(PORTFOLIO_FORMAT_FILE); + $button->reset_formats(); // reset what we set before, since it's multi-file $output .= $button->to_html(); } } diff --git a/mod/chat/lib.php b/mod/chat/lib.php index cd93a43a965..5f0d2082172 100644 --- a/mod/chat/lib.php +++ b/mod/chat/lib.php @@ -1272,7 +1272,7 @@ class chat_portfolio_caller extends portfolio_module_caller_base { /** * @return array */ - public static function supported_formats() { + public static function base_supported_formats() { return array(PORTFOLIO_FORMAT_PLAINHTML); } /** diff --git a/mod/data/export.php b/mod/data/export.php index 09cd98328d6..244b09f42c0 100644 --- a/mod/data/export.php +++ b/mod/data/export.php @@ -101,7 +101,7 @@ if (array_key_exists('portfolio', $formdata) && !empty($formdata['portfolio'])) // fake portfolio callback stuff and redirect $formdata['id'] = $cm->id; $formdata['exporttype'] = 'csv'; // force for now - $url = portfolio_fake_add_url($formdata['portfolio'], 'data_portfolio_caller', '/mod/data/lib.php', $formdata); + $url = portfolio_fake_add_url($formdata['portfolio'], 'data_portfolio_caller', '/mod/data/lib.php', $formdata, array()); redirect($url); } diff --git a/mod/data/export_form.php b/mod/data/export_form.php index db1af478fff..46f60c8dd33 100644 --- a/mod/data/export_form.php +++ b/mod/data/export_form.php @@ -33,6 +33,9 @@ class mod_data_export_form extends moodleform { $typesarray[] = &MoodleQuickForm::createElement('select', 'delimiter_name', null, $choices); $typesarray[] = &MoodleQuickForm::createElement('radio', 'exporttype', null, get_string('excel', 'data'), 'xls'); $typesarray[] = &MoodleQuickForm::createElement('radio', 'exporttype', null, get_string('ods', 'data'), 'ods'); + if ($CFG->enableportfolios) { + $typesarray[] = &MoodleQuickForm::createElement('radio', 'exporttype', null, get_string('format_leap2a', 'portfolio'), 'leap2a'); + } $mform->addGroup($typesarray, 'exportar', '', array(''), false); $mform->addRule('exportar', null, 'required'); $mform->setDefault('exporttype', 'csv'); @@ -56,10 +59,10 @@ class mod_data_export_form extends moodleform { } $this->add_checkbox_controller(1, null, null, 1); require_once($CFG->libdir . '/portfoliolib.php'); - if (has_capability('mod/data:exportallentries', get_context_instance(CONTEXT_MODULE, $this->_cm->id))) { + if ($CFG->enableportfolios && has_capability('mod/data:exportallentries', get_context_instance(CONTEXT_MODULE, $this->_cm->id))) { if ($portfoliooptions = portfolio_instance_select( portfolio_instances(), - call_user_func(array('data_portfolio_caller', 'supported_formats')), + call_user_func(array('data_portfolio_caller', 'base_supported_formats')), 'data_portfolio_caller', null, '', true, true)) { $mform->addElement('header', 'notice', get_string('portfolionotfile', 'data') . ':'); $portfoliooptions[0] = get_string('none'); diff --git a/mod/data/lib.php b/mod/data/lib.php index 0c2de57a475..3ce3ac4775e 100755 --- a/mod/data/lib.php +++ b/mod/data/lib.php @@ -2915,7 +2915,6 @@ class data_portfolio_caller extends portfolio_module_caller_base { $this->exporttype = 'single'; list($formats, $files) = self::formats($this->fields, $this->singlerecord); - $this->supportedformats = $formats; if (count($files) == 1 && count($this->fields) == 1) { $this->singlefile = $files[0]; $this->exporttype = 'singlefile'; @@ -3104,7 +3103,6 @@ class data_portfolio_caller extends portfolio_module_caller_base { * @param array $fields * @param object $record * @uses PORTFOLIO_FORMAT_PLAINHTML - * @uses PORTFOLIO_FORMAT_FILE * @uses PORTFOLIO_FORMAT_RICHHTML * @return array */ @@ -3119,10 +3117,14 @@ class data_portfolio_caller extends portfolio_module_caller_base { if (count($includedfiles) == 1 && count($fields) == 1) { $formats= array(portfolio_format_from_file($includedfiles[0])); } else if (count($includedfiles) > 0) { - $formats = array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICHHTML); + $formats = array(PORTFOLIO_FORMAT_RICHHTML); } return array($formats, $includedfiles); } + + public static function base_supported_formats() { + return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML); + } } function data_extend_navigation($navigation, $course, $module, $cm) { diff --git a/mod/forum/lib.php b/mod/forum/lib.php index 92468d7ef41..6c9aa8d3a0f 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -3508,7 +3508,7 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa if (empty($attachments)) { $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML); } else { - $button->set_formats(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICHHTML); + $button->set_formats(PORTFOLIO_FORMAT_RICHHTML); } $porfoliohtml = $button->to_html(PORTFOLIO_ADD_TEXT_LINK); @@ -7978,7 +7978,8 @@ class forum_portfolio_caller extends portfolio_module_caller_base { private $forum; private $discussion; private $posts; - private $keyedfiles; + private $keyedfiles; // just using multifiles isn't enough if we're exporting a full thread + /** * @return array */ @@ -8033,11 +8034,11 @@ class forum_portfolio_caller extends portfolio_module_caller_base { $modcontext = get_context_instance(CONTEXT_MODULE, $this->cm->id); if ($this->post) { - $this->set_file_and_format_data($this->attachment, $modcontext->id, 'forum_attachment', $this->post->id); + $this->set_file_and_format_data($this->attachment, $modcontext->id, 'forum_attachment', $this->post->id, 'timemodified', false); if (!empty($this->multifiles)) { $this->keyedfiles[$this->post->id] = $this->multifiles; } else if (!empty($this->singlefile)) { - $this->keyedfiles[$this->post->id] = $this->singlefile; + $this->keyedfiles[$this->post->id] = array($this->singlefile); } } else { // whole thread $fs = get_file_storage(); @@ -8050,14 +8051,17 @@ class forum_portfolio_caller extends portfolio_module_caller_base { $this->multifiles = array_merge($this->multifiles, array_values($this->keyedfiles[$post->id])); } } - if ($this->attachment) { - // do nothing - } else if (!empty($this->multifiles) || !empty($this->singlefile)) { - $this->supportedformats = array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICHHTML); + if (empty($this->multifiles) && !empty($this->singlefile)) { + $this->multifiles = array($this->singlefile); // copy_files workaround + } + // depending on whether there are files or not, we might have to change richhtml/plainhtml + if (!empty($this->multifiles)) { + $this->add_format(PORTFOLIO_FORMAT_RICHHTML); } else { - $this->supportedformats = array(PORTFOLIO_FORMAT_PLAINHTML); + $this->add_format(PORTFOLIO_FORMAT_PLAINHTML); } } + /** * @global object * @return string @@ -8082,35 +8086,103 @@ class forum_portfolio_caller extends portfolio_module_caller_base { return array($navlinks, $this->cm); } /** + * either a whole discussion + * a single post, with or without attachment + * or just an attachment with no post + * * @global object * @global object * @uses PORTFOLIO_FORMAT_RICH * @return mixed */ function prepare_package() { - global $CFG, $SESSION; - // either a whole discussion - // a single post, with or without attachment - // or just an attachment with no post - $manifest = ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH); - if (!$this->post) { // whole discussion - $content = ''; + global $CFG; + + // set up the leap2a writer if we need it + $writingleap = false; + if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { + $leapwriter = $this->exporter->get('format')->leap2a_writer(); + $writingleap = true; + } + if ($this->attachment) { // simplest case first - single file attachment + $this->copy_files(array($this->singlefile), $this->attachment); + if ($writingleap) { // if we're writing leap, make the manifest to go along with the file + $entry = new portfolio_format_leap2a_entry($id, $this->attachment->get_filename(), 'resource', $this->attachment); + $entry->add_category('offline', 'resource_type'); + $leapwriter->add_entry($entry); + return $this->exporter->write_new_file($leapwriter->to_xml(), $this->exporter->get('format')->manifest_name(), true); + } + + } else if (empty($this->post)) { // exporting whole discussion + $content = ''; // if we're just writing HTML, start a string to add each post to + $ids = array(); // if we're writing leap2a, keep track of all entryids so we can add a selection element foreach ($this->posts as $post) { - $content .= '

' . $this->prepare_post($post); + $posthtml = $this->prepare_post($post); + if ($writingleap) { + $ids[] = $this->prepare_post_leap2a($leapwriter, $post, $posthtml); + } else { + $content .= $posthtml . '

'; + } } $this->copy_files($this->multifiles); - return $this->get('exporter')->write_new_file($content, 'discussion.html', $manifest); + $name = 'discussion.html'; + $manifest = ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH); + if ($writingleap) { + // add on an extra 'selection' entry + $selection = new portfolio_format_leap2a_entry('forumdiscussion' . $this->discussionid, get_string('discussion', 'forum'), 'selection'); + $leapwriter->add_entry($selection); + $leapwriter->make_selection($selection, $ids, 'Grouping'); + $content = $leapwriter->to_xml(); + $name = $this->exporter->get('format')->manifest_name(); + } + $this->get('exporter')->write_new_file($content, $name, $manifest); + + } else { // exporting a single post + $posthtml = $this->prepare_post($this->post); + + $content = $posthtml; + $name = 'post.html'; + $manifest = ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH); + + if ($writingleap) { + $this->prepare_post_leap2a($leapwriter, $this->post, $posthtml); + $content = $leapwriter->to_xml(); + $name = $this->exporter->get('format')->manifest_name(); + } + $this->copy_files($this->multifiles); + $this->get('exporter')->write_new_file($content, $name, $manifest); } - if ($this->attachment) { - return $this->copy_files(array($this->singlefile), $this->attachment); // all we need to do - } - $this->copy_files($this->multifiles, $this->attachment); - $post = $this->prepare_post($this->post); - $this->get('exporter')->write_new_file($post, 'post.html', $manifest); } + + /** + * helper function to add a leap2a entry element + * that corresponds to a single forum post, + * including any attachments + * + * the entry/ies are added directly to the leapwriter, which is passed by ref + * + * @param portfolio_format_leap2a_writer $leapwriter writer object to add entries to + * @param object $post the stdclass object representing the database record + * @param string $posthtml the content of the post (prepared by {@link prepare_post} + * + * @return int id of new entry + */ + private function prepare_post_leap2a(portfolio_format_leap2a_writer $leapwriter, $post, $posthtml) { + $entry = new portfolio_format_leap2a_entry('forumpost' . $post->id, $post->subject, 'resource', $posthtml); + if (is_array($this->keyedfiles) && array_key_exists($post->id, $this->keyedfiles) && is_array($this->keyedfiles[$post->id])) { + foreach ($this->keyedfiles[$post->id] as $file) { + // copying the file into the package area is handled elsewhere + $entry->add_attachment($file); + } + } + $entry->add_category('web', 'resource_type'); + $leapwriter->add_entry($entry); + return $entry->id; + } + /** * @param array $files - * @param bool $justone + * @param mixed $justone false of id of single file to copy * @return bool|void */ private function copy_files($files, $justone=false) { @@ -8119,7 +8191,7 @@ class forum_portfolio_caller extends portfolio_module_caller_base { } foreach ($files as $f) { if ($justone && $f->get_id() != $justone) { - continue; // support multipe files later + continue; } $this->get('exporter')->copy_existing_file($f); if ($justone && $f->get_id() == $justone) { @@ -8134,7 +8206,7 @@ class forum_portfolio_caller extends portfolio_module_caller_base { * @param int $post * @return string */ - private function prepare_post($post) { + private function prepare_post($post, $fileoutputextras=null) { global $DB; static $users; if (empty($users)) { @@ -8175,12 +8247,12 @@ class forum_portfolio_caller extends portfolio_module_caller_base { $output .= $formattedtext; - if (is_array($this->keyedfiles) && array_key_exists($post->id, $this->keyedfiles) && is_array($this->keyedfiles[$post->id])) { + if (is_array($this->keyedfiles) && array_key_exists($post->id, $this->keyedfiles) && is_array($this->keyedfiles[$post->id]) && count($this->keyedfiles[$post->id]) > 0) { $output .= '
'; $output .= '
' . get_string('attachments', 'forum') . ':

'; $format = $this->get('exporter')->get('format'); foreach ($this->keyedfiles[$post->id] as $file) { - $output .= $format->file_output($file) . '
'; + $output .= $format->file_output($file) . '
'; } $output .= "
"; } @@ -8238,6 +8310,10 @@ class forum_portfolio_caller extends portfolio_module_caller_base { public static function display_name() { return get_string('modulename', 'forum'); } + + public static function base_supported_formats() { + return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A); + } } /** diff --git a/mod/glossary/export.php b/mod/glossary/export.php index e669f91a70e..752347f963f 100644 --- a/mod/glossary/export.php +++ b/mod/glossary/export.php @@ -73,7 +73,7 @@ echo $OUTPUT->box_start('glossarydisplay generalbox'); if ($DB->count_records('glossary_entries', array('glossaryid' => $glossary->id))) { require_once($CFG->libdir . '/portfoliolib.php'); $button = new portfolio_add_button(); - $button->set_callback_options('glossary_csv_portfolio_caller', array('id' => $cm->id), '/mod/glossary/lib.php'); + $button->set_callback_options('glossary_full_portfolio_caller', array('id' => $cm->id), '/mod/glossary/lib.php'); $button->render(); } echo $OUTPUT->box_end(); diff --git a/mod/glossary/lib.php b/mod/glossary/lib.php index 12795b436d5..e2f2465a133 100644 --- a/mod/glossary/lib.php +++ b/mod/glossary/lib.php @@ -985,6 +985,18 @@ function glossary_print_entry_icons($course, $cm, $glossary, $entry, $mode='',$h && has_capability('mod/glossary:exportownentry', $context))) { $button = new portfolio_add_button(); $button->set_callback_options('glossary_entry_portfolio_caller', array('id' => $cm->id, 'entryid' => $entry->id)); + + $filecontext = $context; + if ($entry->sourceglossaryid == $cm->instance) { + if ($maincm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) { + $filecontext = get_context_instance(CONTEXT_MODULE, $maincm->id); + } + } + $fs = get_file_storage(); + if ($files = $fs->get_area_files($filecontext->id, 'glossary_attachment', $entry->id, "timemodified", false)) { + $button->set_formats(PORTFOLIO_FORMAT_RICHHTML); + } + $return .= $button->to_html(PORTFOLIO_ADD_ICON_LINK); } $return .= "  "; // just to make up a little the output in Mozilla ;) @@ -2666,7 +2678,7 @@ function glossary_supports($feature) { * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class glossary_csv_portfolio_caller extends portfolio_module_caller_base { +class glossary_full_portfolio_caller extends portfolio_module_caller_base { private $glossary; private $exportdata; @@ -2736,8 +2748,10 @@ class glossary_csv_portfolio_caller extends portfolio_module_caller_base { $categories[$cat->entryid][] = $cat->name; } } + // TODO detect format here $csv = glossary_generate_export_csv($entries, $aliases, $categories); return $this->exporter->write_new_file($csv, clean_filename($this->cm->name) . '.csv', false); + // TODO when csv, what do we do with attachments?! } /** * @return bool @@ -2751,6 +2765,10 @@ class glossary_csv_portfolio_caller extends portfolio_module_caller_base { public static function display_name() { return get_string('modulename', 'glossary'); } + + public static function base_supported_formats() { + return array(PORTFOLIO_FORMAT_FILE); + } } /** @@ -2758,7 +2776,7 @@ class glossary_csv_portfolio_caller extends portfolio_module_caller_base { * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class glossary_entry_portfolio_caller extends portfolio_module_caller_base { +class glossary_entry_portfolio_caller extends portfolio_module_caller_base { // TODO files support private $glossary; private $entry; @@ -2790,7 +2808,14 @@ class glossary_entry_portfolio_caller extends portfolio_module_caller_base { // in case we don't have USER this will make the entry be printed $this->entry->approved = true; } - $this->supportedformats = array(PORTFOLIO_FORMAT_PLAINHTML); + $context = get_context_instance(CONTEXT_MODULE, $this->cm->id); + if ($this->entry->sourceglossaryid == $this->cm->instance) { + if ($maincm = get_coursemodule_from_instance('glossary', $this->entry->glossaryid)) { + $context = get_context_instance(CONTEXT_MODULE, $maincm->id); + } + } + $fs = get_file_storage(); + $this->multifiles = $fs->get_area_files($context->id, 'glossary_attachment', $this->entry->id, "timemodified", false); } /** * @return string @@ -2821,13 +2846,22 @@ class glossary_entry_portfolio_caller extends portfolio_module_caller_base { $entry = clone $this->entry; glossary_print_entry($this->get('course'), $this->cm, $this->glossary, $entry, null, null, false); $content = ob_get_clean(); - return $this->exporter->write_new_file($content, clean_filename($this->entry->concept) . '.html', false); + if ($this->multifiles) { + foreach ($this->multifiles as $file) { + $this->exporter->copy_existing_file($file); + } + } + return $this->exporter->write_new_file($content, clean_filename($this->entry->concept) . '.html', !empty($files)); } /** * @return string */ public function get_sha1() { - return sha1(serialize($this->entry)); + return sha1(serialize($this->entry) . $this->get_sha1_file()); + } + + public static function base_supported_formats() { + return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML); } } diff --git a/portfolio/add.php b/portfolio/add.php index 905e9e575e5..4954db67ef8 100644 --- a/portfolio/add.php +++ b/portfolio/add.php @@ -47,6 +47,7 @@ $stage = optional_param('stage', PORTFOLIO_STAGE_CONFIG, PARAM_INT); // $postcontrol = optional_param('postcontrol', 0, PARAM_INT); // when returning from some bounce to an external system, this gets passed $callbackfile = optional_param('callbackfile', null, PARAM_PATH); // callback file eg /mod/forum/lib.php - the location of the exporting content $callbackclass = optional_param('callbackclass', null, PARAM_ALPHAEXT); // callback class eg forum_portfolio_caller - the class to handle the exporting content. +$callerformats = optional_param('callerformats', null, PARAM_TAGLIST); // comma separated list of formats the specific place exporting content supports require_login(); // this is selectively called again with $course later when we know for sure which one we're in. $PAGE->set_url('/portfolio/add.php', array('id' => $dataid, 'sesskey' => sesskey())); @@ -91,14 +92,14 @@ if (!empty($dataid)) { if ($cancelsure) { $exporter->cancel_request($logreturn); } else { - $yesurl = $CFG->wwwroot . '/portfolio/add.php?id=' . $dataid . '&cancel=1&cancelsure=1&logreturn=' . $logreturn . '&sesskey=' . sesskey(); - $nourl = $CFG->wwwroot . '/portfolio/add.php?id=' . $dataid . '&sesskey=' . sesskey(); - if ($logreturn) { - $nourl = $CFG->wwwroot . '/user/portfoliologs.php'; - } $exporter->print_header('confirmcancel'); echo $OUTPUT->box_start(); - echo $OUTPUT->confirm(get_string('confirmcancel', 'portfolio'), $yesurl, $nourl); + $yesbutton = html_form::make_button($CFG->wwwroot . '/portfolio/add.php', array('id' => $dataid, 'cancel' => 1, 'cancelsure' => 1, 'logreturn' => $logreturn)); + $nobutton = html_form::make_button($CFG->wwwroot . '/portfolio/add.php', array('id' => $dataid), get_string('no')); + if ($logreturn) { + $nobutton->url = $CFG->wwwroot . '/user/portfoliologs.php'; + } + echo $OUTPUT->confirm(get_string('confirmcancel', 'portfolio'), $yesbutton, $nobutton); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); exit; @@ -151,6 +152,7 @@ if (!empty($dataid)) { // we must be passed this from the caller, we cannot start a new export // without knowing information about what part of moodle we come from. if (empty($callbackfile) || empty($callbackclass)) { + debugging('no callback file or class'); portfolio_exporter::print_expired_export(); } @@ -179,6 +181,9 @@ if (!empty($dataid)) { } $caller = new $callbackclass($callbackargs); $caller->set('user', $USER); + if ($formats = explode(',', $callerformats)) { + $caller->set_formats_from_button($formats); + } $caller->load_data(); // this must check capabilities and either throw an exception or return false. if (!$caller->check_permissions()) { @@ -200,7 +205,25 @@ if (!$exporter->get('instance')) { // in this case the exporter object and the caller object have been set up above // so just make a little form to select the portfolio plugin instance, // which is the last thing to do before starting the export. - $mform = new portfolio_instance_select('', array('id' => $exporter->get('id'), 'caller' => $exporter->get('caller'))); + // + // first check to make sure there is actually a point + $options = portfolio_instance_select( + portfolio_instances(), + $exporter->get('caller')->supported_formats(), + get_class($exporter->get('caller')), + $exporter->get('caller')->get('singlefile'), + 'instance', + true, + true + ); + if (empty($options)) { + throw new portfolio_export_exception($exporter, 'noavailableplugins', 'portfolio'); + } else if (count($options) == 1) { + // no point displaying a form, just redirect. + $instance = array_shift(array_keys($options)); + redirect($CFG->wwwroot . '/portfolio/add.php?id= ' . $exporter->get('id') . '&instance=' . $instance . '&sesskey=' . sesskey()); + } + $mform = new portfolio_instance_select('', array('id' => $exporter->get('id'), 'caller' => $exporter->get('caller'), 'options' => $options)); if ($mform->is_cancelled()) { $exporter->cancel_request(); } else if ($fromform = $mform->get_data()){ diff --git a/portfolio/mahara/lib.php b/portfolio/mahara/lib.php index 2a4b61261f4..8889b1d3080 100644 --- a/portfolio/mahara/lib.php +++ b/portfolio/mahara/lib.php @@ -32,6 +32,8 @@ class portfolio_plugin_mahara extends portfolio_plugin_pull_base { public static function supported_formats() { return array(PORTFOLIO_FORMAT_FILE); + // TODO remove above line once leap over mnet is tested + return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A); } public function expected_time($callertime) { diff --git a/user/portfoliologs.php b/user/portfoliologs.php index b059d9e6704..8eb6686c841 100644 --- a/user/portfoliologs.php +++ b/user/portfoliologs.php @@ -68,7 +68,7 @@ include('tabs.php'); echo $OUTPUT->box_start(); -$queued = $DB->get_records('portfolio_tempdata', array('userid' => $USER->id), '', 'id, expirytime'); +$queued = $DB->get_records('portfolio_tempdata', array('userid' => $USER->id), 'expirytime DESC', 'id, expirytime'); if (count($queued) > 0) { $table = new html_table(); $table->head = array(