diff --git a/.gitignore b/.gitignore index b09fbf512c29d1135dd4c68158ed0ecddfb25a36..fd639334bdd3d44a7667ccdb1063d6ea1f303cc5 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ docs/_build/ # enviroment variables .env -venv/ \ No newline at end of file +venv/ +.vscode \ No newline at end of file diff --git a/README.md b/README.md index b86da8b6d7cc79cfc60f4915e31f4846bbf61189..d5dd27eda209ec4cce0cdae581b752d13194bf93 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ To run this CLI you need python and pip installed, as well as ```magic``` librar sudo apt-get install libmagic1 # Mac OS X - brew install libmagic1 + brew install libmagic ``` Test if the installation was successful by running ```$ bb --version``` command. You should see something like this: diff --git a/bbcli/__version__.py b/bbcli/__version__.py index c0f8fee20b8ba20da6ce271fad420ef1f32a0029..3911194713dcd2915d63192e795fdc61aa96593b 100644 --- a/bbcli/__version__.py +++ b/bbcli/__version__.py @@ -1,4 +1,4 @@ -VERSION = (1, 0, 0) +VERSION = (1, 0, 1) PRERELEASE = None # alpha, beta or rc REVISION = None diff --git a/bbcli/bblib.py b/bbcli/bblib.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/bbcli/cli.py b/bbcli/cli.py index 1bba8d8e6f225b07f092ff3e7e9248fb1d6a32e2..f1b10fb5e86e2ac16c1baa32d874150fffc3344c 100644 --- a/bbcli/cli.py +++ b/bbcli/cli.py @@ -1,9 +1,8 @@ import requests -from bbcli.utils.utils import handle_fish_shell_completion, set_cookies, set_headers +from bbcli.utils.utils import set_cookies, set_headers from bbcli import __app_name__, __version__ import os -import shutil from dotenv import load_dotenv from bbcli import check_valid_date import click @@ -11,32 +10,12 @@ 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 - -def initiate_session(): - bb_cookie = { - 'name': 'BbRouter', - 'value': os.getenv("BB_ROUTER") - } - xsrf = {'X-Blackboard-XSRF': os.getenv('XSRF')} - - session = requests.Session() - set_cookies(session, [bb_cookie]) - set_headers(session, [xsrf]) - session.headers.update({'Content-Type': 'application/json'}) - return session - -def authenticate_user(): - load_dotenv() - bb_cookie = os.getenv('BB_ROUTER') - is_authorized = True if bb_cookie != None and check_valid_date(bb_cookie) else False - if not is_authorized: - click.echo('You are not logged in. Executing authorization script...') - login() - +""" +ENTRY POINT WHERE ALL COMMANDS GO THROUGH +""" @click.group() @click.pass_context @@ -47,65 +26,26 @@ def entry_point(ctx): """ LOGIN AND LOGOUT COMMANDS """ -@click.command(name='login') + +@click.command(name='login', help='Authorize user with username and password') def authorize_user(): - """ - Authorize user with username and password - """ login() -@click.command(name='logout') +@click.command(name='logout', help='Logout user') def logout(): - """ - Logout user - """ - open(f'{os.path.dirname(os.path.abspath(__file__))}/.env', 'w').close() - click.echo('Sucessfully logged out') + clear_env() entry_point.add_command(authorize_user) entry_point.add_command(logout) -""" -SHELL COMPLETION COMMANDS -""" -@click.command(name='activate-shell-completion', help='Activate shell completion') -@click.argument('shell', required=True, type=str) -def activate_shell_completion(shell: str): - if shell == 'fish': - handle_fish_shell_completion() - else: - if shell == 'bash' or shell == 'zsh': - is_activated = False - path = os.path.join(os.path.expanduser('~'), f'.{shell}rc') - append_text = f'. ~/.bb-complete.{shell}' - with open(path, 'rb') as f, \ - mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as s: - if s.find(bytearray(append_text.encode())) != -1: - is_activated = True - - if not is_activated: - shutil.copy(f'{os.path.dirname(os.path.abspath(__file__))}/shell-completion/.bb-complete.bash', os.path.expanduser('~')) - with open(os.path.join(os.path.expanduser('~'), f'.{shell}rc'), 'a') as f: - f.write('\n. ~/.bb-complete.bash\n') - click.echo('Shell completion activated. Restart shell to load changes') - else: - click.echo('Shell completion already activated') - else: - click.echo('Shell not recognized, or CLI shell completion is not compatible with your Shell') - - -entry_point.add_command(activate_shell_completion) """ COURSE COMMANDS ENTRY POINT """ -@entry_point.group() +@entry_point.group(help='Commands for listing courses') @click.pass_context def courses(ctx): - """ - Commands for listing courses - """ authenticate_user() load_dotenv() session = initiate_session() @@ -114,18 +54,13 @@ def courses(ctx): courses.add_command(list_courses) - """ ANNOUNCEMENT COMMANDS ENTRY POINT """ - -@entry_point.group() +@entry_point.group(help='Commands for listing, creating, deleting and updating announcements') @click.pass_context def announcements(ctx): - """ - Commands for listing, creating, deleting and updating announcements - """ authenticate_user() load_dotenv() session = initiate_session() @@ -136,13 +71,13 @@ announcements.add_command(create_announcement) announcements.add_command(delete_announcement) announcements.add_command(update_announcement) +""" +ASSIGNMENTS COMMANDS ENTRY POINT +""" -@entry_point.group() +@entry_point.group(help='Commands for creating, listing and submitting assignments') @click.pass_context def assignments(ctx): - """ - Commands for creating, listing and submitting assignments - """ authenticate_user() load_dotenv() session = initiate_session() @@ -151,62 +86,51 @@ def assignments(ctx): assignments.add_command(get_assignments) assignments.add_command(create_assignment) assignments.add_command(grade_assignment) +assignments.add_command(submit_attempt) +""" +ASSIGNMENTS ATTEMPTS SUBCOMMANDS GROUP +""" -@assignments.group() +@assignments.group(help='Commands for creating, submitting and listing attempts for an assignment') @click.pass_context def attempts(ctx): - """ - Commands for creating, submitting and listing attempts for an assignment - """ pass - attempts.add_command(get_attempts) attempts.add_command(get_attempt) -attempts.add_command(submit_attempt) attempts.add_command(submit_draft) -attempts.add_command(update_attempt) +# attempts.add_command(update_attempt) """ CONTENT COMMANDS ENTRY POINT """ - -@entry_point.group() +@entry_point.group(help='Commands for listing, creating, deleting, updating and downloading content') @click.pass_context def contents(ctx): - """ - Commands for listing, creating, deleting, updating and downloading content - """ authenticate_user() load_dotenv() session = initiate_session() ctx.obj['SESSION'] = session - contents.add_command(list_contents) contents.add_command(get_content) contents.add_command(delete_content) contents.add_command(update_content) """ -CONTENTS CREATE COMMANDS ENTRY POINT +CONTENTS CREATE SUBCOMMANDS ENTRY POINT """ - -@contents.group() +@contents.group(help='Commands for creating different types of content types in blackboard') @click.pass_context def create(ctx): - """ - Commands for creating different types of content types in blackboard - """ authenticate_user() load_dotenv() session = initiate_session() ctx.obj['SESSION'] = session - create.add_command(create_document) create.add_command(create_file) create.add_command(create_web_link) @@ -214,3 +138,32 @@ create.add_command(create_folder) # create.add_command(create_courselink) create.add_command(upload_attachment) create.add_command(create_assignment_from_contents) + +""" +HELPER FUNCTIONS +""" + +def initiate_session(): + bb_cookie = { + 'name': 'BbRouter', + 'value': os.getenv("BB_ROUTER") + } + xsrf = {'X-Blackboard-XSRF': os.getenv('XSRF')} + + session = requests.Session() + set_cookies(session, [bb_cookie]) + set_headers(session, [xsrf]) + session.headers.update({'Content-Type': 'application/json'}) + return session + +def authenticate_user(): + load_dotenv() + bb_cookie = os.getenv('BB_ROUTER') + is_authorized = True if bb_cookie != None and check_valid_date(bb_cookie) else False + if not is_authorized: + click.echo('You are not logged in. Executing authorization script...') + login() + +def clear_env(): + open(f'{os.path.dirname(os.path.abspath(__file__))}/.env', 'w').close() + click.echo('Sucessfully logged out') \ No newline at end of file diff --git a/bbcli/commands/announcements.py b/bbcli/commands/announcements.py index 9cd4179c2e1bf05a28d1639b5cefde05593f1851..7a1dacdb1d3f4a5f2d111046169fed16d2a1d320 100644 --- a/bbcli/commands/announcements.py +++ b/bbcli/commands/announcements.py @@ -1,34 +1,44 @@ from datetime import datetime +import json import click from bbcli.entities.content_builder_entitites import DateInterval -from bbcli.services import announcements_service +from bbcli.services import announcements_services 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_views import os +# TODO: Find out there is a way to display announcements in a clearer way @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): - response = None - +def list_announcements(ctx: click.core.Context, course_id: str, announcement_id: str, print_json: bool) -> None: if announcement_id: - response = announcements_service.list_announcement( + if not course_id: + click.echo('Cannot list specific announcement without COURSE ID') + raise click.Abort() + response = announcements_services.list_announcement( ctx.obj['SESSION'], course_id, announcement_id) - announcement_view.print_announcement(response) + if not print_json: + announcements_views.print_announcement(response) elif course_id: - response = announcements_service.list_course_announcements( + response = announcements_services.list_course_announcements( ctx.obj['SESSION'], course_id) - announcement_view.print_course_announcements(response) + if not print_json: + announcements_views.print_course_announcements(response) else: user_name = os.getenv('BB_USERNAME') - response = announcements_service.list_announcements( + response = announcements_services.list_announcements( ctx.obj['SESSION'], user_name) - announcement_view.print_announcements(response) + if not print_json: + announcements_views.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,38 +46,49 @@ 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.option('-md', '--markdown', required=False, is_flag=True, help='Use this flag if you want to use markdown in body') @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: click.core.Context, course_id: str, title: str, start_date: str, end_date: str, print_json: bool, markdown: bool) -> None: date_interval = DateInterval() - if start_date or end_date: - if start_date: - date_interval.start_date = format_date(start_date) - if end_date: - date_interval.end_date = format_date(end_date) - - response = announcements_service.create_announcement( - ctx.obj['SESSION'], course_id, title, date_interval) - announcement_view.print_announcement_created(response) + if start_date: + date_interval.start_date = format_date(start_date) + if end_date: + date_interval.end_date = format_date(end_date) + response = announcements_services.create_announcement( + ctx.obj['SESSION'], course_id, title, date_interval, markdown) + if print_json: + click.echo(json.dumps(response, indent=2)) + else: + announcements_views.print_announcement_created(response) @click.command(name='delete', help='Deletes 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 announcement you want to delete.') @click.pass_context @delete_exception_handler -def delete_announcement(ctx, course_id: str, announcement_id: str): - announcements_service.delete_announcement( +def delete_announcement(ctx: click.core.Context, course_id: str, announcement_id: str) -> None: + announcements_services.delete_announcement( ctx.obj['SESSION'], course_id, announcement_id) - announcement_view.print_announcement_deleted() - + announcements_views.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.option('-md', '--markdown', required=False, is_flag=True, help='Use this flag if you want to use markdown in body') +@click.option('--advanced', required=False, is_flag=True, help='Use this flag if you also want to update the advanced settings of the announcement') @click.pass_context @update_exception_handler -def update_announcement(ctx, course_id: str, announcement_id: str): - response = announcements_service.update_announcement( - ctx.obj['SESSION'], course_id, announcement_id) - announcement_view.print_announcement_updated(response) +def update_announcement(ctx: click.core.Context, course_id: str, announcement_id: str, print_json: bool, markdown: bool, advanced: bool) -> None: + if advanced: + response = announcements_services.update_announcement_advanced(ctx.obj['SESSION'], course_id, announcement_id, markdown) + else: + response = announcements_services.update_announcement( + ctx.obj['SESSION'], course_id, announcement_id, markdown) + if print_json: + click.echo(json.dumps(response, indent=2)) + else: + announcements_views.print_announcement_updated(response) diff --git a/bbcli/commands/assignments.py b/bbcli/commands/assignments.py index 7421099600524c16916a5b3ac1578c2d508ba774..91a9887f3c5fc93246e907c971896077ab4deefd 100644 --- a/bbcli/commands/assignments.py +++ b/bbcli/commands/assignments.py @@ -1,9 +1,12 @@ +import json import click +from markdown import markdown 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.services import assignments_services, contents_services 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_views def attempt_options(function): @@ -28,6 +31,8 @@ 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') +@click.option('-md', '--markdown', required=False, is_flag=True, help='Use this flag if you want to use markdown in body') @standard_options @grading_options @click.pass_context @@ -36,7 +41,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: bool, markdown: bool): standard_options = StandardOptions(hide_content, reviewable) grading_options = GradingOptions( attempts_allowed=max_attempts, is_unlimited_attemps_allowed=unlimited_attempts, score_possible=score) @@ -44,28 +49,35 @@ def create_assignment(ctx, course_id: str, parent_id: str, title: str, set_dates(standard_options, start_date, end_date) grading_options.due = format_date(due_date) - response = contents_service.create_assignment( - ctx.obj['SESSION'], course_id, parent_id, title, standard_options, grading_options, attachments) - click.echo(response) - + response = contents_services.create_assignment( + ctx.obj['SESSION'], course_id, parent_id, title, standard_options, grading_options, attachments, markdown) + assignments_views.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): - assignment_service.get_assignments(ctx.obj['SESSION'], course_id) - +def get_assignments(ctx, course_id, print_json): + response = assignments_services.get_assignments(ctx.obj['SESSION'], course_id) + if print_json: + click.echo(json.dumps(response, indent=2)) + else: + assignments_views.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): - assignment_service.get_column_attempts( - ctx.obj['SESSION'], course_id, column_id, print_submitted=submitted) +def get_attempts(ctx, course_id, column_id, submitted, print_json): + response = assignments_services.get_column_attempts(ctx.obj['SESSION'], course_id, column_id) + if submitted: + assignments_views.print_submitted_attempts(response, print_json) + else: + assignments_views.print_all_attempts(response, print_json) # TODO: Retrieve the submission w/ attachments. @@ -73,62 +85,82 @@ 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): - assignment_service.get_column_attempt( - ctx.obj['SESSION'], course_id, column_id, attempt_id) - +def get_attempt(ctx, course_id, column_id, attempt_id, print_json): + response = assignments_services.get_column_attempt(ctx.obj['SESSION'], course_id, column_id, attempt_id) + if print_json: + click.echo(response) + else: + assignments_views.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.') @click.option('-a', '--assignment', 'column_id', required=True, help='ASSIGNMENT ID, of the assignment you want to submit to.') -@click.option('--studentComments', help='The student comments associated with this attempt.') -@click.option('--studentSubmission', 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('--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('--file', type=click.Path(exists=True), 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, studentComments, studentSubmission, file, draft): - assignment_service.create_column_attempt( - ctx.obj['SESSION'], course_id, column_id, studentComments=studentComments, studentSubmission=studentSubmission, dst=file, status='needsGrading', draft=draft) - +def submit_attempt(ctx, course_id, column_id, student_comments, student_submission, file, draft, print_json): + response = assignments_services.create_column_attempt( + ctx.obj['SESSION'], course_id, column_id, studentComments=student_comments, studentSubmission=student_submission, dst=file, status='needsGrading', draft=draft) + if print_json: + click.echo(response) + else: + assignments_views.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): - assignment_service.update_column_attempt( +def submit_draft(ctx, course_id, column_id, attempt_id, print_json): + response = assignments_services.update_column_attempt( ctx.obj['SESSION'], course_id=course_id, column_id=column_id, attempt_id=attempt_id, status='needsGrading') - - -@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): - 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) - + if print_json: + click.echo(response) + else: + assignments_views.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' - assignment_service.update_column_attempt(session=ctx.obj['SESSION'], status=status, course_id=course_id, column_id=column_id, + response = assignments_services.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) + if print_json: + click.echo(response) + else: + assignments_views.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 2768d5ae1be9ad2635e4ba1e46c3771dee015555..4dbd9f111c8a5083d357fa5c4e86180e1835a76e 100644 --- a/bbcli/commands/contents.py +++ b/bbcli/commands/contents.py @@ -1,15 +1,22 @@ +import json +from bbcli.utils.URL_builder import URL_builder 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 from bbcli.entities.content_builder_entitites import FileOptions, GradingOptions, StandardOptions, WeblinkOptions -from bbcli.services import contents_service -import click +from bbcli.services import contents_services import concurrent.futures from bbcli.entities.Node import Node from bbcli.utils import content_utils from bbcli.utils.content_handler import content_handler -from bbcli.views import contents_view +from bbcli.views import contents_views + +url_builder = URL_builder() + +""" +GROUPS OF REUSEABLE OPTIONS +""" def standard_options(function): function = click.option('-h', '--hide-content', is_flag=True, @@ -22,7 +29,6 @@ def standard_options(function): '--end-date', type=str, help='When to make content unavailable. Format: DD/MM/YY HH:MM:SS')(function) return function - def grading_options(function): function = click.option('-d', '--due-date', type=str, help='Set a sumbission deadline for assignment. Format: DD/MM/YY HH:MM:SS')(function) @@ -34,19 +40,16 @@ def grading_options(function): type=int, help='Set assignment score reward')(function) return function - def file_options(function): function = click.option('-n', '--new-window', 'launch_in_new_window', is_flag=True)(function) return function - def web_link_options(function): function = click.option('-n', '--new-window', 'launch_in_new_window', is_flag=True)(function) return function - @click.command(name='list', help='List contents\n\nFolders are blue and files are white') @click.option('-c', '--course', 'course_id', required=True, type=str, help='COURSE ID') @click.option('-f', '--folder', 'folder_id', required=False, type=str, help='FOLDER ID') @@ -54,14 +57,14 @@ def web_link_options(function): @click.option('-ct', '--content-type', required=False, type=click.Choice(content_handler.keys(), case_sensitive=False)) @click.pass_context @list_exception_handler -def list_contents(ctx, course_id: str, folder_id: str, content_type, folders_only: bool = False): +def list_contents(ctx: click.core.Context, course_id: str, folder_id: str, content_type: str, folders_only: bool) -> None: if folder_id: content_utils.check_content_handler(ctx, course_id, folder_id) else: ct = 'content' if content_type is None else content_type click.echo(f'Listing the {ct}s...') - response = contents_service.list_contents( + response = contents_services.list_contents( ctx.obj['SESSION'], course_id) data = response.json()['results'] folder_ids = [] @@ -81,53 +84,54 @@ def list_contents(ctx, course_id: str, folder_id: str, content_type, folders_onl for t in threads: root_node = t.result() if root_node is not None: - contents_view.list_tree(root_node, folder_ids, node_ids) + contents_views.list_tree(root_node, folder_ids, node_ids) else: click.ClickException( 'Cannot list folders only and a specific content type. Try either one.' ).show() return - - @click.command(name='get', help='Get content') @click.option('-c', '--course', 'course_id', required=True, type=str, help='COURSE ID') @click.option('-co', '--content', 'node_id', required=True, type=str, help='CONTENT ID') -@click.option('-p', '--path', required=False, type=str, help='Path to be downloaded to') +@click.option('-p', '--path', required=False, type=click.Path(exists=True), help='Path to be downloaded to') @click.pass_context @list_exception_handler -def get_content(ctx, course_id: str, node_id: str, path: str): +def get_content(ctx: click.core.Context, course_id: str, node_id: str, path: str) -> None: content_utils.check_content_handler(ctx, course_id, node_id, path) - @click.command(name='attachment', help='Add attachment to content\n\nOnly supports contents of type document and assignment') @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: click.core.Context, course_id: str, content_id: str, file_path: str, print_json: bool) -> None: + response = contents_services.upload_attachment( ctx.obj['SESSION'], course_id, content_id, file_path) - + contents_views.print_created_attachment_response(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') +@click.option('-md', '--markdown', required=False, is_flag=True, help='Use this flag if you want to use markdown in body') @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: click.core.Context, course_id: str, parent_id: str, title: str, + hide_content: bool, reviewable: bool, start_date: str, end_date: str, + attachments: tuple, print_json: bool, markdown: bool) -> None: 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) - + response = contents_services.create_document( + ctx.obj['SESSION'], course_id, parent_id, title, standard_options, attachments, markdown) + contents_views.print_created_content_response(response, print_json) @click.command(name='file', help='Create file content') @click.option('-c', '--course', 'course_id', required=True, type=str, help='COURSE ID') @@ -135,19 +139,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, +def create_file(ctx: click.core.Context, 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, end_date: str, print_json: bool) -> None: 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( + response = contents_services.create_file( ctx.obj['SESSION'], course_id, parent_id, title, file_path, file_options, standard_options) - click.echo(response) + contents_views.print_created_content_response(response, print_json) @click.command(name='web-link', help='Create web link content') @@ -155,19 +160,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, +def create_web_link(ctx: click.core.Context, 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, end_date: str, print_json: bool) -> None: 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( + response = contents_services.create_externallink( ctx.obj['SESSION'], course_id, parent_id, title, url, web_link_options, standard_options) - click.echo(response) + contents_views.print_created_content_response(response, print_json) @click.command(name='folder', help='Create folder') @@ -175,17 +181,19 @@ 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') +@click.option('-md', '--markdown', required=False, is_flag=True, help='Use this flag if you want to use markdown in body') @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): +def create_folder(ctx: click.core.Context, course_id: str, parent_id: str, title: str, + hide_content: bool, reviewable: bool, is_bb_page: bool, + start_date: str, end_date: str, print_json: bool, markdown: bool) -> None: 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) + response = contents_services.create_folder( + ctx.obj['SESSION'], course_id, parent_id, title, is_bb_page, standard_options, markdown) + contents_views.print_created_content_response(response, print_json) @click.command(name='course-link', help='Create course link content\n\nRedirects user to the target content') @@ -193,17 +201,19 @@ 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') +@click.option('-md', '--markdown', required=False, is_flag=True, help='Use this flag if you want to use markdown in body') @standard_options @click.pass_context @create_exception_handler -def create_courselink(ctx, course_id: str, parent_id: str, title: str, target_id: str, +def create_courselink(ctx: click.core.Context, 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, end_date: str, print_json: bool, markdown: bool) -> None: 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) + response = contents_services.create_courselink( + ctx.obj['SESSION'], course_id, parent_id, title, target_id, standard_options, markdown) + contents_views.print_created_content_response(response, print_json) @click.command(name='assignment', help='Create assignment') @@ -211,18 +221,17 @@ 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') +@click.option('-md', '--markdown', required=False, is_flag=True, help='Use this flag if you want to use markdown in body') @standard_options @grading_options @click.pass_context @create_exception_handler -def create_assignment_from_contents(ctx, course_id: str, parent_id: str, title: str, +def create_assignment_from_contents(ctx: click.core.Context, 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): - """ - Create assignment - """ + attachments: tuple, print_json: bool, markdown: bool) -> None: standard_options = StandardOptions(hide_content, reviewable) grading_options = GradingOptions( attempts_allowed=max_attempts, is_unlimited_attemps_allowed=unlimited_attempts, score_possible=score) @@ -230,43 +239,43 @@ def create_assignment_from_contents(ctx, course_id: str, parent_id: str, title: set_dates(standard_options, start_date, end_date) grading_options.due = format_date(due_date) - response = contents_service.create_assignment( - ctx.obj['SESSION'], course_id, parent_id, title, standard_options, grading_options, attachments) - click.echo(response) - + response = contents_services.create_assignment( + ctx.obj['SESSION'], course_id, parent_id, title, standard_options, grading_options, attachments, markdown) + contents_views.print_created_content_response(response, print_json) -# TODO: ADD RESPONSES @click.command(name='delete', help='Delete content') @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('--delete-grades', is_flag=True, help='Delete grades if a grade column is associated with the content') @click.pass_context @delete_exception_handler -def delete_content(ctx, course_id: str, content_id: str, delete_grades: bool): - response = contents_service.delete_content( +def delete_content(ctx: click.core.Context, course_id: str, content_id: str, delete_grades: bool) -> None: + contents_services.delete_content( ctx.obj['SESSION'], course_id, content_id, delete_grades) - click.echo(response) - -# TODO: ADD RESPONSES - + contents_views.print_deleted_content_response() @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.option('-md', '--markdown', required=False, is_flag=True, help='Use this flag if you want to use markdown in body') +@click.option('--advanced', required=False, is_flag=True, help='Use this flag if you also want to update the advanced settings of the content') @click.pass_context @update_exception_handler -def update_content(ctx, course_id: str, content_id: str): - response = contents_service.update_content( - ctx.obj['SESSION'], course_id, content_id) - click.echo(response) +def update_content(ctx: click.core.Context, course_id: str, content_id: str, print_json: bool, markdown: bool, advanced: bool) -> None: + if advanced: + response = contents_services.update_content_advanced(ctx.obj['SESSION'], course_id, content_id, markdown) + else: + response = contents_services.update_content( + ctx.obj['SESSION'], course_id, content_id, markdown) + contents_views.print_updated_content_response(response, print_json) """ HELPER FUNCTIONS """ - -def set_dates(standard_options: StandardOptions, start_date: str, end_date: str): +def set_dates(standard_options: StandardOptions, start_date: str, end_date: str) -> None: if start_date: standard_options.date_interval.start_date = format_date(start_date) if end_date: diff --git a/bbcli/commands/courses.py b/bbcli/commands/courses.py index 39f063c33b7b58b064249095cf1cc617c6ac86ce..c03d019bd8d30cb951b809d598f903d01a609f53 100644 --- a/bbcli/commands/courses.py +++ b/bbcli/commands/courses.py @@ -1,30 +1,23 @@ import click -from bbcli.services import courses_service +from bbcli.services import courses_services from bbcli.utils.error_handler import list_exception_handler -from bbcli.views import course_view +from bbcli.views import courses_views import os import requests -# , help='List a spesific course with the corresponding id' + +# TODO: Hear with Donn whether it is okay to always list all courses? @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): - response = None - +def list_courses(ctx: click.core.Context, course_id: str, print_json: bool) -> None: if course_id: - response = courses_service.list_course( - session=ctx.obj['SESSION'], course_id=course_id) - course_view.print_course(response) + response = courses_services.list_course(ctx.obj['SESSION'], course_id) + courses_views.print_course(response, print_json) else: user_name = os.getenv('BB_USERNAME') - if show_all: - response = courses_service.list_all_courses( - session=ctx.obj['SESSION'], user_name=user_name) - else: - response = courses_service.list_courses( - session=ctx.obj['SESSION'], user_name=user_name) - course_view.print_courses(response) + response = courses_services.list_all_courses(ctx.obj['SESSION'], user_name) + courses_views.print_courses(response, print_json) diff --git a/bbcli/config.py b/bbcli/config.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/bbcli/services/announcements_service.py b/bbcli/services/announcements_services.py similarity index 53% rename from bbcli/services/announcements_service.py rename to bbcli/services/announcements_services.py index f957f0c6f5d1a414b31943451164a3f134cabfe9..abe565dc0682793019528dc13e1dddb406e79fac 100644 --- a/bbcli/services/announcements_service.py +++ b/bbcli/services/announcements_services.py @@ -1,20 +1,22 @@ from datetime import datetime import json from subprocess import call -from typing import Dict, Any +from typing import Dict, Any, List import requests from bbcli.entities.content_builder_entitites import DateInterval -from bbcli.services.courses_service import list_courses -from bbcli.utils.utils import input_body, set_cookies +from bbcli.services.courses_services import list_all_courses +from bbcli.utils.utils import input_body import click +import markdown +import markdownify from bbcli.utils.URL_builder import URL_builder url_builder = URL_builder() -def list_announcements(session: requests.Session, user_name: str): - courses = list_courses(session, user_name=user_name) +def list_announcements(session: requests.Session, user_name: str) -> List: + courses = list_all_courses(session, user_name=user_name) announcements = [] for course in courses: course_announcements = list_course_announcements(session, course['id'], True) @@ -27,8 +29,7 @@ def list_announcements(session: requests.Session, user_name: str): }) return announcements - -def list_course_announcements(session: requests.Session, course_id: str, allow_bad_request: bool=False): +def list_course_announcements(session: requests.Session, course_id: str, allow_bad_request: bool=False) -> Dict: url = url_builder.base_v1().add_courses().add_id( course_id).add_announcements().create() course_announcements = session.get(url) @@ -37,8 +38,7 @@ def list_course_announcements(session: requests.Session, course_id: str, allow_b course_announcements = json.loads(course_announcements.text) return course_announcements - -def list_announcement(session: requests.Session, course_id: str, announcement_id: str): +def list_announcement(session: requests.Session, course_id: str, announcement_id: str) -> Dict: url = url_builder.base_v1().add_courses().add_id( course_id).add_announcements().add_id(announcement_id).create() announcement = session.get(url) @@ -46,15 +46,13 @@ def list_announcement(session: requests.Session, course_id: str, announcement_id announcement = json.loads(announcement.text) return announcement -# TODO: Test if the duration actually makes it unavailable/available when it should - - -def create_announcement(session: requests.Session, course_id: str, title: str, date_interval: DateInterval): +def create_announcement(session: requests.Session, course_id: str, title: str, date_interval: DateInterval, is_markdown: bool) -> Dict: if title == '': raise click.BadParameter('Argument TITLE cannot be empty!') body = input_body() - + if is_markdown: + body = markdown.markdown(body) data = { 'title': title, 'body': body @@ -80,36 +78,88 @@ def create_announcement(session: requests.Session, course_id: str, title: str, d course_id).add_announcements().create() response = session.post(url, data=data) response.raise_for_status() - - return response.text + response = json.loads(response.text) + return response -def delete_announcement(session: requests.Session, course_id: str, announcement_id: str): +def delete_announcement(session: requests.Session, course_id: str, announcement_id: str) -> str: url = url_builder.base_v1().add_courses().add_id( 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, is_markdown: bool) -> Dict: + + announcement = list_announcement( + session=session, course_id=course_id, announcement_id=announcement_id) + + new_title = edit_title(announcement) + new_data = edit_body(announcement, is_markdown) + data = json.dumps({ + 'title': new_title, + 'body': new_data + }) -def update_announcement(session: requests.Session, course_id: str, announcement_id: str): + url = url_builder.base_v1().add_courses().add_id( + course_id).add_announcements().add_id(announcement_id).create() + response = session.patch(url, data=data) + response.raise_for_status() + response = json.loads(response.text) + return response +def update_announcement_advanced(session: requests.Session, course_id: str, announcement_id: str, is_markdown: bool) -> Dict: announcement = list_announcement( session=session, course_id=course_id, announcement_id=announcement_id) + if is_markdown: + announcement['body'] = markdownify.markdownify(announcement['body']) + MARKER = '# Everything below is ignored.\n' - editable_data = { - 'title': announcement['title'], - 'body': announcement['body'], - 'created': announcement['created'], - 'availability': announcement['availability'], - 'draft': announcement['draft'] - } - announcement = json.dumps(editable_data, indent=2) - new_data = click.edit(announcement + '\n\n' + MARKER) + + announcement = json.dumps(announcement, indent=2) + data = click.edit(announcement + '\n\n' + MARKER) + new_data = data if data != None else announcement + if new_data is not None: + new_data = new_data.split(MARKER, 1)[0].rstrip('\n') + + if is_markdown: + new_data = json.loads(new_data) + new_data['body'] = markdown.markdown(new_data['body']) + new_data = json.dumps(new_data, indent=2) url = url_builder.base_v1().add_courses().add_id( course_id).add_announcements().add_id(announcement_id).create() response = session.patch(url, data=new_data) response.raise_for_status() - - return response.text + response = json.loads(response.text) + return response + +""" +HELPER FUNCTIONS +""" + +def edit_title(data: Dict) -> str: + MARKER_TITLE = '# Edit title. Everything below is ignored.\n' + title = click.edit(data['title'] + '\n\n' + MARKER_TITLE) + new_title = title if title != None else data['title'] + if new_title is not None: + new_title = new_title.split(MARKER_TITLE, 1)[0].rstrip('\n') + return new_title + +def edit_body(data: Dict, is_markdown: bool) -> str: + try: + data['body'] + except KeyError: + data['body'] = '' + if is_markdown: + data['body'] = markdownify.markdownify(data['body']) + MARKER_BODY = '# Edit body. Everything below is ignored.\n' + body = click.edit(data['body'] + '\n\n' + MARKER_BODY) + new_body = body if body != None else data['body'] + if new_body is not None: + new_body = new_body.split(MARKER_BODY, 1)[0].rstrip('\n') + if is_markdown: + new_body = markdown.markdown(new_body) + return new_body \ No newline at end of file diff --git a/bbcli/services/assignment_service.py b/bbcli/services/assignments_services.py similarity index 64% rename from bbcli/services/assignment_service.py rename to bbcli/services/assignments_services.py index 74d514f01a7bc6c2cdf68469907d5fa84248f300..5b4b22bcd3ebd4c521925a7e73abeae24b57f234 100644 --- a/bbcli/services/assignment_service.py +++ b/bbcli/services/assignments_services.py @@ -3,7 +3,7 @@ import json import click import requests import dateutil.parser -from bbcli.services.contents_service import upload_file +from bbcli.services.contents_services import upload_file from bbcli.services.utils.attempt_builder import AttemptBuilder from tabulate import tabulate @@ -19,30 +19,12 @@ def get_assignments(session: requests.Session, course_id): response.raise_for_status() response = json.loads(response.text) results = response['results'] - print_assignments(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, print_submitted): +def get_column_attempts(session: requests.Session, course_id, column_id): url = url_builder.base_v2().add_courses().add_id(course_id).add_gradebook( ).add_columns().add_id(column_id).add_attempts().create() @@ -50,41 +32,7 @@ def get_column_attempts(session: requests.Session, course_id, column_id, print_s response.raise_for_status() response = json.loads(response.text) results = response['results'] - - if print_submitted: - print_submitted_attempts(results) - else: - print_all_attempts(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) - + return results 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( @@ -93,7 +41,7 @@ def get_column_attempt(session: requests.Session, course_id, column_id, attempt_ response = session.get(url) attempt = json.loads(response.text) attempt = json.dumps(attempt, indent=2) - click.echo(attempt) + return attempt def create_column_attempt(session: requests.Session, course_id, column_id, studentComments=None, studentSubmission=None, dst: str = None, status=None, draft: bool = False): @@ -109,8 +57,8 @@ def create_column_attempt(session: requests.Session, course_id, column_id, stude data = attempt.create_json() json_data = json.dumps(data, indent=2) response = session.post(url, data=json_data) + response.raise_for_status() response_json = json.loads(response.text) - click.echo(response_json) if dst is not None and response.status_code == 201: attempt_id = response_json['id'] @@ -119,6 +67,9 @@ def create_column_attempt(session: requests.Session, course_id, column_id, stude return update_column_attempt(session, course_id, column_id, attempt_id, status='NeedsGrading') + + print(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): @@ -134,11 +85,11 @@ def update_column_attempt(session: requests.Session, course_id, column_id, attem response = session.patch(url, data=json_data) response.raise_for_status() response = json.loads(response.text) - click.echo(response) if dst is not None: attach_file(session, course_id, attempt_id, dst) + 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/authorization_service.py b/bbcli/services/authorization_service.py index 16c6dcc5874cb36601ab6b223cb40f6f0b10af63..a8469106dad18aa81ad68ef7d6a63dad919c8166 100644 --- a/bbcli/services/authorization_service.py +++ b/bbcli/services/authorization_service.py @@ -21,7 +21,6 @@ saml_response = None # TODO: Add better error handling here. WIth try catch etc. - def login(): click.echo("Logging in...") # TODO: Let user choose between feide log in or ID-gate diff --git a/bbcli/services/contents_service.py b/bbcli/services/contents_services.py similarity index 60% rename from bbcli/services/contents_service.py rename to bbcli/services/contents_services.py index 131da08b31818d02f06e1c4bf318ea1203839e73..936f4d78b9cc08b7f24b7b11135a7425dfd3a03c 100644 --- a/bbcli/services/contents_service.py +++ b/bbcli/services/contents_services.py @@ -1,5 +1,6 @@ import json import os +from typing import Dict, List import requests import magic from bbcli.utils.URL_builder import Builder, URL_builder @@ -8,47 +9,43 @@ from bbcli.entities.content_builder_entitites import FileContent, GradingOptions from bbcli.utils.utils import input_body import click import webbrowser +import markdown +import markdownify -from bbcli.utils.utils import check_response, get_download_path +from bbcli.utils.utils import get_download_path url_builder = URL_builder() content_builder = ContentBuilder() # User gets a tree structure view of the courses content # where each content is listed something like this: _030303_1 Lectures Folder -def list_contents(session: requests.Session, course_id): +def list_contents(session: requests.Session, course_id: str) -> requests.models.Response: url = url_builder.base_v1().add_courses().add_id(course_id).add_contents().create() response = session.get(url) response.raise_for_status() return response # get the children of a specific folder -def get_children(session: requests.Session, course_id: str, node_id: str): +def get_children(session: requests.Session, course_id: str, node_id: str) -> requests.models.Response: url = url_builder.base_v1().add_courses().add_id( course_id).add_contents().add_id(node_id).add_children().create() response = session.get(url) response.raise_for_status() return response -# If it is a folder, list it like a tree structure view like mentioned above. -# If it is a document, download and open the document maybe? -# Find all types of content and have an appropriate response for them. This -# should maybe be handled in the view... - - -def get_content(session: requests.Session, course_id: str, node_id: str): +def get_content(session: requests.Session, course_id: str, node_id: str) -> requests.models.Response: url = url_builder.base_v1().add_courses().add_id( course_id).add_contents().add_id(node_id).create() response = session.get(url) response.raise_for_status() return response -def get_content_targetid(session: requests.Session, course_id: str, target_id: str): +def get_content_targe_tid(session: requests.Session, course_id: str, target_id: str) -> requests.models.Response: url = url_builder.base_v1().add_courses().add_id( course_id).add_contents().add_id(target_id).create() return session.get(url) -def get_attachments(session: requests.Session, course_id: str, node_id: str): +def get_attachments(session: requests.Session, course_id: str, node_id: str) -> requests.models.Response: url = url_builder.base_v1().add_courses().add_id( course_id).add_contents().add_id(node_id).add_attachments().create() response = session.get(url) @@ -75,7 +72,7 @@ def download_attachment(session: requests.Session, course_id: str, node_id: str, click.echo(f'\"{fn}\" was downloaded to \"{path}\".') return downloads_path -def download_attachments(session: requests.Session, course_id: str, node_id: str, attachments, path): +def download_attachments(session: requests.Session, course_id: str, node_id: str, attachments: List, path: str) -> List: paths = [] for attachment in attachments: downloads_path = download_attachment(session, course_id, node_id, attachment, path) @@ -83,10 +80,10 @@ def download_attachments(session: requests.Session, course_id: str, node_id: str paths.append(downloads_path) return paths -def open_file(path): +def open_file(path: str) -> None: webbrowser.open(r'file:'+path) -def upload_attachment(session: requests.Session, course_id: str, content_id: str, file_dst: str): +def upload_attachment(session: requests.Session, course_id: str, content_id: str, file_dst: str) -> Dict: uploaded_file = upload_file(session, file_dst) data = json.dumps(uploaded_file) @@ -94,11 +91,16 @@ 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() + response = json.loads(response.text) + return response - -def create_document(session: requests.Session, course_id: str, parent_id: str, title: str, standard_options: StandardOptions = None, attachments: tuple = None): +def create_document(session: requests.Session, course_id: str, parent_id: str, title: str, + standard_options: StandardOptions, attachments: tuple, is_markdown: bool) -> Dict: data_body = input_body() + if is_markdown: + data_body = markdown.markdown(data_body) + data = content_builder\ .add_parent_id(parent_id)\ .add_title(title)\ @@ -111,15 +113,15 @@ def create_document(session: requests.Session, course_id: str, parent_id: str, t url = generate_create_content_url(course_id, parent_id) response = session.post(url, data=data) response.raise_for_status() + response = json.loads(response.text) - created_content_id = json.loads(response.text)['id'] + created_content_id = response['id'] handle_attachments(session, course_id, created_content_id, attachments) - return response.text + return response -# TODO: Bug that if a file is created with an attachment, the attachment takes the place of the actual file for the content. In addition, -# if two attachments is added, only the last one is added/overwrite the first one -def create_file(session: requests.Session, course_id: str, parent_id: str, title: str, file_dst: str, file_options: FileOptions, standard_options: StandardOptions): +def create_file(session: requests.Session, course_id: str, parent_id: str, title: str, + file_dst: str, file_options: FileOptions, standard_options: StandardOptions) -> Dict: uploaded_file = upload_file(session, file_dst) mime = magic.Magic(mime=True) @@ -142,11 +144,12 @@ def create_file(session: requests.Session, course_id: str, parent_id: str, title url = generate_create_content_url(course_id, parent_id) response = session.post(url, data=data) response.raise_for_status() - - return response.text + response = json.loads(response.text) + return response -def create_externallink(session: requests.Session, course_id: str, parent_id: str, title: str, url: str, web_link_options: WeblinkOptions, standard_options: StandardOptions): +def create_externallink(session: requests.Session, course_id: str, parent_id: str, title: str, + url: str, web_link_options: WeblinkOptions, standard_options: StandardOptions) -> Dict: data = content_builder\ .add_parent_id(parent_id)\ @@ -160,12 +163,16 @@ def create_externallink(session: requests.Session, course_id: str, parent_id: st url = generate_create_content_url(course_id, parent_id) response = session.post(url, data=data) response.raise_for_status() - return response.text + response = json.loads(response.text) + return response -def create_folder(session: requests.Session, course_id: str, parent_id: str, title: str, is_bb_page: bool, standard_options: StandardOptions): +def create_folder(session: requests.Session, course_id: str, parent_id: str,title: str, + is_bb_page: bool, standard_options: StandardOptions, is_markdown: bool) -> Dict: data_body = input_body() + if is_markdown: + data_body = markdown.markdown(data_body) data = content_builder\ .add_title(title)\ @@ -184,13 +191,17 @@ def create_folder(session: requests.Session, course_id: str, parent_id: str, tit data = json.dumps(data) response = session.post(url, data=data) response.raise_for_status() - return response.text + response = json.loads(response.text) + return response # TODO:FUNKER IKKE PGA targetType -def create_courselink(session: requests.Session, course_id: str, parent_id: str, title: str, target_id: str, standard_options: StandardOptions): +def create_courselink(session: requests.Session, course_id: str, parent_id: str, title: str, + target_id: str, standard_options: StandardOptions, is_markdown: bool) -> Dict: data_body = input_body() + if is_markdown: + data_body = markdown.markdown(data_body) data = content_builder\ .add_title(title)\ @@ -204,10 +215,15 @@ def create_courselink(session: requests.Session, course_id: str, parent_id: str, response = session.post(url, data=data) response.raise_for_status() - return response.text + response = json.loads(response.text) + return response -def create_assignment(session: requests.Session, course_id: str, parent_id: str, title: str, standard_options: StandardOptions, grading_options: GradingOptions, attachments: tuple = None): +def create_assignment(session: requests.Session, course_id: str, parent_id: str, title: str, + standard_options: StandardOptions, grading_options: GradingOptions, + attachments: tuple, is_markdown: bool) -> Dict: instructions = input_body() + if is_markdown: + instructions = markdown.markdown(instructions) data = content_builder\ .add_parent_id(parent_id)\ @@ -230,10 +246,10 @@ def create_assignment(session: requests.Session, course_id: str, parent_id: str, course_id).add_contents().add_create_assignment().create() response = session.post(url, data=data) response.raise_for_status() - return response.text - + response = json.loads(response.text) + return response -def delete_content(session: requests.Session, course_id: str, content_id: str, delete_grades: bool): +def delete_content(session: requests.Session, course_id: str, content_id: str, delete_grades: bool) -> requests.models.Response: parameters = { 'deleteGrades': delete_grades } @@ -243,26 +259,55 @@ def delete_content(session: requests.Session, course_id: str, content_id: str, d response.raise_for_status() return response +def update_content(session: requests.Session, course_id: str, content_id: str, is_markdown: bool) -> Dict: + url = url_builder.base_v1().add_courses().add_id( + course_id).add_contents().add_id(content_id).create() + content = session.get(url) + content = json.loads(content.text) + content_type = content['contentHandler']['id'] + + validate_content_type(content_type) + + new_title = edit_title(content) -def update_content(session: requests.Session, course_id: str, content_id: str): + data = update_content_data(content, is_markdown) + data['title'] = new_title + data = json.dumps(data) + + response = session.patch(url, data=data) + response.raise_for_status() + response = json.loads(response.text) + return response + +def update_content_advanced(session: requests.Session, course_id: str, content_id: str, is_markdown: bool) -> Dict: url = url_builder.base_v1().add_courses().add_id( course_id).add_contents().add_id(content_id).create() content = session.get(url) content = json.loads(content.text) + if 'body' in content and is_markdown: + content['body'] = markdownify.markdownify(content['body']) + if not is_editable_content_type(content['contentHandler']['id']): click.echo('This content type is not editable') raise click.Abort() - if 'contentHandler' in content: - del content['contentHandler'] if 'links' in content: del content['links'] MARKER = '# Everything below is ignored.\n' editable_data = json.dumps(content, indent=2) - new_data = click.edit(editable_data + '\n\n' + MARKER) + data = click.edit(editable_data + '\n\n' + MARKER) + new_data = data if data != None else editable_data + if new_data is not None: + new_data = new_data.split(MARKER, 1)[0].rstrip('\n') + + if 'body' in content and is_markdown: + new_data = json.loads(new_data) + new_data['body'] = markdown.markdown(new_data['body']) + new_data = json.dumps(new_data, indent=2) response = session.patch(url, data=new_data) response.raise_for_status() - return response.text + response = json.loads(response.text) + return response """ @@ -272,7 +317,7 @@ HELPER FUNCTIONS """ -def upload_file(session: requests.Session, dst: str): +def upload_file(session: requests.Session, dst: str) -> Dict: del session.headers['Content-Type'] with open(dst, 'rb') as f: @@ -289,7 +334,7 @@ def upload_file(session: requests.Session, dst: str): return file -def generate_create_content_url(course_id: str, content_id: str): +def generate_create_content_url(course_id: str, content_id: str) -> str: return url_builder\ .base_v1()\ .add_courses()\ @@ -300,16 +345,72 @@ def generate_create_content_url(course_id: str, content_id: str): .create() -def handle_attachments(session: requests.Session, course_id: str, content_id: str, attachments: tuple or None): +def handle_attachments(session: requests.Session, course_id: str, content_id: str, attachments: tuple or None) -> None: if attachments: for attachment in attachments: upload_attachment(session, course_id, content_id, attachment) -def is_editable_content_type(content_type: str): +def is_editable_content_type(content_type: str) -> bool: valid_content_types = ['resource/x-bb-assignment', 'resource/x-bb-externallink', 'resource/x-bb-courselink', 'resource/x-bb-file', 'resource/x-bb-document'] for type in valid_content_types: if content_type == type: return True return False + +def validate_content_type(content_type: str) -> None: + if not is_editable_content_type(content_type): + click.echo('This content type is not editable') + raise click.Abort() + +def update_default_content(content: Dict, is_markdown: bool=False) -> Dict: + try: + content['body'] + except KeyError: + content['body'] = '' + if is_markdown: + content['body'] = markdownify.markdownify(content['body']) + MARKER_BODY = '# Edit body. Everything below is ignored.\n' + data = click.edit(content['body'] + '\n\n' + MARKER_BODY) + new_data = data if data != None else content['body'] + if new_data is not None: + new_data = new_data.split(MARKER_BODY, 1)[0].rstrip('\n') + if is_markdown: + new_data = markdown.markdown(new_data) + return {'body': new_data} + +def update_external_link_content(content: Dict) -> Dict: + MARKER_URL = '# Edit URL. Everything below is ignored.\n' + data = click.edit(content['contentHandler']['url'] + '\n\n' + MARKER_URL) + new_data = data if data != None else content['contentHandler']['url'] + if new_data is not None: + new_data = new_data.split(MARKER_URL, 1)[0].rstrip('\n') + return { + 'contentHandler': { + 'id': 'resource/x-bb-externallink', + 'url': new_data + } + } + +def edit_title(data: Dict) -> str: + MARKER_TITLE = '# Edit title. Everything below is ignored.\n' + title = click.edit(data['title'] + '\n\n' + MARKER_TITLE) + new_title = title if title != None else data['title'] + if new_title is not None: + new_title = new_title.split(MARKER_TITLE, 1)[0].rstrip('\n') + return new_title + +def update_file_content() -> None: + return {} + +def update_content_data(content: Dict, is_markdown: bool) -> Dict: + content_type = content['contentHandler']['id'] + + if content_type == 'resource/x-bb-assignment' or content_type == 'resource/x-bb-courselink' or content_type == 'resource/x-bb-document': + data = update_default_content(content, is_markdown) + elif content_type == 'resource/x-bb-externallink': + data = update_external_link_content(content) + elif content_type == 'resource/x-bb-file': + data = update_file_content() + return data \ No newline at end of file diff --git a/bbcli/services/courses_service.py b/bbcli/services/courses_service.py deleted file mode 100644 index 2f5454bffc9ad148372df9fb9d9a79aa012107e4..0000000000000000000000000000000000000000 --- a/bbcli/services/courses_service.py +++ /dev/null @@ -1,96 +0,0 @@ -import json -from typing import Dict, Any, List -import requests -from datetime import date - -from bbcli.utils.URL_builder import URL_builder - -url_builder = URL_builder() - - -def list_courses(session: requests.Session, user_name: str) -> Any: - - terms = get_terms(session) - sort_terms(terms) - - term_1 = terms[len(terms) - 1] - term_2 = terms[len(terms) - 2] - - 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'] - }) - else: - break - - return course_list - - -def list_all_courses(session: requests.Session, user_name: str) -> Any: - course_memberships = get_course_memberships(session, user_name) - - course_list = get_courses_from_course_memberships(session, course_memberships) - - return course_list - - -def list_course(session: requests.Session, course_id: str) -> Any: - url = url_builder.base_v3().add_courses().add_id(course_id).create() - response = session.get(url) - response.raise_for_status() - return json.loads(response.text) - - -""" - -HELPER FUNCTIONS - -""" - - -def take_start_date(elem): - return date.fromisoformat(elem['availability']['duration']['start'].split('T')[0]) - - -def get_terms(session: requests.Session): - url = url_builder.base_v1().add_terms().create() - terms = session.get(url) - terms.raise_for_status() - terms = json.loads(terms.text)['results'] - return terms - - -def sort_terms(terms): - # Sort terms by start date to get the two most recent semesters to determine which courses to show - for term in terms: - if term['availability']['duration']['type'] != 'DateRange': - terms.remove(term) - terms.sort(key=take_start_date) - - -def get_course_memberships(session: requests.Session, user_name: str): - url = url_builder.base_v1().add_users().add_id( - id=user_name, id_type='userName').add_courses().create() - course_memberships = session.get(url) - course_memberships.raise_for_status() - course_memberships = json.loads(course_memberships.text)['results'] - return course_memberships - -def get_courses_from_course_memberships(session: requests.Session, course_memberships: List): - courses = [] - 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.raise_for_status() - response = json.loads(response.text) - courses.append(response) - - return courses \ No newline at end of file diff --git a/bbcli/services/courses_services.py b/bbcli/services/courses_services.py new file mode 100644 index 0000000000000000000000000000000000000000..2a5977ed98ee30b2f69efcf65c8eef641c33b481 --- /dev/null +++ b/bbcli/services/courses_services.py @@ -0,0 +1,92 @@ +import json +from typing import Dict, List +import requests +from datetime import date + +from bbcli.utils.URL_builder import URL_builder + +url_builder = URL_builder() + +# Commented out code depends whether we want all courses or just the ones from most recent semesters + +# def list_courses(session: requests.Session, user_name: str) -> Any: + +# terms = get_terms(session) +# sort_terms(terms) + +# term_1 = terms[len(terms) - 1] +# term_2 = terms[len(terms) - 2] + +# 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(course) +# else: +# break + +# return course_list + + +def list_all_courses(session: requests.Session, user_name: str) -> List: + course_memberships = get_course_memberships(session, user_name) + + course_list = get_courses_from_course_memberships(session, course_memberships) + return course_list + + +def list_course(session: requests.Session, course_id: str) -> Dict: + url = url_builder.base_v3().add_courses().add_id(course_id).create() + response = session.get(url) + response.raise_for_status() + return json.loads(response.text) + +""" + +HELPER FUNCTIONS + +""" + + +# def take_start_date(elem): +# return date.fromisoformat(elem['availability']['duration']['start'].split('T')[0]) + + +# def get_terms(session: requests.Session): +# url = url_builder.base_v1().add_terms().create() +# terms = session.get(url) +# terms.raise_for_status() +# terms = json.loads(terms.text)['results'] +# return terms + + +# def sort_terms(terms): +# # Sort terms by start date to get the two most recent semesters to determine which courses to show +# for term in terms: +# if term['availability']['duration']['type'] != 'DateRange': +# terms.remove(term) +# terms.sort(key=take_start_date) + + +def get_course_memberships(session: requests.Session, user_name: str) -> List: + url = url_builder.base_v1().add_users().add_id( + id=user_name, id_type='userName').add_courses().create() + course_memberships = session.get(url) + course_memberships.raise_for_status() + course_memberships = json.loads(course_memberships.text)['results'] + return course_memberships + +def get_courses_from_course_memberships(session: requests.Session, course_memberships: List) -> List: + courses = [] + for course in course_memberships: + url = url_builder.base_v3().add_courses().add_id( + course['courseId']).create() + response = session.get(url) + response.raise_for_status() + response = json.loads(response.text) + if response['availability']['available'] == 'Yes': + courses.append(response) + + return courses \ No newline at end of file diff --git a/bbcli/shell-completion/.bb-complete.bash b/bbcli/shell-completion/.bb-complete.bash deleted file mode 100644 index eeaf503405fc987c743addc02e78ec8e6ae01326..0000000000000000000000000000000000000000 --- a/bbcli/shell-completion/.bb-complete.bash +++ /dev/null @@ -1 +0,0 @@ -_BB_COMPLETE=bash_source bb > ~/.bb-complete.bash \ No newline at end of file diff --git a/bbcli/shell-completion/.bb-complete.fish b/bbcli/shell-completion/.bb-complete.fish deleted file mode 100644 index ea9ac20c229d291bf44695e0afed70eed91dfd8e..0000000000000000000000000000000000000000 --- a/bbcli/shell-completion/.bb-complete.fish +++ /dev/null @@ -1 +0,0 @@ -_BB_COMPLETE=fish_source bb > ~/.config/fish/completions/bb.fish \ No newline at end of file diff --git a/bbcli/shell-completion/.bb-complete.zsh b/bbcli/shell-completion/.bb-complete.zsh deleted file mode 100644 index f9e49a575ce2559d7d0af8b2f872ae9071da437e..0000000000000000000000000000000000000000 --- a/bbcli/shell-completion/.bb-complete.zsh +++ /dev/null @@ -1 +0,0 @@ -_BB_COMPLETE=zsh_source bb > ~/.bb-complete.zsh \ No newline at end of file diff --git a/bbcli/utils/content_utils.py b/bbcli/utils/content_utils.py index 88b8ac1ad3572ed7ef2cad0559997a624b455fff..c75aa222e5fa4bfd89ace4f338cdedbb026cce96 100644 --- a/bbcli/utils/content_utils.py +++ b/bbcli/utils/content_utils.py @@ -1,11 +1,11 @@ import click import webbrowser -from bbcli.services import contents_service +from bbcli.services import contents_services from bbcli.utils.utils import check_response, html_to_text from bbcli.entities.Node import Node from bbcli.utils.content_handler import content_handler -from bbcli.views import contents_view +from bbcli.views import contents_views def is_folder(node): key = 'contentHandler' @@ -17,7 +17,7 @@ def get_children(ctx, course_id, worklist, folder_ids, node_ids): else: node = worklist.pop(0) node_id = node.data['id'] - response = contents_service.get_children( + response = contents_services.get_children( ctx.obj['SESSION'], course_id, node_id) if check_response(response) == False: return @@ -41,7 +41,7 @@ def get_folders(ctx, course_id, worklist, folder_ids, node_ids): else: node = worklist.pop(0) node_id = node.data['id'] - response = contents_service.get_children( + response = contents_services.get_children( ctx.obj['SESSION'], course_id, node_id) if check_response(response) == False: return @@ -63,7 +63,7 @@ def get_content_type(ctx, course_id, worklist, folder_ids, node_ids, content_typ else: node = worklist.pop(0) node_id = node.data['id'] - response = contents_service.get_children( + response = contents_services.get_children( ctx.obj['SESSION'], course_id, node_id) if check_response(response) == False: return @@ -108,25 +108,25 @@ def list_contents_thread( def check_content_handler(ctx, course_id: str, node_id: str, path: str): session = ctx.obj['SESSION'] - response = contents_service.get_content( + response = contents_services.get_content( ctx.obj['SESSION'], course_id, node_id) if check_response(response) == False: return data = response.json() ch = data['contentHandler']['id'] if ch == content_handler['document']: - response = contents_service.get_attachments(session, course_id, node_id) + response = contents_services.get_attachments(session, course_id, node_id) attachments = response.json()['results'] str = data['title'] + '\n' body = '' if 'body' not in data else html_to_text(data['body']) str += body - contents_view.open_less_page(str) + contents_views.open_less_page(str) if len(attachments) > 0: click.confirm( "This is a document with an attachment(s), do you want to download it?", abort=True) - paths = contents_service.download_attachments(session, course_id, node_id, attachments, path) - [contents_service.open_file(path) for path in paths] + paths = contents_services.download_attachments(session, course_id, node_id, attachments, path) + [contents_services.open_file(path) for path in paths] else: click.echo('The document has no attachments.') elif ch == content_handler['externallink']: @@ -142,7 +142,7 @@ def check_content_handler(ctx, course_id: str, node_id: str, path: str): worklist = [root] get_children(ctx, course_id, worklist, folder_ids, node_ids) root_node = root.preorder() - contents_view.list_tree(root_node, folder_ids, node_ids) + contents_views.list_tree(root_node, folder_ids, node_ids) elif ch == content_handler['courselink']: click.echo('Opening the contents of a courselink...') key = 'targetId' @@ -150,34 +150,34 @@ def check_content_handler(ctx, course_id: str, node_id: str, path: str): target_id = data['contentHandler'][key] check_content_handler(ctx, course_id, target_id) elif ch == content_handler['file']: - response = contents_service.get_attachments( + response = contents_services.get_attachments( session, course_id, node_id) attachments = response.json()['results'] if len(attachments) > 0: click.confirm( "This is a file, do you want to download and open it?", abort=True) - paths = contents_service.download_attachments( + paths = contents_services.download_attachments( session, course_id, node_id, attachments, path) - [contents_service.open_file(path) for path in paths] + [contents_services.open_file(path) for path in paths] elif ch == content_handler['assignment']: click.echo('Opening assignment...') str = data['title'] + '\n' + html_to_text(data['body']) - contents_view.open_less_page(str) - response = contents_service.get_attachments( + contents_views.open_less_page(str) + response = contents_services.get_attachments( session, course_id, node_id) attachments = response.json()['results'] if len(attachments) > 0: click.confirm( "The assignment contains attachment(s), do you want to download?", abort=True) - paths = contents_service.download_attachments( + paths = contents_services.download_attachments( session, course_id, node_id, attachments, path) - [contents_service.open_file(path) for path in paths] + [contents_services.open_file(path) for path in paths] elif ch == content_handler['blankpage']: click.echo('Opening blankpage...') str = data['title'] + '\n' + html_to_text(data['body']) - contents_view.open_less_page(str) + contents_views.open_less_page(str) else: click.echo('The cli does not currently support the content type.') diff --git a/bbcli/utils/utils.py b/bbcli/utils/utils.py index 3c7171775089599f0ee9a446e3ba84d9fdcc6c0e..876ecf7868216f509b87e25998bac02c480862d0 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 @@ -115,4 +115,12 @@ 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]))) + diff --git a/bbcli/views/announcement_view.py b/bbcli/views/announcements_views.py similarity index 64% rename from bbcli/views/announcement_view.py rename to bbcli/views/announcements_views.py index 26fce2b84c56292621e8f18223af989cd132d951..0abb97a5759ee4c5faae8c76a5cec2db0bcbc0b1 100644 --- a/bbcli/views/announcement_view.py +++ b/bbcli/views/announcements_views.py @@ -1,8 +1,9 @@ +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): +def print_announcement(announcement: Dict) -> None: announcement_id = announcement['id'] title = announcement['title'] body = html_to_text(announcement['body']) @@ -13,21 +14,30 @@ def print_announcement(announcement: Dict): click.echo('{:<15} {:<15}'.format('Date: ', created)) click.echo('\n{:<15}\n'.format(body)) -def print_announcements(announcements: List): +def print_announcements(announcements: List) -> None: announcements.reverse() for course in announcements: print_course_announcements(course['course_announcements'], course['course_name']) -def print_course_announcements(course_announcements: List, course_name: str = None): +def print_course_announcements(course_announcements: List, course_name: str = None) -> 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') @@ -36,12 +46,13 @@ def print_course_announcements(course_announcements: List, course_name: str = No click.echo('{:<15} {:<15}'.format('Date: ', created)) click.echo('\n{:<15}\n'.format(body)) -def print_announcement_created(announcement): - click.echo('\nAnnouncement sucessfully created:\n\n' + announcement) - +def print_announcement_created(announcement: Dict) -> None: + click.echo('\nAnnouncement sucessfully created:\n') + print_keys_in_dict(announcement) -def print_announcement_deleted(): +def print_announcement_deleted() -> None: click.echo('\nAnnouncement sucessfully deleted.\n') -def print_announcement_updated(announcement): - click.echo('\nAnnouncement sucessfully updated:\n\n' + announcement) +def print_announcement_updated(announcement: Dict) -> None: + click.echo('\nAnnouncement sucessfully updated:\n') + print_keys_in_dict(announcement) \ No newline at end of file diff --git a/bbcli/views/assignments_views.py b/bbcli/views/assignments_views.py new file mode 100644 index 0000000000000000000000000000000000000000..b5fef147744d85a73423bbb3b90a1bde19351c25 --- /dev/null +++ b/bbcli/views/assignments_views.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 deleted file mode 100644 index 39f320edbddc7c33796cfb224550e30bf028ad9c..0000000000000000000000000000000000000000 --- a/bbcli/views/contents_view.py +++ /dev/null @@ -1,46 +0,0 @@ -from anytree import Node as Nd, RenderTree -from colorama import Fore, Style -from bbcli.utils.utils import html_to_text -import click -import tempfile, os -from subprocess import call - -def list_tree(root, folder_ids, node_ids): - # color = Fore.RESET if only_folders else Fore.BLUE - color = Fore.BLUE - for pre, fill, node in RenderTree(root): - node_id = node.name.split()[0] - if node_id in folder_ids: - click.echo(f'{pre}{color} {node.name} {Style.RESET_ALL}') - elif node_id in node_ids: - click.echo(f'{pre} {node.name}') - else: - click.echo('Neither node nor folder.') - - - - -def open_vim(data): - str = data['title'] + '\n' - str += html_to_text(data['body']) - - EDITOR = os.environ.get('EDITOR','vim') - - initial_message = bytearray(str, encoding='utf8') - - with tempfile.NamedTemporaryFile(suffix=".tmp") as tf: - tf.write(initial_message) - tf.flush() - call([EDITOR, tf.name]) - - # do the parsing with `tf` using regular File operations. - # for instance: - # tf.seek(0) - # edited_message = tf.read() - # print (edited_message.decode("utf-8")) - -def open_less_page(str): - import pydoc - pydoc.pager(str) - - diff --git a/bbcli/views/contents_views.py b/bbcli/views/contents_views.py new file mode 100644 index 0000000000000000000000000000000000000000..576299339a8ccb9db010a85eec8e429f4eb3ac9a --- /dev/null +++ b/bbcli/views/contents_views.py @@ -0,0 +1,71 @@ +import json +from typing import Dict +from anytree import Node as Nd, RenderTree +from colorama import Fore, Style +from bbcli.utils.utils import html_to_text, print_keys_in_dict +import click +import tempfile, os +from subprocess import call + +def list_tree(root: Nd, folder_ids: str, node_ids: str) -> None: + # color = Fore.RESET if only_folders else Fore.BLUE + color = Fore.BLUE + for pre, fill, node in RenderTree(root): + node_id = node.name.split()[0] + if node_id in folder_ids: + click.echo(f'{pre}{color} {node.name} {Style.RESET_ALL}') + elif node_id in node_ids: + click.echo(f'{pre} {node.name}') + else: + click.echo('Neither node nor folder.') + + + + +def open_vim(data: Dict) -> None: + data_string = data['title'] + '\n' + data_string += html_to_text(data['body']) + + EDITOR = os.environ.get('EDITOR','vim') + + initial_message = bytearray(data_string, encoding='utf8') + + with tempfile.NamedTemporaryFile(suffix=".tmp") as tf: + tf.write(initial_message) + tf.flush() + call([EDITOR, tf.name]) + + # do the parsing with `tf` using regular File operations. + # for instance: + # tf.seek(0) + # edited_message = tf.read() + # print (edited_message.decode("utf-8")) + +def open_less_page(data_string: str) -> None: + import pydoc + pydoc.pager(data_string) + + +def print_created_attachment_response(response: Dict, print_json: bool) -> None: + 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: Dict, print_json: bool) -> None: + 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() -> None: + click.echo('\nContent successfully deleted.\n') + +def print_updated_content_response(response: Dict, print_json: bool) -> None: + 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_views.py b/bbcli/views/courses_views.py new file mode 100644 index 0000000000000000000000000000000000000000..1f296fb7099f3c2d9a88ecf829ef5d20c9a51d20 --- /dev/null +++ b/bbcli/views/courses_views.py @@ -0,0 +1,29 @@ +import json +from typing import Dict, List +import click +from tabulate import tabulate + +def print_courses(courses: List, print_json: bool) -> None: + 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: Dict, print_json: bool) -> None: + 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/requirements.txt b/requirements.txt index 0b921f102b749e64bb165d20e6636bf6cf9d7207..1877beb7d8c70c9a23c526e938d72aa1a81b18b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,6 @@ python-magic==0.4.25 python-dateutil==2.8.2 tabulate==0.8.9 pwinput==1.0.2 -nose==1.3.7 \ No newline at end of file +nose==1.3.7 +Markdown==3.3.6 +markdownify==0.11.2 \ No newline at end of file diff --git a/setup.py b/setup.py index 60bbc77de9cacaf41b2e79575c9702ab1c933174..907832fc263100ab086855eb13665657ac1e0dfe 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,9 @@ requires = [ 'python-magic', 'python-dateutil', 'tabulate==0.8.9', - 'pwinput' + 'pwinput', + 'Markdown==3.3.6', + 'markdownify==0.11.2' ] def setup_package(): diff --git a/tests/test_services/test_announcements_services.py b/tests/test_services/test_announcements_services.py index db18c09e2dc36fbf674df19d0631490ce5834091..cd1056cbe8e607a5b106a299f40f8a3e649128e5 100644 --- a/tests/test_services/test_announcements_services.py +++ b/tests/test_services/test_announcements_services.py @@ -8,12 +8,12 @@ from unittest.mock import Mock, patch from nose.tools import assert_list_equal, assert_equal, raises from bbcli.entities.content_builder_entitites import DateInterval -from bbcli.services.announcements_service import create_announcement, delete_announcement, list_announcement, list_announcements, list_course_announcements, update_announcement +from bbcli.services.announcements_services import create_announcement, delete_announcement, list_announcement, list_announcements, list_course_announcements, update_announcement from bbcli.utils.utils import format_date TEST_ANNOUNCEMENT = {"id":"_388961_1","title":"TEST annonucement","body":"This is a test announcement","creator":"_140040_1","draft":False,"availability":{"duration":{"type":"Restricted","start":"2022-04-12T08:06:33.422Z","end":None}},"created":"2022-04-12T08:06:33.423Z","modified":"2022-04-12T08:06:33.452Z","position":2} -UPDATED_TEST_ANNOUNCEMENT = {"id":"_388961_1","title":"TEST annonucement updated","body":"This is a test announcement updated","creator":"_140040_1","draft":False,"availability":{"duration":{"type":"Restricted","start":"2022-04-12T08:06:33.422Z","end":None}},"created":"2022-04-12T08:06:33.423Z","modified":"2022-04-12T08:06:33.452Z","position":2} +UPDATED_TEST_ANNOUNCEMENT = {"id":"_388961_1","title":"TEST TITLE","body":"TEST BODY","creator":"_140040_1","draft":False,"availability":{"duration":{"type":"Restricted","start":"2022-04-12T08:06:33.422Z","end":None}},"created":"2022-04-12T08:06:33.423Z","modified":"2022-04-12T08:06:33.452Z","position":2} TEST_COURSE_ANNOUNCEMENTS_LIST = [{'id': '_389054_1', 'title': 'Test announcement', 'body': 'This is a test announcement', 'creator': '_140040_1', 'draft': False, 'availability': {'duration': {'type': 'Restricted', 'start': '2022-04-15T22:04:00.000Z', 'end': None}}, 'created': '2022-04-15T13:50:15.623Z', 'modified': '2022-04-15T14:38:43.049Z', 'position': 2}, {'id': '_389055_1', 'title': 'Test announcement', 'creator': '_140040_1', 'draft': False, 'availability': {'duration': {'type': 'Restricted', 'start': '2022-04-15T22:04:00.000Z', 'end': None}}, 'created': '2022-04-15T13:55:57.898Z', 'modified': '2022-04-15T13:55:57.926Z', 'position': 1}, {'id': '_389026_1', 'title': 'Testing announcement', 'body': 'Here is a new announcement. Here is a \n<a href="https://ntnu.no">link</a>.', 'creator': '_36000_1', 'draft': False, 'availability': {'duration': {'type': 'Restricted', 'start': '2022-04-13T16:33:01.758Z', 'end': None}}, 'created': '2022-04-13T16:33:01.759Z', 'modified': '2022-04-13T16:33:01.811Z', 'position': 3}] @@ -58,7 +58,7 @@ class TestAnnouncementsServices(object): def setup_class(cls): cls.test_session = requests.Session() - cls.mock_get_patcher = patch('bbcli.services.announcements_service.requests.Session.get') + cls.mock_get_patcher = patch('bbcli.services.announcements_services.requests.Session.get') cls.mock_auth_patcher = patch('bbcli.cli.authenticate_user') cls.mock_get = cls.mock_get_patcher.start() @@ -102,10 +102,10 @@ class TestAnnouncementsServices(object): 'results': TEST_COURSE_ANNOUNCEMENTS_LIST }) - mock_get_courses_patcher = patch('bbcli.services.announcements_service.list_courses') + mock_get_courses_patcher = patch('bbcli.services.announcements_services.list_all_courses') mock_get_courses = mock_get_courses_patcher.start() - mock_get_course_announcements_patcher = patch('bbcli.services.announcements_service.list_course_announcements') + mock_get_course_announcements_patcher = patch('bbcli.services.announcements_services.list_course_announcements') mock_get_course_announcements = mock_get_course_announcements_patcher.start() mock_get_courses.return_value = TEST_COURSES_LIST @@ -122,18 +122,18 @@ class TestAnnouncementsServices(object): def test_create_annonucement(self): self.mock_auth.return_value.ok = True - mock_input_body_patcher = patch('bbcli.services.announcements_service.input_body') - mock_post_patcher = patch('bbcli.services.announcements_service.requests.Session.post') + mock_input_body_patcher = patch('bbcli.services.announcements_services.input_body') + mock_post_patcher = patch('bbcli.services.announcements_services.requests.Session.post') mock_input_body = mock_input_body_patcher.start() mock_post = mock_post_patcher.start() mock_input_body.return_value = 'This is a test announcement' mock_post.return_value.ok = True - mock_post.return_value.text = TEST_CREATED_ANNOUNCEMENT + mock_post.return_value.text = json.dumps(TEST_CREATED_ANNOUNCEMENT) test_date_interval = DateInterval(start_date=format_date('15/04/22 22:00:00')) - response = create_announcement(self.test_session, '_33050_1', 'Test announcement', test_date_interval) + response = create_announcement(self.test_session, '_33050_1', 'Test announcement', test_date_interval, False) mock_input_body_patcher.stop() mock_post_patcher.stop() @@ -144,32 +144,33 @@ class TestAnnouncementsServices(object): def test_create_announcement_with_empty_title(self): self.mock_auth.return_value.ok = True - create_announcement(self.test_session, '_33050_1', '', date_interval=DateInterval()) + create_announcement(self.test_session, '_33050_1', '', DateInterval(), False) @raises(Abort) def test_create_annonucement_with_wrong_date_format(self): self.mock_auth.return_value.ok = True - create_announcement(self.test_session, '_33050_1', 'Test annonucement', DateInterval(start=format_date('16-04-22 12:00'))) + create_announcement(self.test_session, '_33050_1', 'Test annonucement', DateInterval(start=format_date('16-04-22 12:00')), False) def test_delete_announcement(self): self.mock_auth.return_value.ok = True - mock_delete_patcher = patch('bbcli.services.announcements_service.requests.Session.delete') + mock_delete_patcher = patch('bbcli.services.announcements_services.requests.Session.delete') 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): self.mock_auth.return_value.ok = True - mock_delete_patcher = patch('bbcli.services.announcements_service.requests.Session.delete') + mock_delete_patcher = patch('bbcli.services.announcements_services.requests.Session.delete') mock_delete = mock_delete_patcher.start() mock_delete.return_value.ok = False mock_delete.return_value = requests.models.Response() @@ -181,30 +182,27 @@ class TestAnnouncementsServices(object): self.mock_auth.return_value.ok = True mock_update_patcher = patch('bbcli.cli.requests.Session.patch') mock_update = mock_update_patcher.start() - mock_list_annonucement_patcher = patch('bbcli.services.announcements_service.list_announcement') + mock_list_annonucement_patcher = patch('bbcli.services.announcements_services.list_announcement') mock_list_announcement = mock_list_annonucement_patcher.start() mock_list_announcement.return_value.ok = True mock_list_announcement.return_value = TEST_ANNOUNCEMENT - mock_input_body_patcher = patch('bbcli.services.announcements_service.click.edit') - mock_input_body = mock_input_body_patcher.start() + mock_edit_title_patcher = patch('bbcli.services.announcements_services.edit_title') + mock_edit_title = mock_edit_title_patcher.start() + mock_edit_body_patcher = patch('bbcli.services.announcements_services.edit_body') + mock_edit_body = mock_edit_body_patcher.start() - - mock_input_body.return_value = { - 'title': UPDATED_TEST_ANNOUNCEMENT['title'], - 'body': UPDATED_TEST_ANNOUNCEMENT['body'], - 'created': UPDATED_TEST_ANNOUNCEMENT['created'], - 'availability': UPDATED_TEST_ANNOUNCEMENT['availability'], - 'draft': UPDATED_TEST_ANNOUNCEMENT['draft'] - } + mock_edit_title.return_value = 'TEST TITLE' + mock_edit_body.return_value = 'TEST BODY' mock_update.return_value.ok = True mock_update.return_value.text = json.dumps(UPDATED_TEST_ANNOUNCEMENT) - response = update_announcement(self.test_session, 'test_course_id', 'test_announcement_id') + response = update_announcement(self.test_session, 'test_course_id', 'test_announcement_id', False) mock_update_patcher.stop() mock_list_annonucement_patcher.stop() - mock_input_body_patcher.stop() + mock_edit_title_patcher.stop() + mock_edit_body_patcher.stop() - assert_equal(json.loads(response), UPDATED_TEST_ANNOUNCEMENT) \ No newline at end of file + assert_equal(response, UPDATED_TEST_ANNOUNCEMENT) \ No newline at end of file diff --git a/tests/test_services/test_assignments_services.py b/tests/test_services/test_assignments_services.py new file mode 100644 index 0000000000000000000000000000000000000000..ea3e06f0c1af72f3092eabc4617169868b22dde0 --- /dev/null +++ b/tests/test_services/test_assignments_services.py @@ -0,0 +1,107 @@ +import json +from re import M +from typing import List +from click import Abort, BadParameter +import requests + +from unittest.mock import Mock, patch +from nose.tools import assert_list_equal, assert_equal, raises +from bbcli.entities.content_builder_entitites import DateInterval, FileOptions, GradingOptions, StandardOptions, WeblinkOptions +from bbcli.services.assignments_services import create_column_attempt, get_assignments, get_column_attempt, get_column_attempts, update_column_attempt + + +TEST_ASSIGNMENTS_LIST = {"results":[{"id":"_275617_1","name":"Totalt","description":"<p>Den uvektede totalen av alle vurderinger for en bruker.</p>","externalGrade":True,"score":{"possible":3757.00000},"availability":{"available":"No"},"grading":{"type":"Calculated","schemaId":"_328072_1"},"gradebookCategoryId":"_654726_1","formula":{"formula":"{ \"running\":\"True\", \"all\":{\"average\":\"False\"}}"},"includeInCalculations":True,"showStatisticsToStudents":False},{"id":"_277393_1","name":"posted from CLI test","created":"2022-02-23T12:15:42.847Z","contentId":"_1666340_1","score":{"possible":100.00000},"availability":{"available":"Yes"},"grading":{"type":"Attempts","attemptsAllowed":1,"scoringModel":"Last","schemaId":"_328072_1","anonymousGrading":{"type":"None"}},"gradebookCategoryId":"_654732_1","includeInCalculations":True,"showStatisticsToStudents":False,"scoreProviderHandle":"resource/x-bb-assignment"},{"id":"_277394_1","name":"posted from CLI test","created":"2022-02-23T12:18:11.087Z","contentId":"_1666343_1","score":{"possible":100.00000},"availability":{"available":"Yes"},"grading":{"type":"Attempts","attemptsAllowed":1,"scoringModel":"Last","schemaId":"_328072_1","anonymousGrading":{"type":"None"}},"gradebookCategoryId":"_654732_1","includeInCalculations":True,"showStatisticsToStudents":False,"scoreProviderHandle":"resource/x-bb-assignment"},{"id":"_280663_1","name":"Test_04042022_1538","created":"2022-04-04T13:49:32.442Z","contentId":"_1696651_1","score":{"possible":100.00000},"availability":{"available":"Yes"},"grading":{"type":"Attempts","due":"2022-04-05T10:04:00.000Z","attemptsAllowed":1,"scoringModel":"Last","schemaId":"_328072_1","anonymousGrading":{"type":"None"}},"gradebookCategoryId":"_654732_1","includeInCalculations":True,"showStatisticsToStudents":False,"scoreProviderHandle":"resource/x-bb-assignment"},{"id":"_280808_1","name":"testing assignment","created":"2022-04-06T12:52:02.909Z","contentId":"_1698141_1","score":{"possible":69.00000},"availability":{"available":"Yes"},"grading":{"type":"Attempts","attemptsAllowed":1,"scoringModel":"Last","schemaId":"_328072_1","anonymousGrading":{"type":"None"}},"gradebookCategoryId":"_654732_1","includeInCalculations":True,"showStatisticsToStudents":False,"scoreProviderHandle":"resource/x-bb-assignment"},{"id":"_280809_1","name":"testing assignment 2","created":"2022-04-06T12:52:26.148Z","contentId":"_1698143_1","score":{"possible":123.00000},"availability":{"available":"Yes"},"grading":{"type":"Attempts","attemptsAllowed":1,"scoringModel":"Last","schemaId":"_328072_1","anonymousGrading":{"type":"None"}},"gradebookCategoryId":"_654732_1","includeInCalculations":True,"showStatisticsToStudents":False,"scoreProviderHandle":"resource/x-bb-assignment"},{"id":"_280839_1","name":"Mattias har klamma, True or False?","created":"2022-04-07T10:44:18.700Z","contentId":"_1698594_1","score":{"possible":69.00000},"availability":{"available":"Yes"},"grading":{"type":"Attempts","attemptsAllowed":0,"scoringModel":"Last","schemaId":"_328072_1","anonymousGrading":{"type":"None"}},"gradebookCategoryId":"_654732_1","includeInCalculations":True,"showStatisticsToStudents":False,"scoreProviderHandle":"resource/x-bb-assignment"},{"id":"_280850_1","name":"Kommer mattias på DT ikveld mon tro?","created":"2022-04-07T11:58:42.794Z","contentId":"_1698666_1","score":{"possible":96.00000},"availability":{"available":"Yes"},"grading":{"type":"Attempts","attemptsAllowed":1,"scoringModel":"Last","schemaId":"_328072_1","anonymousGrading":{"type":"None"}},"gradebookCategoryId":"_654732_1","includeInCalculations":True,"showStatisticsToStudents":False,"scoreProviderHandle":"resource/x-bb-assignment"},{"id":"_280968_1","name":"Ttest assignment hehehe","created":"2022-04-11T09:08:40.603Z","contentId":"_1699529_1","score":{"possible":100.00000},"availability":{"available":"No"},"grading":{"type":"Attempts","attemptsAllowed":1,"scoringModel":"Last","schemaId":"_328072_1","anonymousGrading":{"type":"None"}},"gradebookCategoryId":"_654732_1","includeInCalculations":True,"showStatisticsToStudents":False,"scoreProviderHandle":"resource/x-bb-assignment"},{"id":"_281043_1","name":"Test assignment","created":"2022-04-18T08:59:50.312Z","contentId":"_1699931_1","score":{"possible":1000.00000},"availability":{"available":"Yes"},"grading":{"type":"Attempts","attemptsAllowed":1,"scoringModel":"Last","schemaId":"_328072_1","anonymousGrading":{"type":"None"}},"gradebookCategoryId":"_654732_1","includeInCalculations":True,"showStatisticsToStudents":False,"scoreProviderHandle":"resource/x-bb-assignment"},{"id":"_281044_1","name":"Test assignment","created":"2022-04-18T09:02:48.606Z","contentId":"_1699933_1","score":{"possible":1000.00000},"availability":{"available":"Yes"},"grading":{"type":"Attempts","attemptsAllowed":1,"scoringModel":"Last","schemaId":"_328072_1","anonymousGrading":{"type":"None"}},"gradebookCategoryId":"_654732_1","includeInCalculations":True,"showStatisticsToStudents":False,"scoreProviderHandle":"resource/x-bb-assignment"},{"id":"_281045_1","name":"Test assignment","created":"2022-04-18T09:03:43.900Z","contentId":"_1699934_1","score":{"possible":1000.00000},"availability":{"available":"Yes"},"grading":{"type":"Attempts","attemptsAllowed":1,"scoringModel":"Last","schemaId":"_328072_1","anonymousGrading":{"type":"None"}},"gradebookCategoryId":"_654732_1","includeInCalculations":True,"showStatisticsToStudents":False,"scoreProviderHandle":"resource/x-bb-assignment"}]} +TEST_ASSIGNMENT_ATTEMPTS_LIST = {"results":[{"id":"_5330107_1","userId":"_111522_1","status":"Completed","displayGrade":{"scaleType":"Score","score":70.00000},"text":"70.00000","score":70.000000000000000,"feedback":"Jaja ca greit nok","studentComments":"Test comment.","studentSubmission":"<p>Test submission.</p>","exempt":False,"created":"2022-04-05T08:36:18.225Z","attemptDate":"2022-04-05T08:36:18.242Z","modified":"2022-04-06T13:38:15.169Z","attemptReceipt":{"receiptId":"e4473469b3674366a7837b0debb4d93e","submissionDate":"2022-04-05T08:36:18.229Z"}},{"id":"_5334307_1","userId":"_140040_1","status":"Completed","displayGrade":{"scaleType":"Score","score":100.00000},"text":"100.00000","score":100.000000000000000,"notes":"Helt UsERR","feedback":"Gratulerer","studentSubmission":"<p>Hva faen, Mattias ga klamma til Chloe \uD83D\uDE2F</p>","exempt":False,"created":"2022-04-06T07:25:46.278Z","attemptDate":"2022-04-06T07:25:46.313Z","modified":"2022-04-06T11:28:51.489Z","attemptReceipt":{"receiptId":"669bea9237164cd2b3dae5feee41f47c","submissionDate":"2022-04-06T07:25:46.283Z"}},{"id":"_5330216_1","userId":"_140955_1","status":"Completed","displayGrade":{"scaleType":"Score","score":100.00000},"text":"100.00000","score":100.000000000000000,"feedback":"Maggie er IKKE taperbb assignments grade --helpbb assignments grade --help","studentComments":"hva skjer","studentSubmission":"<p>adsfadsfadsf</p>","exempt":False,"created":"2022-04-05T08:54:56.286Z","attemptDate":"2022-04-05T08:54:56.302Z","modified":"2022-04-08T16:17:40.022Z","attemptReceipt":{"receiptId":"095f247c4e91492186b4416e94e20c22","submissionDate":"2022-04-05T08:54:56.290Z"}}]} +TEST_ATTEMPT = { + "id": "_5330107_1", + "userId": "_111522_1", + "status": "Completed", + "displayGrade": { + "scaleType": "Score", + "score": 70.0 + }, + "text": "70.00000", + "score": 70.0, + "feedback": "Jaja ca greit nok", + "studentComments": "Test comment.", + "studentSubmission": "<p>Test submission.</p>", + "exempt": False, + "created": "2022-04-05T08:36:18.225Z", + "attemptDate": "2022-04-05T08:36:18.242Z", + "modified": "2022-04-06T13:38:15.169Z", + "attemptReceipt": { + "receiptId": "e4473469b3674366a7837b0debb4d93e", + "submissionDate": "2022-04-05T08:36:18.229Z" + } +} +TEST_SUBMITTED_ATTEMPT = {'id': '_5361991_1', 'userId': '_140040_1', 'status': 'NeedsGrading', 'studentComments': 'I think yes', 'studentSubmission': 'TRUE, Mattias har klamma!', 'exempt': False, 'created': '2022-04-20T11:15:08.978Z'} +TEST_GRADE_ATTEMPT = {'id': '_5340506_1', 'userId': '_111522_1', 'status': 'Completed', 'feedback': 'Great work man!', 'studentComments': 'hallaballa', 'exempt': False, 'created': '2022-04-07T12:01:20.708Z', 'attemptDate': '2022-04-07T12:01:20.708Z', 'modified': '2022-04-20T10:59:43.098Z'} + +class TestAssignmentsServices(object): + @classmethod + def setup_class(cls): + cls.test_session = requests.Session() + cls.mock_post_patcher = patch('bbcli.cli.requests.Session.post') + cls.mock_get_patcher = patch('bbcli.services.announcements_services.requests.Session.get') + cls.mock_auth_patcher = patch('bbcli.cli.authenticate_user') + cls.mock_update_patcher = patch('bbcli.cli.requests.Session.patch') + + cls.mock_post = cls.mock_post_patcher.start() + cls.mock_get = cls.mock_get_patcher.start() + cls.mock_auth = cls.mock_auth_patcher.start() + cls.mock_update = cls.mock_update_patcher.start() + + @classmethod + def teardown_class(cls): + cls.mock_post_patcher.stop() + cls.mock_get_patcher.stop() + cls.mock_auth_patcher.stop() + cls.mock_update_patcher.stop() + cls.test_session.close + + + def test_get_assignments(self): + self.mock_auth.return_value.ok = True + self.mock_get.return_value.ok = True + self.mock_get.return_value.text = json.dumps(TEST_ASSIGNMENTS_LIST) + + response = get_assignments(self.test_session, 'test_course_id') + + assert_equal(response, TEST_ASSIGNMENTS_LIST['results']) + + def test_get_column_attempts(self): + self.mock_auth.return_value.ok = True + self.mock_get.return_value.ok = True + self.mock_get.return_value.text = json.dumps(TEST_ASSIGNMENT_ATTEMPTS_LIST) + + response = get_column_attempts(self.test_session, 'test_course_id', 'test_column_id') + + assert_equal(json.dumps(response), json.dumps(TEST_ASSIGNMENT_ATTEMPTS_LIST['results'])) + + def test_get_column_attempt(self): + self.mock_auth.return_value.ok = True + self.mock_get.return_value.ok = True + self.mock_get.return_value.text = json.dumps(TEST_ATTEMPT) + + response = get_column_attempt(self.test_session, 'test_course_id', 'test_column_id', 'test_attempt_id') + + assert_equal(response, json.dumps(TEST_ATTEMPT, indent=2)) + + def create_column_attempt(self): + self.mock_auth.return_value.ok = True + self.mock_post.return_value.ok = True + self.mock_post.return_value.text = json.dumps(TEST_SUBMITTED_ATTEMPT) + + response = create_column_attempt(self.test_session, 'test_course_id', 'test_column_id') + + assert_equal(response, TEST_SUBMITTED_ATTEMPT) + + def test_update_column_attempt(self): + self.mock_auth.return_value.ok = True + self.mock_update.return_value.ok = True + self.mock_update.return_value.text = json.dumps(TEST_GRADE_ATTEMPT) + + response = update_column_attempt(self.test_session, 'test_course_id', 'test_column_id', 'test_attempt_id') + + assert_equal(json.loads(response), TEST_GRADE_ATTEMPT) + diff --git a/tests/test_services/test_contents_services.py b/tests/test_services/test_contents_services.py index f0f709b7d555cf8c495caf1620340e22061c2812..50614b80eb80b6ed9335e983c9bca0946269abe0 100644 --- a/tests/test_services/test_contents_services.py +++ b/tests/test_services/test_contents_services.py @@ -7,9 +7,7 @@ import requests from unittest.mock import Mock, patch from nose.tools import assert_list_equal, assert_equal, raises from bbcli.entities.content_builder_entitites import DateInterval, FileOptions, GradingOptions, StandardOptions, WeblinkOptions -from bbcli.services.contents_service import create_assignment, create_document, create_externallink, create_file, create_folder, delete_content, update_content -from tests.test_services.test_announcements_services import UPDATED_TEST_ANNOUNCEMENT - +from bbcli.services.contents_services import create_assignment, create_document, create_externallink, create_file, create_folder, delete_content, update_content TEST_CREATED_DOCUMENT = {"id":"_1699922_1","parentId":"_1699223_1","title":"Test document","body":"This is a test document","created":"2022-04-18T08:02:18.316Z","modified":"2022-04-18T08:02:18.410Z","position":0,"launchInNewWindow":False,"reviewable":False,"availability":{"available":"Yes","allowGuests":True,"allowObservers":True,"adaptiveRelease":{}},"contentHandler":{"id":"resource/x-bb-document"},"links":[{"href":"/ultra/courses/_33050_1/cl/outline?legacyUrl=%2Fwebapps%2Fblackboard%2Fexecute%2FdisplayIndividualContent%3Fcourse_id%3D_33050_1%26content_id%3D_1699922_1","rel":"alternate","title":"User Interface View","type":"text/html"}]} TEST_CREATED_FILE = {"id":"_1699925_1","parentId":"_1645559_1","title":"Test file","created":"2022-04-18T08:27:32.497Z","modified":"2022-04-18T08:27:32.497Z","position":16,"launchInNewWindow":False,"reviewable":False,"availability":{"available":"Yes","allowGuests":True,"allowObservers":True,"adaptiveRelease":{}},"contentHandler":{"id":"resource/x-bb-file","file":{"fileName":"pdf-test.pdf"}},"links":[{"href":"/ultra/courses/_33050_1/cl/outline?legacyUrl=%2Fwebapps%2Fblackboard%2Fexecute%2FdisplayIndividualContent%3Fcourse_id%3D_33050_1%26content_id%3D_1699925_1","rel":"alternate","title":"User Interface View","type":"text/html"}]} @@ -18,17 +16,17 @@ TEST_CREATED_FOLDER = {"id":"_1699929_1","parentId":"_1645559_1","title":"Test f TEST_CREATED_ASSIGNMENT = {"contentId":"_1699934_1","gradeColumnId":"_281045_1","attachmentIds":["_4322478_1","_4322479_1"]} TEST_GET_CONTENT = {'id': '_1698141_1', 'parentId': '_1697863_1', 'title': 'testing assignment', 'body': '<p>asdfasdf</p>', 'created': '2022-04-06T12:52:02.900Z', 'modified': '2022-04-11T09:42:33.594Z', 'position': 4, 'hasGradebookColumns': True, 'launchInNewWindow': False, 'reviewable': False, 'availability': {'available': 'Yes', 'allowGuests': True, 'allowObservers': True, 'adaptiveRelease': {}}, 'contentHandler': {'id': 'resource/x-bb-assignment', 'gradeColumnId': '_280808_1', 'groupContent': False}, 'links': [{'href': '/ultra/courses/_33050_1/cl/outline?legacyUrl=%2Fwebapps%2Fblackboard%2Fexecute%2FdisplayIndividualContent%3Fcourse_id%3D_33050_1%26content_id%3D_1698141_1', 'rel': 'alternate', 'title': 'User Interface View', 'type': 'text/html'}]} -TEST_GET_CONTENT_UPDATED = {'id': '_1698141_1', 'parentId': '_1697863_1', 'title': 'testing assignment updated', 'body': '<p>asdfasdf updated</p>', 'created': '2022-04-06T12:52:02.900Z', 'modified': '2022-04-11T09:42:33.594Z', 'position': 4, 'hasGradebookColumns': True, 'launchInNewWindow': False, 'reviewable': False, 'availability': {'available': 'Yes', 'allowGuests': True, 'allowObservers': True, 'adaptiveRelease': {}}, 'contentHandler': {'id': 'resource/x-bb-assignment', 'gradeColumnId': '_280808_1', 'groupContent': False}, 'links': [{'href': '/ultra/courses/_33050_1/cl/outline?legacyUrl=%2Fwebapps%2Fblackboard%2Fexecute%2FdisplayIndividualContent%3Fcourse_id%3D_33050_1%26content_id%3D_1698141_1', 'rel': 'alternate', 'title': 'User Interface View', 'type': 'text/html'}]} +TEST_GET_CONTENT_UPDATED = {'id': '_1698141_1', 'parentId': '_1697863_1', 'title': 'TEST TITLE', 'body': 'TEST BODY', 'created': '2022-04-06T12:52:02.900Z', 'modified': '2022-04-11T09:42:33.594Z', 'position': 4, 'hasGradebookColumns': True, 'launchInNewWindow': False, 'reviewable': False, 'availability': {'available': 'Yes', 'allowGuests': True, 'allowObservers': True, 'adaptiveRelease': {}}, 'contentHandler': {'id': 'resource/x-bb-assignment', 'gradeColumnId': '_280808_1', 'groupContent': False}, 'links': [{'href': '/ultra/courses/_33050_1/cl/outline?legacyUrl=%2Fwebapps%2Fblackboard%2Fexecute%2FdisplayIndividualContent%3Fcourse_id%3D_33050_1%26content_id%3D_1698141_1', 'rel': 'alternate', 'title': 'User Interface View', 'type': 'text/html'}]} TEST_UPLOADED_FILE = {'id': '53-5321C30FA434825104FDC83B173BF720-abcdf665d3294daf8addddc56a670674'} -class TestAnnouncementsServices(object): +class TestContentsServices(object): @classmethod def setup_class(cls): cls.test_session = requests.Session() cls.mock_post_patcher = patch('bbcli.cli.requests.Session.post') - cls.mock_get_patcher = patch('bbcli.services.announcements_service.requests.Session.get') + cls.mock_get_patcher = patch('bbcli.services.announcements_services.requests.Session.get') cls.mock_auth_patcher = patch('bbcli.cli.authenticate_user') cls.mock_post = cls.mock_post_patcher.start() @@ -45,7 +43,7 @@ class TestAnnouncementsServices(object): def test_create_document(self): self.mock_auth.return_value.ok = True - mock_input_body_patcher = patch('bbcli.services.contents_service.input_body') + mock_input_body_patcher = patch('bbcli.services.contents_services.input_body') mock_input_body = mock_input_body_patcher.start() mock_input_body.return_value = 'This is a test document' @@ -53,15 +51,15 @@ class TestAnnouncementsServices(object): self.mock_post.return_value.ok = True self.mock_post.return_value.text = json.dumps(TEST_CREATED_DOCUMENT) - response = create_document(self.test_session, 'test_course_id', 'test_parent_id', 'Test document', standard_options) + response = create_document(self.test_session, 'test_course_id', 'test_parent_id', 'Test document', standard_options, None, False) mock_input_body_patcher.stop() - assert_equal(response, json.dumps(TEST_CREATED_DOCUMENT)) + assert_equal(response, TEST_CREATED_DOCUMENT) def test_create_file(self): self.mock_auth.return_value.ok = True - mock_upload_file_patcher = patch('bbcli.services.contents_service.upload_file') + mock_upload_file_patcher = patch('bbcli.services.contents_services.upload_file') mock_upload_file = mock_upload_file_patcher.start() mock_upload_file.return_value = TEST_UPLOADED_FILE @@ -74,7 +72,7 @@ class TestAnnouncementsServices(object): mock_upload_file_patcher.stop() - assert_equal(response, json.dumps(TEST_CREATED_FILE)) + assert_equal(response, TEST_CREATED_FILE) def test_create_externallink(self): @@ -86,11 +84,11 @@ class TestAnnouncementsServices(object): response = create_externallink(self.test_session, 'test_course_id', 'test_parent_id', 'Test web-link', 'https://vg.no/', WeblinkOptions(), standard_options) - assert_equal(response, json.dumps(TEST_CREATED_EXTERNALLINK)) + assert_equal(response, TEST_CREATED_EXTERNALLINK) def test_create_folder(self): self.mock_auth.return_value.ok = True - mock_input_body_patcher = patch('bbcli.services.contents_service.input_body') + mock_input_body_patcher = patch('bbcli.services.contents_services.input_body') mock_input_body = mock_input_body_patcher.start() mock_input_body.return_value = 'This is a test folder' @@ -100,18 +98,18 @@ class TestAnnouncementsServices(object): self.mock_post.return_value.ok = True self.mock_post.return_value.text = json.dumps(TEST_CREATED_FOLDER) - response = create_folder(self.test_session, 'test_course_id', 'test_parent_id', 'Test folder', False, standard_options) + response = create_folder(self.test_session, 'test_course_id', 'test_parent_id', 'Test folder', False, standard_options, False) mock_input_body_patcher.stop() - assert_equal(response, json.dumps(TEST_CREATED_FOLDER)) + assert_equal(response, TEST_CREATED_FOLDER) def test_create_assignment(self): self.mock_auth.return_value.ok = True - mock_input_body_patcher = patch('bbcli.services.contents_service.input_body') + mock_input_body_patcher = patch('bbcli.services.contents_services.input_body') mock_input_body = mock_input_body_patcher.start() mock_input_body.return_value = 'This is a test assignment' - mock_upload_file_patcher = patch('bbcli.services.contents_service.upload_file') + mock_upload_file_patcher = patch('bbcli.services.contents_services.upload_file') mock_upload_file = mock_upload_file_patcher.start() mock_upload_file.return_value = TEST_UPLOADED_FILE @@ -119,12 +117,12 @@ class TestAnnouncementsServices(object): self.mock_post.return_value.ok = True self.mock_post.return_value.text = json.dumps(TEST_CREATED_ASSIGNMENT) - response = create_assignment(self.test_session, 'test_course_id', 'test_parent_id', 'Test assignment', standard_options, GradingOptions(), ('tests/test_resources/pdf-test.pdf', 'tests/test_resources/pdf-test.pdf')) + response = create_assignment(self.test_session, 'test_course_id', 'test_parent_id', 'Test assignment', standard_options, GradingOptions(), ('tests/test_resources/pdf-test.pdf', 'tests/test_resources/pdf-test.pdf'), False) mock_input_body_patcher.stop() mock_upload_file_patcher.stop() - assert_equal(response, json.dumps(TEST_CREATED_ASSIGNMENT)) + assert_equal(response, TEST_CREATED_ASSIGNMENT) def test_delete_content(self): @@ -144,17 +142,22 @@ class TestAnnouncementsServices(object): self.mock_auth.return_value.ok = True mock_update_patcher = patch('bbcli.cli.requests.Session.patch') mock_update = mock_update_patcher.start() - mock_input_body_patcher = patch('bbcli.services.announcements_service.click.edit') - mock_input_body = mock_input_body_patcher.start() - mock_input_body.return_value.ok = True + mock_edit_title_patcher = patch('bbcli.services.contents_services.edit_title') + mock_edit_title = mock_edit_title_patcher.start() + mock_edit_body_patcher = patch('bbcli.services.contents_services.update_content_data') + mock_edit_body = mock_edit_body_patcher.start() self.mock_get.return_value.text = json.dumps(TEST_GET_CONTENT) + mock_edit_title.return_value = 'TEST TITLE' + mock_edit_body.return_value = {'body' : 'TEST BODY'} + mock_update.return_value.ok = True - mock_update.return_value.text = json.dumps(UPDATED_TEST_ANNOUNCEMENT) + mock_update.return_value.text = json.dumps(TEST_GET_CONTENT_UPDATED) - response = update_content(self.test_session, 'test_course_id', 'test_content_id') + response = update_content(self.test_session, 'test_course_id', 'test_content_id', False) mock_update_patcher.stop() - mock_input_body_patcher.stop() + mock_edit_title_patcher.stop() + mock_edit_body_patcher.stop() - assert_equal(json.loads(response), UPDATED_TEST_ANNOUNCEMENT) \ No newline at end of file + assert_equal(response, TEST_GET_CONTENT_UPDATED) \ No newline at end of file diff --git a/tests/test_services/test_courses_services.py b/tests/test_services/test_courses_services.py index c5620de0e33a6a9d3ae36b464bee5f337a9bc572..236ad2d5f9c00d343406ba412990537268f8262a 100644 --- a/tests/test_services/test_courses_services.py +++ b/tests/test_services/test_courses_services.py @@ -2,7 +2,7 @@ import json from typing import List from click.testing import CliRunner import requests -from bbcli.services.courses_service import list_all_courses, list_course, list_courses +from bbcli.services.courses_services import list_all_courses, list_course from unittest.mock import Mock, patch from nose.tools import assert_list_equal, assert_equal @@ -38,20 +38,20 @@ 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 def setup_class(cls): - cls.mock_get_patcher = patch('bbcli.services.courses_service.requests.Session.get') + cls.mock_get_patcher = patch('bbcli.services.courses_services.requests.Session.get') cls.mock_auth_patcher = patch('bbcli.cli.authenticate_user') - # cls.mock_get_terms_patcher = patch('bbcli.services.courses_service.get_terms') - # cls.mock_get_memberships_patcher = patch('bbcli.services.courses_service.get_course_memberships') + # cls.mock_get_terms_patcher = patch('bbcli.services.courses_services.get_terms') + # cls.mock_get_memberships_patcher = patch('bbcli.services.courses_services.get_course_memberships') cls.mock_get = cls.mock_get_patcher.start() cls.mock_auth = cls.mock_auth_patcher.start() @@ -73,35 +73,35 @@ class TestCoursesServices(object): assert_equal(response, TEST_COURSE) - def test_list_courses(self): - self.mock_auth.return_value.ok = True + # def test_list_courses(self): + # self.mock_auth.return_value.ok = True - mock_get_terms_patcher = patch('bbcli.services.courses_service.get_terms') - mock_get_memberships_patcher = patch('bbcli.services.courses_service.get_course_memberships') - mock_get_courses_patcher = patch('bbcli.services.courses_service.get_courses_from_course_memberships') - mock_get_terms = mock_get_terms_patcher.start() - mock_get_memberships = mock_get_memberships_patcher.start() - mock_get_courses = mock_get_courses_patcher.start() + # mock_get_terms_patcher = patch('bbcli.services.courses_services.get_terms') + # mock_get_memberships_patcher = patch('bbcli.services.courses_services.get_course_memberships') + # mock_get_courses_patcher = patch('bbcli.services.courses_services.get_courses_from_course_memberships') + # mock_get_terms = mock_get_terms_patcher.start() + # mock_get_memberships = mock_get_memberships_patcher.start() + # mock_get_courses = mock_get_courses_patcher.start() - mock_get_terms.return_value = TEST_TERMS_LIST - mock_get_memberships.return_value = TEST_COURSE_MEMBERSHIPS_LIST - mock_get_courses.return_value = TEST_COURSE_LIST + # mock_get_terms.return_value = TEST_TERMS_LIST + # mock_get_memberships.return_value = TEST_COURSE_MEMBERSHIPS_LIST + # mock_get_courses.return_value = TEST_COURSE_LIST - test_session = requests.Session() - # user name is irrelavant here because the API call is mocked anyways - response = list_courses(test_session, 'test_user') + # test_session = requests.Session() + # # user name is irrelavant here because the API call is mocked anyways + # response = list_courses(test_session, 'test_user') - mock_get_terms_patcher.stop() - mock_get_memberships_patcher.stop() - mock_get_courses_patcher.stop() + # mock_get_terms_patcher.stop() + # mock_get_memberships_patcher.stop() + # mock_get_courses_patcher.stop() - assert_equal(response, TEST_COURSE_LIST_SORTED_AND_SHORTENED) + # assert_equal(response, TEST_COURSE_LIST_SORTED_AND_SHORTENED) def test_list_all_courses(self): self.mock_auth.return_value.ok = True - mock_get_memberships_patcher = patch('bbcli.services.courses_service.get_course_memberships') - mock_get_courses_patcher = patch('bbcli.services.courses_service.get_courses_from_course_memberships') + mock_get_memberships_patcher = patch('bbcli.services.courses_services.get_course_memberships') + mock_get_courses_patcher = patch('bbcli.services.courses_services.get_courses_from_course_memberships') mock_get_memberships = mock_get_memberships_patcher.start() mock_get_courses = mock_get_courses_patcher.start()