Skip to content
Snippets Groups Projects
Commit 8a664b69 authored by magnus2142's avatar magnus2142
Browse files

Merge branch 'magnus_feature_views' into main

parents 6e567093 b4f314ff
No related branches found
No related tags found
No related merge requests found
Showing
with 411 additions and 44 deletions
...@@ -17,7 +17,6 @@ def entry_point(): ...@@ -17,7 +17,6 @@ def entry_point():
pass pass
entry_point.add_command(get_user) entry_point.add_command(get_user)
entry_point.add_command(get_course_contents) entry_point.add_command(get_course_contents)
entry_point.add_command(get_assignments) entry_point.add_command(get_assignments)
......
from email.policy import default
import click
from bbcli.services import announcement_service
from bbcli.views import announcement_view
import os
import requests
from bbcli.utils.utils import set_cookies, set_headers
@click.command(name='announcements')
@click.argument('course_id', required=False)
@click.argument('announcement_id', required=False)
def list_announcements(course_id=None, announcement_id=None):
"""
This command lists your announcements.
Either all announcements, all announcements from a spesific course, or one announcement.
"""
bb_cookie = {
'name':'BbRouter',
'value': os.getenv("BB_ROUTER")
}
xsrf = {'X-Blackboard-XSRF': os.getenv('XSRF')}
user_name = os.getenv('BB_USERNAME')
session = requests.Session()
set_cookies(session, [bb_cookie])
set_headers(session, [xsrf])
response = None
if announcement_id:
response = announcement_service.list_announcement(session, course_id, announcement_id)
announcement_view.print_course_announcements([response])
elif course_id:
response = announcement_service.list_course_announcements(session, course_id)
announcement_view.print_course_announcements(response)
else:
response = announcement_service.list_announcements(session, user_name)
announcement_view.print_announcements(response)
from email.policy import default
import click
from bbcli.services import course_service
from bbcli.views import course_view
import os
import requests
from bbcli.utils.utils import set_cookies, set_headers
#, help='List a spesific course with the corresponding id'
@click.command(name='courses')
@click.argument('course_id', required=False)
@click.option('-a', '--all/--no-all', 'show_all', default=False, help='Lists all courses you have ever been signed up for')
def list_courses(course_id=None, show_all=False):
"""
This command lists your courses, by default only the courses from
two last semesters
"""
bb_cookie = {
'name':'BbRouter',
'value': os.getenv("BB_ROUTER")
}
user_name = os.getenv('BB_USERNAME')
session = requests.Session()
set_cookies(session, [bb_cookie])
response = None
if course_id:
response = course_service.list_course(session=session, course_id=course_id)
course_view.print_course(response)
else:
if show_all:
response = course_service.list_all_courses(session=session, user_name=user_name)
else:
response = course_service.list_courses(session=session, user_name=user_name)
course_view.print_courses(response)
from .authorization_service import * from .authorization_service import *
\ No newline at end of file from bbcli.utils.URL_builder import URLBuilder
\ No newline at end of file
...@@ -3,16 +3,20 @@ from subprocess import call ...@@ -3,16 +3,20 @@ from subprocess import call
from typing import Dict, Any from typing import Dict, Any
import requests import requests
from bbcli.services.course_service import list_courses from bbcli.services.course_service import list_courses
from bbcli.utils.utils import set_cookies
import click import click
def list_announcements(cookies: Dict, user_name: str): from bbcli.utils.URL_builder import URLBuilder
courses = list_courses(cookies=cookies, user_name=user_name)
session = requests.Session() url_builder = URLBuilder()
def list_announcements(session: requests.Session, user_name: str):
courses = list_courses(session, user_name=user_name)
announcements = [] announcements = []
for course in courses: for course in courses:
course_announcements = session.get('https://ntnu.blackboard.com/learn/api/public/v1/courses/{}/announcements'.format(course['id']), cookies=cookies) url = url_builder.base_v1().add_courses().add_id(course['id']).add_announcements().create()
course_announcements = session.get(url)
course_announcements = json.loads(course_announcements.text) course_announcements = json.loads(course_announcements.text)
# Adds the course name to each course announcement list to make it easier to display which course the announcement comes from # Adds the course name to each course announcement list to make it easier to display which course the announcement comes from
...@@ -24,19 +28,21 @@ def list_announcements(cookies: Dict, user_name: str): ...@@ -24,19 +28,21 @@ def list_announcements(cookies: Dict, user_name: str):
return announcements return announcements
def list_course_announcements(cookies: Dict, course_id: str): def list_course_announcements(session: requests.Session, course_id: str):
course_announcements = requests.get('https://ntnu.blackboard.com/learn/api/public/v1/courses/{}/announcements'.format(course_id), cookies=cookies) url = url_builder.base_v1().add_courses().add_id(course_id).add_announcements().create()
course_announcements = session.get(url)
course_announcements.raise_for_status()
course_announcements = json.loads(course_announcements.text)['results'] course_announcements = json.loads(course_announcements.text)['results']
return course_announcements return course_announcements
def list_announcement(cookies: Dict, course_id: str, announcement_id: str): def list_announcement(session: requests.Session, course_id: str, announcement_id: str):
announcement = requests.get('https://ntnu.blackboard.com/learn/api/public/v1/courses/{}/announcements/{}'.format(course_id, announcement_id), cookies=cookies) url = url_builder.base_v1().add_courses().add_id(course_id).add_announcements().add_id(announcement_id).create()
announcement = session.get(url)
announcement = json.loads(announcement.text) announcement = json.loads(announcement.text)
return announcement return announcement
# TODO: Add compatibility for flags and options to make a more detailed announcement # TODO: Add compatibility for flags and options to make a more detailed announcement
def create_announcement(cookies: Dict, headers: Dict, course_id: str, title: str): def create_announcement(session: requests.Session, course_id: str, title: str):
MARKER = '# Everything below is ignored\n' MARKER = '# Everything below is ignored\n'
body = click.edit('\n\n' + MARKER) body = click.edit('\n\n' + MARKER)
if body is not None: if body is not None:
...@@ -48,22 +54,24 @@ def create_announcement(cookies: Dict, headers: Dict, course_id: str, title: str ...@@ -48,22 +54,24 @@ def create_announcement(cookies: Dict, headers: Dict, course_id: str, title: str
} }
data = json.dumps(data) data = json.dumps(data)
headers['Content-Type'] = 'application/json' session.headers.update({'Content-Type': 'application/json'})
response = requests.post('https://ntnu.blackboard.com/learn/api/public/v1/courses/{}/announcements'.format(course_id), cookies=cookies, headers=headers, data=data) url = url_builder.base_v1().add_courses().add_id(course_id).add_announcements().create()
response = session.post(url, data=data)
return response.text return response.text
def delete_announcement(cookies: Dict, headers: Dict, course_id: str, announcement_id: str): def delete_announcement(session: requests.Session, course_id: str, announcement_id: str):
response = requests.delete('https://ntnu.blackboard.com/learn/api/public/v1/courses/{}/announcements/{}'.format(course_id, announcement_id), cookies=cookies, headers=headers) url = url_builder.base_v1().add_courses().add_id(course_id).add_announcements().add_id(announcement_id).create()
response = session.delete(url)
if response.text == '': if response.text == '':
return 'Sucessfully deleted announcement!' return 'Sucessfully deleted announcement!'
else: else:
return response.text return response.text
def update_announcement(cookies: Dict, headers: Dict, course_id: str, announcement_id: str): def update_announcement(session: requests.Session, course_id: str, announcement_id: str):
announcement = list_announcement(cookies=cookies, course_id=course_id, announcement_id=announcement_id) announcement = list_announcement(session=session, course_id=course_id, announcement_id=announcement_id)
MARKER = '# Everything below is ignored\n' MARKER = '# Everything below is ignored\n'
editable_data = { editable_data = {
'title': announcement['title'], 'title': announcement['title'],
...@@ -75,7 +83,9 @@ def update_announcement(cookies: Dict, headers: Dict, course_id: str, announceme ...@@ -75,7 +83,9 @@ def update_announcement(cookies: Dict, headers: Dict, course_id: str, announceme
announcement = json.dumps(editable_data, indent=2) announcement = json.dumps(editable_data, indent=2)
new_data = click.edit(announcement + '\n\n' + MARKER) new_data = click.edit(announcement + '\n\n' + MARKER)
headers['Content-Type'] = 'application/json' session.headers.update({'Content-Type': 'application/json'})
response = requests.patch('https://ntnu.blackboard.com/learn/api/public/v1/courses/{}/announcements/{}'.format(course_id, announcement_id), cookies=cookies, headers=headers, data=new_data)
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)
return response.text return response.text
\ No newline at end of file
...@@ -292,8 +292,9 @@ def write_to_env_data(session): ...@@ -292,8 +292,9 @@ def write_to_env_data(session):
xsrf_value = xsrf[1] xsrf_value = xsrf[1]
f = open('.env', 'w') f = open('.env', 'w')
f.write("BB_ROUTER=" + BB_ROUTER + "\n") f.write(f'BB_ROUTER={BB_ROUTER}\n')
f.write("XSRF=" + xsrf_value + "\n") f.write(f'XSRF={xsrf_value}\n')
f.write(f'BB_USERNAME={login_username}\n')
f.close() f.close()
......
import json
from subprocess import call
from typing import Dict, Any
import requests
from bbcli.services.course_service import list_courses
import click
# User gets a tree structure view of the courses content
# where each content is listed something like this: _030303_1 Lectures Folder
def list_course_content(cookies: Dict, course_id: str):
print('Getting course content!')
# 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(cookies: Dict, course_id: str, content_id: str):
print('Getting content by its ID.')
# List all contents of type assignment, should be executed if a flag for example like --content-type assignment or smth is used
def list_assignments(cookies: Dict, course_id: str):
print('Getting all assignments')
# TODO: add methods for all content types like the one above
# Create content. This should have a flag which says what kind of content type it is
# Create assignment. Creates an assignment
# Delete spesific content
# Update spesific content
\ No newline at end of file
import json import json
from typing import Dict, Any from typing import Dict, Any, List
import requests import requests
from datetime import date from datetime import date
def take_start_date(elem): from bbcli.utils.URL_builder import URLBuilder
return date.fromisoformat(elem['availability']['duration']['start'].split('T')[0])
url_builder = URLBuilder()
def list_courses(cookies: Dict, user_name: str) -> Any:
session = requests.Session()
terms = session.get('https://ntnu.blackboard.com/learn/api/public/v1/terms', cookies=cookies) def list_courses(session: requests.Session, user_name: str) -> Any:
terms = json.loads(terms.text)['results']
terms = get_terms(session)
# Sort terms by start date to get the two most recent semesters to determine which courses to show sort_terms(terms)
for term in terms:
if term['availability']['duration']['type'] != 'DateRange':
terms.remove(term)
terms.sort(key=take_start_date)
term_1 = terms[len(terms) - 1] term_1 = terms[len(terms) - 1]
term_2 = terms[len(terms) - 2] term_2 = terms[len(terms) - 2]
course_memberships = session.get('https://ntnu.blackboard.com/learn/api/public/v1/users/userName:{}/courses'.format(user_name), cookies=cookies) course_memberships = get_course_memberships(session, user_name)
course_memberships = json.loads(course_memberships.text)['results']
course_list = [] course_list = []
# Get courses from the correct terms # Get courses from the correct terms
for course in course_memberships: for course in course_memberships:
response = session.get('https://ntnu.blackboard.com/learn/api/public/v3/courses/{}'.format(course['courseId']), cookies=cookies, params={'fields': 'id, name, termId'}) url = url_builder.base_v3().add_courses().add_id(course['courseId']).create()
response = session.get(url, params={'fields': 'id, name, termId'})
response = json.loads(response.text) response = json.loads(response.text)
if response['termId'] == term_1['id'] or response['termId'] == term_2['id']: if response['termId'] == term_1['id'] or response['termId'] == term_2['id']:
course_list.append({ course_list.append({
...@@ -41,10 +35,53 @@ def list_courses(cookies: Dict, user_name: str) -> Any: ...@@ -41,10 +35,53 @@ def list_courses(cookies: Dict, user_name: str) -> Any:
return course_list return course_list
def list_course(cookies: Dict, course_id:str) -> Any: def list_all_courses(session: requests.Session, user_name: str) -> Any:
response = requests.get('https://ntnu.blackboard.com/learn/api/public/v3/courses/{}'.format(course_id), cookies=cookies) course_memberships = get_course_memberships(session, user_name)
course_list = []
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'})
response = json.loads(response.text)
course_list.append(response)
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)
return json.loads(response.text) return json.loads(response.text)
def list_favorite_courses(cookies: Dict, user_name: str) -> Any: def list_favorite_courses(session: requests.Session, user_name: str) -> Any:
return "Blackboard rest api do not have an option for this yet" return "Blackboard rest api do not have an option for this yet"
# response = requests.get('https://ntnu.blackboard.com/learn/api/public/v1/users/userName:{}/courses'.format(user_name), cookies=cookies) # response = requests.get('https://ntnu.blackboard.com/learn/api/public/v1/users/userName:{}/courses'.format(user_name), cookies=cookies)
"""
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 = 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 = json.loads(course_memberships.text)['results']
return course_memberships
\ No newline at end of file
from __future__ import annotations
from typing import Any
from abc import ABC, abstractmethod
DOMAIN = 'https://ntnu.blackboard.com'
API_BASE = '/learn/api/public'
class Builder(ABC):
@property
@abstractmethod
def product(self) -> None:
pass
"""
Returns the base URL which includes the domain and first part of all the endpoints: domain/learn/api/public/vX,
where X is the version from 1 to 3.
"""
@abstractmethod
def base_v1(self) -> Builder:
pass
@abstractmethod
def base_v2(self) -> Builder:
pass
@abstractmethod
def base_v3(self) -> Builder:
pass
@abstractmethod
def add_courses(self) -> Builder:
pass
@abstractmethod
def add_users(self) -> Builder:
pass
@abstractmethod
def add_announcements(self) -> Builder:
pass
@abstractmethod
def add_contents(self) -> Builder:
pass
@abstractmethod
def add_terms(slef) -> Builder:
pass
@abstractmethod
def add_id(self, id: str, id_type: str = None) -> Builder:
pass
class URLBuilder(Builder):
def __init__(self) -> None:
self.reset()
def reset(self) -> None:
self._product = URL()
@property
def product(self) -> URL:
product = self._product
self.reset()
return product
def base_v1(self) -> URLBuilder:
self._product.add(f'{DOMAIN}{API_BASE}/v1')
return self
def base_v2(self) -> URLBuilder:
self._product.add(f'{DOMAIN}{API_BASE}/v2')
return self
def base_v3(self) -> URLBuilder:
self._product.add(f'{DOMAIN}{API_BASE}/v3')
return self
def add_courses(self) -> URLBuilder:
self._product.add('/courses')
return self
def add_users(self) -> URLBuilder:
self._product.add('/users')
return self
def add_announcements(self) -> URLBuilder:
self._product.add('/announcements')
return self
def add_contents(self) -> URLBuilder:
self._product.add('/contents')
return self
def add_terms(self) -> URLBuilder:
self._product.add('/terms')
return self
def add_id(self, id:str, id_type:str=None) -> URLBuilder:
if id_type:
self._product.add(f'/{id_type}:{id}')
else:
self._product.add(f'/{id}')
return self
def create(self) -> str:
url = self._product.get_url()
self._product = URL()
return url
class URL():
def __init__(self) -> None:
self.URL = ''
def add(self, url_part: str) -> None:
self.URL += url_part
def get_url(self) -> None:
return self.URL
\ No newline at end of file
import requests
import click
# ERROR HANDLER SHOULD BE USED IN VIEW??
def HTTP_exception_handler(func):
def inner_function(*args, **kwargs):
try:
func(*args, **kwargs)
except requests.exceptions.HTTPError as err:
click.echo(err)
click.Abort()
return inner_function
\ No newline at end of file
from datetime import datetime from datetime import datetime
from typing import Dict, List
from requests import Session
import html2text
def check_valid_key(obj, key) -> bool: def check_valid_key(obj, key) -> bool:
# print("the keys are", obj.keys()) # print("the keys are", obj.keys())
...@@ -31,3 +35,16 @@ def check_valid_date(cookies) -> bool: ...@@ -31,3 +35,16 @@ def check_valid_date(cookies) -> bool:
return False return False
def set_cookies(session: Session, cookies: List):
for cookie in cookies:
session.cookies.set(cookie['name'], cookie['value'])
def set_headers(session: Session, headers: List):
for header in headers:
session.headers.update(header)
def html_to_text(html_data: str):
to_text = html2text.HTML2Text()
return to_text.handle(html_data)
\ No newline at end of file
import click
from typing import List
from bbcli.utils.utils import html_to_text
def print_announcements(announcements: List):
for course in announcements:
print_course_announcements(course['course_announcements'], course['course_name'])
def print_course_announcements(course_announcements: List, course_name: str = None):
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]
click.echo('----------------------------------------------------------------------\n')
if course_name:
click.echo(f'{course_name}\n')
click.echo('{:<15} {:<15}'.format('Id: ', announcement_id))
click.echo('{:<15} {:<15}'.format('Title: ', title))
click.echo('{:<15} {:<15}'.format('Date: ', created))
click.echo('\n{:<15}\n'.format(body))
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment