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

Metric Update refactor #36

Open
wants to merge 55 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
119797d
reorg / cleanup csv writing
alisonmyers Jul 8, 2024
b445974
add new metrics and output
alisonmyers Jul 8, 2024
ab03d7b
topic_posted_at and topic_created_at
alisonmyers Jul 8, 2024
a118216
add 0 for no replies
alisonmyers Jul 8, 2024
6a236db
add description for summary
alisonmyers Jul 8, 2024
692e569
add better descriptions
alisonmyers Jul 8, 2024
2e0c7e7
Pr to updatemetrics add summary by module (#30)
alisonmyers Sep 4, 2024
bc81489
REFACTOR - being refactor to util fns
alisonmyers Sep 4, 2024
3e5dcfb
REFACTOR - use functions to clean wordcount string
alisonmyers Sep 4, 2024
f57105b
REFACTOR - remove comments
alisonmyers Sep 4, 2024
4b7b3b6
REFACTOR - extract postSummary to function in util
alisonmyers Sep 4, 2024
6bacf28
resolve spacing issue
alisonmyers Sep 12, 2024
218563d
fix numbering
alisonmyers Sep 12, 2024
f75da75
mention all files created
alisonmyers Sep 12, 2024
917b2fa
create warning and error helpers
alisonmyers Sep 12, 2024
377b833
add comments and remove uneccessary exports
alisonmyers Sep 12, 2024
3474f6b
remove trailing spaces
alisonmyers Sep 12, 2024
a09c4ed
move comment to appropriate place
alisonmyers Sep 12, 2024
fe68890
use more meaningful name
alisonmyers Sep 12, 2024
c1a20cf
resolve header comments
alisonmyers Sep 12, 2024
6dbbc45
fix async and await of apis so not sequential
alisonmyers Sep 12, 2024
8ae0b7e
dont need flat
alisonmyers Sep 12, 2024
ea97af8
create conditional promise
alisonmyers Sep 12, 2024
daf6375
rm white space
alisonmyers Sep 12, 2024
250552a
remove sort comparison
alisonmyers Sep 12, 2024
60ef913
address refactor comments
alisonmyers Sep 12, 2024
8090eed
update timestamps and associated functions
alisonmyers Sep 12, 2024
49c86d4
return null if not posted_at instead of 1969 date
alisonmyers Sep 17, 2024
3dea57c
install natural and use for wordcount
alisonmyers Sep 17, 2024
9fef383
add jest and util tests
alisonmyers Sep 17, 2024
750b8c6
standardize date functionality
alisonmyers Sep 17, 2024
7cc1137
add dateDiff and tests
alisonmyers Sep 17, 2024
6383fc2
update jest
alisonmyers Sep 17, 2024
72280fb
update dateDiff tests
alisonmyers Sep 17, 2024
2ec54d5
fix second timestamp
alisonmyers Sep 17, 2024
9607132
add date fns and tests
alisonmyers Sep 17, 2024
9acc570
lint fixing and pass tests
alisonmyers Sep 17, 2024
d5fbb02
use custom datetime fn when pulling data
alisonmyers Sep 17, 2024
7d93632
fix functions and output
alisonmyers Sep 17, 2024
d0902dd
update output and readme for consistency and understanding
alisonmyers Sep 17, 2024
4eac4ed
add extra fn
alisonmyers Sep 17, 2024
82516eb
removes module summary
alisonmyers Sep 18, 2024
e025530
Merge branch 'master' into natural-wordcount
alisonmyers Sep 18, 2024
2557ca3
rm reference to module flag
alisonmyers Sep 18, 2024
e70216d
round output and fix csv error
alisonmyers Sep 18, 2024
4f9afaf
round summaries to 2 decimals
alisonmyers Sep 18, 2024
c49f196
fix duplicate output
alisonmyers Sep 23, 2024
2c596ed
fix tests and remove unused file
alisonmyers Sep 23, 2024
613409d
fix output for csv
alisonmyers Sep 23, 2024
36a192f
fix comments
alisonmyers Sep 23, 2024
1a5c05c
clarify calculations
alisonmyers Sep 23, 2024
ed1251b
use 'response' naming conventions and descriptions
alisonmyers Sep 23, 2024
970ba6a
grammar
alisonmyers Sep 23, 2024
18047ce
rm pdf
alisonmyers Sep 23, 2024
a04b090
rm space and fix numbering
alisonmyers Sep 23, 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
67 changes: 29 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,58 +7,52 @@
### Data
> `{course_id}-discussion.csv`

This project pulls data via the Canvas API the discussions for the specified Canvas course(s) and exports the results as CSV. The columns exported are:
This project pulls discussion data via the Canvas API for the specified Canvas course(s) and exports the results as CSV. The columns exported are:
* 'topic_id',
* 'topic_title',
* 'topic_message',
* 'topic_author_id',
* 'topic_author_name',
* 'topic_created_at',
* 'topic_posted_at',
* 'post_author_id',
* 'post_author_name',
* 'post_id',
* 'post_parent_id',
* 'post_message',
* 'post_likes',
* 'post_timestamp'
* 'response_author_id',
* 'response_author_name',
* 'response_id',
* 'response_parent_id',
* 'response_message',
* 'response_likes',
* 'response_timestamp'

Where a `topic` corresponds to a `discussion_topic` and `post` refers to all replies to the `discussion_topic`. If a `discussion_topic` has no posts then you will see the `topic_` columns filled with no corresponding `post_` data. A `post` may have a `post_parent_id ` if it is part of a threaded response.
Where a `topic` corresponds to a `discussion_topic` and `response` refers to all posts and replies to the `discussion_topic`. If a `discussion_topic` has no posts or replies then you will see the `topic_` columns filled with no corresponding `response_` data. A `response` may have a `response_parent_id` if it is part of a threaded response (i.e is a `reply`).

### Summary Data
> `{course_id}-discussion-summary.csv`

We have calculated summary metrics for each topic. The csv with the summary information includes the following columns:

* 'topic_id',
* 'topic_title',
* 'topic_author_id',
* 'topic_author_name',
* 'topic_created_at',
* 'topic_posted_at',
* 'number_of_posts': the total number of posts and replies in the topic
* 'median_posts_word_count': the median word count for all posts and replies to the topic
* 'average_time_to_post_hours': the average time to post or reply from the topic created_at date
* 'first_reply_timestamp': the timestamp of the first post
* 'average_time_to_post_from_first_reply_hours': the average time to post or reply from the first post (for cases where all discussions are released at once, this may be a more meaningful metric of time to reply)
* 'average_posts_per_author': the average posts per author (does not include enrollments with no posts)
* 'number_of_responses': the total number of posts and replies in the topic
* 'average_responses_per_author': the average posts per author (does not include enrollments with no posts)
* 'median_responses_word_count': the median word count for all posts and replies to the topic
* 'average_days_to_respond_from_posted_at': the average number of days to post from the topic posted_at date. A 'day' is calculated by date, not hours
* 'first_response_timestamp': the timestamp of the first post
* 'average_days_to_respond_from_first_response': the average number of days to post from first topic response. A 'day' is calculated by date, not hours

Where a `post` is a response to a topic, and a `reply` is a reply to the post.

![alt text](image-1.png)
Where a `post` is a direct response to a topic, and a `reply` is a reply to the post. Together the `posts` and `replies` are `responses`.

> `{course_id}-module-discussion-summary.csv`
![alt text](image-1.png)

We have calculated summary metrics at the level of `module` where there are multiple discussion topics. This is optional (see .env creation above) The csv with the summary information includes the following columns:
* 'module_id',
* 'module_name',
* 'module_unlock_at': assuming the course uses an unlock_at date this will be used to calculate,
* 'number_of_posts': the total number of posts and replies in the module
* 'median_posts_word_count': the median word count for all posts and replies to the module topics
* 'average_time_to_post_hours': the average time to post or reply from the module_unlock_at date
* 'first_reply_timestamp': the timestamp of the first post
* 'average_time_to_post_from_first_reply_hours': the average time to post or reply from the first post (for cases where all discussions are released at once, this may be a more meaningful metric of time to reply)
* 'average_posts_per_author': the average posts per author (does not include enrollments with no posts)
### Additional Explanations
- `A 'day' is calculated by date, not hours`
> For instance, for the topic discussion summary, if the topic was posted_at '2024-01-02 12pm' and there was 1 response at '2024-01-03 4pm', then the average_days_to_post would be 1. If the topic as was posted_at '2024-01-01' and all replies were the same day ('2024-01-01') then the average_days_to_post would be 0.

- `does not include enrollments with no posts`
> The calculations are only based on posts or authors who contribute, not expected posts or number of authors; for instance, in a class of 10 students, if 5 students made 1 post each and 5 students made 0 posts the `average_responses_per_author` is 1 (mean: 1,1,1,1,1), **not** 0.5 (mean: 1,1,1,1,1,0,0,0,0,0).

## Getting Started
These instructions will get you a copy of the project up and running on your local machine for use with your own API tokens and Canvas domains.
Expand All @@ -77,24 +71,21 @@ These instructions will get you a copy of the project up and running on your loc
> - See [Get Started with the Canvas API](https://learninganalytics.ubc.ca/guides/get-started-with-the-canvas-api/) for more information.
> - ⚠️ Your Canvas API token is the equivalent to your username and password and must be treated as such (following any security guidelines of your home institution).
1. Create a `.env` file.
1. Add the following: `CANVAS_API_TOKEN={YOUR API TOKEN}`, `CANVAS_API_DOMAIN={YOUR API DOMAIN}`, `COURSE_IDS={YOUR COURSE ID(s)}`. > - At UBC the `CANVAS_API_DOMAIN` is `https://ubc.instructure.com/api/v1`
> - At another institution it might be something like `https://{school}.instructure.com/api/v1`
1. Add `INCLUDE_MODULE_SUMMARY=true` (or `INCLUDE_MODULE_SUMMARY=false`) to indicate whether you would like to include a summary grouped by module. If this is not in the .env it will default to false and no module summary will be created.

1. Add the following: `CANVAS_API_TOKEN={YOUR API TOKEN}`, `CANVAS_API_DOMAIN={YOUR API DOMAIN}`, `COURSE_IDS={YOUR COURSE ID(s)}`.
- At UBC the `CANVAS_API_DOMAIN` is `https://ubc.instructure.com/api/v1`
- At another institution it might be something like `https://{school}.instructure.com/api/v1`
Your .env file should look like
```
CANVAS_API_TOKEN=22322...
CANVAS_API_DOMAIN=https://ubc.instructure.com/api/v1
COURSE_IDS=1111,1112
INCLUDE_MODULE_SUMMARY=false
```
1. Run the script. `npm start`.
1. A `{course_id}-discussion.csv` and a ` {course_id}-discussion-summary.csv` file should be generated with discussion data in the output folder for each provided course_id. If you have set `INCLUDE_MODULE_SUMMARY` to `true` then you will also see a file `{course_id}-module-discussion-summary.csv`.
2. Run the script. `npm start`.
3. A `{course_id}-discussion.csv` and a ` {course_id}-discussion-summary.csv` file should be generated with discussion data in the output folder for each provided course_id.

## Authors

* [justin0022](https://github.com/justin0022) -
**Justin Lee** <[email protected]>
* [justin0022](https://github.com/justin0022) - **Justin Lee** <[email protected]>

## License

Expand Down
Binary file modified image-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 13 additions & 57 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
const capi = require('node-canvas-api')
const { flatten } = require('./util')
const { flatten, toDateTime } = require('./util')
const writeToCSV = require('./writeToCSV')
const writeSummaryToCSV = require('./writeSummaryToCSV')
const writeSummaryByModuleToCSV = require('./writeSummaryByModuleToCSV')
require('dotenv').config()

const envVariableWarning = (msg) => {
console.info(msg)
}

const envVariableError = (msg) => {
console.error(msg)
process.exit(1)

}

const checkEnvVariable = (varName, errMsg) => {
if (!process.env[varName]) {
if (varName === 'INCLUDE_MODULE_SUMMARY') {
envVariableWarning(errMsg)
} else {
envVariableError(`Error: ${errMsg}. See README for an example.env`)
}
envVariableError(`Error: ${errMsg}. See README for an example.env`)
}
}

checkEnvVariable('COURSE_IDS', 'COURSE_IDS environment variable is not defined.')
checkEnvVariable('INCLUDE_MODULE_SUMMARY', 'INCLUDE_MODULE_SUMMARY environment variable is not defined. Define and set to `true` to include summary at module.')
checkEnvVariable('CANVAS_API_TOKEN', 'CANVAS_API_TOKEN environment variable is not defined. You need a token to run this script.')
checkEnvVariable('CANVAS_API_DOMAIN', 'CANVAS_API_DOMAIN environment variable is not defined.')

Expand All @@ -46,7 +41,7 @@ const getNestedReplies = (replyObj, participants, topicId) => {
postAuthorName: authorName,
postMessage: replyObj.message,
postLikes: replyObj.rating_sum || 0,
postTimestamp: new Date(replyObj.created_at),
postTimestamp: toDateTime(replyObj.created_at),
postParentId: replyObj.parent_id || '',
postId: replyObj.id
}, ...replies]
Expand All @@ -55,7 +50,7 @@ const getNestedReplies = (replyObj, participants, topicId) => {
const getDiscussionsAndTopics = async (courseId, topicIds) => {
const fetchDetails = topicId => Promise.all([
capi.getFullDiscussion(courseId, topicId),
capi.getDiscussionTopic(courseId, topicId),
capi.getDiscussionTopic(courseId, topicId)
])

const discussionsAndTopics = await Promise.all(
Expand All @@ -73,8 +68,8 @@ const processDiscussionTopic = ({ discussion, topic }) => {
const topicTitle = topic.title
const topicMessage = topic.message
const author = topic.author
const topicCreatedAt = topic.created_at ? new Date(topic.created_at) : null
const topicPostedAt = topic.posted_at ? new Date(topic.posted_at) : null
const topicCreatedAt = toDateTime(topic.created_at)
const topicPostedAt = toDateTime(topic.posted_at)
const participants = discussion.participants
const replies = discussion.view.length > 0
? discussion.view
Expand All @@ -97,60 +92,21 @@ const processDiscussionTopic = ({ discussion, topic }) => {
const getDiscussions = async courseId => {
const discussionTopicIds = await getDiscussionTopicIds(courseId)
const discussionsAndTopics = await getDiscussionsAndTopics(courseId, discussionTopicIds)

return discussionsAndTopics.map(processDiscussionTopic)
}


const getPublishedModuleDiscussions = async courseId => {

const modules = await capi.getModules(courseId)

const modulesWithDiscussionItems = await Promise.all(modules.map(async module => {
const items = await capi.getModuleItems(courseId, module.id)
const discussionItems = items.filter(item => item.type === "Discussion" && item.published)

const discussionsAndTopics = await getDiscussionsAndTopics(courseId, discussionItems.map(item => item.content_id))
const processedDiscussions = discussionsAndTopics.map(processDiscussionTopic)

const discussionItemWithDiscussionData = discussionItems.map(discussionItem => {
const discussionAndReplies = processedDiscussions.find(d => d.topicId === discussionItem.content_id)
return {
...discussionItem,
discussionAndReplies
}
})

return {
...module,
discussionItems: discussionItemWithDiscussionData
}
}))

return modulesWithDiscussionItems

return discussionsAndTopics.map(processDiscussionTopic)
}

const courseIds = process.env.COURSE_IDS.split(',').map(id => id.trim())
const returnSummaryByModule = process.env.INCLUDE_MODULE_SUMMARY ? process.env.INCLUDE_MODULE_SUMMARY === 'true' : false

Promise.all(
courseIds.map(courseId => {
const basePromise = getDiscussions(courseId).then(discussions =>
getDiscussions(courseId).then(discussions =>
Promise.all([
writeToCSV(courseId, discussions), // Writes detailed discussion data to CSV
writeSummaryToCSV(courseId, discussions) // Writes summary of discussion data to CSV
writeToCSV(courseId, discussions), // Writes detailed discussion data to CSV
writeSummaryToCSV(courseId, discussions) // Writes summary of discussion data to CSV
])
)

const additionalPromise = returnSummaryByModule
? getPublishedModuleDiscussions(courseId).then(modulesWithDiscussionItems =>
writeSummaryByModuleToCSV(courseId, modulesWithDiscussionItems) // Writes summary of module data to CSV
)
: Promise.resolve() // No additional operation if condition is false

return Promise.all([basePromise, additionalPromise])
})
).catch(error => {
console.error('Error processing discussions and modules:', error.message || `An unexpected error occurred: ${error}`)
})
})
Loading