portfolio: MDL-21030 - leap2a portfolio format support.
authorPenny Leach <penny@liip.ch>
Thu, 3 Dec 2009 14:26:37 +0000 (14:26 +0000)
committerPenny Leach <penny@liip.ch>
Thu, 3 Dec 2009 14:26:37 +0000 (14:26 +0000)
This commit includes:

- leap2a portfolio format, and xml writer
- proof of concept implementation in forum and assignment modules
- a lot of refactoring of the portfolio formats in general:
    - addition of "abstract" formats - this is necessary for plugins to be able to support groups of formats
    - addition of the idea of portfolio formats conflicting with eachother - eg richhtml & plainhtml

it touches modules other than assignment and forum, because the format api
changed and now each place in moodle that exports portfolio content has to deal
with the formats it supports slightly differently.

At the moment the Mahara portfolio still doesn't support this format, because I
haven't done the Mahara side yet. The "file download" plugin supports it
though.

Still todo:

- Add support for the other places in Moodle (glossary, data, etc)
- Write tests, once the rest of the portfolio tests have been updated to use the new DB mocking stuff
- Fix a bunch of TODOs

26 files changed:
lang/en_utf8/data.php
lang/en_utf8/portfolio.php
lang/en_utf8/portfolio_format_leap2a.php [new file with mode: 0644]
lib/portfolio/caller.php
lib/portfolio/constants.php
lib/portfolio/exceptions.php
lib/portfolio/exporter.php
lib/portfolio/formats.php
lib/portfolio/formats/leap2a/lib.php [new file with mode: 0644]
lib/portfolio/forms.php
lib/portfolio/plugin.php
lib/portfoliolib.php
lib/simpletest/portfolio_testclass.php
mod/assignment/lib.php
mod/assignment/type/online/assignment.class.php
mod/assignment/type/upload/assignment.class.php
mod/chat/lib.php
mod/data/export.php
mod/data/export_form.php
mod/data/lib.php
mod/forum/lib.php
mod/glossary/export.php
mod/glossary/lib.php
portfolio/add.php
portfolio/mahara/lib.php
user/portfoliologs.php

index e59622f..55aa788 100644 (file)
@@ -216,7 +216,7 @@ $string['pagesize'] = 'Entries per page';
 $string['participants'] = 'Participants';
 $string['picture'] = 'Picture';
 $string['pleaseaddsome'] = 'Please create some below or <a href=\"$a\">choose a predefined set</a> 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';
index 0e93ccf..ed8e471 100644 (file)
@@ -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 (file)
index 0000000..be4b610
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+
+$string['entryalreadyexists'] = 'You tried to add a LEAP2A entry with an id ($a) that already exists in this feed';
+$string['feedtitle'] = 'LEAP2A export from Moodle for $a';
+$string['invalidentryfield'] = 'You tried to set an entry field that didn\'t exist ($a) or you can\'t set directly';
+$string['invalidentryid'] = 'You tried to access an entry by an id that didn\'t exist ($a)';
+$string['missingfield'] = 'Required LEAP2A entry field $a missing';
+$string['nonexistantlink'] = 'A LEAP2A entry ($a->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';
index 0c852ec..ae600aa 100644 (file)
@@ -314,18 +314,22 @@ abstract class portfolio_caller_base {
     * as this function <b>must</b> 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,
index f21dfeb..1492cea 100644 (file)
@@ -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
index dee46e5..28ab057 100644 (file)
@@ -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 {}
index 8935dfe..4934324 100644 (file)
@@ -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.
index e97b823..fdf86b8 100644 (file)
@@ -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 '<img src="' . $path . '" alt="' . $file->get_filename() . '" />';
         }
         return '<a href="' . $path . '">' . $file->get_filename() . '</a>';
     }
+    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 '<a rel="enclosure" href="' . $id . '">' . $file->get_filename() . '</a>';
+    }
+
+    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 (file)
index 0000000..2912ccd
--- /dev/null
@@ -0,0 +1,477 @@
+<?php
+/**
+ * Moodle - Modular Object-Oriented Dynamic Learning Environment
+ *          http://moodle.org
+ * Copyright (C) 1999 onwards Martin Dougiamas  http://dougiamas.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    moodle
+ * @subpackage portfolio
+ * @author     Penny Leach <penny@liip.ch>
+ * @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 <b>must</b> 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('<div>' . $content . '</div>')) {
+            $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 <html><body> </body></html> 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;
+    }
+}
index 5092812..b556674 100644 (file)
@@ -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']);
index f9bb5f6..ca2b689 100644 (file)
@@ -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);
     }
 
     /**
index 6e5aa68..352bc72 100644 (file)
@@ -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" . '<input type="hidden" name="callbackfile" value="' . $this->callbackfile . '" />';
         $formoutput .= "\n" . '<input type="hidden" name="callbackclass" value="' . $this->callbackclass . '" />';
         $formoutput .= "\n" . '<input type="hidden" name="course" value="' . (!empty($COURSE) ? $COURSE->id : 0) . '" />';
+        $formoutput .= "\n" . '<input type="hidden" name="callerformats" value="' . implode(',', $this->formats) . '" />';
         $linkoutput .= 'callbackfile=' . $this->callbackfile . '&amp;callbackclass='
-            . $this->callbackclass . '&amp;course=' . (!empty($COURSE) ? $COURSE->id : 0);
+            . $this->callbackclass . '&amp;course=' . (!empty($COURSE) ? $COURSE->id : 0)
+            . '&amp;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 . '&amp;callbackclass=' . $classname . '&amp;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 .= '&amp;ca_' . $key . '=' . urlencode($value);
+        $url .= '&ca_' . $key . '=' . urlencode($value);
     }
+    $url .= '&callerformats=' . implode(',', $formats);
     return $url;
 }
 
index 7cb77ad..d9185b0 100644 (file)
@@ -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 {
index a7e3c28..d7547b4 100644 (file)
@@ -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 .= '<br />'  . $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);
+    }
 }
 
 /**
index f358694..bd2b68c 100644 (file)
@@ -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 '<div style="text-align:center">'. get_string('guestnosubmit', 'assignment').'</div>';
@@ -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) {
index c6d5985..3d00d6f 100644 (file)
@@ -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();
             }
         }
index cd93a43..5f0d208 100644 (file)
@@ -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);
     }
     /**
index 09cd983..244b09f 100644 (file)
@@ -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);
 }
 
index db1af47..46f60c8 100644 (file)
@@ -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');
index 0c2de57..3ce3ac4 100755 (executable)
@@ -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) {
index 92468d7..6c9aa8d 100644 (file)
@@ -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 .= '<br /><br />' . $this->prepare_post($post);
+                $posthtml =  $this->prepare_post($post);
+                if ($writingleap) {
+                    $ids[] = $this->prepare_post_leap2a($leapwriter, $post, $posthtml);
+                } else {
+                    $content .= $posthtml . '<br /><br />';
+                }
+            }
+            $this->copy_files($this->multifiles);
+            $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);
-            return $this->get('exporter')->write_new_file($content, 'discussion.html', $manifest);
+            $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
+    }
+
+    /**
+     * 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);
+            }
         }
-        $this->copy_files($this->multifiles, $this->attachment);
-        $post = $this->prepare_post($this->post);
-        $this->get('exporter')->write_new_file($post, 'post.html', $manifest);
+        $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 .= '<div class="attachments">';
             $output .= '<br /><b>' .  get_string('attachments', 'forum') . '</b>:<br /><br />';
             $format = $this->get('exporter')->get('format');
             foreach ($this->keyedfiles[$post->id] as $file) {
-                $output .= $format->file_output($file) . '<br/ >';
+                $output .= $format->file_output($file)  . '<br/ >';
             }
             $output .= "</div>";
         }
@@ -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);
+    }
 }
 
 /**
index e669f91..752347f 100644 (file)
@@ -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();
index 12795b4..e2f2465 100644 (file)
@@ -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 .= "&nbsp;&nbsp;"; // 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);
     }
 }
 
index 905e9e5..4954db6 100644 (file)
@@ -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()){
index 2a4b612..8889b1d 100644 (file)
@@ -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) {
index b059d9e..8eb6686 100644 (file)
@@ -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(