diff --git a/bbcli/cli.py b/bbcli/cli.py index 0dfb4aa1fbf784234a9e02271568990bf0722961..b76770cce92017ee2a9c724d46109dbf1bdf982b 100644 --- a/bbcli/cli.py +++ b/bbcli/cli.py @@ -11,7 +11,7 @@ import click from bbcli.commands.courses import list_courses from bbcli.commands.announcements import list_announcements, create_announcement, delete_announcement, update_announcement from bbcli.commands.contents import create_assignment_from_contents, create_courselink, create_folder, delete_content, list_contents, create_document, create_file, create_web_link, update_content, upload_attachment, get_content -from bbcli.commands.assignments import get_assignments, submit_attempt, grade_assignment, get_attempts, get_attempt, submit_draft, update_attempt, submit_draft, create_assignment +from bbcli.commands.assignments import get_assignments, submit_attempt, grade_assignment, get_attempts, get_attempt, submit_draft, submit_draft, create_assignment from bbcli.services.authorization_service import login import mmap @@ -167,7 +167,7 @@ def attempts(ctx): attempts.add_command(get_attempts) attempts.add_command(get_attempt) attempts.add_command(submit_draft) -attempts.add_command(update_attempt) +# attempts.add_command(update_attempt) """ CONTENT COMMANDS ENTRY POINT diff --git a/bbcli/commands/announcements.py b/bbcli/commands/announcements.py index 9cd4179c2e1bf05a28d1639b5cefde05593f1851..d5f04cecaca828ceb33d8eb195ad637d5339360a 100644 --- a/bbcli/commands/announcements.py +++ b/bbcli/commands/announcements.py @@ -1,34 +1,42 @@ from datetime import datetime +import json import click from bbcli.entities.content_builder_entitites import DateInterval from bbcli.services import announcements_service from bbcli.utils.error_handler import create_exception_handler, delete_exception_handler, list_exception_handler, update_exception_handler from bbcli.utils.utils import format_date -from bbcli.views import announcement_view +from bbcli.views import announcements_view import os @click.command(name='list', help='This command lists your announcements.\nEither all announcements, all announcements from a spesific course, or one announcement.') @click.option('-c', '--course', 'course_id', required=False, type=str, help='COURSE ID, list announcements from a spesific course') @click.option('-a', '--announcement', 'announcement_id', required=False, type=str, help='ANNONUCEMENT ID, list a spesific announcement from a course.') +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print data in json format') @click.pass_context @list_exception_handler -def list_announcements(ctx, course_id=None, announcement_id=None): +def list_announcements(ctx, course_id=None, announcement_id=None, print_json=False): response = None if announcement_id: response = announcements_service.list_announcement( ctx.obj['SESSION'], course_id, announcement_id) - announcement_view.print_announcement(response) + if not print_json: + announcements_view.print_announcement(response) elif course_id: response = announcements_service.list_course_announcements( ctx.obj['SESSION'], course_id) - announcement_view.print_course_announcements(response) + if not print_json: + announcements_view.print_course_announcements(response) else: user_name = os.getenv('BB_USERNAME') response = announcements_service.list_announcements( ctx.obj['SESSION'], user_name) - announcement_view.print_announcements(response) + if not print_json: + announcements_view.print_announcements(response) + + if print_json: + click.echo(json.dumps(response, indent=2)) @click.command(name='create', help='Creates an announcement. Add --help for all options available.') @@ -36,9 +44,10 @@ def list_announcements(ctx, course_id=None, announcement_id=None): @click.argument('title', required=True, type=str) @click.option('--start-date', type=str, help='When to make announcement available. Format: DD/MM/YY HH:MM:SS') @click.option('--end-date', type=str, help='When to make announcement unavailable. Format: DD/MM/YY HH:MM:SS') +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print response data in json format') @click.pass_context @create_exception_handler -def create_announcement(ctx, course_id: str, title: str, start_date: str, end_date: str): +def create_announcement(ctx, course_id: str, title: str, start_date: str, end_date: str, print_json: bool): date_interval = DateInterval() if start_date or end_date: if start_date: @@ -48,7 +57,11 @@ def create_announcement(ctx, course_id: str, title: str, start_date: str, end_da response = announcements_service.create_announcement( ctx.obj['SESSION'], course_id, title, date_interval) - announcement_view.print_announcement_created(response) + if print_json: + data = json.loads(response) + click.echo(json.dumps(data, indent=2)) + else: + announcements_view.print_announcement_created(response) @click.command(name='delete', help='Deletes an announcement. Add --help for all options available') @@ -59,15 +72,20 @@ def create_announcement(ctx, course_id: str, title: str, start_date: str, end_da def delete_announcement(ctx, course_id: str, announcement_id: str): announcements_service.delete_announcement( ctx.obj['SESSION'], course_id, announcement_id) - announcement_view.print_announcement_deleted() + announcements_view.print_announcement_deleted() @click.command(name='update', help='Updates an announcement. Add --help for all options available.') @click.option('-c', '--course', 'course_id', required=True, type=str, help='COURSE ID of the course you want to create an announcement in.') @click.option('-a', '--announcement', 'announcement_id', required=True, type=str, help='ANNOUNCEMENT ID, of the annonucement you want to update.') +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print response data in json format') @click.pass_context @update_exception_handler -def update_announcement(ctx, course_id: str, announcement_id: str): +def update_announcement(ctx, course_id: str, announcement_id: str, print_json: bool): response = announcements_service.update_announcement( ctx.obj['SESSION'], course_id, announcement_id) - announcement_view.print_announcement_updated(response) + if print_json: + data = json.loads(response) + click.echo(json.dumps(data, indent=2)) + else: + announcements_view.print_announcement_updated(response) diff --git a/bbcli/commands/assignments.py b/bbcli/commands/assignments.py index 5d3a6587741b54ba31ed6d1ae87206eb9b47007a..500f1425170615b5ebea9806c63fccbf221fb454 100644 --- a/bbcli/commands/assignments.py +++ b/bbcli/commands/assignments.py @@ -1,9 +1,11 @@ +import json import click from bbcli.commands.contents import grading_options, set_dates, standard_options from bbcli.entities.content_builder_entitites import GradingOptions, StandardOptions from bbcli.services import assignment_service, contents_service from bbcli.utils.error_handler import create_exception_handler, list_exception_handler, update_exception_handler from bbcli.utils.utils import format_date +from bbcli.views import assignments_view def attempt_options(function): @@ -28,6 +30,7 @@ def attempt_options(function): @click.option('-f', '--folder', 'parent_id', required=True, type=str, help='FOLDER ID, of the folder you want to place the assignment.') @click.argument('title', required=True, type=str) @click.argument('attachments', required=False, nargs=-1, type=click.Path()) +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @standard_options @grading_options @click.pass_context @@ -36,7 +39,7 @@ def create_assignment(ctx, course_id: str, parent_id: str, title: str, hide_content: bool, reviewable: bool, start_date: str, end_date: str, due_date: str, max_attempts: int, unlimited_attempts: bool, score: int, - attachments: tuple): + attachments: tuple, print_json): standard_options = StandardOptions(hide_content, reviewable) grading_options = GradingOptions( attempts_allowed=max_attempts, is_unlimited_attemps_allowed=unlimited_attempts, score_possible=score) @@ -46,29 +49,33 @@ def create_assignment(ctx, course_id: str, parent_id: str, title: str, response = contents_service.create_assignment( ctx.obj['SESSION'], course_id, parent_id, title, standard_options, grading_options, attachments) - click.echo(response) - + assignments_view.print_created_assignment(json.loads(response), print_json) @click.command(name='list', help='List all assignments from a course.') @click.option('-c', '--course', 'course_id', required=True, help='COURSE ID, of the course you want assignments from.') +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @click.pass_context @list_exception_handler -def get_assignments(ctx, course_id): +def get_assignments(ctx, course_id, print_json): response = assignment_service.get_assignments(ctx.obj['SESSION'], course_id) - assignment_service.print_assignments(response) + if print_json: + click.echo(json.dumps(response, indent=2)) + else: + assignments_view.print_assignments(response) @click.command(name='list', help='List attempts for an assignment.') @click.option('-c', '--course', 'course_id', required=True, help='COURSE ID, of the course you want the assignment attempts from') @click.option('-a', '--assignment', 'column_id', required=True, help='ASSIGNMENT ID, of the assignment you want attempts from') @click.option('--submitted', is_flag=True, help='List only submitted attempts.') +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @click.pass_context @list_exception_handler -def get_attempts(ctx, course_id, column_id, submitted): +def get_attempts(ctx, course_id, column_id, submitted, print_json): response = assignment_service.get_column_attempts(ctx.obj['SESSION'], course_id, column_id) if submitted: - assignment_service.print_submitted_attempts(response) + assignments_view.print_submitted_attempts(response, print_json) else: - assignment_service.print_all_attempts(response) + assignments_view.print_all_attempts(response, print_json) # TODO: Retrieve the submission w/ attachments. @@ -76,11 +83,15 @@ def get_attempts(ctx, course_id, column_id, submitted): @click.option('-c', '--course', 'course_id', required=True, help='COURSE ID, of the course of you want to get attempt from') @click.option('-a', '--assignment', 'column_id', required=True, help='ASSIGNMENT ID, of the assignment you want attempts from') @click.option('-at', '--attempt', 'attempt_id', required=True, help='ATTEMPT ID, of the attempt you want to fetch.') +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @click.pass_context @list_exception_handler -def get_attempt(ctx, course_id, column_id, attempt_id): +def get_attempt(ctx, course_id, column_id, attempt_id, print_json): response = assignment_service.get_column_attempt(ctx.obj['SESSION'], course_id, column_id, attempt_id) - click.echo(response) + if print_json: + click.echo(response) + else: + assignments_view.print_get_attempt(json.loads(response)) @click.command(name='submit', help='Submit assignment attempt.') @click.option('-c', '--course', 'course_id', required=True, help='COURSE ID, of the course to submit an assignment to.') @@ -89,49 +100,65 @@ def get_attempt(ctx, course_id, column_id, attempt_id): @click.option('--student-submission', help='The student submission text associated with this attempt.') @click.option('--file', help='Attach a file to an attempt for a Student Submission. Relative path of file.') @click.option('--draft', is_flag=True) +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @click.pass_context @create_exception_handler -def submit_attempt(ctx, course_id, column_id, student_comments, student_submission, file, draft): +def submit_attempt(ctx, course_id, column_id, student_comments, student_submission, file, draft, print_json): response = assignment_service.create_column_attempt( ctx.obj['SESSION'], course_id, column_id, studentComments=student_comments, studentSubmission=student_submission, dst=file, status='needsGrading', draft=draft) - click.echo(response) + if print_json: + click.echo(response) + else: + assignments_view.print_submitted_attempt(json.loads(response)) @click.command(name='submit-draft', help='Submit assignment draft.') @click.option('-c', '--course', 'course_id', required=True, help='COURSE ID, of the course where the assignment is.') @click.option('-a', '--assignment', 'column_id', required=True, help='ASSIGNMENT ID, of the assignment you want to submit to.') @click.option('-at', '--attempt', 'attempt_id', required=True, help='ATTEMPT ID, of the attempt you want to update.') +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @click.pass_context @update_exception_handler -def submit_draft(ctx, course_id, column_id, attempt_id): +def submit_draft(ctx, course_id, column_id, attempt_id, print_json): response = assignment_service.update_column_attempt( ctx.obj['SESSION'], course_id=course_id, column_id=column_id, attempt_id=attempt_id, status='needsGrading') - click.echo(response) - -@click.command(name='update', help='Update assignment.') -@click.option('-c', '--course', 'course_id', required=True, help='COURSE ID, of the course where the assignment is.') -@click.option('-a', '--assignment', 'column_id', required=True, help='ASSIGNMENT ID, of the assignment you want to submit to.') -@click.option('-at', '--attempt', 'attempt_id', required=True, help='ATTEMPT ID, of the attempt you want to update.') -@attempt_options -@click.option('--studentComments', help='The student comments associated with this attempt.') -@click.option('--studentSubmission', help='The student submission text associated with this attempt.') -@click.pass_context -@update_exception_handler -def update_attempt(ctx, course_id, column_id, attempt_id, status, comments, submission, file): - response = assignment_service.update_column_attempt( - session=ctx.obj['SESSION'], course_id=course_id, column_id=column_id, attempt_id=attempt_id, status=status, studentComments=comments, studentSubmission=submission, dst=file) - click.echo(response) + if print_json: + click.echo(response) + else: + assignments_view.print_submitted_draft(json.loads(response)) + +# @click.command(name='update', help='Update assignment.') +# @click.option('-c', '--course', 'course_id', required=True, help='COURSE ID, of the course where the assignment is.') +# @click.option('-a', '--assignment', 'column_id', required=True, help='ASSIGNMENT ID, of the assignment you want to submit to.') +# @click.option('-at', '--attempt', 'attempt_id', required=True, help='ATTEMPT ID, of the attempt you want to update.') +# @attempt_options +# @click.option('--student-comments', help='The student comments associated with this attempt.') +# @click.option('--student-submission', help='The student submission text associated with this attempt.') +# @click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') +# @click.pass_context +# @update_exception_handler +# def update_attempt(ctx, course_id, column_id, attempt_id, status, student_comments, student_submission, file, print_json, exempt, feedback, notes, score, text, file): +# response = assignment_service.update_column_attempt( +# session=ctx.obj['SESSION'], course_id=course_id, column_id=column_id, attempt_id=attempt_id, status=status, studentComments=student_comments, studentSubmission=student_submission, dst=file) +# if print_json: +# click.echo(response) +# else: +# assignments_view.print_updated_attempt(json.loads(response)) @click.command(name='grade', help='Grade an assignment.') @click.option('-c', '--course', 'course_id', required=True, help='COURSE ID, of the course where the assignment is.') @click.option('-a', '--assignment', 'column_id', required=True, help='ASSIGNMENT ID, of the assignment you want.') @click.option('-at', '--attempt', 'attempt_id', required=True, help='ATTEMPT ID, of the attempt you want to grade.') +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @attempt_options @click.pass_context @update_exception_handler -def grade_assignment(ctx, course_id, column_id, attempt_id, status, score, text, notes, feedback, exempt): +def grade_assignment(ctx, course_id, column_id, attempt_id, status, score, text, notes, feedback, exempt, print_json): if status is None: status = 'Completed' response = assignment_service.update_column_attempt(session=ctx.obj['SESSION'], status=status, course_id=course_id, column_id=column_id, attempt_id=attempt_id, score=score, text=text, notes=notes, feedback=feedback, exempt=exempt) - click.echo(response) \ No newline at end of file + if print_json: + click.echo(response) + else: + assignments_view.print_graded_attempt(json.loads(response)) \ No newline at end of file diff --git a/bbcli/commands/contents.py b/bbcli/commands/contents.py index 8aec8c15bf96a5eef66bfcf85e8f4578f4cbc38b..1d764274f98d4f801c006a55d167703e2fd93632 100644 --- a/bbcli/commands/contents.py +++ b/bbcli/commands/contents.py @@ -1,3 +1,4 @@ +import json from bbcli.utils.utils import format_date from bbcli.utils.error_handler import create_exception_handler, delete_exception_handler, list_exception_handler, update_exception_handler import click @@ -106,29 +107,31 @@ def get_content(ctx, course_id: str, node_id: str): @click.option('-c', '--course', 'course_id', required=True, type=str, help='COURSE ID of the course where the content is located') @click.option('-co', '--content', 'content_id', required=True, type=str, help='CONTENT ID of content to attach a file') @click.argument('file_path', required=True, type=click.Path(exists=True)) +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @click.pass_context @create_exception_handler -def upload_attachment(ctx, course_id: str, content_id: str, file_path: str): - contents_service.upload_attachment( +def upload_attachment(ctx, course_id: str, content_id: str, file_path: str, print_json: bool): + response = contents_service.upload_attachment( ctx.obj['SESSION'], course_id, content_id, file_path) - + contents_view.print_created_attachment_response(json.loads(response), print_json) @click.command(name='document', help='Create document content') @click.option('-c', '--course', 'course_id', required=True, type=str, help='COURSE ID') @click.option('-f', '--folder', 'parent_id', required=True, type=str, help='FOLDER ID') @click.argument('title', required=True, type=str) @click.argument('attachments', required=False, nargs=-1, type=click.Path()) +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @standard_options @click.pass_context @create_exception_handler -def create_document(ctx, course_id: str, parent_id: str, title: str, hide_content: bool, reviewable: bool, start_date: str = None, end_date: str = None, attachments: tuple = None): +def create_document(ctx, course_id: str, parent_id: str, title: str, hide_content: bool, reviewable: bool, start_date: str = None, end_date: str = None, attachments: tuple = None, print_json: bool=False): standard_options = StandardOptions( hide_content=hide_content, reviewable=reviewable) set_dates(standard_options, start_date, end_date) response = contents_service.create_document( ctx.obj['SESSION'], course_id, parent_id, title, standard_options, attachments) - click.echo(response) + contents_view.print_created_content_response(json.loads(response), print_json) @click.command(name='file', help='Create file content') @@ -137,19 +140,20 @@ def create_document(ctx, course_id: str, parent_id: str, title: str, hide_conten @click.argument('title', required=True, type=str) @click.argument('file_path', required=True, type=click.Path(exists=True)) @file_options +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @standard_options @click.pass_context @create_exception_handler def create_file(ctx, course_id: str, parent_id: str, title: str, file_path: str, launch_in_new_window: bool, hide_content: bool, reviewable: bool, - start_date: str = None, end_date: str = None): + start_date: str = None, end_date: str = None, print_json: bool=False): file_options = FileOptions(launch_in_new_window) standard_options = StandardOptions( hide_content=hide_content, reviewable=reviewable) set_dates(standard_options, start_date, end_date) response = contents_service.create_file( ctx.obj['SESSION'], course_id, parent_id, title, file_path, file_options, standard_options) - click.echo(response) + contents_view.print_created_content_response(json.loads(response), print_json) @click.command(name='web-link', help='Create web link content') @@ -157,19 +161,20 @@ def create_file(ctx, course_id: str, parent_id: str, title: str, file_path: str, @click.option('-f', '--folder', 'parent_id', required=True, type=str, help='FOLDER ID') @click.argument('title', required=True, type=str) @click.argument('url', required=True, type=str) +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @standard_options @web_link_options @click.pass_context @create_exception_handler def create_web_link(ctx, course_id: str, parent_id: str, title: str, url: str, launch_in_new_window: bool, hide_content: bool, reviewable: bool, - start_date: str = None, end_date: str = None): + start_date: str = None, end_date: str = None, print_json: bool=False): web_link_options = WeblinkOptions(launch_in_new_window) standard_options = StandardOptions(hide_content, reviewable) set_dates(standard_options, start_date, end_date) response = contents_service.create_externallink( ctx.obj['SESSION'], course_id, parent_id, title, url, web_link_options, standard_options) - click.echo(response) + contents_view.print_created_content_response(json.loads(response), print_json) @click.command(name='folder', help='Create folder') @@ -177,17 +182,18 @@ def create_web_link(ctx, course_id: str, parent_id: str, title: str, url: str, @click.option('-f', '--folder', 'parent_id', required=False, type=str, help='FOLDER ID of the parent folder') @click.argument('title', required=True, type=str) @click.option('--is-bb-page', is_flag=True, help='Make folder a blackboard page') +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @standard_options @click.pass_context @create_exception_handler def create_folder(ctx, course_id: str, parent_id: str, title: str, hide_content: bool, reviewable: bool, is_bb_page: bool = False, - start_date: str = None, end_date: str = None): + start_date: str = None, end_date: str = None, print_json: bool=False): standard_options = StandardOptions(hide_content, reviewable) set_dates(standard_options, start_date, end_date) response = contents_service.create_folder( ctx.obj['SESSION'], course_id, parent_id, title, is_bb_page, standard_options) - click.echo(response) + contents_view.print_created_content_response(json.loads(response), print_json) @click.command(name='course-link', help='Create course link content\n\nRedirects user to the target content') @@ -195,17 +201,18 @@ def create_folder(ctx, course_id: str, parent_id: str, title: str, @click.option('-f', '--folder', 'parent_id', required=True, type=str, help='FOLDER ID') @click.argument('title', required=True, type=str) @click.argument('target_id', required=True, type=str) +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @standard_options @click.pass_context @create_exception_handler def create_courselink(ctx, course_id: str, parent_id: str, title: str, target_id: str, hide_content: bool, reviewable: bool, - start_date: str = None, end_date: str = None): + start_date: str = None, end_date: str = None, print_json: bool=False): standard_options = StandardOptions(hide_content, reviewable) set_dates(standard_options, start_date, end_date) response = contents_service.create_courselink( ctx.obj['SESSION'], course_id, parent_id, title, target_id, standard_options) - click.echo(response) + contents_view.print_created_content_response(json.loads(response), print_json) @click.command(name='assignment', help='Create assignment') @@ -213,6 +220,7 @@ def create_courselink(ctx, course_id: str, parent_id: str, title: str, target_id @click.option('-f', '--folder', 'parent_id', required=True, type=str, help='FOLDER ID') @click.argument('title', required=True, type=str) @click.argument('attachments', required=False, nargs=-1, type=click.Path()) +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @standard_options @grading_options @click.pass_context @@ -221,7 +229,7 @@ def create_assignment_from_contents(ctx, course_id: str, parent_id: str, title: hide_content: bool, reviewable: bool, start_date: str, end_date: str, due_date: str, max_attempts: int, unlimited_attempts: bool, score: int, - attachments: tuple): + attachments: tuple, print_json: bool=False): """ Create assignment """ @@ -234,7 +242,7 @@ def create_assignment_from_contents(ctx, course_id: str, parent_id: str, title: response = contents_service.create_assignment( ctx.obj['SESSION'], course_id, parent_id, title, standard_options, grading_options, attachments) - click.echo(response) + contents_view.print_created_content_response(json.loads(response), print_json) # TODO: ADD RESPONSES @@ -245,9 +253,9 @@ def create_assignment_from_contents(ctx, course_id: str, parent_id: str, title: @click.pass_context @delete_exception_handler def delete_content(ctx, course_id: str, content_id: str, delete_grades: bool): - response = contents_service.delete_content( + contents_service.delete_content( ctx.obj['SESSION'], course_id, content_id, delete_grades) - click.echo(response) + contents_view.print_deleted_content_response() # TODO: ADD RESPONSES @@ -255,12 +263,13 @@ def delete_content(ctx, course_id: str, content_id: str, delete_grades: bool): @click.command(name='update', help='Update content\n\nEditable content types: document, files, assignments, externallinks, courselinks') @click.option('-c', '--course', 'course_id', required=True, type=str, help='COURSE ID.') @click.option('-co', '--content', 'content_id', required=True, type=str, help='CONTENT ID') +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @click.pass_context @update_exception_handler -def update_content(ctx, course_id: str, content_id: str): +def update_content(ctx, course_id: str, content_id: str, print_json: bool): response = contents_service.update_content( ctx.obj['SESSION'], course_id, content_id) - click.echo(response) + contents_view.print_updated_content_response(json.loads(response), print_json) """ diff --git a/bbcli/commands/courses.py b/bbcli/commands/courses.py index 39f063c33b7b58b064249095cf1cc617c6ac86ce..d7e26983b4354992985ce67a291818372ceeea67 100644 --- a/bbcli/commands/courses.py +++ b/bbcli/commands/courses.py @@ -1,7 +1,7 @@ import click from bbcli.services import courses_service from bbcli.utils.error_handler import list_exception_handler -from bbcli.views import course_view +from bbcli.views import courses_view import os import requests @@ -10,15 +10,16 @@ import requests @click.command(name='list', help='List courses') @click.option('-c', '--course', 'course_id', required=False, type=str, help='[COURSE ID] Get information about a specific course') @click.option('-a', '--all/--no-all', 'show_all', default=False, help='List all registered courses on the current user') +@click.option('-j', '--json', 'print_json', required=False, is_flag=True, help='Print the data in json format') @click.pass_context @list_exception_handler -def list_courses(ctx, course_id=None, show_all=False): +def list_courses(ctx, course_id=None, show_all=False, print_json=False): response = None if course_id: response = courses_service.list_course( session=ctx.obj['SESSION'], course_id=course_id) - course_view.print_course(response) + courses_view.print_course(response, print_json) else: user_name = os.getenv('BB_USERNAME') if show_all: @@ -27,4 +28,4 @@ def list_courses(ctx, course_id=None, show_all=False): else: response = courses_service.list_courses( session=ctx.obj['SESSION'], user_name=user_name) - course_view.print_courses(response) + courses_view.print_courses(response, print_json) diff --git a/bbcli/services/announcements_service.py b/bbcli/services/announcements_service.py index f957f0c6f5d1a414b31943451164a3f134cabfe9..071a27dff06a8313c63a313d79e7b03df9df2cfa 100644 --- a/bbcli/services/announcements_service.py +++ b/bbcli/services/announcements_service.py @@ -89,7 +89,7 @@ def delete_announcement(session: requests.Session, course_id: str, announcement_ course_id).add_announcements().add_id(announcement_id).create() response = session.delete(url) response.raise_for_status() - return response + return response.text def update_announcement(session: requests.Session, course_id: str, announcement_id: str): diff --git a/bbcli/services/assignment_service.py b/bbcli/services/assignment_service.py index cd9afd82ca16a60d8b41c479760b717eb1fc1cdb..e9c018c47c0f5c6c7a5daae261a9d593722fa73a 100644 --- a/bbcli/services/assignment_service.py +++ b/bbcli/services/assignment_service.py @@ -21,25 +21,7 @@ def get_assignments(session: requests.Session, course_id): results = response['results'] return results -# TODO: This should be in view -def print_assignments(assignments): - for i in range(len(assignments)): - column_id = assignments[i]['id'] - name = assignments[i]['name'] - due = 'N/A' - if 'grading' in assignments[i]: - if 'due' in assignments[i]['grading']: - due = assignments[i]['grading']['due'] - due_datetime = utc_to_local(dateutil.parser.parse(due)) - date = str(due_datetime.date()) - time = str(due_datetime.time()) - due = f'{date} {time}' - click.echo('{:<12s}{:<40s} due {:<10s}'.format(column_id, name, due)) - - -def utc_to_local(utc_dt): - return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None) def get_column_attempts(session: requests.Session, course_id, column_id): @@ -52,36 +34,6 @@ def get_column_attempts(session: requests.Session, course_id, column_id): results = response['results'] return results - -def print_submitted_attempts(attempts): - table = {'id': [], 'user id': [], 'status': [], 'score': [], 'created': []} - statuses = ['NeedsGrading', 'Completed'] - for attempt in attempts: - for status in statuses: - if (status == attempt['status']): - append_to_table(attempt, table) - continue - - click.echo(tabulate(table, headers='keys')) - - -def print_all_attempts(attempts): - table = {'id': [], 'user id': [], 'status': [], 'score': [], 'created': []} - for attempt in attempts: - append_to_table(attempt, table) - click.echo(tabulate(table, headers='keys')) - - -def append_to_table(attempt, table): - table['id'].append(attempt['id']) - table['user id'].append(attempt['userId']) - table['status'].append(attempt['status']) - table['score'].append( - attempt['score']) if 'score' in attempt else table['score'].append('N/A') - created = utc_to_local(dateutil.parser.parse(attempt['created'])) - table['created'].append(created) - - def get_column_attempt(session: requests.Session, course_id, column_id, attempt_id): url = url_builder.base_v2().add_courses().add_id(course_id).add_gradebook( ).add_columns().add_id(column_id).add_attempts().add_id(attempt_id).create() @@ -115,7 +67,7 @@ def create_column_attempt(session: requests.Session, course_id, column_id, stude update_column_attempt(session, course_id, column_id, attempt_id, status='NeedsGrading') - return response_json + return json.dumps(response_json, indent=2) def update_column_attempt(session: requests.Session, course_id, column_id, attempt_id, status=None, score=None, text=None, notes=None, feedback=None, studentComments=None, studentSubmission=None, exempt=None, dst=None): @@ -135,7 +87,7 @@ def update_column_attempt(session: requests.Session, course_id, column_id, attem if dst is not None: attach_file(session, course_id, attempt_id, dst) - return response + return json.dumps(response, indent=2) def attach_file(session: requests.Session, course_id, attempt_id, dst: str): url = url_builder.base_v1().add_courses().add_id( diff --git a/bbcli/services/contents_service.py b/bbcli/services/contents_service.py index b252889e53773bf241e90bc4d5ef318234fadbf6..bf88206baa02c1eac4499d0571ee17c9d1174716 100644 --- a/bbcli/services/contents_service.py +++ b/bbcli/services/contents_service.py @@ -88,6 +88,7 @@ def upload_attachment(session: requests.Session, course_id: str, content_id: str course_id).add_contents().add_id(content_id).add_attachments().create() response = session.post(url, data=data) response.raise_for_status() + return response.text def create_document(session: requests.Session, course_id: str, parent_id: str, title: str, standard_options: StandardOptions = None, attachments: tuple = None): diff --git a/bbcli/services/courses_service.py b/bbcli/services/courses_service.py index 2f5454bffc9ad148372df9fb9d9a79aa012107e4..1699bea1dc98c0938090dc8f1446baf49035593b 100644 --- a/bbcli/services/courses_service.py +++ b/bbcli/services/courses_service.py @@ -19,14 +19,10 @@ def list_courses(session: requests.Session, user_name: str) -> Any: course_memberships = get_course_memberships(session, user_name) courses = get_courses_from_course_memberships(session, course_memberships) - course_list = [] for course in courses: if course['termId'] == term_1['id'] or course['termId'] == term_2['id']: - course_list.append({ - 'id': course['id'], - 'name': course['name'] - }) + course_list.append(course) else: break @@ -47,7 +43,6 @@ def list_course(session: requests.Session, course_id: str) -> Any: response.raise_for_status() return json.loads(response.text) - """ HELPER FUNCTIONS @@ -88,7 +83,7 @@ def get_courses_from_course_memberships(session: requests.Session, course_member for course in course_memberships: url = url_builder.base_v3().add_courses().add_id( course['courseId']).create() - response = session.get(url, params={'fields': 'id, name, termId'}) + response = session.get(url) response.raise_for_status() response = json.loads(response.text) courses.append(response) diff --git a/bbcli/utils/utils.py b/bbcli/utils/utils.py index 071dabea6f80852769fcd4bf29269459a3759607..590d919782e9e6ce087151d0fd1b3cafb0ae2f6f 100644 --- a/bbcli/utils/utils.py +++ b/bbcli/utils/utils.py @@ -1,7 +1,7 @@ import os from datetime import datetime import mmap -from typing import List +from typing import Dict, List from requests import Session import html2text import click @@ -108,4 +108,11 @@ def handle_fish_shell_completion(): if is_activated == False: with open(path, 'a') as f: f.write(f'\n{append_text}\n') - click.echo('Shell completion activated! Restart shell to load the changes.') \ No newline at end of file + click.echo('Shell completion activated! Restart shell to load the changes.') + +def print_keys_in_dict(dictionary: Dict): + for key in dictionary: + if isinstance(dictionary[key], dict): + print_keys_in_dict(dictionary[key]) + elif dictionary[key] != None: + click.echo('{:<20} {:20}'.format(f'{key}:', str(dictionary[key]))) \ No newline at end of file diff --git a/bbcli/views/announcement_view.py b/bbcli/views/announcements_view.py similarity index 74% rename from bbcli/views/announcement_view.py rename to bbcli/views/announcements_view.py index 26fce2b84c56292621e8f18223af989cd132d951..323e5b11907a443eb3e23f97f28e8308f4ec78fc 100644 --- a/bbcli/views/announcement_view.py +++ b/bbcli/views/announcements_view.py @@ -1,6 +1,7 @@ +import json import click from typing import Dict, List -from bbcli.utils.utils import html_to_text +from bbcli.utils.utils import html_to_text, print_keys_in_dict def print_announcement(announcement: Dict): announcement_id = announcement['id'] @@ -21,13 +22,22 @@ def print_announcements(announcements: List): def print_course_announcements(course_announcements: List, course_name: str = None): course_announcements = course_announcements['results'] course_announcements.reverse() + + click.echo('\n') + table = {'id': [], 'title': [], 'body': [], 'date': []} for announcement in course_announcements: if 'body' in announcement: + announcement_id = announcement['id'] title = announcement['title'] body = html_to_text(announcement['body']) created = announcement['created'].split('T')[0] + table['id'].append(announcement_id) + table['title'].append(title) + table['body'].append(body) + table['date'].append(created) + click.echo('----------------------------------------------------------------------\n') if course_name: click.echo(f'{course_name}\n') @@ -37,11 +47,16 @@ def print_course_announcements(course_announcements: List, course_name: str = No click.echo('\n{:<15}\n'.format(body)) def print_announcement_created(announcement): - click.echo('\nAnnouncement sucessfully created:\n\n' + announcement) + data = json.loads(announcement) + click.echo('\nAnnouncement sucessfully created:\n') + print_keys_in_dict(data) def print_announcement_deleted(): click.echo('\nAnnouncement sucessfully deleted.\n') def print_announcement_updated(announcement): - click.echo('\nAnnouncement sucessfully updated:\n\n' + announcement) + data = json.loads(announcement) + click.echo('\nAnnouncement sucessfully updated:\n') + print_keys_in_dict(data) + \ No newline at end of file diff --git a/bbcli/views/assignments_view.py b/bbcli/views/assignments_view.py new file mode 100644 index 0000000000000000000000000000000000000000..b5fef147744d85a73423bbb3b90a1bde19351c25 --- /dev/null +++ b/bbcli/views/assignments_view.py @@ -0,0 +1,97 @@ +import json +import click +from datetime import timezone +import dateutil.parser +from bbcli.utils.utils import print_keys_in_dict +from tabulate import tabulate + + +def print_created_assignment(response, print_json): + if print_json: + click.echo(json.dumps(response, indent=2)) + else: + click.echo('\nAssignment successfully created: \n') + print_keys_in_dict(response) + + +def print_assignments(assignments): + click.echo('\n') + table = {'id': [], 'title': [], 'due': []} + for i in range(len(assignments)): + column_id = assignments[i]['id'] + name = assignments[i]['name'] + due = 'N/A' + if 'grading' in assignments[i]: + if 'due' in assignments[i]['grading']: + due = assignments[i]['grading']['due'] + due_datetime = utc_to_local(dateutil.parser.parse(due)) + date = str(due_datetime.date()) + time = str(due_datetime.time()) + due = f'{date} {time}' + table['id'].append(column_id) + table['title'].append(name) + table['due'].append(due) + + click.echo(tabulate(table, headers=['Id', 'Title', 'Due date'])) + click.echo('\n') + + +def utc_to_local(utc_dt): + return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None) + +def print_submitted_attempts(attempts, print_json): + if print_json: + for attempt in attempts: + if attempt['status'] == 'InProgress': + attempts.remove(attempt) + + click.echo(json.dumps(attempts, indent=2)) + else: + table = {'id': [], 'user_id': [], 'status': [], 'score': [], 'created': []} + statuses = ['NeedsGrading', 'Completed'] + for attempt in attempts: + for status in statuses: + if (status == attempt['status']): + append_to_table(attempt, table) + continue + + click.echo(tabulate(table, headers=['Id', 'User Id', 'Status', 'Score', 'Created'])) + + +def print_all_attempts(attempts, print_json): + if print_json: + click.echo(json.dumps(attempts, indent=2)) + else: + table = {'id': [], 'user_id': [], 'status': [], 'score': [], 'created': []} + for attempt in attempts: + append_to_table(attempt, table) + click.echo(tabulate(table, headers=['Id', 'User Id', 'Status', 'Score', 'Created'])) + + +def append_to_table(attempt, table): + table['id'].append(attempt['id']) + table['user_id'].append(attempt['userId']) + table['status'].append(attempt['status']) + table['score'].append( + attempt['score']) if 'score' in attempt else table['score'].append('N/A') + created = utc_to_local(dateutil.parser.parse(attempt['created'])) + table['created'].append(created) + +def print_get_attempt(attempt): + print_keys_in_dict(attempt) + +def print_submitted_attempt(attempt): + click.echo('\nAssignment successfully submitted: \n') + print_keys_in_dict(attempt) + +def print_submitted_draft(attempt): + click.echo('\nAssignment draft successfully submitted: \n') + print_keys_in_dict(attempt) + +def print_updated_attempt(attempt): + click.echo('\nAttempt successfully updated: \n') + print_keys_in_dict(attempt) + +def print_graded_attempt(attempt): + click.echo('\nAttempt successfully graded: \n') + print_keys_in_dict(attempt) \ No newline at end of file diff --git a/bbcli/views/contents_view.py b/bbcli/views/contents_view.py index 39f320edbddc7c33796cfb224550e30bf028ad9c..e24ffea0c3f21ba73b3847a81c5a827cfbe96586 100644 --- a/bbcli/views/contents_view.py +++ b/bbcli/views/contents_view.py @@ -1,6 +1,7 @@ +import json from anytree import Node as Nd, RenderTree from colorama import Fore, Style -from bbcli.utils.utils import html_to_text +from bbcli.utils.utils import html_to_text, print_keys_in_dict import click import tempfile, os from subprocess import call @@ -44,3 +45,26 @@ def open_less_page(str): pydoc.pager(str) +def print_created_attachment_response(response, print_json): + if print_json: + click.echo(json.dumps(response, indent=2)) + else: + click.echo('\nAttachment successfully uploaded: \n') + print_keys_in_dict(response) + +def print_created_content_response(response, print_json): + if print_json: + click.echo(json.dumps(response, indent=2)) + else: + click.echo('\nContent successfully created: \n') + print_keys_in_dict(response) + +def print_deleted_content_response(): + click.echo('\nContent successfully deleted.\n') + +def print_updated_content_response(response, print_json): + if print_json: + click.echo(json.dumps(response, indent=2)) + else: + click.echo('\nContent successfully updated: \n') + print_keys_in_dict(response) \ No newline at end of file diff --git a/bbcli/views/course_view.py b/bbcli/views/course_view.py deleted file mode 100644 index 2320342b5c914adba28da36c3b9f6f99d7270c9d..0000000000000000000000000000000000000000 --- a/bbcli/views/course_view.py +++ /dev/null @@ -1,19 +0,0 @@ -import click - -def print_courses(courses): - click.echo('\n{:<12} {:<5}\n'.format('Id', 'Course Name')) - for course in courses: - course_id = course['id'] - name = course['name'] - click.echo('{:<12} {:<5}'.format(course_id, name)) - click.echo('\n\n') - -def print_course(course): - - primary_id = course['id'] - course_id = course['courseId'] - name = course['name'] - - click.echo('\n{:<12} {:<12}'.format('Id:', primary_id)) - click.echo('{:<12} {:<12}'.format('Course Id:', course_id)) - click.echo('{:<12} {:<12}\n'.format('Name:', name)) \ No newline at end of file diff --git a/bbcli/views/courses_view.py b/bbcli/views/courses_view.py new file mode 100644 index 0000000000000000000000000000000000000000..be832182f4a6b238f77ba8e3c63dfdfee3ec9a1f --- /dev/null +++ b/bbcli/views/courses_view.py @@ -0,0 +1,28 @@ +import json +import click +from tabulate import tabulate + +def print_courses(courses, print_json): + if print_json: + click.echo(json.dumps(courses, indent=2)) + else: + click.echo('\n') + table = {'id': [], 'course_name': []} + for course in courses: + table['id'].append(course['id']) + table['course_name'].append(course['name']) + + click.echo(tabulate(table, headers=['Id', 'Course Name'])) + click.echo('\n') + +def print_course(course, print_json): + if print_json: + click.echo(json.dumps(course, indent=2)) + else: + primary_id = course['id'] + course_id = course['courseId'] + name = course['name'] + + click.echo('\n{:<12} {:<12}'.format('Id:', primary_id)) + click.echo('{:<12} {:<12}'.format('Course Id:', course_id)) + click.echo('{:<12} {:<12}\n'.format('Name:', name)) \ No newline at end of file diff --git a/tests/test_services/test_announcements_services.py b/tests/test_services/test_announcements_services.py index db18c09e2dc36fbf674df19d0631490ce5834091..16cf33aa3ece388ac2f05522b4e3fde982c20cb2 100644 --- a/tests/test_services/test_announcements_services.py +++ b/tests/test_services/test_announcements_services.py @@ -160,10 +160,11 @@ class TestAnnouncementsServices(object): mock_delete = mock_delete_patcher.start() mock_delete.return_value.ok = True mock_delete.return_value.status_code = 204 + mock_delete.return_value.text = '' response = delete_announcement(self.test_session, 'test_course_id', 'test_announcement_id') - - assert_equal(response.status_code, 204) + + assert_equal(response, '') @raises(requests.exceptions.HTTPError) def test_delete_announcement_with_wrong_announcement_id(self): diff --git a/tests/test_services/test_assignments_services.py b/tests/test_services/test_assignments_services.py index 8aa6ca74ac6d3b0ed3021fc2915c652da013b491..07f846b4b0e847f4316e07530989788727f4f3ec 100644 --- a/tests/test_services/test_assignments_services.py +++ b/tests/test_services/test_assignments_services.py @@ -105,5 +105,5 @@ class TestAssignmentsServices(object): response = update_column_attempt(self.test_session, 'test_course_id', 'test_column_id', 'test_attempt_id') - assert_equal(response, TEST_GRADE_ATTEMPT) + assert_equal(json.loads(response), TEST_GRADE_ATTEMPT) diff --git a/tests/test_services/test_courses_services.py b/tests/test_services/test_courses_services.py index c5620de0e33a6a9d3ae36b464bee5f337a9bc572..ee2fc81cf50eb80040f5d1e360062c2a64d8c687 100644 --- a/tests/test_services/test_courses_services.py +++ b/tests/test_services/test_courses_services.py @@ -38,12 +38,12 @@ TEST_COURSE = { 'externalAccessUrl':'https://ntnu.blackboard.com/ultra/courses/_33050_1/cl/outline' } -TEST_COURSE_LIST = [{'id': '_33050_1', 'name': 'Donn Alexander Morrison testrom', 'termId': '_108_1'}, {'id': '_32909_1', 'name': 'Sammenslått - Ingeniørfaglig systemtenkning INGA2300 INGG2300 INGT2300 (2022 VÅR)', 'termId': '_108_1'}, {'id': '_31606_1', 'name': 'INGT2300 Ingeniørfaglig systemtenkning (2022 VÅR)', 'termId': '_108_1'}, {'id': '_32736_1', 'name': 'Sammenslått - Matematiske metoder 3 for dataingeniører IMAX2150 (2021 HØST)', 'termId': '_107_1'}, {'id': '_28936_1', 'name': 'IMAT2150 Matematiske metoder 3 for dataingeniører (2021 HØST)', 'termId': '_107_1'}, {'id': '_27251_1', 'name': 'IDATT2900 Bacheloroppgave (start 2021 HØST)', 'termId': '_107_1'}, {'id': '_26748_1', 'name': 'Sammenslått - Fysikk/kjemi (2021 vår)', 'termId': '_64_1'}, {'id': '_21080_1', 'name': 'IFYT1001 Fysikk (2021 VÅR)', 'termId': '_64_1'}, {'id': '_22151_1', 'name': 'IDATT2106 Systemutvikling 2 med smidig prosjekt (2021 VÅR)', 'termId': '_64_1'}, {'id': '_22056_1', 'name': 'IDATT2105 Full-stack applikasjonsutvikling (2021 VÅR)', 'termId': '_64_1'}, {'id': '_22212_1', 'name': 'IDATT2104 Nettverksprogrammering (2021 VÅR)', 'termId': '_64_1'}, {'id': '_7921_1', 'name': 'Lab IIR', 'termId': '_28_1'}, {'id': '_26511_1', 'name': 'Dataingeniør Trondheim (BIDATA): Kull 2020', 'termId': '_63_1'}, {'id': '_26287_1', 'name': 'Sammenslått - Statistikk ISTX1001 ISTX1002 ISTX1003 (2020 HØST)', 'termId': '_63_1'}, {'id': '_21671_1', 'name': 'ISTT1003 Statistikk (2020 HØST)', 'termId': '_63_1'}, {'id': '_26170_1', 'name': 'IDATT2202 Operativsystemer (2020 HØST)', 'termId': '_63_1'}, {'id': '_22259_1', 'name': 'IDATT2103 Databaser (2020 HØST)', 'termId': '_63_1'}, {'id': '_22398_1', 'name': 'IDATT2101 Algoritmer og datastrukturer (2020 HØST)', 'termId': '_63_1'}, {'id': '_20124_1', 'name': 'IMAT2021 Matematiske metoder 2 for Dataingeniør (2020 VÅR)', 'termId': '_49_1'}, {'id': '_18976_1', 'name': 'IDATT2001 Programmering 2 (2020 VÅR)', 'termId': '_49_1'}, {'id': '_19418_1', 'name': 'IDATT1002 Systemutvikling (2020 VÅR)', 'termId': '_49_1'}, {'id': '_20377_1', 'name': 'Bachelor i Dataingeniør 2019-2022', 'termId': '_46_1'}, {'id': '_18715_1', 'name': 'HMS0002 HMS-kurs for 1. årsstudenter (2019 HØST)', 'termId': '_46_1'}, {'id': '_20187_1', 'name': 'Sammenslått - Ingeniørfaglig innføringsemne (2019 HØST)', 'termId': '_46_1'}, {'id': '_16575_1', 'name': 'INGT1001 Ingeniørfaglig innføringsemne (2019 HØST)', 'termId': '_46_1'}, {'id': '_20275_1', 'name': 'Sammenslått - Matematiske metoder 1 (2019 HØST)', 'termId': '_46_1'}, {'id': '_20016_1', 'name': 'IMAT1001 Matematiske metoder 1 (2019 HØST)', 'termId': '_46_1'}, {'id': '_19119_1', 'name': 'IDATT1001 Programmering 1 (2019 HØST)', 'termId': '_46_1'}] +TEST_COURSE_LIST = [{'id': '_33050_1', 'uuid': '909cbb1f296140f3ab307df8bc1a0ee3', 'externalId': '194_DAMTEST_2022V_1', 'dataSourceId': '_209_1', 'courseId': '194_DAMTEST_2022V_1', 'name': 'Donn Alexander Morrison testrom', 'created': '2022-02-03T14:10:39.451Z', 'modified': '2022-02-03T14:10:45.960Z', 'organization': False, 'ultraStatus': 'Classic', 'allowGuests': False, 'allowObservers': False, 'closedComplete': False, 'termId': '_108_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'id': 'en_US', 'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_33050_1/cl/outline'}, {'id': '_32909_1', 'courseId': 'MERGE_INGA2300_INGG2300_INGT2300_V22', 'name': 'Sammenslått - Ingeniørfaglig systemtenkning INGA2300 INGG2300 INGT2300 (2022 VÅR)', 'modified': '2022-01-26T23:19:18.859Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_108_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_32909_1/cl/outline'}, {'id': '_31606_1', 'courseId': '194_INGT2300_1_2022_V_1', 'name': 'INGT2300 Ingeniørfaglig systemtenkning (2022 VÅR)', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_108_1', 'availability': {'available': 'No', 'duration': {'type': 'Continuous'}}}, {'id': '_32736_1', 'courseId': 'MERGE_IMAT2150_IMAG2150_IMAA2150_H21', 'name': 'Sammenslått - Matematiske metoder 3 for dataingeniører IMAX2150 (2021 HØST)', 'modified': '2021-08-18T05:58:44.953Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_107_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_32736_1/cl/outline'}, {'id': '_28936_1', 'courseId': '194_IMAT2150_1_2021_H_1', 'name': 'IMAT2150 Matematiske metoder 3 for dataingeniører (2021 HØST)', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_107_1', 'availability': {'available': 'No', 'duration': {'type': 'Continuous'}}}, {'id': '_27251_1', 'courseId': '194_IDATT2900_1_2021_H_1', 'name': 'IDATT2900 Bacheloroppgave (start 2021 HØST)', 'modified': '2022-04-24T02:15:13.555Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_107_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_27251_1/cl/outline'}, {'id': '_26748_1', 'courseId': 'MERGE_IFYKJA100X_IFYKJG100X_IFYKJT100X_V21', 'name': 'Sammenslått - Fysikk/kjemi (2021 vår)', 'modified': '2021-12-14T05:55:57.716Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_64_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_26748_1/cl/outline'}, {'id': '_21080_1', 'courseId': '194_IFYT1001_1_2021_V_1', 'name': 'IFYT1001 Fysikk (2021 VÅR)', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_64_1', 'availability': {'available': 'No', 'duration': {'type': 'Continuous'}}}, {'id': '_22151_1', 'courseId': '194_IDATT2106_1_2021_V_1', 'name': 'IDATT2106 Systemutvikling 2 med smidig prosjekt (2021 VÅR)', 'modified': '2021-09-16T07:23:19.806Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_64_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_22151_1/cl/outline'}, {'id': '_22056_1', 'courseId': '194_IDATT2105_1_2021_V_1', 'name': 'IDATT2105 Full-stack applikasjonsutvikling (2021 VÅR)', 'modified': '2021-09-16T07:23:20.422Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_64_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_22056_1/cl/outline'}, {'id': '_22212_1', 'courseId': '194_IDATT2104_1_2021_V_1', 'name': 'IDATT2104 Nettverksprogrammering (2021 VÅR)', 'modified': '2021-09-16T07:23:19.778Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_64_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_22212_1/cl/outline'}, {'id': '_7921_1', 'courseId': '017AU_004DA_006EK_001', 'name': 'Lab IIR', 'description': 'Program: 017AU_004DA_006EK Type organisasjon: Program Enhet: IE-IIR', 'modified': '2021-07-01T05:06:30.725Z', 'organization': True, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_28_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_7921_1/cl/outline'}, {'id': '_26511_1', 'courseId': 'BIDATA_2020_H_001', 'name': 'Dataingeniør Trondheim (BIDATA): Kull 2020', 'description': 'Program: BIDATA Organisasjon: KULL Enhet: IE-IDI', 'modified': '2021-07-01T05:12:33.492Z', 'organization': True, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_63_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_26511_1/cl/outline'}, {'id': '_26287_1', 'courseId': 'MERGE_ISTX1001_ISTX1002_ISTX1003_H20', 'name': 'Sammenslått - Statistikk ISTX1001 ISTX1002 ISTX1003 (2020 HØST)', 'modified': '2021-08-11T07:00:32.233Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_63_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_26287_1/cl/outline', 'guestAccessUrl': 'https://ntnu.blackboard.com/webapps/login?action=guest_login&new_loc=/ultra/courses/_26287_1/cl/outline'}, {'id': '_21671_1', 'courseId': '194_ISTT1003_1_2020_H_1', 'name': 'ISTT1003 Statistikk (2020 HØST)', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_63_1', 'availability': {'available': 'No', 'duration': {'type': 'Continuous'}}}, {'id': '_26170_1', 'courseId': '194_IDATT2202_1_2020_H_1', 'name': 'IDATT2202 Operativsystemer (2020 HØST)', 'modified': '2021-07-01T05:12:02.694Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_63_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'id': 'en_US', 'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_26170_1/cl/outline'}, {'id': '_22259_1', 'courseId': '194_IDATT2103_1_2020_H_1', 'name': 'IDATT2103 Databaser (2020 HØST)', 'modified': '2021-07-01T05:19:34.676Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_63_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_22259_1/cl/outline'}, {'id': '_22398_1', 'courseId': '194_IDATT2101_1_2020_H_1', 'name': 'IDATT2101 Algoritmer og datastrukturer (2020 HØST)', 'modified': '2021-07-01T05:18:39.655Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_63_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_22398_1/cl/outline'}, {'id': '_20124_1', 'courseId': '194_IMAT2021_1_2020_V_1', 'name': 'IMAT2021 Matematiske metoder 2 for Dataingeniør (2020 VÅR)', 'modified': '2021-07-01T05:10:15.667Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_49_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_20124_1/cl/outline'}, {'id': '_18976_1', 'courseId': '194_IDATT2001_1_2020_V_1', 'name': 'IDATT2001 Programmering 2 (2020 VÅR)', 'modified': '2021-07-01T05:17:08.560Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_49_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_18976_1/cl/outline'}, {'id': '_19418_1', 'courseId': '194_IDATT1002_1_2020_V_1', 'name': 'IDATT1002 Systemutvikling (2020 VÅR)', 'modified': '2021-07-01T05:17:51.960Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_49_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_19418_1/cl/outline'}, {'id': '_20377_1', 'courseId': 'BIDATA_2019_H_001', 'name': 'Bachelor i Dataingeniør 2019-2022', 'description': 'Program: BIDATA Type organisasjon: KULL Program Enhet: IE-IDI', 'modified': '2021-07-01T05:10:39.409Z', 'organization': True, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_46_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_20377_1/cl/outline'}, {'id': '_18715_1', 'courseId': '194_HMS0002_1_2019_H_1', 'name': 'HMS0002 HMS-kurs for 1. årsstudenter (2019 HØST)', 'modified': '2021-07-01T05:19:08.996Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_46_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_18715_1/cl/outline'}, {'id': '_20187_1', 'courseId': 'MERGE_INGA1001_INGG1001_INGT1001_H19', 'name': 'Sammenslått - Ingeniørfaglig innføringsemne (2019 HØST)', 'modified': '2021-07-01T05:08:29.664Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_46_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_20187_1/cl/outline'}, {'id': '_16575_1', 'courseId': '194_INGT1001_1_2019_H_1', 'name': 'INGT1001 Ingeniørfaglig innføringsemne (2019 HØST)', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_46_1', 'availability': {'available': 'No', 'duration': {'type': 'Continuous'}}}, {'id': '_20275_1', 'courseId': 'MERGE_IMAT1001_IMAG1001_IMAA1001_H19', 'name': 'Sammenslått - Matematiske metoder 1 (2019 HØST)', 'modified': '2021-07-01T05:10:47.819Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_46_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_20275_1/cl/outline'}, {'id': '_20016_1', 'courseId': '194_IMAT1001_1_2019_H_1', 'name': 'IMAT1001 Matematiske metoder 1 (2019 HØST)', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_46_1', 'availability': {'available': 'No', 'duration': {'type': 'Continuous'}}}, {'id': '_19119_1', 'courseId': '194_IDATT1001_1_2019_H_1', 'name': 'IDATT1001 Programmering 1 (2019 HØST)', 'modified': '2021-07-01T05:18:06.952Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_46_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_19119_1/cl/outline'}] TEST_TERMS_LIST = [{'id': '_22_1', 'name': 'Andre', 'description': '<p>Emner som ikke faller i vanlige semester-terminologi. F.eks. test-emner, sommerkurs o.l.</p>', 'availability': {'available': 'No', 'duration': {'type': 'Continuous'}}}, {'id': '_40_1', 'name': 'Høst 2013', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2013-08-14T22:00:00.000Z', 'end': '2014-01-01T22:59:59.000Z'}}}, {'id': '_41_1', 'name': 'Høst 2014', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2014-08-14T22:00:00.000Z', 'end': '2015-01-01T22:59:59.000Z'}}}, {'id': '_31_1', 'name': 'Høst 2015', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2015-08-14T22:00:00.000Z', 'end': '2016-01-01T22:59:59.000Z'}}}, {'id': '_23_1', 'name': 'Høst 2016', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2016-08-14T22:00:00.000Z', 'end': '2017-01-01T22:59:59.000Z'}}}, {'id': '_28_1', 'name': 'Høst 2017', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2017-08-14T22:00:00.000Z', 'end': '2018-01-01T22:59:59.000Z'}}}, {'id': '_35_1', 'name': 'Høst 2018', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2018-08-14T22:00:00.000Z', 'end': '2019-01-01T22:59:59.000Z'}}}, {'id': '_46_1', 'name': 'Høst 2019', 'description': '<p>2019 HØST</p>', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2019-06-30T22:00:00.000Z', 'end': '2020-01-01T22:59:59.000Z'}}}, {'id': '_63_1', 'name': 'Høst 2020', 'description': '2020 HØST', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2020-06-30T22:00:00.000Z', 'end': '2021-01-01T22:59:59.000Z'}}}, {'id': '_107_1', 'name': 'Høst 2021', 'description': '2021 HØST', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2021-06-30T22:00:00.000Z', 'end': '2022-01-01T22:59:59.000Z'}}}, {'id': '_47_1', 'name': 'Vår 2013', 'description': '2013 VÅR', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2012-12-31T23:00:00.000Z', 'end': '2013-08-15T21:59:59.000Z'}}}, {'id': '_48_1', 'name': 'Vår 2014', 'description': '2014 VÅR', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2013-12-31T23:00:00.000Z', 'end': '2014-08-15T21:59:59.000Z'}}}, {'id': '_39_1', 'name': 'Vår 2015', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2014-12-31T23:00:00.000Z', 'end': '2015-08-15T21:59:59.000Z'}}}, {'id': '_30_1', 'name': 'Vår 2016', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2015-12-31T23:00:00.000Z', 'end': '2016-08-15T21:59:59.000Z'}}}, {'id': '_25_1', 'name': 'Vår 2017', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2016-12-31T23:00:00.000Z', 'end': '2017-08-15T21:59:59.000Z'}}}, {'id': '_29_1', 'name': 'Vår 2018', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2017-12-31T23:00:00.000Z', 'end': '2018-08-15T21:59:59.000Z'}}}, {'id': '_37_1', 'name': 'Vår 2019', 'description': '<p>Vår 2019</p>', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2018-12-31T23:00:00.000Z', 'end': '2019-08-15T21:59:59.000Z'}}}, {'id': '_49_1', 'name': 'Vår 2020', 'description': '2020 VÅR', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2019-12-31T23:00:00.000Z', 'end': '2020-08-15T21:59:59.000Z'}}}, {'id': '_64_1', 'name': 'Vår 2021', 'description': '2021 VÅR', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2020-12-31T23:00:00.000Z', 'end': '2021-08-15T21:59:59.000Z'}}}, {'id': '_108_1', 'name': 'Vår 2022', 'description': '2022 VÅR', 'availability': {'available': 'Yes', 'duration': {'type': 'DateRange', 'start': '2021-12-31T23:00:00.000Z', 'end': '2022-08-15T21:59:59.000Z'}}}] TEST_COURSE_MEMBERSHIPS_LIST = [{'id': '_2202371_1', 'userId': '_140040_1', 'courseId': '_33050_1', 'dataSourceId': '_2_1', 'created': '2022-02-03T14:21:22.934Z', 'modified': '2022-04-07T10:41:10.608Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Instructor', 'lastAccessed': '2022-04-14T07:41:56.840Z'}, {'id': '_2170002_1', 'userId': '_140040_1', 'courseId': '_32909_1', 'childCourseId': '_31606_1', 'dataSourceId': '_190_1', 'created': '2022-01-10T13:52:35.968Z', 'modified': '2022-01-10T13:52:35.968Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-04-12T08:03:47.568Z'}, {'id': '_2170001_1', 'userId': '_140040_1', 'courseId': '_31606_1', 'dataSourceId': '_190_1', 'created': '2022-01-10T13:52:35.968Z', 'modified': '2022-01-10T13:52:40.055Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-04-12T08:09:50.538Z'}, {'id': '_1950113_1', 'userId': '_140040_1', 'courseId': '_32736_1', 'childCourseId': '_28936_1', 'dataSourceId': '_189_1', 'created': '2021-06-16T14:48:23.886Z', 'modified': '2021-08-22T03:39:59.747Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-04-13T08:44:48.605Z'}, {'id': '_1799061_1', 'userId': '_140040_1', 'courseId': '_28936_1', 'dataSourceId': '_189_1', 'created': '2021-06-16T14:48:23.886Z', 'modified': '2021-08-18T05:58:25.413Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-04-13T08:44:50.836Z'}, {'id': '_1799010_1', 'userId': '_140040_1', 'courseId': '_27251_1', 'dataSourceId': '_189_1', 'created': '2021-06-16T14:48:17.698Z', 'modified': '2021-07-01T21:44:55.485Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-04-13T18:49:48.217Z'}, {'id': '_1698193_1', 'userId': '_140040_1', 'courseId': '_26748_1', 'childCourseId': '_21080_1', 'dataSourceId': '_137_1', 'created': '2020-12-01T13:48:27.469Z', 'modified': '2021-07-01T18:56:35.176Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-02-24T12:46:33.685Z'}, {'id': '_1578419_1', 'userId': '_140040_1', 'courseId': '_21080_1', 'dataSourceId': '_137_1', 'created': '2020-12-01T13:48:27.469Z', 'modified': '2021-07-01T18:36:24.374Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student'}, {'id': '_1578296_1', 'userId': '_140040_1', 'courseId': '_22151_1', 'dataSourceId': '_137_1', 'created': '2020-12-01T13:48:17.232Z', 'modified': '2021-07-01T18:33:54.962Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-02-24T12:47:30.901Z'}, {'id': '_1578292_1', 'userId': '_140040_1', 'courseId': '_22056_1', 'dataSourceId': '_137_1', 'created': '2020-12-01T13:48:16.815Z', 'modified': '2021-07-01T21:02:13.469Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-02-23T12:02:28.245Z'}, {'id': '_1578288_1', 'userId': '_140040_1', 'courseId': '_22212_1', 'dataSourceId': '_137_1', 'created': '2020-12-01T13:48:16.419Z', 'modified': '2021-07-01T22:00:31.942Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-04-06T07:11:32.061Z'}, {'id': '_1576797_1', 'userId': '_140040_1', 'courseId': '_7921_1', 'dataSourceId': '_2_1', 'created': '2020-12-01T08:18:46.350Z', 'modified': '2021-07-01T21:52:45.398Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2021-01-29T12:29:44.448Z'}, {'id': '_1471353_1', 'userId': '_140040_1', 'courseId': '_26511_1', 'dataSourceId': '_2_1', 'created': '2020-08-12T08:00:27.724Z', 'modified': '2021-07-01T18:05:27.249Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-01-27T23:14:36.786Z'}, {'id': '_1355366_1', 'userId': '_140040_1', 'courseId': '_26287_1', 'childCourseId': '_21671_1', 'dataSourceId': '_136_1', 'created': '2020-06-24T12:46:53.972Z', 'modified': '2021-07-01T21:46:01.545Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-01-30T20:14:48.151Z'}, {'id': '_1355365_1', 'userId': '_140040_1', 'courseId': '_21671_1', 'dataSourceId': '_136_1', 'created': '2020-06-24T12:46:53.972Z', 'modified': '2021-07-01T21:47:37.536Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student'}, {'id': '_1355339_1', 'userId': '_140040_1', 'courseId': '_26170_1', 'dataSourceId': '_136_1', 'created': '2020-06-24T12:46:50.584Z', 'modified': '2021-07-01T17:50:03.902Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2021-03-04T12:17:07.070Z'}, {'id': '_1355336_1', 'userId': '_140040_1', 'courseId': '_22259_1', 'dataSourceId': '_136_1', 'created': '2020-06-24T12:46:50.328Z', 'modified': '2021-07-01T20:49:45.419Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-02-27T10:06:08.755Z'}, {'id': '_1355334_1', 'userId': '_140040_1', 'courseId': '_22398_1', 'dataSourceId': '_136_1', 'created': '2020-06-24T12:46:50.156Z', 'modified': '2021-07-01T20:41:06.878Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2021-02-22T11:16:48.492Z'}, {'id': '_1157613_1', 'userId': '_140040_1', 'courseId': '_20124_1', 'dataSourceId': '_115_1', 'created': '2019-12-05T11:58:57.000Z', 'modified': '2021-07-01T20:30:11.616Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2021-11-03T12:11:21.815Z'}, {'id': '_1157600_1', 'userId': '_140040_1', 'courseId': '_18976_1', 'dataSourceId': '_115_1', 'created': '2019-12-05T11:58:55.000Z', 'modified': '2021-07-01T20:23:45.086Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-04-06T07:25:32.063Z'}, {'id': '_1157598_1', 'userId': '_140040_1', 'courseId': '_19418_1', 'dataSourceId': '_115_1', 'created': '2019-12-05T11:58:54.000Z', 'modified': '2021-07-01T18:06:54.921Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2021-02-22T11:16:48.492Z'}, {'id': '_1092393_1', 'userId': '_140040_1', 'courseId': '_20377_1', 'dataSourceId': '_2_1', 'created': '2019-09-06T06:46:20.000Z', 'modified': '2021-07-01T20:19:28.268Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2021-01-29T12:29:44.448Z'}, {'id': '_1041298_1', 'userId': '_140040_1', 'courseId': '_18715_1', 'dataSourceId': '_113_1', 'created': '2019-08-15T12:49:52.000Z', 'modified': '2021-07-01T21:38:11.204Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2021-01-29T12:29:44.448Z'}, {'id': '_1017688_1', 'userId': '_140040_1', 'courseId': '_20187_1', 'childCourseId': '_16575_1', 'dataSourceId': '_113_1', 'created': '2019-08-12T10:49:52.000Z', 'modified': '2021-07-01T21:40:08.101Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2022-02-24T12:50:33.685Z'}, {'id': '_1017687_1', 'userId': '_140040_1', 'courseId': '_16575_1', 'dataSourceId': '_113_1', 'created': '2019-08-12T10:49:52.000Z', 'modified': '2021-07-01T17:52:59.620Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student'}, {'id': '_1017686_1', 'userId': '_140040_1', 'courseId': '_20275_1', 'childCourseId': '_20016_1', 'dataSourceId': '_113_1', 'created': '2019-08-12T10:49:52.000Z', 'modified': '2021-07-01T17:19:27.132Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2021-02-22T11:16:48.492Z'}, {'id': '_1017685_1', 'userId': '_140040_1', 'courseId': '_20016_1', 'dataSourceId': '_113_1', 'created': '2019-08-12T10:49:52.000Z', 'modified': '2021-07-01T20:12:10.899Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student'}, {'id': '_1017684_1', 'userId': '_140040_1', 'courseId': '_19119_1', 'dataSourceId': '_113_1', 'created': '2019-08-12T10:49:52.000Z', 'modified': '2021-07-01T19:41:17.783Z', 'availability': {'available': 'Yes'}, 'courseRoleId': 'Student', 'lastAccessed': '2021-02-22T11:16:48.492Z'}] -TEST_COURSE_LIST_SORTED_AND_SHORTENED = [{'id': '_33050_1', 'name': 'Donn Alexander Morrison testrom'}, {'id': '_32909_1', 'name': 'Sammenslått - Ingeniørfaglig systemtenkning INGA2300 INGG2300 INGT2300 (2022 VÅR)'}, {'id': '_31606_1', 'name': 'INGT2300 Ingeniørfaglig systemtenkning (2022 VÅR)'}, {'id': '_32736_1', 'name': 'Sammenslått - Matematiske metoder 3 for dataingeniører IMAX2150 (2021 HØST)'}, {'id': '_28936_1', 'name': 'IMAT2150 Matematiske metoder 3 for dataingeniører (2021 HØST)'}, {'id': '_27251_1', 'name': 'IDATT2900 Bacheloroppgave (start 2021 HØST)'}] +TEST_COURSE_LIST_SORTED_AND_SHORTENED = [{'id': '_33050_1', 'uuid': '909cbb1f296140f3ab307df8bc1a0ee3', 'externalId': '194_DAMTEST_2022V_1', 'dataSourceId': '_209_1', 'courseId': '194_DAMTEST_2022V_1', 'name': 'Donn Alexander Morrison testrom', 'created': '2022-02-03T14:10:39.451Z', 'modified': '2022-02-03T14:10:45.960Z', 'organization': False, 'ultraStatus': 'Classic', 'allowGuests': False, 'allowObservers': False, 'closedComplete': False, 'termId': '_108_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'id': 'en_US', 'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_33050_1/cl/outline'}, {'id': '_32909_1', 'courseId': 'MERGE_INGA2300_INGG2300_INGT2300_V22', 'name': 'Sammenslått - Ingeniørfaglig systemtenkning INGA2300 INGG2300 INGT2300 (2022 VÅR)', 'modified': '2022-01-26T23:19:18.859Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_108_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_32909_1/cl/outline'}, {'id': '_31606_1', 'courseId': '194_INGT2300_1_2022_V_1', 'name': 'INGT2300 Ingeniørfaglig systemtenkning (2022 VÅR)', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_108_1', 'availability': {'available': 'No', 'duration': {'type': 'Continuous'}}}, {'id': '_32736_1', 'courseId': 'MERGE_IMAT2150_IMAG2150_IMAA2150_H21', 'name': 'Sammenslått - Matematiske metoder 3 for dataingeniører IMAX2150 (2021 HØST)', 'modified': '2021-08-18T05:58:44.953Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_107_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_32736_1/cl/outline'}, {'id': '_28936_1', 'courseId': '194_IMAT2150_1_2021_H_1', 'name': 'IMAT2150 Matematiske metoder 3 for dataingeniører (2021 HØST)', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_107_1', 'availability': {'available': 'No', 'duration': {'type': 'Continuous'}}}, {'id': '_27251_1', 'courseId': '194_IDATT2900_1_2021_H_1', 'name': 'IDATT2900 Bacheloroppgave (start 2021 HØST)', 'modified': '2022-04-24T02:15:13.555Z', 'organization': False, 'ultraStatus': 'Classic', 'closedComplete': False, 'termId': '_107_1', 'availability': {'available': 'Yes', 'duration': {'type': 'Continuous'}}, 'enrollment': {'type': 'InstructorLed'}, 'locale': {'force': False}, 'externalAccessUrl': 'https://ntnu.blackboard.com/ultra/courses/_27251_1/cl/outline'}] class TestCoursesServices(object): @classmethod