Skip to content

Commit eb79401

Browse files
committed
add tinstance import/export option
1 parent 8b1fa61 commit eb79401

20 files changed

+297
-91
lines changed

fuel/app/classes/materia/api/v1.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,11 @@ static public function widget_instance_update($inst_id=null, $name=null, $qset=n
386386
}
387387
}
388388

389+
public static function widget_instance_import($widget_id=null, $name=null, $qset=null, $is_draft=null, $open_at=null, $close_at=null, $attempts=null)
390+
{
391+
return static::widget_instance_new($widget_id, $name, $qset, $is_draft);
392+
}
393+
389394
/**
390395
* Validates media URLs in qset
391396
* @param object $data

fuel/app/classes/materia/widget/instance/manager.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ public static function get_paginated_instance_search(string $input, $page_number
182182
*/
183183
public static function get_widget_instance_search(string $input, int $offset = 0, int $limit = 80): array
184184
{
185+
\Log::Error($input);
185186
$results = \DB::select()
186187
->from('widget_instance')
187188
->where('id', 'LIKE', "%$input%")

fuel/app/tests/api/v1.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,29 @@ public function test_validate_asset_urls()
294294
self::assertTrue(true);
295295
}
296296

297+
public function test_widget_instance_import()
298+
{
299+
// AS STUDENT
300+
$this->_as_student();
301+
302+
$widget = $this->make_disposable_widget();
303+
304+
// NEW DRAFT
305+
$title = "My Test Widget";
306+
$question = 'This is another word for test';
307+
$answer = 'Assert';
308+
$qset = $this->create_new_qset($question, $answer);
309+
310+
$inst = Api_V1::widget_instance_import($widget->id, $title, $qset, true);
311+
$this->assert_is_widget_instance($inst);
312+
313+
// ==== AS NO-AUTHOR =====
314+
$this->_as_noauth();
315+
316+
$output = Api_V1::widget_instance_import($widget->id, $title, $qset, true);
317+
$this->assert_validation_error_message($output);
318+
}
319+
297320
public function test_widget_instance_update_qset()
298321
{
299322
$this->_as_student();

public/img/export.svg

Lines changed: 16 additions & 0 deletions
Loading

public/img/import.svg

Lines changed: 16 additions & 0 deletions
Loading

src/components/hooks/useExportInstance.jsx

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { useMutation } from 'react-query'
2+
import { apiGetQuestionSet, apiGetWidgetInstance } from '../../util/api'
3+
4+
export default function useExportType() {
5+
const exportQset = useMutation(
6+
apiGetQuestionSet,
7+
{
8+
onSuccess: (data) => {
9+
const a = document.createElement('a')
10+
a.href = URL.createObjectURL(new Blob([JSON.stringify(data)], {type: 'application/json'}))
11+
a.download = 'qset.json'
12+
a.click()
13+
},
14+
onError: (error, variables) => {
15+
variables.errorFunc(error)
16+
}
17+
}
18+
)
19+
20+
const exportInstance = useMutation(
21+
apiGetWidgetInstance,
22+
{
23+
onSuccess: (data) => {
24+
const a = document.createElement('a')
25+
a.href = URL.createObjectURL(new Blob([JSON.stringify(data)], {type: 'application/json'}))
26+
a.download = 'instance.json'
27+
a.click()
28+
},
29+
onError: (error, variables) => {
30+
variables.errorFunc(error)
31+
}
32+
}
33+
)
34+
35+
const exportType = async (asset_type, inst_id, onError, timestamp = null) => {
36+
if (!timestamp) timestamp = Date.now();
37+
38+
switch (asset_type) {
39+
case 'qset':
40+
// use the available API call to fetch the qset so we have better error handling
41+
exportQset.mutate({instId: inst_id, playId: null, timestamp}, {
42+
onError: onError
43+
})
44+
break;
45+
case 'instance':
46+
exportInstance.mutate({instId: inst_id, loadQset: true}, {
47+
onError: onError
48+
})
49+
break;
50+
case 'all':
51+
case 'media':
52+
// use direct download for media
53+
try {
54+
const apiEndpoint = asset_type === 'all' ? `/widgets/export/${inst_id}/all/${timestamp}`
55+
: `/widgets/export/${inst_id}/media/${timestamp}`;
56+
57+
const a = document.createElement('a');
58+
a.href = apiEndpoint;
59+
a.download = `${asset_type}.zip`;
60+
a.click();
61+
} catch (error) {
62+
onError(error);
63+
}
64+
break;
65+
default:
66+
onError('Invalid asset type');
67+
return;
68+
}
69+
};
70+
71+
return exportType
72+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useMutation } from 'react-query'
2+
import { apiImportInstance } from '../../util/api'
3+
4+
export default function useImportInstance() {
5+
const importInstanceMutation = useMutation(
6+
apiImportInstance,
7+
{
8+
onSuccess: (inst, variables) => {
9+
if (inst.type === 'error') {
10+
// remove this when API error handling PR is merged
11+
variables.errorFunc(inst)
12+
return
13+
}
14+
console.log(variables)
15+
variables.successFunc(inst)
16+
},
17+
onError: (err, variables, context) => {
18+
variables.errorFunc(err)
19+
}
20+
21+
}
22+
)
23+
24+
const importInstance = async (inst_id, onSuccess, onError) => {
25+
try {
26+
const input = document.createElement('input')
27+
input.type = 'file'
28+
input.accept = 'application/json'
29+
input.onchange = e => {
30+
const file = e.target.files[0]
31+
const reader = new FileReader()
32+
reader.onload = e => {
33+
const instance = JSON.parse(e.target.result)
34+
importInstanceMutation.mutate({
35+
widget_id: instance.widget.id,
36+
name: instance.name,
37+
qset: instance.qset,
38+
is_draft: true
39+
},
40+
{
41+
successFunc: onSuccess,
42+
errorFunc: onError
43+
})
44+
}
45+
reader.readAsText(file)
46+
}
47+
input.click()
48+
} catch(err) {
49+
onError(err)
50+
}
51+
}
52+
53+
return importInstance
54+
}

src/components/hooks/useImportQset.jsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,23 @@ import { useMutation } from 'react-query'
22
import { apiUpdateQset } from '../../util/api'
33

44
export default function useImportQset() {
5-
const importQsetMutation = useMutation(apiUpdateQset, {
6-
onSuccess: (inst, variables) => {
7-
if (inst.type === 'error') {
8-
// remove this when API error handling PR is merged
9-
variables.errorFunc(inst)
10-
return
5+
const importQsetMutation = useMutation(
6+
apiUpdateQset,
7+
{
8+
onSuccess: (inst, variables) => {
9+
if (inst.type === 'error') {
10+
// remove this when API error handling PR is merged
11+
variables.errorFunc(inst)
12+
return
13+
}
14+
variables.successFunc(inst)
15+
},
16+
onError: (err, variables, context) => {
17+
variables.errorFunc(err)
1118
}
12-
variables.successFunc(inst)
13-
},
14-
onError: (err, variables, context) => {
15-
variables.errorFunc(err)
16-
}
1719

18-
})
20+
}
21+
)
1922

2023
const importQset = async (inst_id, onSuccess, onError) => {
2124
try {
@@ -41,5 +44,5 @@ export default function useImportQset() {
4144
}
4245
}
4346

44-
return { importQset }
47+
return importQset
4548
}

src/components/lti/open-preview.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const SelectItem = () => {
1515

1616
const { data: instance } = useQuery({
1717
queryKey: 'instance',
18-
queryFn: () => apiGetWidgetInstance(instID),
18+
queryFn: () => apiGetWidgetInstance({instID}),
1919
placeholderData: {},
2020
staleTime: Infinity,
2121
onSuccess: (data) => {

src/components/my-widgets-page.scss

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@
177177
// // border-top: 1px solid rgba(100, 100, 100, 0.2);
178178

179179
.widget_list {
180-
height: calc(100% - 74px); // combined height of top and search sections
180+
height: calc(100% - 128px); // combined height of top and search sections
181181
overflow: auto;
182182

183183
.icon {
@@ -297,6 +297,37 @@
297297
text-shadow: #184863 1px 1px 1px;
298298
font-weight: 900;
299299
}
300+
301+
.import-button {
302+
position: absolute;
303+
width: 100%;
304+
bottom: 0;
305+
text-align: center;
306+
307+
p {
308+
font-weight: bold;
309+
transition: font-size 0.2s;
310+
&:hover {
311+
cursor: pointer;
312+
font-size: 1.1em;
313+
}
314+
315+
&:active {
316+
font-size: 1em;
317+
}
318+
319+
.import-icon {
320+
background: url(/img/import.svg) no-repeat center bottom;
321+
margin: 0;
322+
width: 21px;
323+
height: 21px;
324+
display: inline-block;
325+
vertical-align: middle;
326+
margin-top: -5px;
327+
margin-right: 5px;
328+
}
329+
}
330+
}
300331
}
301332

302333
.description h1 {

0 commit comments

Comments
 (0)