5

I'm trying to access the web services of a Moodle installation I have using Python's requests library. I have the API's documentation and an example project written in php (I haven't looked at php before and is way more difficult than I would expect for me to understand) but am really struggling to properly format the request. The site is returning invalid paramater detected so I'm pretty sure my endpoint, authorization token, and server config is working and it's just the format of the data that is letting me down.

First here is the error...

<?xml version="1.0" encoding="UTF-8" ?>
<EXCEPTION class="invalid_parameter_exception">
<ERRORCODE>invalidparameter</ERRORCODE>
<MESSAGE>Invalid parameter value detected</MESSAGE>
</EXCEPTION>

And now my code...

import requests

target = 'http://example.com/moodle/webservice/rest/server.php?'
moodle_create_token = 'xxx'
moodle_enrol_token = 'yyy'
url_payload = {
    "wstoken":moodle_create_token,
   "wsfunction":"core_user_create_users"
    }

###not sure if I should just be passing this as a dict or some deeper more layered struct
payload = {
    "username":"testuser",
    "password":'testpass',
    "firstname":'testf',
    "lastname":'testl',
    "email":"test@example.com",
    "idnumber":"1234"
}

###not sure how to include the payload as the last argument in the function (currently data=)
###I feel like at this point I've just been throwing random data at it and hoping something sticks haha.
r=requests.post(target, params=url_payload, data=payload)

Here is the site's documentation

moodle api general structure

moodle api XML-RPC (PHP structure)

moodle api REST (POST parameters)

moodle response format 1

moodle response format 2

Finally the example in php.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>V6</title>
</head>

<body>

<?php
//load curl.php
require_once('curl.php');

function randomPassword() //according to Moodle password requirements
{
    $part1 = "";
    $part2 = "";
    $part3 = "";

    //alphanumeric LOWER
    $alphabet = "abcdefghijklmnopqrstuwxyz";
    $password_created = array(); //remember to declare $pass as an array
    $alphabetLength = strlen($alphabet) - 1; //put the length -1 in cache
    for ($i = 0; $i < 3; $i++) 
    {
        $pos = rand(0, $alphabetLength); // rand(int $min , int $max)
        $password_created[] = $alphabet[$pos];
    }
    $part1 = implode($password_created); //turn the array into a string
    //echo"<br/>part1 = $part1";

    //alphanumeric UPPER
    $alphabet = "ABCDEFGHIJKLMNOPQRSTUWXYZ";
    $password_created = array(); //remember to declare $pass as an array
    $alphabetLength = strlen($alphabet) - 1; //put the length -1 in cache
    for ($i = 0; $i < 3; $i++) 
    {
        $pos = rand(0, $alphabetLength); // rand(int $min , int $max)
        $password_created[] = $alphabet[$pos];
    }   
    $part2 = implode($password_created); //turn the array into a string
    //echo"<br/>part2 = $part2";

    //alphanumeric NUMBER
    $alphabet = "0123456789";
    $password_created = array(); //remember to declare $pass as an array
    $alphabetLength = strlen($alphabet) - 1; //put the length -1 in cache
    for ($i = 0; $i < 2; $i++) 
    {
        $pos = rand(0, $alphabetLength); // rand(int $min , int $max)
        $password_created[] = $alphabet[$pos];
    }   
    $part3 = implode($password_created); //turn the array into a string
    //echo"<br/>part3 = $part3";

    $password = $part1 . $part2 . $part3 . "#";

    return $password;
}

function getCDate()
{
    $format = "Ymd";
    $fulldate = date($format);  
    //echo"<br/>fulldate = $fulldate";
    return $fulldate;
}

function enrol($user_id, $course_id) 
{
    $role_id = 5; //assign role to be Student

    $domainname = 'http://www.yoursite.eu'; //paste your domain here
    $wstoken = '8486ed14f3ghjec8967a0229d0a28zzz'; //here paste your enrol token 
    $wsfunctionname = 'enrol_manual_enrol_users';

    $enrolment = array( 'roleid' => $role_id, 'userid' => $user_id, 'courseid' => $course_id );
    $enrolments = array($enrolment);
    $params = array( 'enrolments' => $enrolments );

    header('Content-Type: text/plain');
    $serverurl = $domainname . "/webservice/rest/server.php?wstoken=" . $wstoken . "&wsfunction=" . $wsfunctionname;
    $curl = new curl;
    $restformat = ($restformat == 'json')?'&moodlewsrestformat=' . $restformat:'';
    $resp = $curl->post($serverurl . $restformat, $params);
    print_r($resp);
}

function getUserDetails()
{
    $firstname  = "TestUser";
    $lastname   = "TestUser";
    $email      = "TestUser@zzz.gr";
    $city       = "Thessaloniki";
    $country    = "EL";
    $description= "ZZZ";

    //assign username
    //get first two letters of name and surname
    //$strlength_user = strlen($firstname);
    //$strlength_pass = strlen($lastname);
    $rest_firstname = substr($firstname, 0, 2);
    $rest_lastname  = substr($lastname, 0, 2);
    $part1 = $rest_firstname . $rest_lastname;
    $part1 = strtolower($part1);
    //echo"<br/>part1 = $part1";
    $dt = getCDate();
    $part2 = substr($dt, -4);
    //echo"<br/>part2 = $part2";

    $username = $part1 . "." . $part2;
    echo"<br/>Username = $username";

    //assign password
    $password = randomPassword();
    echo"<br/>Password = $password";

    //call WS core_user_create_user of moodle to store the new user
    $domainname = 'http://www.yoursite.eu';
    $wstoken = 'ed1f6d3ebadg372f95f28cd96bd43zzz'; //here paste your create user token 
    $wsfunctionname = 'core_user_create_users';
    //REST return value
    $restformat = 'xml'; 
    //parameters
    $user1 = new stdClass();
    $user1->username    = $username;
    $user1->password    = $password;
    $user1->firstname   = $firstname;
    $user1->lastname    = $lastname;
    $user1->email       = $email;
    $user1->auth        = 'manual';
    $user1->idnumber    = 'numberID';
    $user1->lang        = 'en';
    $user1->city        = $city;
    $user1->country     = $country;
    $user1->description = $description;

    $users = array($user1);
    $params = array('users' => $users);
    //REST call
    header('Content-Type: text/plain');
    $serverurl = $domainname . "/webservice/rest/server.php?wstoken=" . $wstoken . "&wsfunction=" . $wsfunctionname;
    $curl = new curl;
    $restformat = ($restformat == 'json')?'&moodlewsrestformat=' . $restformat:'';
    $resp = $curl->post($serverurl . $restformat, $params);
    print_r($resp);\


    //get id from $resp
    $xml_tree = new SimpleXMLElement($resp);
    print_r($xml_tree);         
    $value = $xml_tree->MULTIPLE->SINGLE->KEY->VALUE;
    $user_id = intval(sprintf("%s",$value));
    echo"<br/>user_id number = $user_id";

    //enrol_manual_enrol_users 
    //for($i = 64; $i < 70; $i++) //where 64,65,66,67,68,69 are the six ids of the six courses of phase 1
    for($i = 64; $i < 65; $i++)
    {
        echo "\nThe user has been successfully enrolled to course " . $i;
        $course_id = $i;
        enrol($user_id, $course_id);
    }   
}

getUserDetails();

?>
</body>
</html>
grhmstl
  • 51
  • 1
  • 2
  • try to change `data=payload` to `json=payload` – KC. Nov 06 '18 at 08:14
  • Thanks I had tried that before. I think the problem is the nesting of the data I'm passing. I'm sending a single level structure... data = [pair 1, pair 2, pair 3] Where what I need to be doing is something a level deeper user=[pair 1, pair 2, pair 3] data = [user[]] I just can't get it to quite work. – grhmstl Nov 06 '18 at 22:02

3 Answers3

2

Here is an example drawn from mrcinv/moodle_api.py that shows the usage of Python's requests to hit the Moodle Web Services API:

from requests import get, post
# Module variables to connect to moodle api
KEY = "SECRET API KEY"
URL = "https://moodle.site.com"
ENDPOINT="/webservice/rest/server.php"

def rest_api_parameters(in_args, prefix='', out_dict=None):
    """Transform dictionary/array structure to a flat dictionary, with key names
    defining the structure.

    Example usage:
    >>> rest_api_parameters({'courses':[{'id':1,'name': 'course1'}]})
    {'courses[0][id]':1,
     'courses[0][name]':'course1'}
    """
    if out_dict==None:
        out_dict = {}
    if not type(in_args) in (list,dict):
        out_dict[prefix] = in_args
        return out_dict
    if prefix == '':
        prefix = prefix + '{0}'
    else:
        prefix = prefix + '[{0}]'
    if type(in_args)==list:
        for idx, item in enumerate(in_args):
            rest_api_parameters(item, prefix.format(idx), out_dict)
    elif type(in_args)==dict:
        for key, item in in_args.items():
            rest_api_parameters(item, prefix.format(key), out_dict)
    return out_dict

def call(fname, **kwargs):
    """Calls moodle API function with function name fname and keyword arguments.

    Example:
    >>> call_mdl_function('core_course_update_courses',
                           courses = [{'id': 1, 'fullname': 'My favorite course'}])
    """
    parameters = rest_api_parameters(kwargs)
    parameters.update({"wstoken": KEY, 'moodlewsrestformat': 'json', "wsfunction": fname})
    response = post(URL+ENDPOINT, parameters).json()
    if type(response) == dict and response.get('exception'):
        raise SystemError("Error calling Moodle API\n", response)
    return response

class CourseList():
    """Class for list of all courses in Moodle and order them by id and idnumber."""
    def __init__(self):
        # TODO fullname atribute is filtered
        # (no <span class="multilang" lang="sl">)
        courses_data = call('core_course_get_courses')
        self.courses = []
        for data in courses_data:
            self.courses.append(Course(**data))
        self.id_dict = {}
        self.idnumber_dict = {}
        for course in self.courses:
            self.id_dict[course.id] = course
            if course.idnumber:
                self.idnumber_dict[course.idnumber] = course
    def __getitem__(self, key):
        if 0<= key < len(self.courses):
            return self.courses[key]
        else:
            raise IndexError

    def by_id(self, id):
        "Return course with given id."
        return self.id_dict.get(id)

    def by_idnumber(self, idnumber):
        "Course with given idnumber"
        return self.idnumber_dict.get(idnumber)

    def update_courses(courses_to_update, fields):
        "Update a list of courses in one go."
        if not ('id' in fields):
            fields.append('id')
        courses = [{k: c.__dict__[k] for k in fields} for c in courses_to_update]
        return call("core_course_update_courses", 
             courses = courses)

.. and also shows how to define custom classes for Course. In the same fashion one could create classes for User, Grades, etc.

Furthermore, there are some wrapper modules on PyPi, e.g. moodle, and moodle-ws-client.

wp78de
  • 18,207
  • 7
  • 43
  • 71
0

Okay so I found a solution that works but I suspect it is a bit hodgepodge and not utilizing requests library to its fullest.

What I did was pass all the arguments as parameters in the url.

target = 'http://example.com/moodle/webservice/rest/server.php'
moodle_create_token = 'xxx'

payload = {
    "wstoken":moodle_create_token,
    "moodlewsrestformat":"json", #just to get response as json
    "wsfunction":"core_user_create_users",
    "users[0][username]":"testusername",
    "users[0][password]":'testpassword',
    "users[0][firstname]":'testfirstname',
    "users[0][lastname]":'testlastname',
    "users[0][email]":"testemail@example.com",
    "users[0][idnumber]":"0000001"
    }

r=requests.post(target, params=payload)

Obviously I won't usually have the data hard-coded as strings but apparently the list of dictionaries for url params will be.

grhmstl
  • 51
  • 1
  • 2
0

I have made a python library named moodlepy

pip install moodlepy

Its easy to use, example

from moodle import Moodle

target = 'http://example.com/moodle/webservice/rest/server.php'
moodle_create_token = 'xxx'

moodle = Moodle(target, moodle_create_token)
r = moodle(
    'core_user_create_users',
    username="testusername",
    password='testpassword',
    firstname='testfirstname',
    lastname='testlastname',
    email="testemail@example.com",
    idnumber="0000001"
)  # return the data (dict, list, etc)

You can also use typed response, for example calling core_webservice_get_site_info

site_info = moodle.core.webservice.get_site_info()
site_info.username
site_info.version

Note: Not all functions are implemented (yet).

Habib R.
  • 1
  • 2