Adding a Portfolio Button to a page
Introduction
Adding an 'Add to Portfolio' button to any page is relatively trivial, there are just two things that you need to do. Add the button to a page, and write a class to handle it.
A very basic example
This example demonstrates the exporting of a single file from somewhere in moodle (in this case a single forum attachment). It is for the purpose of showing how to handle the filetype/format setting, but doesn't exhaustively implement all abstract methods that are described in this document.
Add the button to a page:
$button = new portfolio_add_button();
$button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()));
$button->set_format_by_file($file);
echo $button->to_html(PORTFOLIO_ADD_ICON_LINK);
Write the handler class:
class forum_portfolio_caller extends portfolio_module_caller_base {
protected $postid;
protected $attachment;
public static function expected_callbackargs() {
return array(
'postid' => true,
'attachment' => true,
);
}
public function load_data() {
if (!$this->post = $DB->get_record('forum_posts', array('id' => $this->postid))) {
throw new portfolio_caller_exception('invalidpostid', 'forum');
}
$this->set_file_and_format_data($this->attachment); // sets $this->singlefile for us
}
function prepare_package() {
return $this->get('exporter')->copy_existing_file($this->singlefile);
}
}
// implement other abstract methods
Write a subclass
You can either subclass portfolio_caller_base for the general case, or portfolio_module_caller_base if you're somewhere inside mod/
This sounds scary, but really it's not! It's a very small class. portfolio_caller_base has abstract functions that you must override, and some functions that you can override if you want to do something special. You should call it something like $module_portfolio_caller, or in a more complicated case (say assignment/type/upload, assignment_upload_portfolio_caller)
If you're adding the portfolio button somewhere in a module, it's better to subclass portfolio_module_caller_base, which implements 2 of the below abstract methods for you.
Also, you have the following member variables available to you in the parent: id (int - cmid), course (stdclass object), exportconfig (named array - access via get_export_config), user (stdclass object), and exporter (a reference to the portfolio_exporter object). With the exception of exportconfig, you should access through the get method.
You should also use singlefile (stored_file) and multifiles (array) which are set in the parent class.
Please read the supported_formats section of this page for how to handle the different export formats.
Methods you must override
load_data
This function is where any data gets loaded out of the database into the object. The reason this is split out from the constructor, is that the caller objects get constructed and thrown away often and you don't want to do anything heavy weight in the constructor. This function should throw exceptions if data is wrong.
expected_callbackargs (static)
This function must return an array with the keys being the names of the variables you want, and the value being a boolean whether it is required or not (if it is missing and required, the parent constructor will throw an exception).
You must have at least protected visibility member variables to match each of the callback argument keys.
During the export screens, it's desirable to still have some sensible navigation that logically follows from the place the user was before they started the export process. This function should return components to pass to build_nagivation (extralinks and cm). portfolio_module_caller_base implements this for you
prepare_package
prepares the package up before control is passed to the portfolio plugin. You should copy any files (or write out any files) into the temporary directory provided, where they'll be found by the portfolio plugin.
Access to the temporary directory is via the exporter object, using the methods copy_existing_file, and write_new_file. You should never use the files api directly, as this will cause problems for the unit tests.
See also Adding_a_Portfolio_Button_to_a_page#setting portfolio internal
expected_time
You should return a constant here to indicate how long the transfer is expected to take. This should be based on the size of the file. There are three options, PORTFOLIO_TIME_LOW, PORTFOLIO_TIME_MODERATE, and PORTFOLIO_TIME_HIGH. The first means the user will not be asked if they want to wait for the transfer or not, they will just wait. The second and third mean they'll be given the option (and in the case of the third, advised not to). The portfolio plugin can override this if it wants (eg in the case of download, they always want to wait for the transfer)
There are helper functions in the portfolio lib to figure this out for you, either for largely file based transfers or largely database content:
public function expected_time() {
// a file based export
return portfolio_expected_time_file($this->exportfiles);
// or for database exports
return portfolio_expected_time_db(count($this->recordstoexport));
}
As you can see, portfolio_expected_time_file takes an array of stored_file objects, adds up the size of them all and makes a decision about what to return based on the admin settings for the time thresholds. The portfolio_expected_time_db function takes an integer which loosely represents the number of database records you will be exporting, and does something similar.
check_permissions
portfolio/add.php will expect the caller to verify the user is allowed to export the given content. This function should perform any has_capability checks it needs to and return a booelan.
get_return_url
This is used for redirecting the user in the case of a cancelled export, or at the end of their export, they are offered the option of continuing back to where they were (what this function returns) or on to their portfolio. portfolio_module_caller_base implements this for you (but will use mod/modname/view.php)
display_name (static)
A nice language string for displaying the location of this export to the user (this is used to notify the user in case of duplicate exports that originated from different places in moodle (Eg exporting an assignment upload and a forum post attachment that are the same file)
get_sha1
Return a sha1 of the content being exported - used to detect duplicate exports later.
Methods you can override
__construct
The base class generally takes care of assigning the values in callbackargs to member variables in your class (which have to be protected), based on expected_callbackargs. If you need to set anything else here, you can override it, but generally you wouldn't want to do that, and extra settings happen in load_data method. You certainly do not want to do anything that involves loading data in the constructor, it's really only for verifying the callback arguments.
supported_formats (static) (read this even if you don't want to override this function, it's important)
The formats this caller can support. At export time, both the plugin and the caller are polled for which formats they can support, and then the intersection is used to determine the export format. In the case that the intersection is greater than 1, the user is asked for their selection.
The available formats you can choose from are in portfolio_supported_formats and are constants PORTFOLIO_FORMAT_XXX. By default, the subclass defines PORTFOLIO_FORMAT_FILE.
This function is static, but if the caller object has been instantiated it is passed as an argument.
You can also set $this->supportedformats in the constructor if you want to change it at constructor time and the implementation in the base class will detect that case. Currently, there is an example of this in the forum code - attachments that are images are handled in this way.
Additionally, if your caller is going to export just one file, you should do something like this in your constructor:
$this->set_file_and_format_data($this->filetoexport);
And the following on the button class:
$button->set_format_by_file($filetoexport);
where $filetoexport is a stored_file object. this function will detect the mimetype of the file and return the appropriate subformat of PORTFOLIO_FORMAT_FILE.
As your caller adds support for more formats (eg Moodlebackup or IMS or LEAP or whatever standards get implemented later), you could do
$this->set_file_and_format_data($this->filetoexport);
$this->supportedformats = array_merge($this->supportedformats, array(PORTFOLIO_FORMAT_MBKP);
and for places that pass custom formats to portfolio_add_button, please consider also using this function.
Again, check both the forum_portfolio_caller constructor and the calls in forum_print_attachments of examples of all this.
has_export_config
If there's any addition config during the export process (for example, extra metadata), you can override this function to return true. If you do this, you must also override export_config_form and get_export_summary.
export_config_form
This function is called, and passed a moodle form object by reference to add elements to it.
export_config_validation
This follows the exact same format as the validation() function in the moodleform object.
get_allowed_export_config
If at any point, your caller is going to use set_export_config, you must implement this function to return an array of allowed config fields. (Note that you can set export time config even if you're not using interactive user config)
get_export_summary
If your plugin has overridden has_export_config, you must implement this to display nicely to the user on the confirmation screen. It should return a named array (keys are nice strings to describe the config, values are the config options)
Add the button to the page
Now that you've implemented this class, you just need to add the button. To do this, you require_once("$CFG->libdir/portfoliolib.php"); and build up a button object. There are a few ways to do this, but the most common is this:
$button = new portfolio_add_button();
$button->set_callback_options($classname, $callbackargs, $classfile);
$button->set_formats($formats);
$button->render($displayformat, $addstr);
or
$output .= $button->to_html($displayformat, $addstr
Callback options
$classnmae
That's the name of your class you just made,
$callbackargs
An associative array of key=>value pairs you want passed to the constructor of your class. These must be primitives, as they are added as hidden form fields and cleaned to either PARAM_ALPHAEXT, PARAM_NUMERIC or PARAM_PATH. Weird stuff will happen if they're not compliant.
$classfile
This can be autodetected from the backtrace of where this function was called, but if your class definition isn't in the same file as the caller (eg if your caller is some .php script, but the class is in a lib.php file), you can pass it explicitly here.
Display Options
$displayformat
Whether you want a button, icon or link, and whether you want the dropdown menu of available plugins, and whether you want form tags. Options are PORTFOLIO_ADD_FULL_FORM (full form, with dropdown menu and submit button), PORTFOLIO_ADD_ICON_FORM (form with dropdown but icon rather than button), PORTFOLIO_ADD_ICON_LINK (just the icon, no form and no drop menu) and PORTFOLIO_ADD_TEXT_LINK (just a text link)
The last two (no dropdown) will introduce a whole new screen in the wizard first for the user to select the active portfolio plugin.
This argument is optional and defaults to PORTFOLIO_ADD_FULL_FORM.
$addstr
The string to use on the button, link and alt text of the icon. Optional and defaults to 'Add to Portfolio'
Formats
The formats this caller supports. See https://docs.moodle.org/en/Development:Adding_a_Portfolio_Button_to_a_page#supported_formats_.28static.29_.28read_this_even_if_you_don.27t_want_to_override_this_function.2C_it.27s_important.29 for more information.
Other ways to integrate
If it's undesirable to add a form, there are a couple of things you can do. For an example, see the export tab of the 'data' module, which has already an export form, but just adds an option to export to portfolio rather than file:
require_once($CFG->libdir . '/portfoliolib.php');
if (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')),
'data_portfolio_caller', '', true, true)) {
$mform->addElement('header', 'notice', get_string('portfolionotfile', 'data') . ':');
$portfoliooptions[0] = get_string('none');
ksort($portfoliooptions);
$mform->addElement('select', 'portfolio',
get_string('portfolio', 'portfolio'), $portfoliooptions);
}
}
This code adds a select option to the existing export form containing the available portfolio instances.
Then in the form handler:
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);
redirect($url);
}
The portfolio_fake_add_url function returns the url that you need to redirect to, that would normally be the result of portfolio_add_button form being submitted.
A few extra notes on the caller base class
protected $course
There is a protected member variable, $course, that subclasses can set (with $this->set('course', $course); ).
portfolio_add_button tries to look for a course object at the point that it's called, by doing a global $COURSE which is hackish but mostly works. This is also used to build the navigation during the export process. If for some reason, your navigation doesn't include the current course, you can set it like this. You shouldn't need to in the majority of cases.
serialization
The caller object is stored in the database in serialized form. The portfolio code works around this by loading the class definitions and then serializing and unserializing the objects again. However, if you're storing any real objects in your caller class, you will need to do this as well. See the assignment implementation for how this done (using php5's __wakeup function):
public function __wakeup() {
require_once($this->assignmentfile);
$this->assignment = unserialize(serialize($this->assignment));
}
setting portfolio internal
Sometimes during prepare_package, you need to call functions in the libraries to render content as HTML that would normally also call portfolio_add_button. This will result in 'you already have an export active in this session' error - to get around this, do something like
define('PORTFOLIO_INTERNAL', true);
modulename_print_some_htmlcontent();
Error handling
Your code should always throw exceptions of type portfolio_caller_exception. You should NOT call error or print_error or whatever, and also any third party exceptions should really be caught and rethrown although most of the time other exceptions will be caught too by the portfolio code (although this will trigger a warning in debug mode)