Skip to content
This repository has been archived by the owner on Aug 9, 2021. It is now read-only.

Commit

Permalink
Merge pull request #60 from apo5698/issues/2021/01/31
Browse files Browse the repository at this point in the history
Helli v0.5.1
  • Loading branch information
Dingdong Yao authored Feb 4, 2021
2 parents 987e78c + 7a09e3b commit c5226b1
Show file tree
Hide file tree
Showing 29 changed files with 406 additions and 210 deletions.
10 changes: 9 additions & 1 deletion app/controllers/api/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@

module Api
class ApplicationController < ActionController::API
rescue_from ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid do |exception|
rescue_from ActiveRecord::RecordInvalid do |exception|
render json: exception, status: :unprocessable_entity
end

rescue_from ActiveRecord::RecordNotFound do |exception|
render json: exception, status: :not_found
end

rescue_from Encoding::UndefinedConversionError do |exception|
render plain: "#{exception.class.name} - #{exception}", status: :internal_server_error
end
end
end
27 changes: 27 additions & 0 deletions app/controllers/api/assignments_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Api
class AssignmentsController < ApplicationController
# GET https://api.helli.app/assignments/:assignment_id/rubrics/items
def rubrics_items
assignment = Assignment.find(params.require(:assignment_id))
render json: assignment.rubric_items
end

# POST https://api.helli.app/assignments/:assignment_id/zybooks
def zybooks
csv = params.require(:_json)
csv.each do |record|
participant = Participant.find_by(
assignment_id: params.require(:assignment_id),
email_address: record[:email]
)
Redis.current.set(participant.zybooks_redis_key, record[:total])
end

render status: :ok
rescue Redis::BaseError => e
render plain: e.message, status: :unprocessable_entity
end
end
end
6 changes: 5 additions & 1 deletion app/controllers/api/constants_controller.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# frozen_string_literal: true

# noinspection RailsI18nInspection
module Api
class ConstantsController < ApplicationController
def checkstyle
# noinspection RailsI18nInspection
render json: I18n.t('checkstyle').to_json
end

def zybooks
render json: I18n.t('zybooks').to_json
end
end
end
10 changes: 5 additions & 5 deletions app/controllers/api/grade_items_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ def show
# GET https://api.helli.app/grade_items/:id/attachment
def attachment
attachment = GradeItem.find(params.require(:grade_item_id)).attachment

if attachment
render json: attachment, serializer: ActiveStorageAttachmentSerializer
else
render json: nil, status: :not_found
unless attachment
raise ActiveRecord::RecordNotFound,
"Couldn't find attachment of GradeItem with 'id'=#{params.require(:grade_item_id)}"
end

render json: attachment, serializer: ActiveStorageAttachmentSerializer
end

# PUT https://api.helli.app/grade_items/:id
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/rubric_items_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ def destroy
private

def rubric_item_params
params.require(:rubric_item).permit!
params[:rubric_item].permit!
end
end
2 changes: 1 addition & 1 deletion app/javascript/components/HelliApiUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const HelliApiUrl = (url = '') => `${window.location.protocol}//api.${window.location.host}/${url}`;
export const HelliApiUrl = (url = '') => `${window.location.protocol}//api.${window.location.host}/${url}`;

const fetchHelliApi = async (url, method, body = {}) => {
if (method === 'GET') {
Expand Down
88 changes: 49 additions & 39 deletions app/javascript/components/grading/v2/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ import {
Table,
Tabs,
Tag,
Tooltip,
} from 'antd';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import { deleteHelliApi, getHelliApi, putHelliApi } from '../../HelliApiUtil';
import { deleteHelliApi, getHelliApi, HelliApiUrl, putHelliApi } from '../../HelliApiUtil';

import Compile from './options/Compile';
import Execute from './options/Execute';
import Checkstyle from './options/Checkstyle';
import Zybooks from './options/Zybooks';

interface RubricItem {
id: number,
Expand Down Expand Up @@ -54,6 +56,7 @@ const optionComponents = {
Compile,
Execute,
Checkstyle,
Zybooks,
};

function nameSorter(a: string, b: string): number {
Expand Down Expand Up @@ -85,9 +88,11 @@ const columns: any = [
dataIndex: 'status',
render: (_, record) => (
<Badge size="small" count={record.error}>
<Tag color={statusTagColors[record.status] || 'default'} key={record.status}>
{record.status}
</Tag>
<Tooltip title={record.feedback}>
<Tag color={statusTagColors[record.status] || 'default'} key={record.status}>
{record.status}
</Tag>
</Tooltip>
</Badge>
),
},
Expand All @@ -107,10 +112,10 @@ const columns: any = [
},
];

const Page = (props: { rubricItemIds: number[] }) => {
const { rubricItemIds } = props;
const Page = (props: { assignmentId: number }) => {
const { assignmentId } = props;

const [currentRubricItemId, setCurrentRubricItemId] = useState<number>(rubricItemIds[0]);
const [currentRubricItemId, setCurrentRubricItemId] = useState<number>(0);
const [rubricItem, setRubricItem] = useState<RubricItem>({
id: null,
type: null,
Expand Down Expand Up @@ -141,20 +146,23 @@ const Page = (props: { rubricItemIds: number[] }) => {
};

useEffect(() => {
getHelliApi(`assignments/${assignmentId}/rubrics/items`)
.then((data: RubricItem[]) => {
setRubricItems(data);
setCurrentRubricItemId(data[0].id);
});
}, []);

useEffect(() => {
if (currentRubricItemId === 0) {
return;
}

getHelliApi(`rubrics/items/${currentRubricItemId}`)
.then((data) => setRubricItem(data));
fetchGradeItems();
}, [currentRubricItemId]);

useEffect(() => {
const arr = [];
rubricItemIds.forEach((i) => {
getHelliApi(`rubrics/items/${i}`)
.then((data) => arr.push(data));
});
setRubricItems(arr);
}, []);

const run = async (options) => {
if (!selectedRowKeys.length) {
setNoSelectionWarning(
Expand Down Expand Up @@ -202,11 +210,20 @@ const Page = (props: { rubricItemIds: number[] }) => {
return;
}

getHelliApi(`grade_items/${record.id}/attachment`)
.then((attachment) => {
attachments[record.id] = attachment;
fetch(HelliApiUrl(`grade_items/${record.id}/attachment`))
.then((response) => {
if (!response.ok) { throw response.text(); }
return response.json();
})
.then((data) => {
attachments[record.id] = data;
setAttachments((prevAttachments) => ({ ...prevAttachments, ...attachments }));
});
})
.catch((error) => error.then((text) => {
attachments[record.id] = text;
setAttachments((prevAttachments) => ({ ...prevAttachments, ...attachments }));
message.error(text);
}));
};

const renderExpanded = (record: GradeItem) => {
Expand All @@ -215,25 +232,18 @@ const Page = (props: { rubricItemIds: number[] }) => {
return (
<>
<Card type="inner" title={attachment?.filename} loading={attachment === undefined}>
{
attachment === null
? <span>Cannot load attachment.</span>
: (
<SyntaxHighlighter
language="java"
style={tomorrow}
codeTagProps={{ style: { fontSize: '12px' } }}
showLineNumbers
>
{attachment?.data}
</SyntaxHighlighter>
)
}
<SyntaxHighlighter
language="java"
style={tomorrow}
codeTagProps={{ style: { fontSize: '12px' } }}
showLineNumbers
>
{attachment?.data || attachment}
</SyntaxHighlighter>
</Card>
{
attachment === null
? null
: (
attachment?.id
? (
<Card style={{ fontSize: '12px' }}>
<pre style={{ whiteSpace: 'pre-wrap' }}>{record.stdout}</pre>
<pre
Expand All @@ -246,6 +256,7 @@ const Page = (props: { rubricItemIds: number[] }) => {
<pre>Process finished with exit code {record.exitstatus}</pre>
</Card>
)
: null
}
</>
);
Expand Down Expand Up @@ -343,14 +354,13 @@ const Page = (props: { rubricItemIds: number[] }) => {
</Tabs>
<Form
form={form}
wrapperCol={{ lg: { span: 16 } }}
layout="vertical"
onFinish={(value) => {
// noinspection JSIgnoredPromiseFromCall
run(value);
}}
>
<Options form={form} />
<Options form={form} assignmentId={assignmentId} />
{noSelectionWarning}
<Table
columns={columns}
Expand Down
131 changes: 131 additions & 0 deletions app/javascript/components/grading/v2/options/Zybooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import { Button, Form, Input, message, Space, Upload } from 'antd';
import { FormInstance } from 'antd/lib/form';
import { InboxOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import Papa from 'papaparse';
import { getHelliApi, HelliApiUrl } from '../../../HelliApiUtil';

const { Dragger } = Upload;

const Zybooks = (props: { form: FormInstance, assignmentId: number }) => {
const { assignmentId } = props;
const [fileList, updateFileList] = useState([]);
const [defaultScale, setDefaultScale] = useState([]);

const fileProps = {
fileList,
accept: 'text/csv, application/vnd.ms-excel',
beforeUpload(file) {
if (file.type !== 'text/csv' && file.type !== 'application/vnd.ms-excel') {
message.error(`${file.name} is not a csv file.`);
return false;
}

message.loading({ content: `Parsing ${file.name}...`, key: 'message' });

const reader = new FileReader();
reader.onload = (event) => {
const str = event
.target
.result
.toString()
.replace(/Total \(\d+\)/, 'Total');

const json = Papa
.parse(str, { header: true, skipEmptyLines: true })
.data
.map((e) => ({ email: e['School email'], total: e.Total }));

fetch(HelliApiUrl(`assignments/${assignmentId}/zybooks`), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(json),
})
.then((response) => {
if (!response.ok) {
updateFileList([]);
throw response;
}
})
.then(() => {
message.success({ content: `${file.name} uploaded.`, key: 'message' });
})
.catch((error) => {
error
.text()
.then((text) => {
message.error({ content: text, key: 'message' });
});
});
};
reader.readAsText(file);

return false;
},
};

useEffect(() => {
getHelliApi('zybooks')
.then((data) => {
setDefaultScale(data);
});
}, []);

useEffect(() => { props.form.resetFields(); }, [defaultScale]);

return (
<Space direction="vertical">
<Dragger {...fileProps}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">Click or drag file to this area to upload</p>
<p className="ant-upload-hint">
The filename should look like NCSUCSC116BalikSpring2021_report_004_2021-01-29_2359.csv
</p>
</Dragger>
<Form.List name="scale" initialValue={defaultScale}>
{
(fields, { add, remove }) => (
<>
{
fields.map((field) => (
<Space key={field.key} style={{ display: 'flex' }} align="baseline">
<Form.Item
{...field}
name={[field.name, 'total']}
fieldKey={[field.fieldKey, 'total']}
rules={[{ required: true, message: 'Missing total score' }]}
>
<Input placeholder="Total score >=" />
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'point']}
fieldKey={[field.fieldKey, 'point']}
rules={[{ required: true, message: 'Missing point' }]}
>
<Input placeholder="Points" />
</Form.Item>
<MinusCircleOutlined onClick={() => remove(field.name)} />
</Space>
))
}
<Form.Item>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
Add scale
</Button>
</Form.Item>
</>
)
}
</Form.List>
</Space>
);
};

export default Zybooks;
Loading

0 comments on commit c5226b1

Please sign in to comment.