Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions moodle/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ class InvalidCredentialException(BaseException):
message: str = "Wrong username or password!"


@dataclass
class UploadUrlException(BaseException):
message: str = "File Upload URL can not be determined!"


@dataclass
class NetworkMoodleException(BaseException):
"""Moodle wrapper for network related network error"""
Expand Down
55 changes: 54 additions & 1 deletion moodle/mdl.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,52 @@ def post(self, wsfunction: str, moodlewsrestformat="json", **kwargs: Any) -> Any
return self.process_response(data)
return res.text

def post_upload(
self, *files,
moodlewsrestformat="json",
itemid: int = 0, filepath: str = "/"
) -> Any:
"""Send post request to file upload endpoint.

Args:
files (file-like-objects, multiple): The files to upload.
moodlewsrestformat (str, optional): Expected format. Defaults to "json".
itemid (int, optional): itemid to upload into. Defaults to 0.
filepath (str, optional): file path under which to upload. Defaults to '/'.

Raises:
NetworkMoodleException: If request failed
EmptyResponseException: If the response empty
UploadUrlException: If the upload endpoint URL can't be inferred
MoodleException: Error from server

Returns:
Any: Raw data (str) or dict
"""
if not self.url.endswith("/rest/server.php"):
raise UploadUrlException()

params = {
"token": self.token,
"moodlewsrestformat": moodlewsrestformat,
"itemid": itemid,
"filepath": filepath,
}
try:
res = self.session.post(
self.url.replace("/rest/server.php", "/upload.php"),
params=params,
files={f"file_{i}": file for i, file in enumerate(files, start=1)},
)
except RequestException as e:
raise NetworkMoodleException(e)
if not res.ok or not res.text:
raise EmptyResponseException()
if res.ok and moodlewsrestformat == "json":
data = json.loads(res.text)
return self.process_response(data)
return res.text

def process_response(self, data: Any) -> Any:
"""Process data to handle exception or warnings

Expand All @@ -120,7 +166,14 @@ def process_response(self, data: Any) -> Any:
elif isinstance(data["warnings"], dict):
warning = MoodleWarning(**data["warnings"]) # type: ignore
self.logger.warning(str(warning))
if "exception" in data or "errorcode" in data:
if "error" in data:
# upload.php errors are sometimes structured a bit differently
error = data.pop("error")
data.pop("stacktrace")
data.pop("reproductionlink")
data["message"] = error
raise MoodleException(**data) # type: ignore
elif "exception" in data or "errorcode" in data:
raise MoodleException(**data) # type: ignore
return data

Expand Down
6 changes: 6 additions & 0 deletions moodle/moodle.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any

from moodle.mdl import Mdl
from moodle.upload import Upload

from moodle.auth import Auth
from moodle.block import Block
Expand All @@ -20,6 +21,11 @@ def __init__(self, url: str, token: str):
def __call__(self, wsfunction: str, moodlewsrestformat="json", **kwargs) -> Any:
return self.post(wsfunction, moodlewsrestformat, **kwargs)

@property # type: ignore
@lazy
def upload(self) -> Upload:
return Upload(self)

@property # type: ignore
@lazy
def auth(self) -> Auth:
Expand Down
8 changes: 8 additions & 0 deletions moodle/upload/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from .file import File

from .upload import Upload

__all__ = [
"File",
"Upload",
]
32 changes: 32 additions & 0 deletions moodle/upload/file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from moodle.attr import dataclass


@dataclass
class File:
"""File

Args:
component (str): component
contextid (int): contextid
userid (str): userid
filearea (str): filearea
filename (str): filename
filepath (str): filepath
itemid (int): itemid
license (str): license
author (str): author
source (str): source
filesize (int): filesize
"""

component: str
contextid: int
userid: str
filearea: str
filename: str
filepath: str
itemid: int
license: str
author: str
source: str
filesize: int
23 changes: 23 additions & 0 deletions moodle/upload/upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import List

from moodle import BaseMoodle
from . import File


class Upload(BaseMoodle):
def __call__(
self, *files, itemid: int = 0, filepath: str = "/"
) -> List[File]:
"""Uploads files to moodle.

Args:
files (file-like-objects, multiple): The files to upload.
moodlewsrestformat (str, optional): Expected format. Defaults to "json".
itemid (int, optional): itemid to upload into. Defaults to 0.
filepath (str, optional): file path under which to upload. Defaults to '/'.

Returns:
List[File]: The details for the uploaded files
"""
data = self.moodle.post_upload(*files, itemid=itemid, filepath=filepath)
return self._trs(File, data)
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def domain() -> str:
@fixture
def moodle(domain: str) -> Moodle:
username = "manager"
password = "moodle2024"
password = "moodle25"
return Moodle.login(domain, username, password)


Expand Down
14 changes: 14 additions & 0 deletions tests/test_upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from moodle import Moodle
from moodle.upload import File


class TestUpload:
def test_upload(self, moodle: Moodle):
files = moodle.upload(
('LICENSE', open('LICENSE', 'rb')),
)
assert type(files) == list
assert len(files) == 1
for f in files:
assert isinstance(f, File)
assert f.filename == 'LICENSE'