Giter Site home page Giter Site logo

bbfetch's Introduction

Blackboard Grade Centre command line interface

Setup

First, create a virtual environment and install the dependencies.

pyvenv-3.5 venv
source venv/bin/activate
pip install -r requirements.txt

Next, copy the two files in roberto-dSik to your own directory and adjust them, filling out the details.

The course ID of a Blackboard courses is found by inspecting the course URL. If it looks like:

https://blackboard.au.dk/webapps/blackboard/execute/content/blankPage?cmd=view&content_id=_347138_1&course_id=_43290_1

then the course id is _43290_1.

Usage

Simply run the shell script ./grading, which will activate the virtual environment and run your file grading.py:

cd path/to/grading
./grading --help

To download handins that have not been graded yet:

./grading -d

To download both graded and ungraded handins:

./grading -dd

To download graded and ungraded handins for all students in the course (and not just those made visible by grading.py):

./grading -ddd

To upload feedback:

./grading -u

To run in offline mode without internet access:

./grading -n

Grading handins

When handins are downloaded, they are stored in the directories pointed to by attempt_directory_name.

In order to upload feedback to the students, you must create a new file in this directory named comments.txt and include either the word "Accepted" or "re-handin" ("Godkendt"/"Genaflevering" in Danish). To use other words, adjust rehandin_regex and accept_regex, or override the get_feedback_score function to change the scoring behavior.

The -u (--upload) argument will look for handins that need grading and have a comments.txt file, and then upload the comments to the student.

By default, if the student has handed in a file name my-pretty-handin.pdf and you create a file with the same name followed by _ann ("annotated"), e.g. my-pretty-handin_ann.pdf, it will be uploaded along with the feedback. This is the naming convention used by PDFAnnotater. You can change this behavior by overriding get_feedback_attachments.

Unzipping student handins

By default, if the student has submitted a .zip-file, it is extracted into the same directory as the rest of the student handin files. If you want to change this behavior or handle other kinds of archives automatically, you need to override Grading.extract_archive.

Refreshing student data

With no arguments, grading will refetch the list of students that have assignments that need to be graded.

If you have deleted student attempts in Blackboard, you need to run grading -a to refresh the list of old attempts. This is not refreshed automatically since it takes longer than simply getting the list of assignments needing grading.

If students have been added to groups or removed from groups, you need to run grading -g to get the new list of group memberships. This is not refreshed automatically since it can take a while.

Password security

This project uses the keyring 3rd party module from the Python package index (PyPI) to store your login password to Blackboard so you don't have to enter it every time.

Thus, your Blackboard password will be accessible to all Python programs, making it possible for anyone with access to your computer to read your password. Keep your computer safe from malicious people!

Example

In the following shell transcript, the ./grading program in ~/TA/dADS2-2016 is run with -d to download new student handins. Then, the handins are graded (not shown), and finally, the feedback is uploaded to the students with ./grading -u.

In this way, no browser interaction with Blackboard is needed, and the script takes just 30 seconds to download the 9 handins and 30 seconds to upload the feedback (but your mileage may vary).

rav@novascotia:~/TA/dADS2-2016$ ./grading -d
[2016-04-29 08:29:42,808 INFO] Refresh gradebook
[2016-04-29 08:29:43,189 INFO] Sending login details to WAYF
[2016-04-29 08:29:52,556 INFO] Download Group Attempt Gruppe 2 - 01 28/04/16 /home/rav/TA/dADS2-2016/A3-2/01_26421/afl3.pdf (None bytes)
[2016-04-29 08:29:55,132 INFO] Download Group Attempt Gruppe 2 - 03 28/04/16 /home/rav/TA/dADS2-2016/A3-2/03_26348/main.pdf (None bytes)
[2016-04-29 08:29:57,650 INFO] Saving student_comments.txt for attempt Group Attempt Gruppe 2 - 04 26/04/16
[2016-04-29 08:29:57,735 INFO] Download Group Attempt Gruppe 2 - 04 26/04/16 /home/rav/TA/dADS2-2016/A3-2/04_26294/aflevering-3(2).pdf (None bytes)
[2016-04-29 08:30:00,379 INFO] Download Group Attempt Gruppe 2 - 05 28/04/16 /home/rav/TA/dADS2-2016/A3-2/05_26373/A3_Gruppe5.pdf (None bytes)
[2016-04-29 08:30:03,088 INFO] Download Group Attempt Gruppe 2 - 06 28/04/16 /home/rav/TA/dADS2-2016/A3-2/06_26429/Dads2Afl3.pdf (None bytes)
[2016-04-29 08:30:05,649 INFO] Download Group Attempt Gruppe 2 - 07 28/04/16 /home/rav/TA/dADS2-2016/A3-2/07_26416/Handin3.pdf (None bytes)
[2016-04-29 08:30:08,251 INFO] Download Group Attempt Gruppe 2 - 10 27/04/16 /home/rav/TA/dADS2-2016/A3-2/10_26316/aflevering10.pdf (None bytes)
[2016-04-29 08:30:10,968 INFO] Download Group Attempt Gruppe 2 - 11 28/04/16 /home/rav/TA/dADS2-2016/A3-2/11_26405/A3.pdf (None bytes)
Username Name                           Group  |  1    |  2    |  3    |  4    |  5    |  6
auxxxxxx xxxxxxxxxxxxxx                 DA2-01 || ✘✔    | !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-01 || ✘✔    | !     |       |       |
auxxxxxx xxxxxxxxxxxxxx                 DA2-02 | ✘✔    ||       |       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-03 ||| !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxx           DA2-03 | ✘✔    || !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxx           DA2-03 | ✘✔    || !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-04 ||       |       |       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-05 | ✘✔    | ✘✔    | !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-06 ||| !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-06 ||| !     |       |       |
auxxxxxx xxxxxxxxxxxx                   DA2-06 ||| !     |       |       |
auxxxxxx xxxxxxxxxxxx                   DA2-07 | ✘✔    || !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-08 ||       |       |       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-09 | ✘✔    || !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-09 | ✘✔    || !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-09 | ✘✔    || !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-10 ||| !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxx           DA2-10 ||| !     |       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-11 ||| !     |       |       |
rav@novascotia:~/TA/dADS2-2016$ ./grading -u
[2016-04-29 09:56:30,295 INFO] Refresh gradebook
[2016-04-29 09:56:35,660 DEBUG] goodMsg1: Success: Grade Submitted.
[2016-04-29 09:56:39,362 DEBUG] goodMsg1: Success: Grade Submitted.
[2016-04-29 09:56:42,853 DEBUG] goodMsg1: Success: Grade Submitted.
[2016-04-29 09:56:46,993 DEBUG] goodMsg1: Success: Grade Submitted.
[2016-04-29 09:56:50,802 DEBUG] goodMsg1: Success: Grade Submitted.
[2016-04-29 09:56:54,116 DEBUG] goodMsg1: Success: Grade Submitted.
[2016-04-29 09:56:57,708 DEBUG] goodMsg1: Success: Grade Submitted.
[2016-04-29 09:57:01,419 DEBUG] goodMsg1: Success: Grade Submitted.
[2016-04-29 09:57:01,419 INFO] Refresh gradebook
Username Name                           Group  |  1    |  2    |  3    |  4    |  5    |  6
auxxxxxx xxxxxxxxxxxxxx                 DA2-01 || ✘✔    ||       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-01 || ✘✔    ||       |       |
auxxxxxx xxxxxxxxxxxxxx                 DA2-02 | ✘✔    ||       |       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-03 ||||       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxx           DA2-03 | ✘✔    |||       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxx           DA2-03 | ✘✔    |||       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-04 ||       |       |       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-05 | ✘✔    | ✘✔    ||       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-06 ||||       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-06 ||||       |       |
auxxxxxx xxxxxxxxxxxx                   DA2-06 ||||       |       |
auxxxxxx xxxxxxxxxxxx                   DA2-07 | ✘✔    |||       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-08 ||       |       |       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-09 | ✘✔    |||       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-09 | ✘✔    |||       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxxx          DA2-09 | ✘✔    |||       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-10 ||||       |       |
auxxxxxx xxxxxxxxxxxxxxxxxxxx           DA2-10 ||||       |       |
auxxxxxx xxxxxxxxxxxxxxxxx              DA2-11 ||||       |       |

Customizing how feedback is stored and found

If you have a different workflow for grading handins, you might be able to customize Grading to suit your workflow if you are ready to write a bit of Python code.

For instance, in the Machine Learning course, I store the feedback for accepted handins in the directory graded1/godkendt and for re-handins in graded1/genaflevering.

To support this, I have added a method named get_ml_feedback to my Grading class which finds the feedback and score of a given attempt, and then I have overriden has_feedback, get_feedback and get_feedback_attachments to use get_ml_feedback.

The implementations are as follows.

def get_ml_feedback(self, attempt):
    """
    Compute (score, feedback_file) for given attempt, or (None, None)
    if no feedback exists.
    """
    if attempt != attempt.assignment.attempts[-1]:
        # This attempt is not the last attempt uploaded by the student,
        # so we do not give any feedback to this attempt.
        return None, None
    if any(a.score is not None for a in attempt.assignment.attempts[:-1]):
        # We already graded previous attempts, so this is an actual
        # re-handin from the student, which we do not handle with this
        # method.
        return None, None

    # Feedback for group 42 is stored in a file named comments_42.pdf
    group_name = attempt.group_name
    group_name = re.sub(self.student_group_display_regex[0],
                        self.student_group_display_regex[1],
                        group_name)
    filename = 'comments_%02d.pdf' % int(group_name)
    assignment = self.get_assignment_name_display(attempt.assignment)

    # Re-handin comments are stored separately from accepted handins.
    # The directory determines whether the assignment is accepted or not.
    accept_file = 'graded%s/godkendt/%s' % (assignment, filename)
    has_accept = os.path.exists(accept_file)
    reject_file = 'graded%s/genaflevering/%s' % (assignment, filename)
    has_reject = os.path.exists(reject_file)
    # Check that we don't have both accept and re-handin feedback.
    assert not (has_accept and has_reject)
    if has_accept:
        return 1, accept_file
    elif has_reject:
        return 0, reject_file
    else:
        return None, None

def has_feedback(self, attempt):
    score, filename = self.get_ml_feedback(attempt)
    if filename:
        return True
    # No ML feedback, but maybe we want to give feedback to this attempt
    # in the standard bbfetch way, so we delegate to superclass.
    return super().has_feedback(attempt)

def get_feedback(self, attempt):
    score, filename = self.get_ml_feedback(attempt)
    if score == 0:
        # This string must contain 're-handin' so that get_feedback_score
        # will compute the score correctly.
        return ('Re-handin. ' +
                'Deadline November 3, 2016 at 9:00 (same as Hand-in 2). ' +
                'See comments in attached PDF.')
    if score == 1:
        # This string must contain 'accepted' so that get_feedback_score
        # will compute the score correctly.
        return ('Accepted. ' +
                'See comments in attached PDF.')
    # No ML feedback, but we delegate to superclass.
    return super().get_feedback(attempt)

def get_feedback_attachments(self, attempt):
    score, filename = self.get_ml_feedback(attempt)
    if filename:
        return [filename]
    # No ML feedback, but we delegate to superclass.
    return super().get_feedback_attachments(attempt)

Implementation

This project contains classes to access the Blackboard installation at Aarhus University with the Python Requests framework, and is useful for teaching assistants and teachers who wish to automate the Blackboard tedium.

The main component is a wrapper around requests.Session named blackboard.BlackboardSession with methods to automatically login and resubmit an HTTP request, automatically follow HTML redirects, save and load cookies, save and load login passwords.

For grading handins, the class blackboard.grading.Grading should be extended with information on which course and students should have their handins graded by the user.

For other Blackboard automation purposes, the blackboard/examples/ directory contains examples of how to download all forum posts for a course, how to download the list of groups, how to download a list of email addresses for each group of students, and how to download the list of when students last accessed the course website.

The project uses the following 3rd party modules:

  • requests (HTTP client for Python 2/3)
  • html5lib (to parse and query HTML)
  • keyring (to store your Blackboard password)
  • html2text (to convert HTML forum posts to Markdown)
  • six (bridges incompatibilities between Python 2 and 3)

Install these requirements with pip install -r requirements.txt.

bbfetch's People

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

bbfetch's Issues

Fejl når jeg downloader afleveringer

Jeg får følgende fejl, når jeg forsøger at downloade de studerendes afleveringer:

[2020-02-06 09:39:33,521 INFO] Sending login details to WAYF
[2020-02-06 09:39:34,809 INFO] Refresh gradebook
[2020-02-06 09:39:34,886 INFO] Fetching student group memberships
[2020-02-06 09:39:35,916 DEBUG] Switch to edit mode
[2020-02-06 09:39:38,974 DEBUG] Fetching datatable took 4.1 s
[2020-02-06 09:39:38,990 INFO] Refresh gradebook
[2020-02-06 09:39:39,056 INFO] Fetching 224 attempt lists
[2020-02-06 09:39:40,681 INFO] Fetch details for attempt Group Attempt Gruppe 1 - 15 05/02/20
[2020-02-06 09:39:42,276 ERROR] Uncaught exception
Traceback (most recent call last):
  File "/home/zii/repos/bbfetch/blackboard/grading.py", line 809, in execute_from_command_line
    grading.main(args, session, grading)
  File "/home/zii/repos/bbfetch/blackboard/grading.py", line 655, in main
    visible=True, needs_grading=True)
  File "/home/zii/repos/bbfetch/blackboard/grading.py", line 338, in download_all_attempt_files
    self.download_attempt_files(attempt)
  File "/home/zii/repos/bbfetch/blackboard/grading.py", line 393, in download_attempt_files
    files = self.get_attempt_files(attempt)
  File "/home/zii/repos/bbfetch/blackboard/grading.py", line 473, in get_attempt_files
    rubrics = self.get_rubrics(attempt)
  File "/home/zii/repos/bbfetch/blackboard/grading.py", line 105, in get_rubrics
    return [self.get_rubric(attempt_rubric) for attempt_rubric in rubrics]
  File "/home/zii/repos/bbfetch/blackboard/grading.py", line 105, in <listcomp>
    return [self.get_rubric(attempt_rubric) for attempt_rubric in rubrics]
  File "/home/zii/repos/bbfetch/blackboard/grading.py", line 72, in get_rubric
    assoc_id = attempt_rubric['assocEntityId']
KeyError: 'assocEntityId'

Det er den samme fejl uanset om jeg forsøger at downloade for alle grupper eller kun en enkelt.

attempt_rubric består af følgende:

{
  'id': '_2599_1', 
  'title': 'ProgSprog', 
  'client_changed': 'false', 
  'rubricEvalId': None, 
  'feedback': None, 
  'total_value': None,
  'override_value': None,
  'calculated_percent': None,
  'max_value': '3.000000000000000',
  'rows': [
    {'row_id': '_7050_1', 'cell_id': None, 'rubric_cell_eval': None, 'feedback': None, 'selected_percent': None}
  ]
}

Jeg kan se listen af grupper og deres submissing vha. ./grading.

Er der andre der oplever denne fejl (det kan være Blackboard har ændret responsformat), eller er det mere sandsynligt en fejl i min opsætning?

ParserError when there are only student comments and no submission files/texts

Issuing ./grading -d successfully downloads a bunch of handins, but then fails with a parseerror for one handin:

[2016-11-19 14:20:22,120 INFO] Fetch details for attempt (...)
[2016-11-19 14:20:23,157 ERROR] Parsing error
No currentAttempt_submissionList
ParserError logged to 2016-11-19_1420_parseerror.txt

As a result, the program terminates, and all of the remaining handins are not downloaded either.

Limited to 3 attempts?

Is bbfetch only showing three attempts per student for each handin? I just realized that some students apparently resubmitted handins a long time ago in Blackboard, although this does not show up in bbfetch's overview.

Parsing error upon "Fetch details for attempt ..."

The command ´./grading -d´ fails with a Parsing error:

christofferqa:handins cqa$ ./grading -d
[2016-12-14 08:58:58,349 INFO] Refresh gradebook
[2016-12-14 08:58:59,751 INFO] Fetch details for attempt Group Attempt Gruppe 1 - 02 11-12-16
[2016-12-14 08:59:00,604 ERROR] Parsing error
No currentAttempt_submissionList
ParserError logged to 2016-12-14_0859_parseerror.txt

The relevant group has made an attempt with a comment, but has forgotten to upload the actual handin. Grading the attempt in Blackboard, does not seem to remove the problem.

Grade Table ignores overridden grades

When manually overriding a grade in the Grade Centre in Blackboard, the initial grade given for an attempt is still shown in bbfetch's grade table.

If, for instance, I fail a student's assignment, but later change the assignment's grade to "passed", the table shown when doing ./grading -a will still say "✘".

The calculated points at the end of the row is still correct, though.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.