Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instance importer/exporter #1561

Open
wants to merge 23 commits into
base: dev/10.3.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
97429a9
Adds import/export to save history and widget page, adds test cases
cayb0rg Jan 30, 2024
a6bfe7b
Fix spacing
cayb0rg Jan 30, 2024
b6bf2d0
Remove logs
cayb0rg Jan 30, 2024
25f07e0
Fix media export name
cayb0rg Jan 30, 2024
c1bee43
Check array length too
cayb0rg Jan 31, 2024
48ffd8b
remove debugging and call to unlink a non-existent file
cayb0rg Jan 31, 2024
09dcb8d
conclude merge
cayb0rg Jan 31, 2024
b1c7fcb
Adds back success toast
cayb0rg Feb 1, 2024
7087d21
Move import/export to their own hooks and add toast hook
cayb0rg Feb 1, 2024
3980454
adds attempt to use api endpoint for historical purposes
cayb0rg Feb 6, 2024
a93094f
update tests
cayb0rg Feb 6, 2024
55f75e0
Merge remote-tracking branch 'upstream/dev/10.1.0' into feature/insta…
cayb0rg Feb 6, 2024
8b1fa61
remove old code
cayb0rg Feb 6, 2024
eb79401
add tinstance import/export option
cayb0rg Feb 12, 2024
51d4ee7
Merge branch 'master' of github.com:ucfopen/Materia into feature/inst…
cayb0rg Feb 12, 2024
b2f2d81
Update UI styles and switch import type in admin panels
cayb0rg Feb 12, 2024
6cdcdf7
remove logs
cayb0rg Feb 12, 2024
0bc662a
clean toast styling
cayb0rg Feb 12, 2024
3eb55ce
itty bitty styling fix to meatball menu
cayb0rg Feb 12, 2024
1c5044b
Merge branch 'dev/9.2.0' of github.com:ucfopen/Materia into feature/i…
cayb0rg Apr 2, 2024
d283a2b
Create export modal, replace menu in my widgets and export instance i…
cayb0rg Apr 3, 2024
01db07a
Recursively search for assets to map to qset
cayb0rg Apr 4, 2024
88e2c48
Fix test
cayb0rg Apr 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"destroy-everything": "php oil r admin:destroy_everything --quiet",
"oil-install-quiet": "php oil r install --skip_prompts=true",
"widgets-install-test": "php oil r widget:install fuel/app/tests/widget_packages/*.wigt",
"test": "php fuel/vendor/bin/phpunit -c fuel/app/phpunit.xml",
"test": "php fuel/vendor/bin/phpunit --verbose -c fuel/app/phpunit.xml",
"test-with-xdebug": "php -dzend_extension=xdebug.so fuel/vendor/bin/phpunit -c fuel/app/phpunit.xml",
"coverage": "php oil test --coverage-html=coverage --coverage-clover=coverage.xml --coverage-text=coverage.txt",
"heroku-extract-widgets": "php oil r widget:extract_from_config",
Expand Down
9 changes: 5 additions & 4 deletions fuel/app/classes/basetest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,20 @@ protected static function clear_fuel_input()
$property->setValue($class, null);
}

protected function create_new_qset($question_text, $answer_text, $version=0)
protected function create_new_qset($question_text, $answer_text, $version=0, $assets=[])
{
$qset = (object) ['version' => '1', 'data' => null];
$json_assets = json_encode($assets);

switch ($version)
{
case 0:
default:
$qset->data = json_decode('{"items":[{"items":[{"name":null,"type":"QA","assets":null,"answers":[{"text":"'.$answer_text.'","options":{},"value":"100"}],"questions":[{"text":"'.$question_text.'","options":{},"value":""}],"options":{},"id":0}],"name":"","options":{},"assets":[],"rand":false}],"name":"","options":{"partial":false,"attempts":5},"assets":[],"rand":false}');
$qset->data = json_decode('{"items":[{"items":[{"name":null,"type":"QA","assets":'.$json_assets.',"answers":[{"text":"'.$answer_text.'","options":{},"value":"100"}],"questions":[{"text":"'.$question_text.'","options":{},"value":""}],"options":{},"id":0}],"name":"","options":{},"assets":'.$json_assets.',"rand":false}],"name":"","options":{"partial":false,"attempts":5},"assets":'.$json_assets.',"rand":false}', true);
break;

case 1:
$qset->data = json_decode('{"items":[{"items":[{"name":null,"type":"QA","assets":null,"answers":[{"text":"'.$answer_text.'","options":{},"value":"100"}],"questions":[{"text":"'.$question_text.'","options":{},"value":""}],"options":{},"id":0}],"name":"","options":{},"assets":[],"rand":false}],"name":"","options":{"partial":false,"attempts":5},"assets":[],"rand":false}');
$qset->data = json_decode('{"items":[{"items":[{"name":null,"type":"QA","assets":'.$json_assets.',"answers":[{"text":"'.$answer_text.'","options":{},"value":"100"}],"questions":[{"text":"'.$question_text.'","options":{},"value":""}],"options":{},"id":0}],"name":"","options":{},"assets":'.$json_assets.',"rand":false}],"name":"","options":{"partial":false,"attempts":5},"assets":'.$json_assets.',"rand":false}', true);
break;
}

Expand Down Expand Up @@ -369,7 +370,7 @@ protected function assert_is_qset($qset)
$this->assertIsObject($qset);
$this->assertObjectHasAttribute('data', $qset);
$this->assertObjectHasAttribute('version', $qset);
$this->assertArrayHasKey('id', $qset->data);
$this->assertArrayHasKey('items', $qset->data);
$questions = \Materia\Widget_Instance::find_questions($qset->data);
foreach ($questions as $question)
{
Expand Down
115 changes: 115 additions & 0 deletions fuel/app/classes/controller/widgets.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,121 @@ public function get_preview_widget($inst_id, $is_embedded = false)
}
}

/**
* Pass a zip file with the qset(s) and all the assets associated with it to the client
* @param string $inst_id The instance id of the widget to export
* @param string $asset (optional) The asset type to export. Can be either 'all', 'qset', or 'media'
* @param string $timestamp (optional) The timestamp of the qset to export
*/
public function get_export(string $inst_id, string $asset = 'all', string $timestamp = '')
{
// check if instance exists
if ( ! $inst = Materia\Widget_Instance_Manager::get($inst_id)) throw new HttpNotFoundException;
// check if user has permission to instance
if ( ! Materia\Perm_Manager::user_has_any_perm_to(\Model_User::find_current_id(), $inst_id, Materia\Perm::INSTANCE, [Materia\Perm::FULL, Materia\Perm::VISIBLE]))
{
return new Response('You do not have permission to access the requested content.', 403);
}

$filename = preg_replace('/[^a-zA-Z0-9]/', '', $inst->name);

$qset = Materia\Api_V1::question_set_get($inst_id, null, $timestamp);
if ($qset instanceof \Materia\Msg) return $qset;

if ($asset == 'qset')
{
// return just the JSON file
header('Content-Type: application/json');
header("Content-Disposition: attachment; filename={$filename}.json");
echo json_encode($qset);
exit;
}

// For all assets, we need to zip them up
$zip = new \ZipArchive();
$zip->open('assets_export', \ZipArchive::CREATE);

if ($asset == 'instance_and_media')
{
$inst->qset = $qset;
$zip->addFromString('instance.json', json_encode($inst));
}
if ($asset == 'qset_and_media')
{
$zip->addFromString('qset.json', json_encode($qset));
}

$asset_ids = Materia\Api_V1::assets_get_for_instance($inst_id, false, $qset->id);
$size = 'original';

if (count($asset_ids) == 0 && $asset == 'media')
{
// $zip->addFromString('no_assets.txt', 'No assets found for this widget.');
$zip->setArchiveComment('zipped on '.date('Y-M-d'));
$zip->close();

return new Response('No assets found for this widget.', 404);
}

foreach ($asset_ids as $id)
{
$asset = Materia\Widget_Asset::fetch_by_id($id);

if ( ! ($asset instanceof Materia\Widget_Asset))
{
$zip->close();
throw new HttpNotFoundException;
}

try
{
// requested size doesnt exist?
$driver = \Config::get('materia.asset_storage_driver', 'db');
$_storage_driver = Materia\Widget_Asset::get_storage_driver($driver);
if ( ! $_storage_driver->exists($asset->id, $size))
{
// if size is original, just 404
if ($size === 'original') throw new \Exception("Missing asset data for asset: {$asset->id} {$size}");

// rebuild the size (hopefully - we may not )
$asset_path = $asset->build_size($size);
}
else
{
$asset_path = $asset->copy_asset_to_temp_file($asset->id, $size);
}
} catch (\Throwable $e)
{
$zip->close();
throw new \HttpNotFoundException;
}

// register a shutdown function that will render the image
// allowing all of fuel's other shutdown methods to do their jobs
\Event::register('fuel-shutdown', function() use($asset_path) {

if ( ! file_exists($asset_path)) throw new \HttpNotFoundException;
// turn off and clean output buffer
while (ob_get_level() > 0) ob_end_clean();
});

// Add asset to zip file
$zip->addFromString($asset->title, file_get_contents($asset_path));
}

$zip->setArchiveComment('zipped on '.date('Y-M-d'));
$zip->close();

// send zip file back to user
header('Content-Type: application/zip');
header("Content-Disposition: attachment; filename={$filename}.zip");

fpassthru(fopen('assets_export', 'rb'));
unlink('assets_export');

exit;
}

/* ============================== PROTECTED ================================== */


Expand Down
118 changes: 118 additions & 0 deletions fuel/app/classes/materia/api/v1.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,97 @@ static public function widget_instance_update($inst_id=null, $name=null, $qset=n
}
}

public static function widget_instance_import($widget_id=null, $name=null, $qset=null, $is_draft=null, $open_at=null, $close_at=null, $attempts=null)
{
return static::widget_instance_new($widget_id, $name, $qset, $is_draft);
}

/**
* Validates media URLs in qset
* @param object $data
* Returns false if any URL is invalid, true otherwise
*/
static function validate_asset_urls($data)
{
foreach ($data as $key => $value)
{
if ($key === 'url' && is_string($value))
{
// Check if the key is 'url' and the value is a string
// media id must be 5 characters long and end with the ID
$url_regex = '/^https:\/\/\S+\/media\/[A-Za-z0-9]{5}$/';
if ( ! preg_match($url_regex, $value))
{
return false;
}
}
elseif (is_array($value) || is_object($value))
{
// If the value is an array, recursively validate its elements
if ( ! self::validate_asset_urls($value))
{
return false;
}
}
}
return true;
}

/**
* Replace the qset for an instance
* @param int $inst_id
* @param object $qset
* @return array Updated instance
*/
static public function widget_instance_update_qset($inst_id, $qset)
{
if (\Service_User::verify_session() !== true) return Msg::no_login();
if ( ! Util_Validator::is_valid_hash($inst_id)) return new Msg(Msg::ERROR, 'Instance id is invalid');
if ( ! static::has_perms_to_inst($inst_id, [Perm::VISIBLE, Perm::FULL])) return Msg::no_perm();

$inst = Widget_Instance_Manager::get($inst_id);
if ( ! $inst) return new Msg(Msg::ERROR, 'Widget instance could not be found.');

// Validate every single field in the qset
// if any field is invalid, return an error message
// if all fields are valid, update the qset and return the updated instance
if (empty($qset->data) || empty($qset->version))
{
return new Msg(Msg::ERROR, 'Invalid qset');
}
else
{
$questions = \Materia\Widget_Instance::find_questions($qset->data);
foreach ($questions as $q)
{
if ( ! $q instanceof \Materia\Widget_Question)
{
return new Msg(Msg::ERROR, 'Invalid qset');
}
// Validate whether $q->type (widget name) is same as $inst->widget->id
// (TODO: Might be impossible without changing qset)
}

if ( ! empty($qset->data->items) && ! self::validate_asset_urls($qset->data->items))
{
return new Msg(Msg::ERROR, 'Invalid qset');
}

$inst->qset = $qset;
}

try
{
$inst->db_store();
return $inst;
}
catch (\Exception $e)
{
return new Msg(Msg::ERROR, 'Widget could not be updated with new question set.');
}

}

/**
* Lock a widget to prevent others from editing it
* @return true if we have or are able to get a lock on this game
Expand Down Expand Up @@ -588,6 +679,33 @@ static public function assets_get()
return Widget_Asset_Manager::get_assets_by_user(\Model_User::find_current_id(), Perm::FULL);
}

/**
* Returns asset IDs associated with a given instance or qset
* @param string $inst_id The widget instance ID
* @return array An array of asset IDs
*/
static public function assets_get_for_instance($inst_id, $get_all_qsets=false,$qset_id=null)
{
if (\Service_User::verify_session() !== true) return Msg::no_login();
if ($get_all_qsets === true)
{
$asset_ids = Widget_Asset_Manager::get_assets_ids_by_game($inst_id);
}
elseif ($qset_id !== null)
{
$asset_ids = Widget_Asset_Manager::get_assets_ids_by_qset($qset_id);
}
else
{
// get the latest qset id for this instance
$inst = Widget_Instance_Manager::get($inst_id, true);
$qset_id = $inst->qset->id;
$asset_ids = Widget_Asset_Manager::get_assets_ids_by_qset($qset_id);
}

return $asset_ids;
}

/**
* Returns all scores for the given widget instance recorded by the current user, and attmepts remaining in the current context.
* If no launch token is supplied, the current semester will be used as the current context.
Expand Down
3 changes: 1 addition & 2 deletions fuel/app/classes/materia/perm/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ static public function is_super_user()
// The session caching has been removed due to issues related to the cache when the role is added or revoked
// Ideally we can still find a way to cache this and make it more performant!!
return (\Fuel::$is_cli === true && ! \Fuel::$is_test) || self::does_user_have_role([\Materia\Perm_Role::SU]);

}

/**
Expand Down Expand Up @@ -351,10 +350,10 @@ static public function remove_users_from_roles_system_only(Array $user_ids = [],
->execute();
}
}

return $success;
}


/*
********************** User to Object Rights ***************************************
*/
Expand Down
1 change: 0 additions & 1 deletion fuel/app/classes/materia/widget/asset.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ public function db_update()
'file_size' => $this->file_size,
'created_at' => time(),
'is_deleted' => $this->is_deleted

])
->where('id','=',$this->id)
->execute();
Expand Down
24 changes: 23 additions & 1 deletion fuel/app/classes/materia/widget/asset/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,28 @@ static public function get_assets_ids_by_game($inst_id)
// return the array (making sure there are no duplicate values)
return array_unique($objects);
}

static public function get_assets_ids_by_qset($qset_id)
{
// select all assets that belong to a certain qset
$results = \DB::select('a.id')
->from(['asset', 'a'])
->join(['map_asset_to_object', 'm'])
->on('a.id', '=', 'm.asset_id')
->on('m.object_type', '=', \DB::expr(\Materia\Widget_Asset::MAP_TYPE_QSET))
->where('m.object_id', '=', $qset_id)
->execute();

// add these objects to an array
$objects = [];
foreach ($results as $r)
{
$objects[] = $r['id'];
}
// return the array (making sure there are no duplicate values)
return array_unique($objects);
}

/**
* NEEDS DOCUMENTATION
* @param unknown NEEDS DOCUMENTATION
Expand Down Expand Up @@ -284,7 +306,7 @@ static protected function register_asset_to_item($item_type, $item_id, $id)
static public function register_assets_to_item($item_type, $item_id, $assets_list)
{
// asset List is an array
if (count($assets_list) > 0)
if (is_array($assets_list) && count($assets_list) > 0)
{
foreach ($assets_list as $asset)
{
Expand Down
Loading
Loading