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

CReated create file and document method. ADded features to content builder.

parent 3fad7cba
No related branches found
No related tags found
No related merge requests found
......@@ -13,9 +13,19 @@ 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 list_contents, create_content
from bbcli.commands.contents import list_contents, create_content, create_document, create_file
from bbcli.services.authorization_service import login
load_dotenv()
cookies = {'BbRouter' : os.getenv("BB_ROUTER")}
headers = {'X-Blackboard-XSRF': os.getenv('XSRF')}
#----- AUTHORIZATION MODULE -----#
# @app.command(name='login', help='Authorize the user.')
def authorize_user():
if cookies['BbRouter'] == None or check_valid_date(cookies) == False:
login()
def initiate_session():
bb_cookie = {
'name':'BbRouter',
......@@ -26,6 +36,7 @@ def initiate_session():
session = requests.Session()
set_cookies(session, [bb_cookie])
set_headers(session, [xsrf])
session.headers.update({'Content-Type': 'application/json'})
return session
......@@ -83,14 +94,20 @@ def contents(ctx):
pass
contents.add_command(list_contents)
contents.add_command(create_content)
# contents.add_command(upload_file)
load_dotenv()
cookies = {'BbRouter' : os.getenv("BB_ROUTER")}
headers = {'X-Blackboard-XSRF': os.getenv('XSRF')}
"""
CONTENTS CREATE COMMANDS ENTRY POINT
"""
#----- AUTHORIZATION MODULE -----#
# @app.command(name='login', help='Authorize the user.')
def authorize_user():
if check_valid_date(cookies) == False:
login()
@contents.group()
@click.pass_context
def create(ctx):
"""
Commands for creating different types of content types in blackboard
"""
pass
# create.add_command(create_content)
create.add_command(create_document)
create.add_command(create_file)
\ No newline at end of file
from datetime import datetime
import click
from bbcli.entities.content_builder_entitites import FileOptions, StandardOptions
from bbcli.services import contents_service
from bbcli.views import content_view
import os
def standard_options(function):
function = click.option('-h', '--hide-content', is_flag=True)(function)
function = click.option('-r', '--reviewable', is_flag=True)(function)
function = click.option('--start-date', type=str)(function)
function = click.option('--end-date', type=str)(function)
return function
def file_options(function):
function = click.option('-n', '--new-window', 'launch_in_new_window', is_flag=True)(function)
return function
#, help='List a spesific course with the corresponding id'
@click.command(name='list')
......@@ -29,3 +42,65 @@ def list_contents(ctx, course_id: str=None, content_id: str=None):
@click.pass_context
def create_content(ctx, course_id: str, content_id: str):
contents_service.test_create_assignment(ctx.obj['SESSION'], course_id, content_id)
@click.command(name='upload')
@click.pass_context
def upload_file(ctx):
contents_service.test_upload_file(ctx.obj['SESSION'], '/home/magnus/Downloads/3_meeting_notes.pdf')
@click.command(name='document')
@click.argument('course_id', required=True, type=str)
@click.argument('parent_id', required=True, type=str)
@click.argument('title', required=True, type=str)
@standard_options
@click.pass_context
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):
"""
Creates a document content in blackboard
"""
standard_options = StandardOptions(hide_content=hide_content, reviewable=reviewable)
validate_dates(standard_options, start_date, end_date)
response = contents_service.create_document(ctx.obj['SESSION'], course_id, parent_id, title, standard_options)
print(response)
@click.command(name='file')
@click.argument('course_id', required=True, type=str)
@click.argument('parent_id', required=True, type=str)
@click.argument('title', required=True, type=str)
@click.argument('file_path', required=True, type=click.Path(exists=True))
@file_options
@standard_options
@click.pass_context
def create_file(ctx, course_id: str, parent_id: str, title: str, file_path: str,
launch_in_new_window:bool, hide_content: bool, reviewable: bool,
start_date: str=None, end_date: str=None):
"""
Creates a file content in blackboard
"""
file_options = FileOptions(launch_in_new_window)
standard_options = StandardOptions(hide_content=hide_content, reviewable=reviewable)
validate_dates(standard_options, start_date, end_date)
response = contents_service.create_file(ctx.obj['SESSION'], course_id, parent_id, title, file_path, file_options, standard_options)
print(response)
def validate_dates(standard_options: StandardOptions, start_date: str, end_date: str):
if start_date:
try:
standard_options.date_interval.start_date = datetime.strptime(start_date, '%d/%m/%y %H:%M:%S')
except ValueError:
click.echo('Value format is not valid, please see --help for more info')
raise click.Abort()
if end_date:
try:
standard_options.date_interval.end_date = datetime.strptime(end_date, '%d/%m/%y %H:%M:%S')
except ValueError:
click.echo('Value format is not valid, please see --help for more info')
raise click.Abort()
from dataclasses import dataclass
from datetime import date
"""
OPTIONS DATA CLASSES
"""
@dataclass
class DateInterval:
start_date: date = None
end_date: date = None
@dataclass
class StandardOptions:
hide_content: bool = False
reviewable: bool = False
date_interval: DateInterval = DateInterval()
@dataclass
class FileOptions:
launch_in_new_window: bool = False
@dataclass
class WeblinkOptions:
launch_in_new_window: bool = False
"""
CONTENT-TYPE DATA CLASSES
"""
@dataclass
class FileContent:
upload_id: str
file_name: str
mime_type: str
duplicate_file_handling: str = 'Rename' # Options are Rename, Replace, ThrowError
......@@ -3,7 +3,7 @@ from subprocess import call
from typing import Dict, Any
import requests
from bbcli.services.courses_service import list_courses
from bbcli.utils.utils import set_cookies
from bbcli.utils.utils import input_body, set_cookies
import click
from bbcli.utils.URL_builder import URLBuilder
......@@ -43,10 +43,7 @@ def list_announcement(session: requests.Session, course_id: str, announcement_id
# TODO: Add compatibility for flags and options to make a more detailed announcement
def create_announcement(session: requests.Session, course_id: str, title: str):
MARKER = '# Everything below is ignored\n'
body = click.edit('\n\n' + MARKER)
if body is not None:
body = body.split(MARKER, 1)[0].rstrip('\n')
body = input_body()
data = {
'title': title,
......@@ -54,7 +51,6 @@ def create_announcement(session: requests.Session, course_id: str, title: str):
}
data = json.dumps(data)
session.headers.update({'Content-Type': 'application/json'})
url = url_builder.base_v1().add_courses().add_id(course_id).add_announcements().create()
response = session.post(url, data=data)
......@@ -83,8 +79,6 @@ def update_announcement(session: requests.Session, course_id: str, announcement_
announcement = json.dumps(editable_data, indent=2)
new_data = click.edit(announcement + '\n\n' + MARKER)
session.headers.update({'Content-Type': 'application/json'})
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)
......
import base64
from datetime import date
import json
import os
from subprocess import call
from tarfile import ENCODING
from typing import Dict, Any
from typing import Dict, Any, List
import requests
import magic
from bbcli.services.courses_service import list_courses
from bbcli.utils.URL_builder import URLBuilder
from bbcli.services.utils.content_builder import ContentBuilder
from bbcli.entities.content_builder_entitites import DateInterval, FileContent, StandardOptions, FileOptions, WeblinkOptions
from bbcli.utils.utils import input_body
url_builder = URLBuilder()
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_course_content(cookies: Dict, course_id: str):
print('Getting course content!')
......@@ -40,47 +48,144 @@ def list_assignments(cookies: Dict, course_id: str):
# Update spesific content
# NB: Alle options sende som values i enkle fields i bodyen
# Alle disse blir sendt til post content endpoint. title, body er samme. Options varirerer. Største ulikheten
# er i content-handler. None kan også ha attachments.
def test_create_assignment(session: requests.Session, course_id: str, content_id: str):
# Tror svaret er en ContentBuilder
# Hvis dato er inkludert for visning av content etter en spesifikk tid osv. oppdater ny regel ellr hva det nå er
# Kilde: https://docs.blackboard.com/rest-apis/learn/getting-started/adaptive-release. KANSKJE IKKE NØDVENDIG
# Title, body, eventuelt attachements?, Standard Options: permit users to view, track number of views, date (start, end)
def create_document(session:requests.Session, course_id: str, parent_id: str, title: str, standard_options: StandardOptions=None, attachments: List[str]=None):
data_body = input_body()
data = content_builder\
.add_parent_id(parent_id)\
.add_title(title)\
.add_body(data_body)\
.add_standard_options(standard_options)\
.add_content_handler_document()\
.create()
with open('/home/magnus/Downloads/3_meeting_notes.pdf', 'rb') as f:
byte_content = f.read()
base64_bytes = base64.b64encode(byte_content)
base64_string = base64_bytes.decode(ENCODING)
data = {
"parentId": content_id,
"title": "Test file",
'body': 'jaja',
"description": "string",
"position": 0,
"launchInNewWindow": True,
"availability": {
"available": "Yes",
"allowGuests": True,
"allowObservers": True,
"adaptiveRelease": {
"start": "2022-03-29T09:32:35.571Z",
}
},
"contentHandler": {
'id':'resource/x-bb-file',
'file': base64_string
},
}
data = json.dumps(data)
session.headers.update({'Content-Type': 'application/json'})
# files = {
# 'pdf_document': open('/home/magnus/Downloads/3_meeting_notes.pdf', 'rb')
# }
url = generate_create_content_url(course_id, parent_id)
response = session.post(url, data=data)
return response.text
# Returns the string: domain + /learn/api/public/v1/courses/{courseId}/contents/{contentId}/children
url = url_builder.base_v1().add_courses().add_id(course_id).add_contents().add_id(content_id).add_children().create()
# Title, file itself, FIle Options: open in new window, add alignment to content, Standard Options: as above
def create_file(session: requests.Session, course_id: str, parent_id: str, title: str, file_dst: str, file_options: FileOptions, standard_options: StandardOptions):
uploaded_file = upload_file(session, file_dst)
mime = magic.Magic(mime=True)
mime_type = mime.from_file(file_dst)
with open(file_dst, 'rb') as f:
file_name = os.path.basename(f.name)
file_content = FileContent(uploaded_file['id'], file_name, mime_type)
data = content_builder\
.add_parent_id(parent_id)\
.add_title(title)\
.add_standard_options(standard_options)\
.add_file_options(file_options)\
.add_content_handler_file(file_content)\
.create()
data = json.dumps(data)
url = generate_create_content_url(course_id, parent_id)
response = session.post(url, data=data)
print(response.text)
return response.text
# Title, URL, body, eventuelt attachments?, Weblink options: open in new window?, Standard options: as above
def create_externallink():
pass
# Title, body, Standard options: as above
def create_folder():
pass
# Title, body, location(?), targetId content, Standard Options: as above
def create_courselink():
pass
\ No newline at end of file
# Se egen metode i BBL REST API
def create_assignment():
pass
# Vet ikke enda
def create_forumlink():
pass
# Vet ikke enda
def create_blti_link():
pass
# TODO: Check how to publish audio and image, it doesn't have a content handler
# SOLUTION: See the upload files section. There it says something about uploading images and audio and videoetc,
# SOLUTION 2: THIS IS EMBEDDED IN THE BODY I BELIEVE
# TODO: Module page has it own 'resource/x-bb-module-page' content typ, but is not mentioned in the documentation
# Do reserach on this later
# TODO: Same as the one above, just with blank page 'resource/x-bb-blankpage', Both are under new page option in the
# bb web interface
# TODO: 'x-bb-lesson'
# TODO: CReat an own method for attachments posting for either being
# called straight after a content of type file, document, or assignment is created.
# OR afterwards
"""
HELPER FUNCTIONS
"""
def upload_file(session: requests.Session, dst: str):
del session.headers['Content-Type']
with open(dst, 'rb') as f:
file_name = os.path.basename(f.name)
files = {file_name: f.read()}
url = url_builder.base_v1().add_uploads().create()
response = session.post(url, files=files)
session.headers.update({
'Content-Type': 'application/json'
})
file = json.loads(response.text)
return file
def generate_create_content_url(course_id: str, content_id: str):
return url_builder\
.base_v1()\
.add_courses()\
.add_id(course_id)\
.add_contents()\
.add_id(content_id)\
.add_children()\
.create()
\ No newline at end of file
from __future__ import annotations
from datetime import date, datetime
from typing import Any, Dict
from abc import ABC, abstractmethod
from bbcli.entities.content_builder_entitites import FileContent, FileOptions, StandardOptions, WeblinkOptions
class Builder(ABC):
@property
@abstractmethod
def product(self) -> None:
pass
@abstractmethod
def add_parent_id(self, title: str) -> Builder:
pass
@abstractmethod
def add_title(self, title: str) -> Builder:
pass
@abstractmethod
def add_body(self, body: str) -> Builder:
pass
@abstractmethod
def add_standard_options(self, standard_options: StandardOptions) -> Builder:
pass
# Alignment option is available in creation in the web interface, but not in the actual content objects that is created
@abstractmethod
def add_file_options(self, file_options: FileOptions) -> Builder:
pass
@abstractmethod
def add_weblink_options(self, web_link_options: WeblinkOptions) -> Builder:
pass
@abstractmethod
def add_content_handler_document(self) -> Builder:
pass
@abstractmethod
def add_content_handler_file(self, file_content: FileContent) -> Builder:
pass
@abstractmethod
def add_content_handler_externallink(self, url: str) -> Builder:
pass
@abstractmethod
def add_content_handler_folder(self, is_bb_page: bool) -> Builder:
pass
# Possible target types:
# Unset
# CourseAssessment
# CourseTOC
# Forum
# Tool
# CollabSession (deprecated since 3000.1.0)
# Group
# BlogJournal
# StaffInfo
# ModulePage
@abstractmethod
def add_content_handler_courselink(self, target_id: str, target_type: str='Unset') -> Builder:
pass
class ContentBuilder(Builder):
def __init__(self) -> None:
self.reset()
def reset(self) -> None:
self._product = Content()
@property
def product(self) -> Content:
product = self._product
self.reset()
return product
def add_parent_id(self, parent_id: str) -> Builder:
self._product.add({
'parentId': parent_id
})
return self
def add_title(self, title: str) -> Builder:
self._product.add({
'title': title
})
return self
def add_body(self, body: str) -> Builder:
self._product.add({
'body': body
})
return self
def add_standard_options(self, standard_options: StandardOptions) -> Builder:
start_date_str = datetime.strftime(standard_options.date_interval.start_date,'%Y-%m-%dT%H:%m:%S.%fZ') if standard_options.date_interval.start_date else None
end_date_str = datetime.strftime(standard_options.date_interval.end_date, '%Y-%m-%dT%H:%m:%S.%fZ') if standard_options.date_interval.end_date else None
self._product.add({
'reviewable': standard_options.reviewable,
'availability': {
'available': 'No' if standard_options.hide_content else 'Yes',
'allowGuests': True,
'allowObservers': True,
'adaptiveRelease': {
"start": start_date_str,
"end": end_date_str
}
}
})
return self
# Missing an extra option, but don't know what it is called
def add_file_options(self, file_options: FileOptions) -> Builder:
self._product.add({
'launchInNewWindow': file_options.launch_in_new_window
})
return self
def add_weblink_options(self, web_link_options: WeblinkOptions) -> Builder:
self._product.add({
'launchInNewWindow': web_link_options.launch_in_new_window
})
return self
def add_content_handler_document(self) -> Builder:
self._product.add({
'contentHandler' : {
'id': 'resource/x-bb-document'
}
})
return self
def add_content_handler_file(self, file_content: FileContent) -> Builder:
self._product.add({
'contentHandler' : {
'id': 'resource/x-bb-file',
'file': {
'uploadId': file_content.upload_id,
'fileName': file_content.file_name,
'mimeType': file_content.mime_type,
'duplicateFileHandling': file_content.duplicate_file_handling
}
}
})
return self
def add_content_handler_externallink(self, url: str) -> Builder:
self._product.add({
'contentHandler' : {
'id': 'resource/x-bb-externallink',
'url': url
}
})
return self
def add_content_handler_folder(self, is_bb_page: bool) -> Builder:
self._product.add({
'contentHandler' : {
'id': 'resource/x-bb-folder',
'isBbPage': is_bb_page
}
})
return self
def add_content_handler_courselink(self, target_id: str, target_type: str = 'Unset') -> Builder:
self._product.add({
'contentHandler' : {
'id': 'resource/x-bb-folder',
'targetId': target_id,
'targeType': target_type
}
})
return self
def create(self) -> Dict:
content = self._product.get_content()
self._product = Content()
return content
class Content():
def __init__(self) -> None:
self.content = {}
def add(self, content_part: str) -> None:
self.content.update(content_part)
def get_content(self) -> None:
return self.content
\ No newline at end of file
......@@ -59,6 +59,10 @@ class Builder(ABC):
def add_attachments(self) -> Builder:
pass
@abstractmethod
def add_uploads(self) -> Builder:
pass
@abstractmethod
def add_id(self, id: str, id_type: str = None) -> Builder:
pass
......@@ -74,7 +78,6 @@ class URLBuilder(Builder):
@property
def product(self) -> URL:
product = self._product
self.reset()
return product
......@@ -120,6 +123,10 @@ class URLBuilder(Builder):
def add_attachments(self) -> Builder:
self._product.add('/attachments')
return self
def add_uploads(self) -> Builder:
self._product.add('/uploads')
return self
def add_id(self, id:str, id_type:str=None) -> URLBuilder:
if id_type:
......
......@@ -2,6 +2,7 @@ from datetime import datetime
from typing import Dict, List
from requests import Session
import html2text
import click
def check_valid_key(obj, key) -> bool:
......@@ -47,4 +48,11 @@ def set_headers(session: Session, headers: List):
def html_to_text(html_data: str):
to_text = html2text.HTML2Text()
return to_text.handle(html_data)
\ No newline at end of file
return to_text.handle(html_data)
def input_body():
MARKER = '# Everything below is ignored\n'
body = click.edit('\n\n' + MARKER)
if body is not None:
body = body.split(MARKER, 1)[0].rstrip('\n')
return body
\ No newline at end of file
......@@ -6,4 +6,6 @@ requests==2.27.1
python-dotenv==0.19.2
beautifulsoup4==4.10.0
lxml==4.8.0
anytree==2.8.0
\ No newline at end of file
anytree==2.8.0
html2text==2020.1.16
python-magic==0.4.25
\ 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