pax_global_header00006660000000000000000000000064140753327650014526gustar00rootroot0000000000000052 comment=1105b362275eaf3c777793ea62bf3de9e90bc663 wimsapi-0.5.11/000077500000000000000000000000001407533276500132635ustar00rootroot00000000000000wimsapi-0.5.11/.github/000077500000000000000000000000001407533276500146235ustar00rootroot00000000000000wimsapi-0.5.11/.github/workflows/000077500000000000000000000000001407533276500166605ustar00rootroot00000000000000wimsapi-0.5.11/.github/workflows/publish.yml000066400000000000000000000026731407533276500210610ustar00rootroot00000000000000name: Publish on: push: tags: ['**'] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [ 3.5, 3.6, 3.7, 3.8, 3.9 ] steps: - uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Setup the WIMS server run: | docker run -itd --cpuset-cpus=$(($(cat /proc/cpuinfo | grep -e "processor\s*:\s*\d*" | wc -l) - 1)) -p 7777:80 --name wims-minimal qcoumes/wims-minimal docker exec -i wims-minimal ./bin/apache-config docker exec -i wims-minimal service apache2 restart - name: Install Tox and any other packages run: | pip install tox - name: Run Tox run: tox -e py - name: Upload coverage to Codecov if: matrix.python-version == 3.8 uses: codecov/codecov-action@v1 with: file: ./coverage.xml publish: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Creating Built Distributions run: python setup.py sdist - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@master with: password: ${{ secrets.pypi_password }} skip_existing: true wimsapi-0.5.11/.github/workflows/test.yml000066400000000000000000000021131407533276500203570ustar00rootroot00000000000000name: Tests on: push: branches-ignore: - master pull_request: branches: - master - '*.*.*' schedule: - cron: '0 8 * * 1' jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.5, 3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Setup the WIMS server run: | docker run -itd --cpuset-cpus=$(($(cat /proc/cpuinfo | grep -e "processor\s*:\s*\d*" | wc -l) - 1)) -p 7777:80 --name wims-minimal qcoumes/wims-minimal docker exec -i wims-minimal ./bin/apache-config docker exec -i wims-minimal service apache2 restart - name: Install Tox and any other packages run: | pip install tox - name: Run Tox run: tox -e py - name: Upload coverage to Codecov if: matrix.python-version == 3.8 uses: codecov/codecov-action@v1 with: file: ./coverage.xml wimsapi-0.5.11/.gitignore000066400000000000000000000023121407533276500152510ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # Pycharm .idea/ .xml wimsapi-0.5.11/CHANGES.md000066400000000000000000000100171407533276500146540ustar00rootroot00000000000000# Changelog #### 0.5.11 * `api.WimsAPI.authuser` now accept an `ip` argument, allowing a persistent session for the same IP. (Contributed by Gianluca Amato) #### 0.5.10 * WIMS accept request saving user with invalid `quser`, removing or changing invalid character. But `wimsapi` was taking this change into account, the `quser` attribute of the user was thus invalid, causing problem when further communicating with the WIMS server. To solve this problem `User.save()` now has a `adapt=True` keyword argument. When `True`, the `quser` attribute will be modified to match the one used by WIMS. If `False`, the user created on the WIMS server with the modifier `quser` will be deleted and the new exception `InvalidIdentifier` will be raised. #### 0.5.9 * Keyword argument that will be passed to every call of `request.post()` can now be given to `WimsApi` constructor. * Every method of `Class` creating a `WimsAPI` can also receive such argument (`check()`, `save()`, `get()`, `list()`) * Now use `sdist` instead of `bdist` to create new distribution. #### 0.5.8 * Now use Github action for testing and publishing #### 0.5.7 * Added `__str__` method to InvalidResponseError. #### 0.5.6 * Added `response` field to InvalidResponseError. #### 0.5.5 * Added InvalidResponseError exception in api.py when WIMS send a badly formatted response. #### O.5.4 * Append `/` at the end of the WIMS server's url if it is not present when using `WimsAPI`. * Added adm/raw API to the documentation. #### 0.5.3 * Default timeout for low level API is now 10 seconds (instead of 120). #### 0.5.2 * Parameters of `api.py` requests are now encoding in `ISO-8859-1`, mathching WIMS' default encoding * Adding `__repr__` and `__str__` method to `Class` and `Item` subtypes. * Getting Exams from the WIMS server now retrieve the correct status. #### 0.5.1 * Fix sheet's score computation ### 0.5.0 * Added classes `Exam` and `ExamScore`, `ExerciseScore` and `SheetScore` to store scores * `Sheet` / `Exam`: * Title and description are now optionnal in constructor. * Added method `scores(user=None)` to retrieve the score of one or every user. * Added class method `check()` to `Class` to check wheter a class exists or not. * Better `__eq__` and `__hash__` for every class. #### 0.4.1 * Listing functions now return an empty list when needed ### 0.4.0 * Added new item : `Sheet` * Added the possibility to list items and classes through `Class.list()` and `class.listitem()`. * Added `__eq__()` for items and classes. * Fixed some documentation #### 0.3.9 * Renamed Class member `date` to `expiration` to match the *ADM/RAW* argument. * `Class.limit` is now an *int* when retrieving the class from a *WIMS* server. * Now propagate exception if expiration in Class `__init__` is not `yyyymmdd`. #### 0.3.7 & 0.3.8 * `check_exists` is now used properly #### 0.3.6 * Added `check_exists=True` parameter to item's save method. If check_exists is True, the api will check if an item with the same ID exists on the WIMS' server. If it exists, save will instead modify this item instead of trying to create new one. `wclass.additem()` will now use `check_exists=False`. * Fix response check in `wclass.save()` #### 0.3.5 * Fix missing `self.lang = lang` in **Class**' `__init__` #### 0.3.4 * Fix `long_description` in setup.py #### 0.3.3 * `qclass` argument is now optionnal in Class constructor, allowing WIMS to choose a free `qclass` when saving for the fist time. #### 0.3.2 * Fixed `WimsAPI.putexo()`. * Updated tests and unskipped some according to latest WIMS version. #### 0.3.1 * Fixed buggy import in setup.py ### 0.3.0 * Adding **Classe** higher level API, allowing to manipulate a WIMS' Class. * Adding **User** higher level API, allowing to manipulate a WIMS' User. * Adding documentation. #### 0.2.2 * More tests. #### 0.2.1 * Fixed travis CI. ### 0.2.0 * Add User and Class higher level classes. * Add some tests fomr WimsApi. * Add Travis CI. ### 0.1.0 * Initial public release. wimsapi-0.5.11/LICENSE000066400000000000000000000020571407533276500142740ustar00rootroot00000000000000MIT License Copyright (c) 2018 Coumes Quentin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wimsapi-0.5.11/MANIFEST.in000066400000000000000000000000151407533276500150150ustar00rootroot00000000000000include *.md wimsapi-0.5.11/README.md000066400000000000000000000112461407533276500145460ustar00rootroot00000000000000[![Python package](https://github.com/qcoumes/wimsapi/workflows/Python%20package/badge.svg)](https://github.com/qcoumes/wimsapi/actions/) [![codecov](https://codecov.io/gh/qcoumes/wimsapi/branch/master/graph/badge.svg)](https://codecov.io/gh/qcoumes/wimsapi) [![CodeFactor](https://www.codefactor.io/repository/github/qcoumes/wimsapi/badge)](https://www.codefactor.io/repository/github/qcoumes/wimsapi) [![Documentation Status](https://readthedocs.org/projects/wimsapi/badge/?version=master)](https://wimsapi.readthedocs.io/?badge=master) [![PyPI Version](https://badge.fury.io/py/wimsapi.svg)](https://badge.fury.io/py/wimsapi) [![Python 3.5+](https://img.shields.io/badge/python-3.5+-brightgreen.svg)](#) [![License MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/qcoumes/wimsapi/blob/master/LICENSE) # Python API for WIMS' adm/raw module **WimsAPI** is an API written in python3 allowing to communicate with a *WIMS* server through its *adm/raw* extension. For more information about *adm/raw*, [see its documentation](https://wims.auto.u-psud.fr/wims/wims.cgi?module=adm/raw&job=help) Here the [documentation of wimsapi](https://wimsapi.readthedocs.io/en/latest/). ## Installation The latest stable version is available on [PyPI](https://pypi.org/project/wimsapi/) : ```bash pip install wimsapi ``` or from the sources: ```bash git clone https://github.com/qcoumes/wimsapi cd wimsapi python3 setup.py install ``` ## Configuration ### Global configuration In order for *WIMS* to accept requests from **WimsAPI**, a file must be created in `[WIMS_HOME]/log/classes/.connections/`, the file's name will serve as the identifier name for **WimsAPI**. Here an exemple of such file: `[WIMS_HOME]/log/classes/.connections/myself` ``` ident_site=172.17.0.1 ident_desc=This WIMS server ident_agent=python-requests # http / https. ident_protocol=http # password must be a word composed of alpha-numeric characters. ident_password=toto ident_type=json # The address and identifier/password pair for calling back. back_url=http://localhost/wims/wims.cgi back_ident=myself back_password=toto ``` Here a description of the important parameters: * `ident_site`: a space separated list of IP allowed to send request to this *WIMS* server. * `ident_agent`: ***Must*** be set to `python-requests`. * `ident_password`: Used alongside the file's name as *identifier* in the request to authenticate yourself on *WIMS*. * `ident_type`: ***Must*** be set to `json`. The above example would allow a computer/server of ip `172.17.0.1` to send a request to the *WIMS* server with identifier *myself* and password *toto*. ### Class Configuration If you create a class thanks to this API, everything should work perfectly. However, if you want to use it with an already existing class, some more configuration must be done. You must edit the file `[WIMS_HOME]/log/classes/[CLASS_ID]/.def` and add this line at the end of the file: ``` !set class_connections=+IDENT/RCLASS+ ``` Where **IDENT** is the identifier use by the API (name of the corresponding file in `[WIMS_HOME]/log/classes/.connections/` as defined above) and **RCLASS** is an identifier sent in the request to authenticate yourself on the class. Basically, to authenticate yourself on a class on your *WIMS* server, you will need : * `url` : URL to the *WIMS* (`https://wims.unice.fr/wims/wims.cgi` for instance) * `ident` : Name of the file in `[WIMS_HOME]/log/classes/.connections/` * `passwd` : Value of `ident_password` in `[WIMS_HOME]/log/classes/.connections/[IDENT]` * `rclass` : Value set after the **/** in `class_connections` in `[WIMS_HOME]/log/classes/[CLASS_ID]/.def` ## Example ```python from wimsapi import Class, User c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") c.institution = "Another institution" # Modify class' institution c.save() u = User.get(c, "qcoumes") u.email = "coumes.quentin@gmail.com" # Modify user's email u.save() new = User("quser", "lastname", "firstname", "password", "mail@mail.com") c.additem(new) # Add the new user to the class. ``` For more informations about usage or example : Here the complete [documentation of wimsapi](https://wimsapi.readthedocs.io/en/latest/). ## Testing To test *wimsapi*, you will need a running WIMS' server. If needed, you can set up one quickly with docker using the DockerFile [here](https://github.com/qcoumes/docker-wims-minimal), following the *README* steps. The default URL used for tests is `http://localhost:7777/wims/wims.cgi`, you can override it with the environment variable `WIMS_URL`. For instance: ```bash WIMS_URL=http://mywims.com/wims/wims.cgi pytest ``` wimsapi-0.5.11/docs/000077500000000000000000000000001407533276500142135ustar00rootroot00000000000000wimsapi-0.5.11/docs/.readthedocs.yml000066400000000000000000000000431407533276500172760ustar00rootroot00000000000000requirements_file: requirements.txtwimsapi-0.5.11/docs/CHANGES.md000066400000000000000000000100171407533276500156040ustar00rootroot00000000000000# Changelog #### 0.5.11 * `api.WimsAPI.authuser` now accept an `ip` argument, allowing a persistent session for the same IP. (Contributed by Gianluca Amato) #### 0.5.10 * WIMS accept request saving user with invalid `quser`, removing or changing invalid character. But `wimsapi` was taking this change into account, the `quser` attribute of the user was thus invalid, causing problem when further communicating with the WIMS server. To solve this problem `User.save()` now has a `adapt=True` keyword argument. When `True`, the `quser` attribute will be modified to match the one used by WIMS. If `False`, the user created on the WIMS server with the modifier `quser` will be deleted and the new exception `InvalidIdentifier` will be raised. #### 0.5.9 * Keyword argument that will be passed to every call of `request.post()` can now be given to `WimsApi` constructor. * Every method of `Class` creating a `WimsAPI` can also receive such argument (`check()`, `save()`, `get()`, `list()`) * Now use `sdist` instead of `bdist` to create new distribution. #### 0.5.8 * Now use Github action for testing and publishing #### 0.5.7 * Added `__str__` method to InvalidResponseError. #### 0.5.6 * Added `response` field to InvalidResponseError. #### 0.5.5 * Added InvalidResponseError exception in api.py when WIMS send a badly formatted response. #### O.5.4 * Append `/` at the end of the WIMS server's url if it is not present when using `WimsAPI`. * Added adm/raw API to the documentation. #### 0.5.3 * Default timeout for low level API is now 10 seconds (instead of 120). #### 0.5.2 * Parameters of `api.py` requests are now encoding in `ISO-8859-1`, mathching WIMS' default encoding * Adding `__repr__` and `__str__` method to `Class` and `Item` subtypes. * Getting Exams from the WIMS server now retrieve the correct status. #### 0.5.1 * Fix sheet's score computation ### 0.5.0 * Added classes `Exam` and `ExamScore`, `ExerciseScore` and `SheetScore` to store scores * `Sheet` / `Exam`: * Title and description are now optionnal in constructor. * Added method `scores(user=None)` to retrieve the score of one or every user. * Added class method `check()` to `Class` to check wheter a class exists or not. * Better `__eq__` and `__hash__` for every class. #### 0.4.1 * Listing functions now return an empty list when needed ### 0.4.0 * Added new item : `Sheet` * Added the possibility to list items and classes through `Class.list()` and `class.listitem()`. * Added `__eq__()` for items and classes. * Fixed some documentation #### 0.3.9 * Renamed Class member `date` to `expiration` to match the *ADM/RAW* argument. * `Class.limit` is now an *int* when retrieving the class from a *WIMS* server. * Now propagate exception if expiration in Class `__init__` is not `yyyymmdd`. #### 0.3.7 & 0.3.8 * `check_exists` is now used properly #### 0.3.6 * Added `check_exists=True` parameter to item's save method. If check_exists is True, the api will check if an item with the same ID exists on the WIMS' server. If it exists, save will instead modify this item instead of trying to create new one. `wclass.additem()` will now use `check_exists=False`. * Fix response check in `wclass.save()` #### 0.3.5 * Fix missing `self.lang = lang` in **Class**' `__init__` #### 0.3.4 * Fix `long_description` in setup.py #### 0.3.3 * `qclass` argument is now optionnal in Class constructor, allowing WIMS to choose a free `qclass` when saving for the fist time. #### 0.3.2 * Fixed `WimsAPI.putexo()`. * Updated tests and unskipped some according to latest WIMS version. #### 0.3.1 * Fixed buggy import in setup.py ### 0.3.0 * Adding **Classe** higher level API, allowing to manipulate a WIMS' Class. * Adding **User** higher level API, allowing to manipulate a WIMS' User. * Adding documentation. #### 0.2.2 * More tests. #### 0.2.1 * Fixed travis CI. ### 0.2.0 * Add User and Class higher level classes. * Add some tests fomr WimsApi. * Add Travis CI. ### 0.1.0 * Initial public release. wimsapi-0.5.11/docs/adm-raw.md000066400000000000000000000432641407533276500160760ustar00rootroot00000000000000# Protocol for WIMS direct connection with other web servers WIMS direct communication with another web server is two-directional. It can receive http/https requests from the other server, and/or send http/https requests to the other. The connectable server must be declared in a file within the directory `WIMS_HOME/log/classes/.connections/`. Requests sent to WIMS should obey the format described below. Results of such requests can be formatted according to the need of the connecting software. Outgoing requests sent by WIMS can be formatted according to the specification of the receiving software, while the result of the request should be formatted according to the need of WIMS, as described below. _________________________________________________________________ ## Request format A request from a connecting server is an http/https request sent to the main cgi program of the WIMS server, followed by parameter definitions as in a usual http protocol. All requests must contain the following common parameters: --- | Name | Value | |--------|----------------------------| | module | adm/raw | | ident | sender identifier (a word, according to the definition in `WIMS_HOME/log/classes/.connections/`) | | passwd | sender password (as defined in `WIMS_HOME/log/classes/.connections/`) | | code | a random word. This word will be sent back to the sender, in order to allow it to check whether the result is from the good request. | | job | type of request, see below. | And the following parameters may be added according to the type of the request. | Name | Value | |----------|----------------------------| | qclass | identifier of the class on the receiving server. It should be an integer. | | qprogram | | | quser | user identifier in the receiving server (when the request concerns a particular user). | | qsheet | sheet identifier in the receiving server (when the request concerns a particular sheet). | | rclass | identifier of the class on the sending server. | | format | Format of the data. | | option | Different meaning according to request. | | data1 | Different meaning according to request. | For example, the following request checks whether the class `12345` exists on the WIMS server `wims.unice.fr`, sent by a connecting server called `friend1` (by wims.unice.fr), with password `abcde`. `https://wims.unice.fr/wims/wims.cgi?module=adm/raw&ident=friend1&passwd=abcde&code=afdqreaf1r783&job=checkclass&qclass=12345&rclass=myclass` Note that for this check to return OK, the class `12345` on wims.unice.fr must have declared `friend1/myclass` as connectable. This can be done on an existing class by adding `friend1/myclass` to the `class_connections` parameter in the class `12345` .def file. _______________________________________________________________________________ ## Query output The query output (that is, the result of the http query) is always of text/plain type (even if sometimes the output is a binary file). **WIMS OUTPUT TYPE** : (old fashioned way, non-recommended) The first line of the output content is a status line, which is either a word OK followed by the random code contained in the request, or the word ERROR. If the first line is not as such, then there is a serious error in the request or a bug in the server software. In case the status is OK, the remaining of the output content is the content of the data. Otherwise the second line contains the nature of the error. **JSON OUTPUT TYPE** : (recommended) output values are returned json formatted. (see [http://json.org]() for more details) _______________________________________________________________________________ ## Types of the requests. A request to WIMS can have the following types (defined by the parameter 'job') ### job=help Show this text. ### job=checkident Check whether the connection is accepted. ### job=checkclass Check whether the class accepts connection. ### job=checkuser Check whether the user exists. ### job=checksheet Check whether the sheet exists. ### job=addclass Add a class on the receiving server. For this request, `data1` should be a multi-line text defining the properties of the new class. Each line contains a definition in the format `name=value`. (This text must be reformatted for http query string) The following names may be present in the definitions: * **mandatory:** * description = name of the class * institution = name of the institution * supervisor = full name of the supervisor * email = contact email address * password = password for user registration * lang = class language (en, fr, es, it, etc) * **optional:** * expiration = class expiration date (yyyymmdd) (optional, defaults to one year later) * limit = limit of number of participants (optional, defaults to 30) * level = level of the class (optional, defaults to H4) Valid levels: E1, E2, ..., E6, H1, ..., H6, U1, ..., U5, G, R * secure = secure hosts * bgcolor = page background color * refcolor = menu background color * css = css file (must be existing css on the WIMS server) `data2` should be a multi-line text defining the supervisor account, in the same format as for data1. The following names may be present in the definitions: * **mandatory:** * lastname = last name of the supervisor user * firstname = first name of the supervisor user * password = supervisor user's password, non-crypted. * **optional** * email = supervisor email address * comments = any comments * regnum = registration number * photourl = url of a user picture * participate = list classes (if in a class group) where user participates * courses = * classes = * supervise = * supervisable = * external_auth = * agreecgu = Boolean indicating if user accepted CGU * regprop[1..5] = custom properties ### job=adduser Add a user to the specified class. `data1` should be a multi-line text defining the user account. The following names may be present in the definitions: * **mandatory:** * lastname = user's lastname * firstname = user's firstname * password = user's password, non-crypted. * **optional** * email = email address * comments = any comments * regnum = registration number * photourl = url of user’s photo * participate = list classes where user participates (only for group and portal) * courses = special for portal * classes = special for portal * supervise = List classes where teacher are administator (only for group and portal) * supervisable = `yes/no` ; give right to the user to supervise a class (only for group and portal) * external_auth = login for external_auth (by cas or shiboleth for example). Not useful for Moodle * agreecgu = if yes, the user will not be asked when he enters for the first time to agree the cgu * regprop[1..5] = custom variables, upon to you (i.e : you can set here an external group for example) ### job=modclass Modify the properties of a class. `data1` should be a multi-line text containing the properties to be redefined. Only modified properties need to be present in data1. ### job=moduser Modify the properties of a user. As modclass. ### job=delclass Delete a class. ### job=deluser Delete a user. ### job=recuser Recover a deleted user. ### job=getclass Get the properties of a class. Optionally, the query parameter 'option' may contain the names of fields queried. In this case, only the queried properties are returned. **Note**: definitions for portals have beed added to output variables but are not yet in addclass: typename = programs = courses = classes = levels = icourses = subclasses = ### job=getsheet Get the properties of a sheet (of a class). Optionally, the query parameter 'option' may contain the names of fields queried. In this case, only the queried properties are returned. OUTPUT variables: queryclass : the requested class querysheet : the requested sheet sheet_properties : list of properties of the requested sheet (sheet status,expiration date,title,description) exocnt : number of exercices included exotitlelist : list of the exercices included (module:params) ### job=listsheets List all the sheets of a class. OUTPUT variables: queryclass : the requested class nbsheet : number of sheets in this class sheettitlelist : list of the sheets with the format "sheet_id:title" ### job=listclasses List all the classes with connection between the `rclass` and `ident/rclass`. Optionally, the query parameter 'option' contains the name of fields related to classes asked: e.g. : option=description,supervisor ### job=getclassesuser List all the classes with connection between the rclass and `ident/rclass` where the user exists. Optionally, the query parameter 'option' may contain the names of fields queried. In this case, only the queried properties of the classes are returned. ### job=getuser Get the properties of a user (of a class). Optionally, the query parameter 'option' may contain the names of fields to be queried. In this case, only the queried properties are returned. The output format is as for 'getclass'. ### job=getcsv Get data of the class, under the form of a csv/tsv spreatsheet file. The query parameter 'format' may be used to specify the desired output format (csv or tsv, defaults to csv). The query parameter 'option' should contain a list of desired data columns. The following names can be included in 'option', with their respective meanings: * login : user identifiers * password : user passwords (uncrypted) * name : user names (last name and first name) * lastname : user family names * firstname : user given names * email : user email addresses * regnum : user registration numbers * allscore : all score fields (averages and details) * averages : score averages (average0, average1, average2) * average0 : global score average (as computed by WIMS) * average1 : average of scores automatically attributed by WIMS * average2 : average of teacher-entered scores * exams : exam1+exam2+... * exam1, exam2, ...: scores of each exam * sheets : sheet1+sheet2+... * sheet1, sheet2, ...: scores of each worksheet * manuals : manual1+manual2+... * manual1, manual2, ...: first, second, ... teacher-entered scores. The output content (below the status line in WIMS format) is a csv/tsv spreadsheet table. The first row of the table contains the names of the fields. The second row gives short descriptions of each field. The third row is blank. The rest is the table content, with one row for each user. ### job=lightpopup Presents an exercise without the top, bottom, and left menu The syntax is `job=lightpopup&emod=MODULE` where `MODULE` is an exercise module with its parameters. option: * about : show "about" which gives author information about the exercise (default) * noabout : the "about" will not appear. SAMPLES REQUESTS: * H3/analysis/oeflinf.fr intro in lightpopup mode: * [http://127.0.0.1/wims/wims.cgi?module=adm/raw&job=lightpopup&emod=H3%2Fanalysis%2Foeflinf.fr]() * "antecedant" exercice from H3/analysis/oeflinf.fr module in lightpopup mode: * [http://127.0.0.1/wims/wims.cgi?module=adm/raw&job=lightpopup&emod=H3%2Fanalysis%2Foeflinf.fr&parm=cmd=new;exo=antecedant;qnum=1;qcmlevel=3&option=noabout]() ### job=putcsv Put data into the class. The data to put is sent as the value of the query parameter `data1`, in the same format as for the query `getcsv` above. And with the following particularities: Field `login` must be present. The second row (short descriptions) is not necessary. WIMS-attributed scores cannot be uploaded, and will be ignored. If all the necessary fields corresponding to user properties (lastname, firstname, password, etc.) are present, non-existent users will be added to the class. This can be used to install the whole user accounts of the class with one request. ### job=getlog Get the detailed activity registry of a user. ### job=gettime Get the current time of the server Can be used for synchronization purposes. ### job=update Ask WIMS to update data in a class. Upon reception of this request, WIMS server will issue queries back to the remote server, in order to get the up-to-date information and update the class. The query parameter 'option' contains the nature of the update request. It may be one of the following: * class : update class properties (corresponding to modclass) * user : update properties of a user (moduser) * scores : teacher-entered scores (putcsv) ### job=authuser Get an authentification for a user. User's password is not required. Accepts the query parameter `option=hashlogin`: If called with option=hashlogin, `quser` should be the external identification of user and the function hashlogin is called to convert such id to a WIMS login. If the user exists in class, it returns a session number as above. If the user does not exists, the WIMS login is returned in the error message. OUTPUT : * wims_session : a session number under which the user can connect with no need of further authentification * home_url : a complete URL to connect as authentified. ### job=addsheet Add a new sheet to the specified class `qclass`. `data1` may be defined as a multi-line text defining the sheet defs The following names may be present in the definitions: **(optional)** * title = name of the sheet (defaults to "sheet n#") * description = description of the sheet (defaults to "sheet n#") * expiration = expiration date (yyyymmdd) defaults to one year later * sheetmode = the mode of the sheet: * 0 : pending (default) * 1 : active * 2 : expired * 3 : expired + hidden * weight = weight of the sheet in class score * formula = How the score is calculated for this sheet (0 to 6) * indicator = what indicator will be used in the score formula (0 to 2) * contents = the contents for the multi-line file to be created. The semicolons (;) in this parameter will be interpreted as new lines. Equal characters (=) must be replaced by the character AT (@). There is no check made, so the integrity of the contents is up to you only! (defaults to "") **OUTPUT** : * queryclass : the requested class * sheet_id : id of the new created sheet ### job=addexam Add a new exam to the specified class `qclass`. `data1` may be defined as a multi-line text defining the sheet defs, with the same parameters as in addsheet (see above), plus these additional optional parameters: * duration = exam duration * attempts = number of authorized attempts * cut_hours = Cut hours (format : `yyyymmdd.hh:mm` (separate several hours by spaces). * opening = Opening Restrictions (IP ranges / Schedules) opening can set a time restriction for recording notes by adding the words > yyyymmdd.hh:mm (start) and/or < yyyymmdd.hh:mm (end). Dates and times must be in server local time and must be separated by spaces. ### job=addexo Add the source file (data1) of an exercise directly to the class, under the name `qexo` ### job=putexo Add content (an existing exercise) to the sheet `qsheet` of the `qclass` class `data1` may be defined as a multi-line text defining the exo defs, according to these parameters: * **mandatory** * module = the module where comes the erexercice from * **optional** * params = list of parameters to add to the module * points = number of requested points * weight = Weight of the exercise * title = title of the exercise in the sheet * description = description of the exercise in the sheet ### job=modsheet Modify an existing sheet in the specified class. `data1` may be defined as a multi-line text defining the sheet defs (cf addsheet) ### job=listexos Lists all exercices presents in class `qclass` ### job=movexo Moves exercice `qexo` from class `qclass` to class `data1` Condition : Both 2 classes must be linked by `rclass` *option*: you can use `copy` to copy file instead of moving it. ### job=movexos Moves ALL exercice from class `qclass` to class `data1` Condition: Both 2 classes must be linked by `rclass` *option*: you can use `copy` to copy files instead of moving them. ### job=sharecontent Declare neighbour classes, allowing class "qclass" to share content with class "data1" Condition: Both 2 classes must be linked by `rclass` The `option` parameter can be used to declare which type of content to share (currently, only the "exo" content type is supported) ### job=linksheet Add all exercices from sheet `qsheet` to exam `qexam` ### job=getscore Get all scores from user `quser` (optionaly, you can filter with sheet `qsheet`) ### job=getsheetscores Get all scores from sheet `qsheet` - JSON OUTPUT only ### job=getexamscores Get all scores from exam `qexam` - JSON OUTPUT only wimsapi-0.5.11/docs/api.md000066400000000000000000001122371407533276500153140ustar00rootroot00000000000000The main object-orientated API is built on top of *WimsAPI* class. Each method on WimsAPI maps one-to-one with an *adm/raw* request, and returns the response of the *WIMS* server. It’s possible to use WimsAPI directly. Some basic things (e.g. getting a class from server) consist of several API calls and are complex to do with the low-level API, but it’s useful if you need extra flexibility and power. ## `class WimsAPI` **`WimsAPI(url, ident, passwd)`** This class allow a python3 script to communicate with a WIMS server. ***Parameters:*** * url - (str) url to the wims server CGI. e.g. `https://wims.unice.fr/wims/wims.cgi` * ident - (str) Sender identifier (a word, according to the definition in `WIMS_HOME/log/classes/.connections/`) * passwd - (str) Sender password (as defined in `WIMS_HOME/log/classes/.connections/`) * kwargs - (dict) Keyword argument that will be passed to the request.post() calls. ___ Two optionnal parameter can be passed to every method of this class: * code - (str) a word sent to the request. A random code will be created by the method if none is provided. This word will be sent back, in order to allow to check whether the result is from the good request. * verbose - (boolean) Default to False. Tell whether or not showing the whole response in the the exception if the response could not be parsed. * Any additional keyword argument will be passed to the request.post() function if the same argument is present in both the instance and the method's call, the method's call one will prevail. ___ Every method return a tuple `(boolean, dictionnary)`, where dictionnary contains at least **status**, and **code** keys. **Status** is either the word **'OK'** (which set `boolean` to True), or the word **'ERROR'** (which set `boolean` to False). In case the status is **'OK'**, the dictionnary can contains additionnals keys corresponding to the *adm/raw* response. In case the status is **'ERROR'**, key **'message'** contains the nature of the error. **/!\ Warning:** output must be set `ident_type=json` in `WIMS_HOME/log/classes/.connections/IDENT` for this API to work properly. See [configuration](index.md) For more informations about *adm/raw*, see http://wims.unice.fr/wims/?module=adm/raw&job=help ___ ## `addclass` **`addclass(self, qclass, rclass, class_info, supervisor_info, verbose=False, code=None, **kwargs)`** Add a class on the receiving server. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * class_info - (dict) properties of the new class, following keys may be present: * Mandatory: * description - (str) name of the class * institution - (str) name of the institution * supervisor - (str) full name of the supervisor * email - (str) contact email address * password - (str) password for user registration * Optionnal: * expiration - (str) class expiration date (yyyymmdd, defaults to one year later) * limit - (str) limit of number of participants (defaults to 30) * level - (str) level of the class (defaults to H4) Valid levels: E1, E2, ..., E6, H1, ..., H6, U1, ..., U5, G, R * secure - (str) secure hosts * bgcolor - (str) page background color * refcolor - (str) menu background color * css - (str) css file (must be existing css on the WIMS server) supervisor_info - (dict) properties of the supervisor account, folowing keys may be * present: Mandatory * lastname - (str) last name of the supervisor user * firstname - (str) first name of the supervisor user * password - (str) user's password, non-crypted. Optionnal: * email - (str) supervisor email address * comments - (str) any comments * regnum - (str) registration number * photourl - (str) url of an user picture * participate - (str) list classes (if in a class group) where user participates * agreecgu - (str) Boolean indicating if user accepted CGU * regprop1, regprop2, ... regprop5 - (str) custom properties ___ ## addexam **`addexam(self, qclass, rclass, exam_info, verbose=False, code=None, **kwargs)`** Add an exam to the specified class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * exam_info - (dict) properties of the exam, following keys may be present: * title - (str) title of the exam * description - (str) description of the exam * expiration - (str) exam expiration date (yyyymmdd) * duration - (int) duration (in minutes) * attempts - (int) number of attempts ## addexo **`addexo(self, qclass, rclass, qexo, exo_src, no_build=False, verbose=False, code=None, **kwargs)`** Add an exercice to the specified class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qexo - (str) exo identifier on the receiving server. * exo_src - (str) source of the exercice. * no_build - (bool) Do not compile the exercise. Improves the speed when there is a lot of exercices to handle at the same time. Do not forget to call buildexos() to compile them at the end (defaults to False) ## addsheet **`addsheet(self, qclass, rclass, sheet_info, verbose=False, code=None, **kwargs)`** Add a sheet to the specified class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * sheet_info - (dict) properties of the sheet, following keys may be present: * title - (str) name of the sheet (defaults to "sheet n#") * description - (str) description of the sheet (defaults to "sheet n#") * expiration - (str) expiration date (yyyymmdd) defaults to one year later * sheetmode - (str) the mode of the sheet. 0 : pending (default), 1 : active, 2 : expired, 3 : expired + hidden * weight - (str) weight of the sheet in class score * formula - (str) How the score is calculated for this sheet (0 to 6) * indicator - (str) what indicator will be used in the score formula (0 to 2) * contents - (str) the contents for the multi-line file to be created. The semicolons (;) in this parameter will be interpreted as new lines. Equal characters (=) must be replaced by the character AT (@). There is no check made, so the integrity of the contents is up to you only! (defaults to "") ## adduser **`adduser(self, qclass, rclass, quser, user_info, verbose=False, code=None, **kwargs)`** Add an user to the specified class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. * user_info - (dict) properties of the user, following keys may be present: * Mandatory * lastname - (str) last name of the user * firstname - (str) first name of the user * password - (str) user's password, non-crypted. * Optionnal: * email - (str) email address * comments - (str) any comments * regnum - (str) registration number * photourl - (str) url of user's photo * participate - (str) list classes where user participates * courses - (str) special for portal * classes - (str) special for portal * supervise - (str) List classes where teacher are administator * supervisable - (str) yes/no ; give right to the user to supervise a class * external_auth - (str) login for external_auth * agreecgu - (str) if yes, the user will not be asked when he enters for the first time to agree the cgu * regprop[1..5] - (str) custom variables ## authuser **`authuser(self, qclass, rclass, quser, hashlogin=None, verbose=False, code=None, **kwargs)`** Get an authentification token for an user. User's password is not required. If parameter hashlogin is set to an hash function name, quser should be the external identification of user and the function hashlogin is called to convert such id to a WIMS login. If the user exists in class, it returns a session number as above. If the user does not exists, the WIMS login is returned in the error message. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. * hashlogin - (str) hash function to use for an external authentification ## buildexos **`buildexos(self, qclass, rclass, verbose=False, code=None, **kwargs)`** Compile every exercises of the specified class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. ## checkclass **`checkclass(self, qclass, rclass, verbose=False, code=None, **kwargs)`** Check whether the class accepts connection. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. ## checkexam **`checkexam(self, qclass, rclass, qexam, verbose=False, code=None, **kwargs)`** Check whether the exam exists. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qexam - (str) exam identifier on the receiving server. ## checkident **`checkident(self, verbose=False, code=None, **kwargs)`** Check whether the connection is accepted. ## checksheet **`checksheet(self, qclass, rclass, qsheet, verbose=False, code=None, **kwargs)`** Check whether the sheet exists. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (str) identifier of the sheet on the receiving server. ## checkuser `**checkuser(self, qclass, rclass, quser, verbose=False, code=None, **kwargs)**` Check whether the user exists. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. ## cleanclass **`cleanclass(self, qclass, rclass, verbose=False, code=None, **kwargs)`** Delete users (but supervisor) and all work done by students on the specified class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. ## copyclass **`copyclass(self, qclass, rclass, verbose=False, code=None, **kwargs)`** Copy a class. Do not copy users or work done by students. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. ## delclass **`delclass(self, qclass, rclass, verbose=False, code=None, **kwargs)`** Delete a class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. ## delexam **`delexam(self, qclass, rclass, qexam, verbose=False, code=None, **kwargs)`** Delete an exam. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qexam - (str) exam identifier on the receiving server. ## delexo **`delexo(self, qclass, rclass, qexo, verbose=False, code=None, **kwargs)`** Delete an exo. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qexo - (str) exo identifier on the receiving server. ## delsheet **`delsheet(self, qclass, rclass, qsheet, verbose=False, code=None, **kwargs)`** Delete a sheet ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (int) identifier of the sheet on the receiving server. * options - (list) names of fields queried. ## deluser **`deluser(self, qclass, rclass, quser, verbose=False, code=None, **kwargs)`** Delete an user. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. ## getclass **`getclass(self, qclass, rclass, options=None, verbose=False, code=None, **kwargs)`** Get the properties of a class. Optionally, the parameter 'options' may contain the names of fields queried. In this case, only the queried properties are returned. Existing properties are: password, creator, secure, external_auth, mixed_external_auth, cas_auth, php_auth, authidp, supervisor, description, institution, lang, email, expiration, limit, topscores, superclass, type, level, parent, typename, bgcolor, bgimg, scorecolor, css, logo, logoside, refcolor, ref_menucolor, ref_button_color, ref_button_bgcolor, ref_button_help_color, ref_button_help_bgcolor, theme, theme_icon, connections, creation, userlist, usercount, examcount, sheetcount ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * options - (list) names of fields queried. ## getclassesuser **`getclassesuser(self, rclass, quser, verbose=False, code=None, **kwargs)`** List all the classes having connection with rclass where quser exists. Optionally, the parameter 'options' may contain the names of fields queried for each class. In this case, only the queried properties are returned. Existing properties are: password, creator, secure, external_auth, mixed_external_auth, cas_auth, php_auth, authidp, supervisor, description, institution, lang, email, expiration, limit, topscores, superclass, type, level, parent, typename, bgcolor, bgimg, scorecolor, css, logo, logoside, refcolor, ref_menucolor, ref_button_color, ref_button_bgcolor, ref_button_help_color, ref_button_help_bgcolor, theme, theme_icon, connections, creation, userlist, usercount, examcount, sheetcount ***Parameters:*** * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. * options - (list) names of fields queried. ## getclassfile **`getclassfile(self, qclass, rclass, filename, code=None, **kwargs)`** Download the file of the specified class. ***Parameters:*** * qclass - (str) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * filename - (str) path to the file relative to `log/classes/[qclass]/`. ## getclassmodif **`getclassmodif(self, qclass, rclass, date, verbose=False, code=None, **kwargs)`** List all the files modified on the specified class since . ***Parameters:*** * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. * date - (str) date (yyyymmdd) ## getclasstgz **`getclasstgz(self, qclass, rclass, code=None, **kwargs)`** Download the class in a compressed (tar-gzip) file. ***Parameters:*** * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. ## getcsv **`getcsv(self, qclass, rclass, options, format='csv', code=None, **kwargs)`** Get data of the class, under the form of a csv/tsv/xls spreatsheet file. The parameter 'format' may be used to specify the desired output format (csv or tsv, defaults to csv). The parameter 'options' should contain a list of desired data columns. The following names can be included in 'option', with their respective meanings: * login : user identifiers * password : user passwords (uncrypted) * name : user names (last name and first name) * lastname : user family names * firstname : user given names * email : user email addresses * regnum : user registration numbers * allscore : all score fields (averages and details) * averages : score averages (average0, average1, average2) * average0 : global score average (as computed by WIMS) * average1 : average of scores automatically attributed by WIMS * average2 : average of teacher-entered scores * exams : exam1+exam2+... * exam1, exam2, ...: scores of each exam * sheets : sheet1+sheet2+... * sheet1, sheet2, ...: scores of each worksheet * manuals : manual1+manual2+... * manual1, manual2, ...: first, second, ... teacher-entered scores. The output content (below the status line in WIMS format) is a csv/tsv spreadsheet table. The first row of the table contains the names of the fields. The second row gives short descriptions of each field. The third row is blank. The rest is the table content, with one row for each user. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * options - (list) list of desired data columns. * format - (str) output format ('csv', 'tsv' or 'xls', defaults to csv) ## getexam **`getexam(self, qclass, rclass, qexam, verbose=False, code=None, **kwargs)`** Get an exam from a class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qexam - (int) identifier of the exam on the receiving server. ## getexamlog **`getexamlog(self, qclass, rclass, quser, qexam, verbose=False, code=None, **kwargs)`** Get the logs of on inside of a class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * quser - (int) identifier of the user on the receiving server. * qexam - (int) identifier of the exam on the receiving server. ## getexamscores **`getexamscores(self, qclass, rclass, qexam, verbose=False, code=None, **kwargs)`** Get all scores from exam. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qexam - (int) identifier of the exam on the receiving server. getexo(self, qclass, rclass, qsheet, qexo, verbose=False, code=None, **kwargs) Get an exercice from a sheet. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (int) identifier of the sheet on the receiving server. * qexo - (int) identifier of the exo on the receiving server. ## getexofile **`getexofile(self, qclass, rclass, qexo, code=None, **kwargs)`** Download the source file of the specified class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qexo - (int) identifier of the exo on the receiving server. ## getexosheet **`getexosheet(self, qclass, rclass, qsheet, qexo, verbose=False, code=None, **kwargs)`** Get informations of inside of of the specified class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (int) identifier of the sheet on the receiving server. * qexo - (int) identifier of the exo on the receiving server. ## getinfoserver **`getinfoserver(self, verbose=False, code=None, **kwargs)`** Get informations about the WIMS server. ## getlog **`getlog(self, qclass, rclass, quser, verbose=False, code=None, **kwargs)`** Get the detailed activity registry of an user. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. ## getmodule **`getmodule(self, module, verbose=False, code=None, **kwargs)`** Get informations about . ***Parameters:*** * module - (str) path of a module (i.e. 'E1/geometry/oefsquare.fr') ## getscore **`getscore(self, qclass, rclass, quser, qsheet=None, verbose=False, code=None, **kwargs)`** Get all scores from user. Can optionnally filter from a sheet. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. * qsheet - (int) identifier of the sheet on the receiving server. Used to filter the scores. ## getscores **`getscores(self, qclass, rclass, options, code=None, **kwargs)`** Call `getcsv()` method with format='xls'. For more informations, see `WimsAPI.getcsv()` documentation. ## getsession **`getsession(self, data1='adm/createxo', verbose=False, code=None, **kwargs)`** Open a WIMS session and return its ID ## getsheet **`getsheet(self, qclass, rclass, qsheet, options=None, verbose=False, code=None, **kwargs)`** Get the properties of a sheet (of a class). Optionally, the parameter 'options' may contain the names of fields queried. In this case, only the queried properties are returned. Existing properties are: exo_cnt, sheet_properties, sheet_status, sheet_expiration, sheet_title, sheet_description, exolist, title, params, points, weight, description ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (int) identifier of the sheet on the receiving server. * options - (list) names of fields queried. ## getsheetscores **`getsheetscores(self, qclass, rclass, qsheet, verbose=False, code=None, **kwargs)`** Get all scores from sheet. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (int) identifier of the sheet on the receiving server. ## getsheetstats **`getsheetstats(self, qclass, rclass, qsheet, verbose=False, code=None, **kwargs)`** Get stats about work of students for every exercise of . ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (int) identifier of the sheet on the receiving server. ## gettime **`gettime(self, verbose=False, code=None, **kwargs)`** Get the current time of the server. Can be used for synchronization purposes. ## getuser **`getuser(self, qclass, rclass, quser, options=None, verbose=False, code=None, **kwargs)`** Get the properties of an user (of a class). Optionally, the parameter 'options' may contain the names of fields queried. In this case, only the queried properties are returned. Existing properties are: firstname, lastname, email, comments, regnum, photourl, participate, password, courses, classes, supervise, supervisable, external_auth, agreecgu, regprop1, regprop2, regprop3, regprop4, regprop5 ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. * options - (list) names of fields queried. ## lightpopup **`lightpopup(self, qclass, rclass, quser, session, exercice, about=True, code=None, **kwargs)`** Presents an exercise without the top, bottom, and left menu ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. * session - (str) session identifer returned by authuser(). * exercice - (str) addresse of the exercice found in 'About this module / this exercice' on the page of the exercice eg: 'classes/en&exo=Exercise1&qnum=1&qcmlevel=1&scoredelay=700,700 ' * about - (bool) If True (default), show "about" which gives author information about the exercise ## linkexo **`linkexo(self, qclass, rclass, qsheet, qexo, qexam, verbose=False, code=None, **kwargs)`** Add exercise of the sheet to . ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (int) identifier of the sheet on the receiving server. * qexam - (int) identifier of the exam on the receiving server. ## linksheet **`linksheet(self, qclass, rclass, qsheet, qexam, verbose=False, code=None, **kwargs)`** Add all exercices from sheet to exam. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (int) identifier of the sheet on the receiving server. * qexam - (int) identifier of the exam on the receiving server. ## listclasses **`listclasses(self, rclass, verbose=False, code=None, **kwargs)`** List all the classes having connection with rclass. Optionally, the parameter 'options' may contain the names of fields queried for each class. In this case, only the queried properties are returned. Existing properties are: password, creator, secure, external_auth, mixed_external_auth, cas_auth, php_auth, authidp, supervisor, description, institution, lang, email, expiration, limit, topscores, superclass, type, level, parent, typename, bgcolor, bgimg, scorecolor, css, logo, logoside, refcolor, ref_menucolor, ref_button_color, ref_button_bgcolor, ref_button_help_color, ref_button_help_bgcolor, theme, theme_icon, connections, creation, userlist, usercount, examcount, sheetcount ***Parameters:*** * rclass - (str) identifier of the class on the sending server. * options - (list) names of fields queried. ## listexams **`listexams(self, qclass, rclass, verbose=False, code=None, **kwargs)`** Lists all exams presents in class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. ## listexos **`listexos(self, qclass, rclass, verbose=False, code=None, **kwargs)`** Lists all exercices presents in class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. ## listlinks **`listlinks(self, qclass, rclass, qsheet, qexam, verbose=False, code=None, **kwargs)`** Get the number of exercise of linked to . ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (int) identifier of the sheet on the receiving server. * qexam - (int) identifier of the exam on the receiving server. ## listmodules **`listmodules(self, level='H4', verbose=False, code=None, **kwargs)`** Get the number of exercise of linked to . ***Parameters:*** * level - (str) level of the module (defaults to H4). Valid levels are E1, E2, ..., E6, H1, ..., H6, U1, ..., U5, G, R ## listsheets **`listsheets(self, qclass, rclass, verbose=False, code=None, **kwargs)`** List all the sheets of a class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. ## modclass **`modclass(self, qclass, rclass, class_info, verbose=False, code=None, **kwargs)`** Modify the properties of a class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * class_info - (dict) Only modified properties need to be present, following keys * * may be present: * description - (str) name of the class * institution - (str) name of the institution * supervisor - (str) full name of the supervisor * email - (str) contact email address * password - (str) password for user registration * lang - (str) class language (en, fr, es, it, etc) * expiration - (str) class expiration date (yyyymmdd, defaults to one year later) * limit - (str) limit of number of participants (defaults to 30) * level - (str) level of the class (defaults to H4) Valid levels: E1, E2, ..., E6, H1, ..., H6, U1, ..., U5, G, R * secure - (str) secure hosts * bgcolor - (str) page background color * refcolor - (str) menu background color * css - (str) css file (must be existing css on the WIMS server) ## modexam **`modexam(self, qclass, rclass, qexam, exam_info, verbose=False, code=None, **kwargs)`** Modify the property of an exam. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * exam_info - (dict) Only modified properties need to be present, following keys may be present: * title - (str) title of the exam * description - (str) description of the exam * expiration - (str) exam expiration date (yyyymmdd) * duration - (int) duration (in minutes) * attempts - (int) number of attempts ## modexosheet **`modexosheet(self, verbose=False, code=None, **kwargs)`** ***Not implemented yet.*** ## modsheet **`modsheet(self, qclass, rclass, qsheet, sheet_info, verbose=False, code=None, **kwargs)`** Modify the properties of a sheet. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (int) identifier of the sheet on the receiving server. * sheet_info - (dict) Only modified properties need to be present, following keys may be present: * title - (str) name of the sheet (defaults to "sheet n#") * description - (str) description of the sheet (defaults to "sheet n#") * expiration - (str) expiration date (yyyymmdd) defaults to one year later * status - (str) the mode of the sheet. 0 : pending (default), 1 : active, 2 : expired, 3 : expired + hidden * weight - (str) weight of the sheet in class score * formula - (str) How the score is calculated for this sheet (0 to 6) * indicator - (str) what indicator will be used in the score formula (0 to 2) * contents - (str) the contents for the multi-line file to be created. The semicolons (;) in this parameter will be interpreted as new lines. Equal characters (=) must be replaced by the character AT (@). There is no check made, so the integrity of the contents is up to you only! (defaults to "") ## moduser **`moduser(self, qclass, rclass, quser, user_info, verbose=False, code=None, **kwargs)`** Modify the properties of an user. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. * user_info - (dict) Only modified properties need to be present, following keys may be present: * lastname - (str) last name of the supervisor user * firstname - (str) first name of the supervisor user * password - (str) user's password, non-crypted. * email - (str) email address * comments - (str) any comments * regnum - (str) registration number * photourl - (str) url of user's photo * participate - (str) list classes where user participates * courses - (str) special for portal * classes - (str) special for portal * supervise - (str) List classes where teacher are administator * supervisable - (str) yes/no ; give right to the user to supervise a class * external_auth - (str) login for external_auth * agreecgu - (str) if yes, the user will not be asked when he enters * * for the first time to agree the cgu * regprop[1..5] - (str) custom variables ## movexo **`movexo(self, qclass, qclass2, rclass, qsheet, copy=False, verbose=False, code=None, **kwargs)`** Moves exercice from qclass to qclass2. Condition : Both 2 classes must be linked by. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server, source of the exo. * qclass2 - (int) identifier of the class on the receiving server, destination of the exo. * rclass - (str) identifier of the class on the sending server. * qsheet - (int) identifier of the sheet on the receiving server. * copy - (bool) If set to True, copy the exo instead of moving it. ## movexos **`movexos(self, qclass, qclass2, rclass, copy=False, verbose=False, code=None, **kwargs)`** Moves ALL exercices from qclass to qclass2. Condition : Both 2 classes must be linked by. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server, source of the exos. * qclass2 - (int) identifier of the class on the receiving server, destination of the exo. * rclass - (str) identifier of the class on the sending server. * copy - (bool) If set to True, copy the exo instead of moving it. ## putcsv **`putcsv(self, qclass, rclass, csv, file=True, verbose=False, code=None, **kwargs)`** Put data into the class. csv should respect this format: The first row of the table contains the names of the fields. The second row gives short descriptions of each field. The second row is blank. The rest is the table content, with one row for each user. The following data columns can be included in the csv, with their respective meanings: * login : user identifiers * password : user passwords (uncrypted) * name : user names (last name and first name) * lastname : user family names * firstname : user given names * email : user email addresses * regnum : user registration numbers * allscore : all score fields (averages and details) * averages : score averages (average0, average1, average2) * average0 : global score average (as computed by WIMS) * average1 : average of scores automatically attributed by WIMS * average2 : average of teacher-entered scores * exams : exam1+exam2+... * exam1, exam2, ...: scores of each exam * sheets : sheet1+sheet2+... * sheet1, sheet2, ...: scores of each worksheet * manuals : manual1+manual2+... * manual1, manual2, ...: first, second, ... teacher-entered scores. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * csv - (str) path to a csv if 'file' is True (default), content of a csv otherwise. * file - (bool) Whether csv must be interpreted as a path to a csv or a .csv string ## putexo **`putexo(self, qclass, rclass, qsheet, module, options=None, verbose=False, code=None, **kwargs)`** Add 's exercise to of a specified class. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * qsheet - (str) sheet identifier on the receiving server. * module - (str) path of a module (i.e. 'E1/geometry/oefsquare.fr'). * options - (dict) A dictionnary of optionnal parameters: * params - (str) string of parameters to add to the module. * points - (int) number of requested points. * weight - (int) weight of the exercise. * title - (str) title of the exercise in the sheet. * description - (str) description of the exercise in the sheet. ## recuser **`recuser(self, qclass, rclass, quser, verbose=False, code=None, **kwargs)`** Recover a deleted user. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. * quser - (str) user identifier on the receiving server. ## repairclass **`repairclass(self, qclass, rclass, verbose=False, code=None, **kwargs)`** Try to detect and correct eventual problems. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * rclass - (str) identifier of the class on the sending server. ## search **`search(self, verbose=False, code=None, **kwargs)`** ***Not implemented yet.*** ## sharerecontent **`sharerecontent(self, qclass, qclass2, rclass, options=('exo',), verbose=False, code=None, **kwargs)`** Declares neighbour classes, allowing class "qclass" to share content with class "data1". Both classes must be linked by. ***Parameters:*** * qclass - (int) identifier of the class on the receiving server. * qclass2 - (int) identifier of the second class on the receiving server. * rclass - (str) identifier of the class on the sending server. * options - (list) content to share (currently, only the "exo" content type is supported). ## testexo **`testexo(self, exo_src, verbose=False, code=None, **kwargs)`** Allow to test the compilation of an exercise without adding it to a class. ***Parameters:*** * exo_src - (str) source of the exercice. ## update **`update(self, verbose=False, code=None, **kwargs)`** ***Not implemented yet.*** wimsapi-0.5.11/docs/class.md000066400000000000000000000213031407533276500156410ustar00rootroot00000000000000# Class On *WIMS*, a **Class** is the main structure. Each class contains its own [Users](user.md), [Sheets](sheet.md), [Exercises](exercise.md) and [Exams](exam.md). ## Instantiation To get a **Class** instance, you have two possibility. You can either create a new instance, or get one from the *WIMS* server. To create a new instance, you'll need an [User](user.md) to use as a supervisor. ```python from wimsapi import Class, User user = User("quser", "lastname", "firstname", "password", "mail@mail.com") c = Class("rclass", "Title", "Institution", "mail@mail.com", "password", user) ``` **Note:** The quser of the [User](user.md) used to create the supervisor does not matter, it will be overwritten with "supervisor" as soon as the class is saved. **Class** can also take a lot of optionnal argument: > `Class(rclass, name, institution, email, password, supervisor, qclass=None, lang="en", expiration=None, limit=30, level="H4", secure='all', bgcolor='', refcolor='', css='')` Where: * rclass - (str) identifier of the class on the sending server. * name - (str) name of the class. * institution - (str) name of the institution. * email - (str) contact email address. * password - (str) password for user registration. * supervisor - (wimsapi.user.User) An WIMS user instance representing the supervisor. * qclass - (int) identifier of the class on the receiving server. * lang - (str) class language (en, fr, es, it, etc). * expiration - (str) class expiration date (yyyymmdd, defaults to one year later). * limit - (str) limit of number of participants (defaults to 30). * level - (str) level of the class (defaults to H4) Valid levels: E1, E2, ..., E6, H1, ..., H6, U1, ..., U5, G, R * secure - (str) secure hosts. * bgcolor - (str) page background color. * refcolor - (str) menu background color. * css - (str) css file (must be existing css on the WIMS server).""" if provided, qclass will be the identifier of the newly created WIMS class when this instance is saved. The identifier is randomly chosen by the WIMS server if qclass is not provided. ___ To get an instance from the *WIMS* server, you can use the class method : > `Class.get(url, ident, passwd, qclass, rclass)` Where : * `url` is the url to the *WIMS* server's cgi (eg. https://wims.unice.fr/wims/wims.cgi). * `ident` and `password` are credentials as defined in `[WIMS_HOME]/log/classes/.connections/` (see [configuration](index.md#configuration)) * `qclass` is the ID of the class on the *WIMS* server (name of the directory corresponding to the class in `[WIMS_HOME]/log/classes/[ID]`) * `rclass` the identifier corresponding to the class (again, see [configuration](index.md#configuration)). ## Saving Any changes made to a **Class** instance can be reflected on the *WIMS* server with the method `save()` : ```python from wimsapi import Class c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") c.institution = "Another institution" c.save() ``` If the **Class** has been instantiated through its constructor, and not with `Class.get()` method, and has not been saved yet, you will need to provide the server's url, ident, and passwd (see [configuration](index.md#configuration)) : ```python c = Class("myclass", "Title", "Institution", "mail@mail.com", "password", user) c.save("https://wims.unice.fr/wims/wims.cgi", "myself", "toto") c.institution = "Another institution" c.save() ``` ## Reloading an instance To reflect server-side changes on an instance of **Class**, use `refresh()` : ```python c = Class(9999, "myclass", "Title", "Institution", "mail@mail.com", "password", supervisor) c.save("https://wims.unice.fr/wims/wims.cgi", "myself", "toto") c2 = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") c.institution = "Another institution" c.save() c2.institution # "Institution" c2.refresh() c2.institution # "Another institution" ``` ## Deleting To delete an already saved **Class**, simply use `delete()`: ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") c.delete() ``` ## List classes of a server You can obtain the list of every classes of a server using a particular *rclass* with : `Class.list(url, ident, passwd, rclass)` ```python Class.list("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", "myclass") ``` ## More Data Once the **Class** has been saved you can acceed the additionnal fields `ident`, `passwd`, `url` and `infos` : ```python c = Class(999999, "myclass", "Title", "Institution", "mail@mail.com", "password", user) c.save("https://wims.unice.fr/wims/wims.cgi", "myself", "toto") c.ident # "myself" c.passwd # "toto" c.url # "https://wims.unice.fr/wims/wims.cgi" c.infos # { # 'query_class': '999999', 'rclass': 'myclass', 'password': 'password', # 'creator': '172.17.0.1', 'secure': 'all', 'external_auth': '', # 'mixed_external_auth': '', 'cas_auth': '', 'php_auth': '', # 'authidp': '', 'supervisor': 'Firstname Lastname', # 'description': 'Title', 'institution': 'Institution', 'lang': 'en', # 'email': 'mail@mail.com', 'expiration': '20191127', 'limit': '30', # 'topscores': '', 'superclass': '', 'type': '0', 'level': 'H4', # 'parent': '', 'typename': 'class', 'bgcolor': '', 'bgimg': '', # 'scorecolor': '#ffffff, #ff0000, #ff0000, #ff0000, #ffa500, #ffa500, # #fff500, #d2ff00, #b9ff00, #2fc42f, #259425', # 'css': '-theme-', 'logo': '', 'logoside': '', 'refcolor': '', # 'ref_menucolor': '', 'ref_button_color': '', 'ref_button_bgcolor': '', # 'ref_button_help_color': '', 'ref_button_help_bgcolor': '', # 'theme': 'standard', 'theme_icon': '', 'sendmailteacher': '', # 'connections': '+myself/myclass+ ', 'creation': '20181127', # 'userlist': [''], 'usercount': '0', 'examcount': '0', 'sheetcount': '0' # } ``` ## Items **Class** has 4 methods allowing it to interact with its [User](user.md), [Sheets](sheet.md), [Exercises](exercise.md) and [Exams](exam.md). ### Add an item You can add an item with the method `additem(item)`, where *item* is an instance of [User](user.md), [Sheets](sheet.md), [Exercises](exercise.md) or [Exams](exam.md). ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") user = User("quser", "lastname", "firstname", "password", "mail@mail.com") c.additem(user) ``` ### Get an item You can retrieve an item with the method `getitem(identifier, cls)`, where *cls* is the python class [User](user.md), [Sheets](sheet.md), [Exercises](exercise.md) or [Exams](exam.md), and *identifier* it's ID on the WIMS class. ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") user = User("quser", "lastname", "firstname", "password", "mail@mail.com") c.additem(user) u = c.getitem("quser", User) ``` ### Delete an item You can delete an item from a *WIMS* class with the method `delitem(item, cls=None)`. *Item* must be either an instance of [User](user.md), [Sheets](sheet.md), [Exercises](exercise.md) or [Exams](exam.md) ; or string. If item is a string, cls must be provided and be the corresponding python class. ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") user = User("quser", "lastname", "firstname", "password", "mail@mail.com") c.additem(user) c.delitem(user) # OR c.delitem("quser", User) ``` ### Check if an item is in the class You can check if an item is in a *WIMS* class with the method `checkitem(item, cls=None)`. *Item* must be either an instance of [User](user.md), [Sheets](sheet.md), [Exercises](exercise.md), [Exams](exam.md), or string. If item is a string, cls must be provided and be the corresponding python class. This method returns True if the item is present in the WIMS class, False otherwise. ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") user = User("quser", "lastname", "firstname", "password", "mail@mail.com") unknown = User("unknown", "lastname", "firstname", "password", "mail@mail.com") c.additem(user) c.checkitem(user) # True c.checkitem(unknown) # False # OR c.checkitem("quser", User) # True c.checkitem("unknown", User) # False ``` You can also use `item in class` operator, where item is an instance of [User](user.md), [Sheets](sheet.md), [Exercises](exercise.md) or [Exams](exam.md). ```python user in c # True unknown in c # False ``` ### List items of a class You can list every item in a *WIMS* class with the method `listitem(cls)`. `cls` must be the python's class corresponding to the item to be listed. This method returns a list of instances of `cls`. ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") users = c.listitem(User) sheets = c.listitem(Sheet) ``` wimsapi-0.5.11/docs/exam.md000066400000000000000000000000241407533276500154630ustar00rootroot00000000000000Not implemented yet.wimsapi-0.5.11/docs/exceptions.md000066400000000000000000000014321407533276500167160ustar00rootroot00000000000000# Exceptions ## wimsapi.WimsAPIError The base exception for all **wimsapi** exceptions. ## wimsapi.NotSavedError Raised trying to use a method needing an object to be saved, without the object being actually saved (e.g. deleting an unsaved [Class](class.md)) ## wimsapi.InvalidItemTypeError Raised by * `Class.additem(object, cls)` * `Class.delitem(object, cls)` * `Class.getitem(object, cls)` * `Class.checkitem(object, cls)` if given `object` or `cls` is not a valid *WIMS* class item type (See [Class](class.md)). ## wimsapi.AdmRawError Raised either when sending an invalid request to the *WIMS* server (e.g. getting a user that does not exists) or when the *WIMS* server encounter an unknown error. The response from the *WIMS* server is usually in the exception's message. wimsapi-0.5.11/docs/exercise.md000066400000000000000000000000241407533276500163400ustar00rootroot00000000000000Not implemented yet.wimsapi-0.5.11/docs/index.md000066400000000000000000000055421407533276500156520ustar00rootroot00000000000000# Python API for WIMS' adm/raw module **WimsAPI** is an API written in python3 allowing to communicate with a *WIMS* server through its *adm/raw* extension. For more information about *adm/raw*, [see its documentation](https://wims.auto.u-psud.fr/wims/wims.cgi?module=adm/raw&job=help) Here the [documentation of wimsapi](https://wimsapi.readthedocs.io/en/latest/). ## Installation The latest stable version is available on [PyPI](https://pypi.org/project/wimsapi/) : ```bash pip install wimsapi ``` or from the sources: ```bash git clone https://github.com/qcoumes/wimsapi cd wimsapi python3 setup.py install ``` ## Configuration ### Global configuration In order for *WIMS* to accept requests from **WimsAPI**, a file must be created in `[WIMS_HOME]/log/classes/.connections/`, the file's name will serve as the identifier name for **WimsAPI**. Here an exemple of such file: `[WIMS_HOME]/log/classes/.connections/myself` ``` ident_site=172.17.0.1 ident_desc=This WIMS server ident_agent=python-requests # http / https. ident_protocol=http # password must be a word composed of alpha-numeric characters. ident_password=toto ident_type=json # The address and identifier/password pair for calling back. back_url=http://localhost/wims/wims.cgi back_ident=myself back_password=toto ``` Here a description of the important parameters: * `ident_site`: a space separated list of IP allowed to send request to this *WIMS* server. * `ident_agent`: ***Must*** be set to `python-requests`. * `ident_password`: Used alongside the file's name as *identifier* in the request to authenticate yourself on *WIMS*. * `ident_type`: ***Must*** be set to `json`. The above example would allow a computer/server of ip `172.17.0.1` to send a request to the *WIMS* server with identifier *myself* and password *toto*. ### Class Configuration If you create a class thanks to this API, everything should work perfectly. However, if you want to use it with an already existing class, some more configuration must be done. You must edit the file `[WIMS_HOME]/log/classes/[CLASS_ID]/.def` and add this line at the end of the file: ``` !set class_connections=+IDENT/RCLASS+ ``` Where **IDENT** is the identifier use by the API (name of the corresponding file in `[WIMS_HOME]/log/classes/.connections/` as defined above) and **RCLASS** is an identifier sent in the request to authenticate yourself on the class. Basically, to authenticate yourself on a class on your *WIMS* server, you will need : * `url` : URL to the *WIMS* (`https://wims.unice.fr/wims/wims.cgi` for instance) * `ident` : Name of the file in `[WIMS_HOME]/log/classes/.connections/` * `passwd` : Value of `ident_password` in `[WIMS_HOME]/log/classes/.connections/[IDENT]` * `rclass` : Value set after the **/** in `class_connections` in `[WIMS_HOME]/log/classes/[CLASS_ID]/.def` wimsapi-0.5.11/docs/license.md000066400000000000000000000020571407533276500161630ustar00rootroot00000000000000MIT License Copyright (c) 2018 Coumes Quentin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wimsapi-0.5.11/docs/requirements.txt000066400000000000000000000000151407533276500174730ustar00rootroot00000000000000mkdocs==1.0.4wimsapi-0.5.11/docs/sheet.md000066400000000000000000000127301407533276500156500ustar00rootroot00000000000000# Sheet The first time a **Sheet** is saved in a [Class](class.md), the corresponding instance of the class is then link to the **Sheet** through the `.wclass` attribute. This allow to use some methods such as `delete()` or `save()` without any argument. The instance of [Class](class.md) is also link to a **Sheet** obtained through `Class.getitem()` or `Sheet.get()`. ## Instantiation To get a **Sheet** instance, you have two possibility. You can either create a new instance, or get one from the *WIMS* server. To create a new instance : ```python from wimsapi import Sheet sheet = Sheet("Title", "Description") ``` **Sheet** can also take a lot of optionnal argument: > `Sheet(title, description, expiration=None, sheetmode=0, weight=1, formula=2, indicator=1, contents="")` Where: * title - (str) name of the sheet (defaults to "sheet n#") * description - (str) description of the sheet (defaults to "sheet n#") * expiration - (str) expiration date (yyyymmdd) defaults to one year later * sheetmode - (str) the mode of the sheet: * 0 : pending (default) * 1 : active * 2 : expired * 3 : expired + hidden * weight - (int) weight of the sheet in the class score (default to 1), use 0 if you want this sheet's score to be ignored. * formula - (str) How the score is calculated for this sheet (0 to 6, default to 2) * 0 : Very lenient: maximum between percentage and quality. * 1 : Quality is not taken into account. You get maximum of grade once all the required work is done. * 2 : Quality has only slight effect over the grade. * 3 : More effect of quality. * 4 : To have a grade of 10, you must gather all required points (100%) without making any error (quality=10). * 5 : Unfinished work is over-punished. * 6 : Any mistake is over-punished. * indicator - (str) what indicator will be used in the score formula (0 to 2, default to 1) * contents - (str) the contents for the multi-line file to be created. The semicolons (;) in this parameter will be interpreted as new lines. Equal characters (=) must be replaced by the character AT (@). There is no check made, so the integrity of the contents is up to you only! (defaults to "")""" ___ To get an instance from the *WIMS* server, you can use either of the following class method : > `Sheet.get(wclass, qsheet)` Where : * `wclass` is an instance of [Class](class.md) which the **Sheet** belong to. * `Sheet` is the identifier corresponding to the sheet. or > `c.getitem(qsheet, Sheet)` Where : * `c` is an instance of [Class](class.md) which the **Sheet** belong to. * `qsheet` is the identifier corresponding to the sheet. * `Sheet` the Sheet class. ## Saving Any changes made to a **Sheet** instance can be reflected on the *WIMS* server with the method `save()` : ```python from wimsapi import Class, Sheet c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") s = Sheet.get(c, "qsheet") s.firstname = "Newname" s.save() ``` If the **Sheet** has been instantiated through its constructor, and not with one of the `get` method, and has not been saved yet, you will need to provide a [Class](class.md) which had already been saved on the server. ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") s = Sheet("qsheet", "lastname", "firstname", "password", "mail@mail.com") s.save(c) ``` To add an **Sheet** to a **Class**, you can also use `c.additem(sheet)`. ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") s = Sheet("qsheet", "lastname", "firstname", "password", "mail@mail.com") c.additem(s) ``` In fact, `c.additem(sheet)` will call `sheet.save(c)`, therefore if a same sheet is added to multiple classes through `s.save(c)` or `c.additem(s)`, future call to `s.save()` will only save changed on the last Class the Sheet has been saved to. ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") c2 = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 8888, "myclass") s = Sheet("qsheet", "lastname", "firstname", "password", "mail@mail.com") c.additem(s) s.save(c2) s.firstname = "Newname" s.save() # Only save changes on c2 ``` ## Reloading an instance To reflect server-side changes on an instance of **Sheet**, use `refresh()` : ```python s.title = "Old" c.save(s) s2 = c.getitem(s.qsheet, Sheet) s2.title = "New" s2.save() s.institution # "Old" s.refresh() s.institution # "New" ``` ## Deleting To delete an already saved **Sheet** `s` from a [Class](class.md) `c`, you have a lot possibility: * `s.delete()` * `Sheet.delete(c, s)` * `Sheet.delete(c, "qsheet")` where `"qsheet" == s.qsheet` * `c.delitem(s)` * `c.delitem("qsheet", Sheet)` where `"qsheet" == s.qsheet` ## Check if a sheet exists in a WIMS class To check whether **Sheet** `u` is in a [Class](class.md) `c`, you have once again a lot of possibility: * `Sheet.check(c, s)` * `Sheet.check(c, "qsheet")` where `"qsheet" == s.qsheet` * `c.checkitem(s)` * `c.checkitem("qsheet", Sheet)` where `"qsheet" == s.qsheet` * `s in c` All of these methods return `True` if the sheet exists in the class, `False` otherwise. ## More Data Once the **Sheet** has been saved you can acceed the additionnal fields `infos` and `wclass` which is the instance of [Class](class.md) this sheet is saved on: ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") s = Sheet("Title", "Description") c.additem(s) s.wclass == c # True c.infos ``` wimsapi-0.5.11/docs/user.md000066400000000000000000000140161407533276500155150ustar00rootroot00000000000000# User A weird characteristic of *WIMS* is that an **User** is link to a specific [Class](class.md), there is no server-wide **User**. Many of the methods should thus be used against a [Class](class.md). The first time an **User** is saved in a [Class](class.md), the corresponding instance of the class is then link to the **User** through the `.wclass` attribute. This allow to use some methods such as `delete()` or `save()` without any argument. The instance of [Class](class.md) is also link to an **User** obtained through `Class.getitem()` or `User.get()`. ## Instantiation To get an **User** instance, you have two possibility. You can either create a new instance, or get one from the *WIMS* server. To create a new instance : ```python from wimsapi import User user = User("quser", "lastname", "firstname", "password", "mail@mail.com") ``` **User** can also take a lot of optionnal argument: > `User(quser, lastname, firstname, password, email="", comments="", regnum="", photourl="", participate="", courses="", classes="", supervise="", supervisable="no", external_auth="", agreecgu="yes", regprop1="", regprop2="", regprop3="", regprop4="", regprop5="")` Where: * quser - (str) user identifier on the receiving server. * lastname - (str) last name of the user. * firstname - (str) first name of the user. * password - (str) user's password, non-crypted. * email - (str) email address. * comments - (str) any comments. * regnum - (str) registration number. * photourl - (str) url of user's photo. * participate - (str) list classes where user participates. * courses - (str) special for portal. * classes - (str) special for portal. * supervise - (str) List classes where teacher are administator. * supervisable - (str) yes/no ; give right to the user to supervise a class (default to 'no'). * external_auth - (str) login for external_auth. * agreecgu - (str) yes/ no ; if yes, the user will not be asked when he enters for the first time to agree the cgu (default to "yes"). * regprop[1..5] - (str) custom variables. ___ To get an instance from the *WIMS* server, you can use either of the following class method : > `User.get(wclass, quser)` Where : * `wclass` is an instance of [Class](class.md) which the **User** belong to. * `quser` is the identifier corresponding to the user. or > `c.getitem(quser, User)` Where : * `c` is an instance of [Class](class.md) which the **User** belong to. * `quser` is the identifier corresponding to the user. * `User` the User class. ## Saving Any changes made to a **User** instance can be reflected on the *WIMS* server with the method `save()` : ```python from wimsapi import Class, User c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") u = User.get(c, "quser") u.firstname = "Newname" u.save() ``` If the **User** has been instantiated through its constructor, and not with one of the `get` method, and has not been saved yet, you will need to provide a [Class](class.md) which had already been saved on the server. ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") u = User("quser", "lastname", "firstname", "password", "mail@mail.com") u.save(c) ``` To add an **User** to a **Class**, you can also use `c.additem(user)`. ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") u = User("quser", "lastname", "firstname", "password", "mail@mail.com") c.additem(u) ``` In fact, `c.additem(user)` will call `user.save(c)`, therefore if a same user is added to multiple classes through `u.save(c)` or `c.additem(u)`, future call to `u.save()` will only save changed on the last Class the User has been saved to. ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") c2 = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 8888, "myclass") u = User("quser", "lastname", "firstname", "password", "mail@mail.com") c.additem(u) u.save(c2) u.firstname = "Newname" u.save() # Only save changes on c2 ``` ## Reloading an instance To reflect server-side changes on an instance of **User**, use `refresh()` : ```python supervisor.firstname = "Jhon" c = Class(9999, "myclass", "Title", "Institution", "mail@mail.com", "password", supervisor) c.save("https://wims.unice.fr/wims/wims.cgi", "myself", "toto") supervisor2 = c.getitem("supervisor", User) supervisor2.firstname = "James" supervisor2.save() supervisor.institution # "Jhon" supervisor.refresh() supervisor.institution # "James" ``` ## Deleting To delete an already saved **User** `u` from a [Class](class.md) `c`, you have a lot possibility: * `u.delete()` * `User.delete(c, u)` * `User.delete(c, "quser")` where `"quser" == u.quser` * `c.delitem(u)` * `c.delitem("quser", User)` where `"quser" == u.quser` ## Check if an user exists in a WIMS class To check whether **User** `u` is in a [Class](class.md) `c`, you have once again a lot of possibility: * `User.check(c, u)` * `User.check(c, "quser")` where `"quser" == u.quser` * `c.checkitem(u)` * `c.checkitem("quser", User)` where `"quser" == u.quser` * `u in c` All of these methods return `True` if the user exists in the class, `False` otherwise. ## More Data You can acceed to the user fullname with `user.fullname`. Once the **User** has been saved you can acceed the additionnal fields `infos` and `wclass` which is the instance of [Class](class.md) this user is saved on: ```python c = Class.get("https://wims.unice.fr/wims/wims.cgi", "myself", "toto", 9999, "myclass") u = User("quser", "lastname", "firstname", "password", "mail@mail.com") u.fullname # Firstname Lastname c.additem(u) u.wclass == c # True c.infos #{ # 'query_class': '9001', 'queryuser': 'quser', 'firstname': 'firstname', # 'lastname': 'lastname', 'email': 'mail@mail.com', 'comments': '', 'regnum': '', # 'photourl': '', 'participate': '', 'courses': '', 'classes': '', 'supervise': '', # 'supervisable': 'no', 'external_auth': '', 'agreecgu': 'yes', 'regprop1': '', # 'regprop2': '', 'regprop3': '', 'regprop4': '', 'regprop5': '' #} ``` wimsapi-0.5.11/mkdocs.yml000066400000000000000000000007261407533276500152730ustar00rootroot00000000000000site_name: WimsAPI Documentation site_author: Quentin Coumes repo_url: https://github.com/qcoumes/wimsapi nav: - Installation: index.md - API: - Class: class.md - User: user.md - Sheet: sheet.md - Exercise: exercise.md - Exam: exam.md - Exceptions: exceptions.md - "Low-Level API": api.md - "Adm/raw API": adm-raw.md - "Change log": CHANGES.md - License: license.md - Github: https://github.com/qcoumes/wimsapi theme: readthedocs wimsapi-0.5.11/setup.cfg000066400000000000000000000022301407533276500151010ustar00rootroot00000000000000# Coverage settings [coverage:report] exclude_lines = pragma: no cover def __repr__ def __str__ def __hash__ if not status: \spass\s [coverage:run] branch = True omit = */apps.py */wsgi.py */settings.py */config.py */tests.py */manage.py */__init__.py */tests/* */venv/* */migrations/* */htmlcov/* *.tox/* [coverage:html] title = WimsAPI Coverage # Tox settings [tox:tox] skipsdist = True distshare = {homedir}/.tox/distshare envlist = py{35,36,37,38,39} skip_missing_interpreters = true indexserver = pypi = https://pypi.python.org/simple [testenv] setenv = PYTHONPATH = {toxinidir} commands = pip install -e . pycodestyle wimsapi/ coverage run --source wimsapi -m pytest -sv tests coverage xml deps = pytest coverage pytest-cov pycodestyle #################################### ##### PEP 8 & PEP 257 settings ##### #################################### [pycodestyle] count = True # https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes ignore = E303,W293,E241,W503,E701,E741 max-line-length = 100 max-doc-length = 100 [tool:pytest] addopts = -vvl wimsapi-0.5.11/setup.py000066400000000000000000000024531407533276500150010ustar00rootroot00000000000000"""Setuptools entry point.""" import codecs import os try: from setuptools import setup except ImportError: from distutils.core import setup CLASSIFIERS = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Topic :: Software Development :: Libraries :: Python Modules' ] dirname = os.path.dirname(__file__) long_description = ( codecs.open(os.path.join(dirname, 'README.md'), encoding='utf-8').read() + '\n\n______\n\n' + codecs.open(os.path.join(dirname, 'CHANGES.md'), encoding='utf-8').read() ) setup( name='wimsapi', version="0.5.11", description='A Python 3 implementation of WIMS adm/raw module.', long_description=long_description, long_description_content_type='text/markdown', author='Coumes Quentin', author_email='coumes.quentin@gmail.com', url='https://github.com/qcoumes/wimsapi', packages=['wimsapi'], install_requires=['requests'], classifiers=CLASSIFIERS ) wimsapi-0.5.11/tests/000077500000000000000000000000001407533276500144255ustar00rootroot00000000000000wimsapi-0.5.11/tests/__init__.py000066400000000000000000000000001407533276500165240ustar00rootroot00000000000000wimsapi-0.5.11/tests/resources/000077500000000000000000000000001407533276500164375ustar00rootroot00000000000000wimsapi-0.5.11/tests/resources/6948902.tgz000077500000000000000000000040761407533276500200440ustar00rootroot00000000000000?]][o6쾬+X /xx,ڦN@H#ne]%Lj~eHCP1m!0;! c@")&b@,l;e(DQ(꺡}~]^=/hJ)ڟ@tOzyI|q#aRCrlF, Ckeř[,|OVTNf0;6ҤùiAL%2khhh?f3Iu\Z喇a3b1hhh#u괮x:ѥiVi$mkhhhEOlǗ?u544444tG"hO)e?0D641[ pE8Z@/̑ZF6zeѢ#mw1[,8#|ʔʠƆoP1iekc& c&ݯiƤUbyûzj?m~6zx{7ke{?x<?Đ?1/SQX$*218>-\^}-p b?%Ӌ՗ 57)6"?K#@cPAk?' 7&s?$|\xc?$d^e 5Ly>:9~S塃  d\I| VJ* t{yXjcΣb| P_?O"?OqG L; >zNr;C>f6x> l|lƀI|ަ"̥mdhZ Τk ʨ_̔G j? Cb߶?eL&AXk 4:;k /"E^FT3MjnYkywE7!w6K)x[UwqmJ5A%)rq#tю8|洗De?R%S *?j? є36A'"L#Z@Æ#|@^hCG PeHG `iF{H䯰z k5^Ũ?1/ Z'}8ds*C/O_T3?&AmU^F\$z7 {yʯn6+4@,q/@ |yv !˸_,kX(niԳAf <]?f5~t n}OI,}0 Ö\"8gIPL$;s1CҾ\) qd|{Rjb:Q6ɜ*9ʌ;B]4۔(x鞛#۴)#?SIěޯE >+ UusAzUEP_%o^WQdFQVZAո"ID޾9E)wjh3<[jEQVSn6^:Ѧ^YY+" e'f` X^%7.2ClYE\<◵N6ӌ"3nߛx\vܔEO&vX%+rQ7 nR3c_>w[^Lu;٩%8z19*JWzwimsapi-0.5.11/tests/test_api.py000066400000000000000000000604361407533276500166200ustar00rootroot00000000000000import os import unittest from io import BytesIO from tarfile import TarError, TarFile from wimsapi.api import WimsAPI WIMS_URL = os.getenv("WIMS_URL") or "http://localhost:7777/wims/wims.cgi/" CSV = """login,password,name,lastname,firstname,email,regnum desc,desc,desc,desc,desc,desc,desc jdoe,password,Jhon Doe,Doe,Jhon,jhon@email.fr,1 qcoumes,password,Quentin Coumes,Coumes,Quentin,quentin@mail.fr,2""" class WimsAPITestCase(unittest.TestCase): @classmethod def setUpClass(cls): """Remove class potentially created by tests""" api = WimsAPI(WIMS_URL, "myself", "toto") api.delclass(999999, "myclass") api.delclass(999666, "myclass") api.delclass(999990, "myclass") def test_init_and_properties(self): api = WimsAPI(WIMS_URL, "myself", "toto") self.assertEqual(api.url, WIMS_URL) self.assertEqual(api.ident, "myself") self.assertEqual(api.passwd, "toto") def test_0_addclass_id(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.addclass( "myclass", { 'description': "Test class", "institution": "Test institution", "supervisor": "Test supervisor", "email": "Test@mail.com", "password": "password", "lang": "fr", "limit": 500 }, { "lastname": "Doe", "firstname": "Jhon", "password": "password" }, 999999 ) self.assertTrue(status) self.assertEqual(response['message'], "class 999999 correctly added") self.assertEqual(response['class_id'], "999999") def test_0_addclass_random(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.addclass( "myclass", { 'description': "Test class", "institution": "Test institution", "supervisor": "Test supervisor", "email": "Test@mail.com", "password": "password", "lang": "fr", "limit": 500 }, { "lastname": "Doe", "firstname": "Jhon", "password": "password" }, ) self.assertTrue(status) self.assertIn('class_id', response) class_id = response['class_id'] self.assertEqual(response['message'], "class %s correctly added" % class_id) status, response = api.delclass(class_id, "myclass") if not status: self.fail("Could not delete class of id %s. This class must be deleted before testing." % class_id) def test_addexam(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.addexam(999999, "myclass", {}) self.assertTrue(status) self.assertEqual(response['message'], "exam #1 correctly added") def test_addexo(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.addexo(999999, "myclass", 1, r"\text{ a = \ma_matrice[1;] } ", True) self.assertTrue(status) self.assertEqual(response['message'], "exercice 1 successfully added") def test_addsheet(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.addsheet(999999, "myclass", {}) self.assertTrue(status) def test_adduser(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.adduser( 999999, "myclass", "jdoe", { "lastname": "Doe", "firstname": "Jhon", "password": "password" } ) self.assertTrue(status) self.assertEqual(response['message'], "user jdoe correctly added") def test_authuser(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.authuser(999999, "myclass", "supervisor") self.assertTrue(status) self.assertTrue('wims_session' in response) def test_authuser_hash(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.authuser(999999, "myclass", "supervisor", "SHA1") self.assertTrue(status) self.assertTrue('wims_session' in response) def test_buildexos(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.buildexos(999999, "myclass") self.assertFalse(status) self.assertEqual(response['message'], "COMPILATION ERROR No statement defined.") def test_checkclass(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.checkclass(999999, "myclass") self.assertTrue(status) self.assertEqual(response['message'], "Class exists") def test_checkexam(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.checkexam(999999, "myclass", 1) self.assertTrue(status) self.assertEqual(response['message'], "exam 1 exists") def test_checkident(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.checkident() self.assertTrue(status) self.assertEqual(response['message'], "Connection accepted") def test_checksheet(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.checksheet(999999, "myclass", 1) self.assertTrue(status) self.assertEqual(response['message'], "sheet 1 exists") def test_checkuser(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.checkuser(999999, "myclass", "supervisor") self.assertTrue(status) self.assertEqual(response['message'], 'user supervisor exists') def test_cleanclass(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.cleanclass(999999, "myclass") self.assertTrue(status) self.assertEqual(response['message'], 'class 999999 correctly cleaned') def test_copyclass(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.copyclass(999999, "myclass") self.assertTrue(status) self.assertIn('new_class', response) api.delclass(response['new_class'], "myclass") def test_delclass(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.addclass( "myclass", { 'description': "Test class", "institution": "Test institution", "supervisor": "Test supervisor", "email": "Test@mail.com", "password": "password", "lang": "fr" }, { "lastname": "Doe", "firstname": "Jhon", "password": "password" }, 999666 ) self.assertTrue(status) self.assertEqual(response['message'], "class 999666 correctly added") status, response = api.delclass(999666, "myclass") self.assertTrue(status) self.assertEqual(response['message'], "class 999666 correctly deleted") def test_delexam(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.addexam(999999, "myclass", {}) self.assertTrue(status) self.assertEqual(response['message'], "exam #2 correctly added") status, response = api.delexam(999999, "myclass", 2) self.assertTrue(status) self.assertEqual(response['message'], "Exam #2 of class 999999 correctly deleted") def test_delexo(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.delexo(999999, "myclass", 1) self.assertFalse(status) self.assertEqual(response['message'], "1 NOT exists in this Class !") def test_delsheet(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.addsheet(999999, "myclass", {}) self.assertTrue(status) status, response = api.delsheet(999999, "myclass", 2) self.assertTrue(status) self.assertEqual(response['message'], "sheet #2 of class 999999 correctly deleted") def test_deluser(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.adduser( 999999, "myclass", "jdoe2", { "lastname": "Doe", "firstname": "Jhon", "password": "password" } ) self.assertTrue(status) self.assertEqual(response['message'], "user jdoe2 correctly added") status, response = api.deluser(999999, "myclass", "jdoe2") self.assertTrue(status) self.assertEqual(response['message'], "User jdoe2 moved to trash.") def test_getclass(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getclass(9001, "myclass") self.assertTrue(status) self.assertEqual(response['description'], 'Aide au d veloppement de ressources.') def test_getclass_options(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getclass(9001, "myclass", ["password"]) self.assertTrue(status) self.assertIn("password", response) def test_getclassesuser(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getclassesuser("myclass", "supervisor") self.assertTrue(status) self.assertEqual(response['classes_list'], [{'qclass': '999999'}]) def test_getclassfile(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getclassfile(999999, "myclass", '.log') self.assertTrue(status) self.assertEqual(response[34:49], b"User jdoe creat") def test_getclassmodif(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getclassmodif(999999, "myclass", '19700101') self.assertTrue(status) self.assertEqual(response['since_date'], '197001010000') self.assertEqual(len(response['modifs']), 19) def test_getclasstgz(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getclasstgz(999999, "myclass") self.assertTrue(status) try: TarFile.open(fileobj=BytesIO(response), mode="r:gz") except TarError as e: self.fail("Response was not a valid tgz :\n" + str(e)) def test_getcsv(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getcsv(9001, "myclass", ["login", "password", "name", "email"]) self.assertTrue(status) self.assertEqual(type(response), bytes) def test_getexam(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getexam(999999, "myclass", 1) self.assertTrue(status) self.assertEqual(response['exam_title'], "Examen #1") def test_getexamlog(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getexamlog(999999, "myclass", "supervisor", 1) self.assertFalse(status) self.assertEqual(response['message'], "the user supervisor hasn't any exam log in this class.") def test_getexamscores(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getexamscores(999999, "myclass", 1) self.assertFalse(status) self.assertEqual(response['message'], "Exam #1 must be active") def test_getexo(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getexo(9001, "myclass", 3, 1) self.assertTrue(status) self.assertEqual(response['exo_title'], "Choice 1") def test_getexofile(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getexofile(9001, "myclass", 1) self.assertTrue(status) self.assertEqual(response, b'\\title{Un pr\xe9}\n\\language{fr}\n\\author{Sophie Lemaire}\n\\email{' b'soph' b'ie.lemaire@math.u-psud.fr}\n\\computeanswer{no}\n\\precision{' b'10000}\n\n\\in' b'teger{L = 10*randint(1..10)}\n\\integer{l = 10*randint(' b'1..10)}\n\\integer{pe' b"r = 2*(\\L+\\l)}\n\n\\statement{Donner le p\xe9rim\xe8tre d'un pr\xe9 " b"rect" b'angulaire\n de longueur \\L m et de largeur \\l m.}\n\n\\answer{' b'p\xe9rim\xe8' b'tre (en m)}{\\per}{type=numeric}\n') def test_getinfoserver(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getinfoserver() self.assertTrue(status) self.assertIn("server_version", response) def test_getlog(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getlog(9001, "myclass", "supervisor") self.assertTrue(status) self.assertIn("user_log", response) def test_getmodule(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getmodule('E1/geometry/oefsquare.fr') self.assertTrue(status) self.assertEqual(response['title'], "OEF Dessins") def test_getscore(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getscore(9001, "myclass", "supervisor") self.assertTrue(status) self.assertEqual(response['exam_scores'], [[]]) def test_getscore_sheet(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getscore(9001, "myclass", "supervisor", 1) self.assertTrue(status) self.assertEqual(response['exam_scores'], [[]]) def test_getscores(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getscores(9001, "myclass", ["login", "password", "name", "email"]) self.assertTrue(status) self.assertEqual(type(response), bytes) @unittest.skip("Job not defined in wims yet") def test_getsession(self): # pragma: no cover api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getsession() self.assertTrue(status) self.assertTrue(False) def test_getsheet(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getsheet(9001, "myclass", 3) self.assertTrue(status) self.assertEqual(response['sheet_title'], "Syntaxe des exercices OEF à réponse prédéfinie") def test_getsheetscores(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getsheetscores(9001, "myclass", 3, verbose=True) self.assertTrue(status) self.assertIn('data_scores', response) def test_getsheetstats(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getsheetstats(9001, "myclass", 3) self.assertTrue(status) self.assertIn("sheet_got_details", response) def test_gettime(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.gettime() self.assertTrue(status) self.assertIn("server_time", response) def test_getuser(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.getuser(9001, "myclass", "supervisor") self.assertTrue(status) self.assertEqual(response['firstname'], 'Sophie') def test_lightpopup(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.authuser(999999, "myclass", "supervisor") self.assertTrue(status) self.assertTrue('wims_session' in response) session = response['wims_session'] api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.lightpopup(9001, "myclass", "supervisor", session, "H3/analysis/oeflinf.fr") self.assertTrue(status) self.assertIn(b'The exercises will be randomly selected fr' b'om your selection \n(or otherwise from among all the available exercises ' b'if you didn\'t select any).', response) def test_linkexo(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.linkexo(999999, "myclass", 1, 1, 1) self.assertFalse(status) self.assertEqual(response["message"], "sheet 1 must be active") def test_linksheet(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.linksheet(999999, "myclass", 1, 1) self.assertFalse(status) self.assertEqual(response["message"], "sheet 1 must be active") def test_listclasses(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.listclasses("myclass") self.assertTrue(status) self.assertEqual(response["classes_list"], [{'qclass': '999999'}]) def test_listexams(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.listexams(9001, "myclass") self.assertTrue(status) self.assertEqual(response["nbexam"], '0') def test_listexos(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.listexos(9001, "myclass") self.assertTrue(status) self.assertGreaterEqual(response["exocount"], 114) def test_listlinks(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.listlinks(9001, "myclass", 1, 1) self.assertFalse(status) self.assertEqual(response["message"], "element #1 of type exam does not exist in this class (9001)") def test_listmodules(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.listmodules("H1") self.assertTrue(status) self.assertEqual(response["title"], 'Middle school year 1') def test_listsheets(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.listsheets(9001, "myclass") self.assertTrue(status) self.assertGreaterEqual(response["nbsheet"], "11") def test_modclass(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.modclass(999999, "myclass", {'description': "New title"}) self.assertTrue(status) self.assertEqual(response["message"], 'Modifications done') def test_modexam(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.modexam(999999, "myclass", 1, {'title': "New title"}) self.assertTrue(status) self.assertEqual(response["message"], '1 modifications done on exam 1.') def test_modsheet(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.modsheet(999999, "myclass", 1, {'title': "New title"}) self.assertTrue(status) self.assertEqual(response["message"], '1 modifications done on sheet 1.') def test_moduser(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.moduser(999999, "myclass", "supervisor", {'firstname': "Newname"}) self.assertTrue(status) self.assertEqual(response["message"], 'Modifications done') def test_movexo(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.movexo(9001, 999999, "myclass", 2, True) self.assertFalse(status) self.assertEqual(response["message"], 'COMPILATION ERROR No statement defined.') status, response = api.movexo(9001, 999999, "myclass", 3) self.assertFalse(status) self.assertEqual(response["message"], 'COMPILATION ERROR No statement defined.') def test_movexos(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.movexos(9001, 999999, "myclass", True) self.assertTrue(status) self.assertEqual(response["message"], 'exercices successfully copied') status, response = api.movexos(9001, 999999, "myclass") self.assertTrue(status) self.assertEqual(response["message"], 'exercices successfully moved') @unittest.skip("Job not defined in wims yet") def test_putcsv(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.putcsv(999999, "myclass", CSV, False) self.assertTrue(status) self.assertEqual(response["message"], 'exercices successfully copied') def test_putexo(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.putexo(999999, "myclass", 1, "H3/analysis/oeflinf.fr", { "exo": "fnctlin1", "qnum": "1", "scoredelay": "20,50", "seedrepeat": "2", "qcmlevel": "1" }) self.assertTrue(status) self.assertEqual(response["message"], 'exercice correctly added in sheet #1') def test_recuser(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.adduser( 999999, "myclass", "jdoe3", { "lastname": "Doe", "firstname": "Jhon", "password": "password" } ) self.assertTrue(status) self.assertEqual(response['message'], "user jdoe3 correctly added") status, response = api.deluser(999999, "myclass", "jdoe3") self.assertTrue(status) self.assertEqual(response['message'], "User jdoe3 moved to trash.") status, response = api.recuser(999999, "myclass", "jdoe3") self.assertTrue(status) self.assertEqual(response['message'], "User successfully recovered") def test_repairclass(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.repairclass(999999, "myclass") self.assertTrue(status) self.assertEqual(response['action'], "checkonly") def test_sharecontent(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.addclass( "myclass", { 'description': "Test class", "institution": "Test institution", "supervisor": "Test supervisor", "email": "Test@mail.com", "password": "password", "lang": "fr", "limit": 500 }, { "lastname": "Doe", "firstname": "Jhon", "password": "password" }, 999990 ) self.assertTrue(status) self.assertEqual(response['message'], "class 999990 correctly added") api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.sharecontent(9001, 999990, "myclass") self.assertTrue(status) self.assertEqual(response['message'], 'content successfully shared') def test_testexo(self): api = WimsAPI(WIMS_URL, "myself", "toto") status, response = api.testexo( b'\\title{Un pr\xe9}\n\\language{fr}\n\\author{Sophie Lemaire}\n\\email{' b'soph' b'ie.lemaire@math.u-psud.fr}\n\\computeanswer{no}\n\\precision{' b'10000}\n\n\\in' b'teger{L = 10*randint(1..10)}\n\\integer{l = 10*randint(' b'1..10)}\n\\integer{pe' b"r = 2*(\\L+\\l)}\n\n\\statement{Donner le p\xe9rim\xe8tre d'un pr\xe9 " b"rect" b'angulaire\n de longueur \\L m et de largeur \\l m.}\n\n\\answer{' b'p\xe9rim\xe8' b'tre (en m)}{\\per}{type=numeric}\n') self.assertTrue(status) self.assertEqual(response['compilation_result'], "submit.oef.. -> submit.def") if __name__ == '__main__': unittest.main() wimsapi-0.5.11/tests/test_exam.py000066400000000000000000000156131407533276500167760ustar00rootroot00000000000000import os import subprocess import unittest from wimsapi import ExamScore from wimsapi.api import WimsAPI from wimsapi.exam import Exam from wimsapi.exceptions import AdmRawError, NotSavedError from wimsapi.user import User from wimsapi.wclass import Class WIMS_URL = os.getenv("WIMS_URL") or "http://localhost:7777/wims/wims.cgi/" def command(cmd): p = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True ) out, err = p.communicate() if p.returncode: raise RuntimeError( "Return code : " + str(p.returncode) + " - " + err.decode() + out.decode()) return p.returncode, out.decode().strip(), err.decode() class ExamTestCase(unittest.TestCase): @classmethod def setUpClass(cls): """Create an API and an User to use through the tests.""" cls.api = WimsAPI(WIMS_URL, "myself", "toto") cls.user = User("supervisor", "last", "first", "pass", "mail@mail.com") cls.clas = Class("myclass", "A class", "an institution", "mail@mail.com", "password", cls.user, qclass=999999) cls.api.delclass(999999, "myclass") def tearDown(self): self.api.delclass(999999, "myclass") self.clas._saved = False def test_init_and_properties(self): e = Exam("Title", "Description") self.assertEqual(e.exo_count, 0) self.assertRaises(NotSavedError, lambda: e.infos) self.clas.save(WIMS_URL, "myself", "toto") self.clas.additem(e) self.assertIn("exam_title", e.infos) def test_get_exception(self): with self.assertRaises(NotSavedError): Exam.get(self.clas, 1) self.clas.save(WIMS_URL, "myself", "toto") with self.assertRaises(AdmRawError) as cm: Exam.get(self.clas, 50) self.assertIn("WIMS' server responded with an ERROR:", str(cm.exception)) def test_save_and_refresh(self): self.clas.save(WIMS_URL, "myself", "toto") e = Exam("Title", "Description") with self.assertRaises(NotSavedError): e.refresh() e.save(self.clas) s2 = Exam.get(self.clas, e.qexam) self.assertEqual(s2.title, "Title") e.title = "modified" e.save() self.assertEqual(e.title, "modified") self.assertEqual(s2.title, "Title") s2.refresh() self.assertEqual(s2.title, "modified") def test_check(self): self.clas.save(WIMS_URL, "myself", "toto") e = Exam("Title", "Description") c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) with self.assertRaises(NotSavedError): Exam.check(c, e) self.assertFalse(Exam.check(self.clas, e)) self.clas.additem(e) self.assertTrue(Exam.check(self.clas, e)) def test_save_exceptions(self): e = Exam("Title", "Description") with self.assertRaises(NotSavedError): e.save() with self.assertRaises(NotSavedError): e.save(self.clas) def test_delete(self): self.clas.save(WIMS_URL, "myself", "toto") e = Exam("Title", "Description") with self.assertRaises(NotSavedError): e.delete() e.save(self.clas) Exam.get(self.clas, e.qexam) # Ok e.delete() with self.assertRaises(AdmRawError): Exam.get(self.clas, e.qexam) # Should raise the exception def test_remove(self): self.clas.save(WIMS_URL, "myself", "toto") c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) e = Exam("Title", "Description") with self.assertRaises(NotSavedError): e.remove(c, e) e.save(self.clas) Exam.get(self.clas, e.qexam) # Ok Exam.remove(self.clas, e) with self.assertRaises(AdmRawError): Exam.get(self.clas, e.qexam) # Should raise the exception def test_list(self): s1 = Exam("First", "First one") s2 = Exam("Second", "Second one") s3 = Exam("Third", "Third one") self.clas.save(WIMS_URL, "myself", "toto") self.assertListEqual( [], Exam.list(self.clas) ) self.clas.additem(s1) self.clas.additem(s2) self.clas.additem(s3) self.assertListEqual( sorted([s1, s2, s3], key=lambda i: i.qexam), sorted(Exam.list(self.clas), key=lambda i: i.qexam) ) def test_eq(self): s1 = Exam("First", "First one") s2 = Exam("Second", "Second one") s3 = Exam("Third", "Third one") self.clas.save(WIMS_URL, "myself", "toto") self.clas.additem(s1) self.clas.additem(s2) self.assertEqual(s1, self.clas.getitem(s1.qexam, Exam)) self.assertNotEqual(s2, self.clas.getitem(s1.qexam, Exam)) self.assertNotEqual(s2, 1) self.assertRaises(NotSavedError, lambda: s1 == s3) def test_scores(self): # Put a dummy class with existing scores if not already added qclass = 6948902 if not Class.check(self.api.url, self.api.ident, self.api.passwd, qclass, "myclass"): archive = os.path.join(os.path.dirname(__file__), "resources/6948902.tgz") command("docker cp %s wims-minimal:/home/wims/log/classes/" % archive) command( 'docker exec wims-minimal bash -c ' '"tar -xzf /home/wims/log/classes/6948902.tgz -C /home/wims/log/classes/"' ) command( 'docker exec wims-minimal bash -c "chmod 644 /home/wims/log/classes/6948902/.def"' ) command( 'docker exec wims-minimal bash -c "chown wims:wims /home/wims/log/classes/6948902 ' '-R"' ) command('docker exec wims-minimal bash -c "rm /home/wims/log/classes/6948902.tgz"') command( "docker exec wims-minimal bash -c " "\"echo ':6948902,20200626,Institution,test,en,0,H4,dmi,S S,+myself/myclass+,' " '>> /home/wims/log/classes/.index"' ) with self.assertRaises(NotSavedError): Exam().scores() c = Class.get(self.api.url, self.api.ident, self.api.passwd, qclass, "myclass") e = c.getitem(1, Exam) u = c.getitem("qcoumes", User) score = ExamScore(e, u, 6.67, 1) self.assertEqual(e.scores("qcoumes"), score) self.assertEqual(e.scores(), [score]) with self.assertRaises(ValueError): e.scores("unknown") wimsapi-0.5.11/tests/test_score.py000066400000000000000000000036761407533276500171650ustar00rootroot00000000000000import os import unittest from wimsapi import Exam, ExamScore, ExerciseScore, Sheet, SheetScore, User WIMS_URL = os.getenv("WIMS_URL") or "http://localhost:7777/wims/wims.cgi/" class SheetTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.user = User("supervisor", "last", "first", "pass", "mail@mail.com") cls.sheet = Sheet() cls.sheet.qsheet = 1 cls.exam = Exam() cls.exam.qexam = 1 def test_exercise_score_eq(self): self.assertEqual( ExerciseScore(None, self.user, 10, 10, 10, 10, 10, 10, 1, 1), ExerciseScore(None, self.user, 10, 10, 10, 10, 10, 10, 1, 1) ) self.assertNotEqual( ExerciseScore(None, self.user, 10, 10, 10, 10, 10, 10, 1, 1), ExerciseScore(None, self.user, 10, 9, 10, 10, 10, 10, 1, 1) ) self.assertNotEqual( ExerciseScore(None, self.user, 10, 10, 10, 10, 10, 10, 1, 1), None ) def test_sheet_score_eq(self): self.assertEqual( SheetScore(self.sheet, self.user, 10, 10, 10, 10, 10, 1, []), SheetScore(self.sheet, self.user, 10, 10, 10, 10, 10, 1, []) ) self.assertNotEqual( SheetScore(self.sheet, self.user, 10, 10, 10, 10, 10, 1, []), SheetScore(self.sheet, self.user, 8, 10, 10, 10, 10, 1, []) ) self.assertNotEqual( SheetScore(self.sheet, self.user, 10, 10, 10, 10, 10, 1, []), None ) def test_exam_score_eq(self): self.assertEqual( ExamScore(self.exam, self.user, 10, 1), ExamScore(self.exam, self.user, 10, 1) ) self.assertNotEqual( ExamScore(self.exam, self.user, 10, 1), ExamScore(self.exam, self.user, 10, 2) ) self.assertNotEqual( ExamScore(self.exam, self.user, 10, 1), None ) wimsapi-0.5.11/tests/test_sheet.py000066400000000000000000000211151407533276500171460ustar00rootroot00000000000000import os import subprocess import unittest from wimsapi import ExerciseScore, SheetScore from wimsapi.api import WimsAPI from wimsapi.exceptions import AdmRawError, NotSavedError from wimsapi.sheet import Sheet from wimsapi.user import User from wimsapi.utils import default from wimsapi.wclass import Class WIMS_URL = os.getenv("WIMS_URL") or "http://localhost:7777/wims/wims.cgi/" def command(cmd): p = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True ) out, err = p.communicate() if p.returncode: raise RuntimeError( "Return code : " + str(p.returncode) + " - " + err.decode() + out.decode()) return p.returncode, out.decode().strip(), err.decode() class SheetTestCase(unittest.TestCase): @classmethod def setUpClass(cls): """Create an API and an User to use through the tests.""" cls.api = WimsAPI(WIMS_URL, "myself", "toto") cls.user = User("supervisor", "last", "first", "pass", "mail@mail.com") cls.clas = Class("myclass", "A class", "an institution", "mail@mail.com", "password", cls.user, qclass=999999) cls.api.delclass(999999, "myclass") def tearDown(self): self.api.delclass(999999, "myclass") self.clas._saved = False def test_init_and_properties(self): s = Sheet("Title", "Description") self.assertEqual(s.exo_count, 0) self.assertRaises(NotSavedError, lambda: s.infos) self.clas.save(WIMS_URL, "myself", "toto") self.clas.additem(s) self.assertIn("sheet_title", s.infos) def test_get_exception(self): with self.assertRaises(NotSavedError): Sheet.get(self.clas, 1) self.clas.save(WIMS_URL, "myself", "toto") with self.assertRaises(AdmRawError) as cm: Sheet.get(self.clas, 50) self.assertIn("WIMS' server responded with an ERROR:", str(cm.exception)) def test_save_and_refresh(self): self.clas.save(WIMS_URL, "myself", "toto") s = Sheet("Title", "Description") with self.assertRaises(NotSavedError): s.refresh() s.save(self.clas) s2 = Sheet.get(self.clas, s.qsheet) self.assertEqual(s2.title, "Title") s.title = "modified" s.save() self.assertEqual(s.title, "modified") self.assertEqual(s2.title, "Title") s2.refresh() self.assertEqual(s2.title, "modified") def test_check(self): self.clas.save(WIMS_URL, "myself", "toto") s = Sheet("Title", "Description") c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) with self.assertRaises(NotSavedError): Sheet.check(c, s) self.assertFalse(Sheet.check(self.clas, s)) self.clas.additem(s) self.assertTrue(Sheet.check(self.clas, s)) def test_save_exceptions(self): s = Sheet("Title", "Description") with self.assertRaises(NotSavedError): s.save() with self.assertRaises(NotSavedError): s.save(self.clas) def test_delete(self): self.clas.save(WIMS_URL, "myself", "toto") s = Sheet("Title", "Description") with self.assertRaises(NotSavedError): s.delete() s.save(self.clas) Sheet.get(self.clas, s.qsheet) # Ok s.delete() with self.assertRaises(AdmRawError): Sheet.get(self.clas, s.qsheet) # Should raise the exception def test_remove(self): self.clas.save(WIMS_URL, "myself", "toto") c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) s = Sheet("Title", "Description") with self.assertRaises(NotSavedError): s.remove(c, s) s.save(self.clas) Sheet.get(self.clas, s.qsheet) # Ok Sheet.remove(self.clas, s) with self.assertRaises(AdmRawError): Sheet.get(self.clas, s.qsheet) # Should raise the exception def test_list(self): s1 = Sheet("First", "First one") s2 = Sheet("Second", "Second one") s3 = Sheet("Third", "Third one") self.clas.save(WIMS_URL, "myself", "toto") self.assertListEqual( [], Sheet.list(self.clas) ) self.clas.additem(s1) self.clas.additem(s2) self.clas.additem(s3) self.assertListEqual( sorted([s1, s2, s3], key=lambda i: i.qsheet), sorted(Sheet.list(self.clas), key=lambda i: i.qsheet) ) def test_eq(self): s1 = Sheet("First", "First one") s2 = Sheet("Second", "Second one") s3 = Sheet("Third", "Third one") self.clas.save(WIMS_URL, "myself", "toto") self.clas.additem(s1) self.clas.additem(s2) self.assertEqual(s1, self.clas.getitem(s1.qsheet, Sheet)) self.assertNotEqual(s2, self.clas.getitem(s1.qsheet, Sheet)) self.assertNotEqual(s2, 1) self.assertRaises(NotSavedError, lambda: s1 == s3) def test_default(self): d = { "a": [1, 2] } self.assertEqual(default(d, "a", 0), 1) self.assertEqual(default(d, "a", 1), 2) self.assertEqual(default(d, "a", 3), None) self.assertEqual(default(d, "a", 3, -1), -1) self.assertEqual(default(d, "b", 1), None) self.assertEqual(default(d, "b", 1, -1), -1) def test_compute_grade(self): formula = ("max(I,Q)", "I", "I*Q^0.3", "I*Q^0.5", "I*Q", "I^2*Q", "(I*Q)^2") I = (0, 1, 2) Q = 3.3 cumul = best = acquired = 100 expected = { formula[0]: {0: 10, 1: 10, 2: 10}, formula[1]: {0: 10, 1: 10, 2: 10}, formula[2]: {0: 7.17, 1: 7.17, 2: 7.17}, formula[3]: {0: 5.74, 1: 5.74, 2: 5.74}, formula[4]: {0: 3.3, 1: 3.3, 2: 3.3}, formula[5]: {0: 3.3, 1: 3.3, 2: 3.3}, formula[6]: {0: 1.09, 1: 1.09, 2: 1.09}, } for f in formula: for i in I: self.assertEqual( Sheet._compute_grade(f, i, Q, cumul, best, acquired), expected[f][i] ) def test_scores(self): # Put a dummy class with existing scores if not already added qclass = 6948902 if not Class.check(self.api.url, self.api.ident, self.api.passwd, qclass, "myclass"): archive = os.path.join(os.path.dirname(__file__), "resources/6948902.tgz") command("docker cp %s wims-minimal:/home/wims/log/classes/" % archive) command('docker exec wims-minimal bash -c ' '"tar -xzf /home/wims/log/classes/6948902.tgz -C /home/wims/log/classes/"' ) command( 'docker exec wims-minimal bash -c "chmod 644 /home/wims/log/classes/6948902/.def"' ) command( 'docker exec wims-minimal bash -c ' '"chown wims:wims /home/wims/log/classes/6948902 -R"' ) command('docker exec wims-minimal bash -c "rm /home/wims/log/classes/6948902.tgz"') command( "docker exec wims-minimal bash -c " "\"echo ':6948902,20200626,Institution,test,en,0,H4,dmi,S S,+myself/myclass+,' " '>> /home/wims/log/classes/.index"' ) with self.assertRaises(NotSavedError): Sheet().scores() c = Class.get(self.api.url, self.api.ident, self.api.passwd, qclass, "myclass") s1 = c.getitem(1, Sheet) s2 = c.getitem(2, Sheet) u = c.getitem("qcoumes", User) es1_1 = ExerciseScore(None, u, 3.3, 10, 10, 10, 0, 10, 1, 3) ss1 = SheetScore(s1, u, 7.17, 3.3, 100, 100, 100, 1, [es1_1]) es2_1 = ExerciseScore(None, u, 4.59, 10, 10, 10, 0, 10, 1, 2) es2_2 = ExerciseScore(None, u, 5.41, 10, 10, 10, 10, 10, 1, 2) ss2 = SheetScore(s2, u, 8.12, 5, 100, 100, 100, 1, [es2_1, es2_2]) self.assertEqual(s1.scores("qcoumes"), ss1) self.assertEqual(s1.scores(), [ss1]) self.assertEqual(s2.scores("qcoumes"), ss2) self.assertEqual(s2.scores(), [ss2]) with self.assertRaises(ValueError): s1.scores("unknown") wimsapi-0.5.11/tests/test_user.py000066400000000000000000000136701407533276500170230ustar00rootroot00000000000000import os import unittest from wimsapi.api import WimsAPI from wimsapi.exceptions import AdmRawError, InvalidIdentifier, NotSavedError from wimsapi.user import User from wimsapi.wclass import Class WIMS_URL = os.getenv("WIMS_URL") or "http://localhost:7777/wims/wims.cgi/" class UserTestCase(unittest.TestCase): @classmethod def setUpClass(cls): """Create an API and an User to use through the tests.""" cls.api = WimsAPI(WIMS_URL, "myself", "toto") cls.user = User("supervisor", "last", "first", "pass", "mail@mail.com") cls.clas = Class("myclass", "A class", "an institution", "mail@mail.com", "password", cls.user, qclass=999999) cls.api.delclass(999999, "myclass") def tearDown(self): self.api.delclass(999999, "myclass") self.clas._saved = False self.user._saved = False self.user._class = None def test_init_and_properties(self): c = Class.get(WIMS_URL, "myself", "toto", 9001, "myclass") u = User.get(c, "supervisor") self.assertIn("firstname", u.infos) u = User("supervisor", "last", "first", "pass", "mail@mail.com") self.assertEqual(u.fullname, "First Last") with self.assertRaises(NotSavedError): u.infos def test_get_exception(self): with self.assertRaises(NotSavedError): User.get(self.clas, "supervisor") self.clas.save(WIMS_URL, "myself", "toto") with self.assertRaises(AdmRawError) as cm: User.get(self.clas, "unknown") self.assertIn("WIMS' server responded with an ERROR:", str(cm.exception)) def test_save_and_refresh(self): self.clas.save(WIMS_URL, "myself", "toto") u = User("Test", "test", "test", "pass", "mail@mail.com") with self.assertRaises(NotSavedError): u.refresh() u.save(self.clas) u2 = User.get(self.clas, u.quser) self.assertEqual(u2.firstname, "test") u.firstname = "modified" u.save() self.assertEqual(u.firstname, "modified") self.assertEqual(u2.firstname, "test") u2.refresh() self.assertEqual(u2.firstname, "modified") def test_save_adapt(self): self.clas.save(WIMS_URL, "myself", "toto") u = User("ap'ostrophe", "test", "test", "pass", "mail@mail.com") u.save(self.clas) self.assertEqual("apostrophe", u.quser) self.assertTrue(User.check(self.clas, "apostrophe")) self.assertFalse(User.check(self.clas, "ap'ostrophe")) def test_check(self): self.clas.save(WIMS_URL, "myself", "toto") u = User("Test", "test", "test", "pass", "mail@mail.com") c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) with self.assertRaises(NotSavedError): User.check(c, u) self.assertFalse(User.check(self.clas, u)) self.clas.additem(u) self.assertTrue(User.check(self.clas, u)) def test_save_exceptions(self): with self.assertRaises(NotSavedError): self.user.save() with self.assertRaises(NotSavedError): self.user.save(self.clas) self.clas.save(WIMS_URL, "myself", "toto") u = User("ap'ostrophe", "test", "test", "pass", "mail@mail.com") with self.assertRaises(InvalidIdentifier): u.save(self.clas, adapt=False) self.assertFalse(User.check(self.clas, "apostrophe")) self.assertFalse(User.check(self.clas, "ap'ostrophe")) def test_delete(self): self.clas.save(WIMS_URL, "myself", "toto") u = User("Test", "test", "test", "pass", "mail@mail.com") with self.assertRaises(NotSavedError): u.delete() u.save(self.clas) User.get(self.clas, u.quser) # Ok u.delete() with self.assertRaises(AdmRawError): User.get(self.clas, u.quser) # Should raise the exception def test_remove(self): self.clas.save(WIMS_URL, "myself", "toto") c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) u = User("Test", "test", "test", "pass", "mail@mail.com") with self.assertRaises(NotSavedError): u.remove(c, u) u.save(self.clas) User.get(self.clas, u.quser) # Ok User.remove(self.clas, u) with self.assertRaises(AdmRawError): User.get(self.clas, u.quser) # Should raise the exception def test_list(self): u1 = User("Test1", "test", "test", "pass", "mail@mail.com") u2 = User("Test2", "test", "test", "pass", "mail@mail.com") u3 = User("Test3", "test", "test", "pass", "mail@mail.com") self.clas.save(WIMS_URL, "myself", "toto") self.assertListEqual( [], User.list(self.clas) ) self.clas.additem(u1) self.clas.additem(u2) self.clas.additem(u3) self.assertListEqual( sorted([u1, u2, u3], key=lambda i: i.quser), sorted(User.list(self.clas), key=lambda i: i.quser) ) def test_eq(self): u1 = User("Test1", "test", "test", "pass", "mail@mail.com") u2 = User("Test2", "test", "test", "pass", "mail@mail.com") u3 = User("Test3", "test", "test", "pass", "mail@mail.com") self.clas.save(WIMS_URL, "myself", "toto") self.clas.additem(u1) self.clas.additem(u2) self.assertEqual(u1, self.clas.getitem(u1.quser, User)) self.assertNotEqual(u2, self.clas.getitem(u1.quser, User)) self.assertNotEqual(u2, 1) with self.assertRaises(NotSavedError): u1 == u3 wimsapi-0.5.11/tests/test_wclass.py000066400000000000000000000241701407533276500173360ustar00rootroot00000000000000import datetime import os import unittest from unittest import mock from wimsapi.api import WimsAPI from wimsapi.exceptions import AdmRawError, InvalidItemTypeError, NotSavedError from wimsapi.sheet import Sheet from wimsapi.user import User from wimsapi.wclass import Class, one_year_later WIMS_URL = os.getenv("WIMS_URL") or "http://localhost:7777/wims/wims.cgi/" class FakeDate(datetime.date): """Used to override datetime.date.today""" @classmethod def today(cls): """Always return a Date corresponding to 1966-11-16.""" return datetime.date(1966, 11, 16) class ClassTestCase(unittest.TestCase): @classmethod def setUpClass(cls): """Create an API and an User to use through the tests.""" cls.api = WimsAPI(WIMS_URL, "myself", "toto") cls.user = User("supervisor", "last", "first", "pass", "mail@mail.com") def tearDown(self): """Delete classes that a test might have created.""" self.api.delclass(999999, "myclass") self.api.delclass(999666, "myclass") self.api.delclass(999990, "myclass") @mock.patch('datetime.date', FakeDate) def test_one_year_later(self): self.assertEqual(one_year_later(), "19671116") def test_init_and_properties(self): c = Class.get(WIMS_URL, "myself", "toto", 9001, "myclass") self.assertEqual(c.url, WIMS_URL) self.assertEqual(c.ident, "myself") self.assertEqual(c.passwd, "toto") self.assertIn("description", c.infos) c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) with self.assertRaises(NotSavedError): c.url with self.assertRaises(NotSavedError): c.ident with self.assertRaises(NotSavedError): c.passwd with self.assertRaises(NotSavedError): c.infos with self.assertRaises(ValueError): Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999, lang="Wrong") with self.assertRaises(ValueError): Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999, level="Wrong") with self.assertRaises(ValueError): Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999, expiration="Wrong") def test_append_slash(self): url = WIMS_URL if not WIMS_URL.endswith('/') else WIMS_URL[:-1] c = Class.get(url, "myself", "toto", 9001, "myclass") self.assertEqual(c.url, url + "/") def test_save_and_refresh(self): c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) with self.assertRaises(NotSavedError): c.save() with self.assertRaises(NotSavedError): c.refresh() c.save(WIMS_URL, "myself", "toto") c = Class.get(WIMS_URL, "myself", "toto", 999999, "myclass") c2 = Class.get(WIMS_URL, "myself", "toto", 999999, "myclass") self.assertEqual(c.institution, "an institution") self.assertEqual(c2.institution, "an institution") c.institution = "modified" c.save() self.assertEqual(c.institution, "modified") self.assertEqual(c2.institution, "an institution") c2.refresh() self.assertEqual(c2.institution, "modified") self.api.delclass(99999999, "myclass") def test_save_without_qclass(self): c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user) with self.assertRaises(NotSavedError): c.save() c.save(WIMS_URL, "myself", "toto") self.assertIsNotNone(c.qclass) c.delete() def test_getitem(self): c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) with self.assertRaises(NotSavedError): c.getitem("supervisor", User) with self.assertRaises(InvalidItemTypeError): c.getitem(1, int) c.save(WIMS_URL, "myself", "toto") user = c.getitem("supervisor", User) self.assertEqual(user.firstname, self.user.firstname) def test_checkitem(self): c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) unknown = User("unknown", "last", "first", "pass", "mail2@mail.com") with self.assertRaises(NotSavedError): c.checkitem("supervisor", User) with self.assertRaises(InvalidItemTypeError): c.checkitem(1) c.save(WIMS_URL, "myself", "toto") self.assertTrue(c.checkitem("supervisor", User)) self.assertTrue(c.checkitem(self.user)) self.assertFalse(c.checkitem("Unknown", User)) self.assertFalse(c.checkitem(unknown)) def test___contains__(self): c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) unknown = User("unknown", "last", "first", "pass", "mail2@mail.com") with self.assertRaises(NotSavedError): unknown in c c.save(WIMS_URL, "myself", "toto") self.assertTrue(self.user in c) self.assertFalse(unknown in c) def test_additem(self): c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) u = User("quser", "last", "first", "pass", "mail2@mail.com") with self.assertRaises(NotSavedError): c.additem(u) with self.assertRaises(InvalidItemTypeError): c.additem(int) c.save(WIMS_URL, "myself", "toto") c.additem(u) self.assertEqual(u._class.qclass, c.qclass) self.assertEqual(u.wclass, True) u2 = c.getitem("quser", User) self.assertEqual(u2.firstname, u.firstname) def test_delitem(self): c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) u = User("quser", "last", "first", "pass", "mail2@mail.com") u2 = User("quser2", "last", "first", "pass", "mail2@mail.com") with self.assertRaises(NotSavedError): c.delitem(u) with self.assertRaises(InvalidItemTypeError): c.delitem(int) c.save(WIMS_URL, "myself", "toto") c.additem(u) c.additem(u2) self.assertTrue(c.checkitem("quser", User)) c.delitem(u) self.assertFalse(c.checkitem("quser", User)) self.assertTrue(c.checkitem("quser2", User)) c.delitem("quser2", User) self.assertFalse(c.checkitem("quser2", User)) def test_delete(self): c = Class("myclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) with self.assertRaises(NotSavedError): c.delete() c.save(WIMS_URL, "myself", "toto") Class.get(WIMS_URL, "myself", "toto", c.qclass, c.rclass) #  Ok c.delete() with self.assertRaises(AdmRawError): Class.get(WIMS_URL, "myself", "toto", c.qclass, c.rclass) # Should raise the exception def test_list(self): c1 = Class("rclass", "A class", "an institution", "mail@mail.com", "password", self.user) c2 = Class("rclass", "A class", "an institution", "mail@mail.com", "password", self.user) c3 = Class("rclass", "A class", "an institution", "mail@mail.com", "password", self.user) c1.save(WIMS_URL, "myself", "toto") c2.save(WIMS_URL, "myself", "toto") c3.save(WIMS_URL, "myself", "toto") self.assertListEqual( sorted([c1, c2, c3], key=lambda i: i.qclass), sorted(Class.list(WIMS_URL, "myself", "toto", "rclass"), key=lambda i: i.qclass) ) self.assertListEqual( [], Class.list(WIMS_URL, "myself", "toto", "unknown_rclass") ) c1.delete() c2.delete() c3.delete() def test_eq(self): c1 = Class("rclass", "A class", "an institution", "mail@mail.com", "password", self.user) c2 = Class("rclass", "A class", "an institution", "mail@mail.com", "password", self.user) c3 = Class("rclass", "A class", "an institution", "mail@mail.com", "password", self.user) with self.assertRaises(NotSavedError): c1 == c3 c1.save(WIMS_URL, "myself", "toto") c2.save(WIMS_URL, "myself", "toto") c3.save(WIMS_URL, "myself", "toto") self.assertEqual(c1, Class.get(WIMS_URL, "myself", "toto", c1.qclass, c1.rclass)) self.assertNotEqual(c2, Class.get(WIMS_URL, "myself", "toto", c1.qclass, c1.rclass)) self.assertNotEqual(c2, 1) c1.delete() c2.delete() c3.delete() def test_listitem(self): c = Class("rclass", "A class", "an institution", "mail@mail.com", "password", self.user, qclass=999999) with self.assertRaises(NotSavedError): c.listitem(Sheet) with self.assertRaises(InvalidItemTypeError): c.listitem(int) s1 = Sheet("First", "First one") s2 = Sheet("Second", "Second one") s3 = Sheet("Third", "Third one") c.save(WIMS_URL, "myself", "toto") self.assertListEqual([], c.listitem(Sheet)) c.additem(s1) c.additem(s2) c.additem(s3) self.assertListEqual( sorted([s1, s2, s3], key=lambda i: i.qsheet), sorted(c.listitem(Sheet), key=lambda i: i.qsheet)) c.delete() wimsapi-0.5.11/wimsapi/000077500000000000000000000000001407533276500147345ustar00rootroot00000000000000wimsapi-0.5.11/wimsapi/__init__.py000066400000000000000000000006041407533276500170450ustar00rootroot00000000000000from .api import WimsAPI from .exam import Exam from .exceptions import (AdmRawError, InvalidItemTypeError, InvalidResponseError, NotSavedError, WimsAPIError) from .score import ExamScore, ExerciseScore, SheetScore from .sheet import Sheet from .user import User from .wclass import Class name = "wimsapi" __title__ = 'wimsapi' __version__ = VERSION = '0.5.11' wimsapi-0.5.11/wimsapi/api.py000066400000000000000000002306401407533276500160640ustar00rootroot00000000000000"""Low-level API of the adm/raw module of WIMS. For higher level classes like Class, User and Sheet, see the other .py files. WIMS direct communication with another web server is two-directional. It can receive http/https requests from the other server, and/or send http/https requests to the other. The connectable server must be declared in a file within the directory 'WIMS_HOME/log/classes/.connections/'. Warning: output must be set 'ident_type=json' and agent must be set to 'ident_agent=python-requests' in 'WIMS_HOME/log/classes/.connections/IDENT' for this API to work properly. For more information, see https://wimsapi.readthedocs.io/adm-raw/""" import json import random import string import requests from wimsapi.exceptions import InvalidResponseError def post(url, **kwargs): """Convert strings to 'ISO-8859-1' before sending the post request.""" for k, v in kwargs["data"].items(): kwargs["data"][k] = v if not isinstance(v, str) else v.encode("ISO-8859-1") kwargs["headers"] = {"Content-Type": "application/x-www-form-urlencoded; charset=ISO-8859-1"} return requests.post(url, **kwargs) def random_code(): """Returns a random code for an adm/raw request.""" return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) def parse_response(request, verbose=False, return_request=False): """Return a dictionary containing at least 'status' and 'message' keys. The goal is that the response is always the same, whether the output type of the WIMS server is set to JSON or WIMS. Warning: output must be set 'ident_type=json' in 'WIMS_HOME/log/classes/.connections/IDENT' for this API to work properly.""" try: response = request.json() except json.JSONDecodeError: status, message = request.text.split('\n', maxsplit=1) code = "N/A" if ' ' in status: status, code = status.split(' ', maxsplit=1) response = { 'status': status, 'message': message if '\n' not in message else message[:-1], 'code': code, } if response['status'] not in ["ERROR", "OK"]: if not return_request: # pragma: no cover if not verbose: msg = ("Use verbose=True to see the received response content " + "or use return_request=True to get the request object.") else: msg = "Received:\n\n" + request.text raise InvalidResponseError("Not a adm/raw response, maybe the URL is incorrect. ", msg) return request return response class WimsAPI: """This class allow a python3 script to communicate with a WIMS server. Parameters: url - (str) url to the wims server CGI. e.g. https://wims.unice.fr/wims/wims.cgi ident - (str) Sender identifier (a word, according to the definition in WIMS_HOME/log/classes/.connections/) passwd - (str) Sender password (as defined in WIMS_HOME/log/classes/.connections/) kwargs - (dict) Keyword argument that will be passed to the request.post() calls. Two optionnal parameter can be passed to every method: code - (str) a word sent to the request. A random code will be created by the method if none is provided. This word will be sent back, in order to allow to check whether the result is from the good request. verbose - (boolean) Default to False. Tell whether or not showing the whole response in the the exception if the response could not be parsed. Any additional keyword argument will be passed to the request.post() function if the same argument is present in both the instance and the method's call, the method's call one will prevail. Every method return a tuple containing a boolean and a dictionary containing at least 'status', 'message' and 'code' keys. Status is either a word OK (which set the boolean to True), or the word ERROR (which set the boolean to False). In case the status is OK, the 'message' key contains the remaining of the data (can be empty), the dictionary can also contains more keys. In case the status is ERROR, key 'message' contains the nature of the error. Warning: output must be set 'ident_type=json' in 'WIMS_HOME/log/classes/.connections/IDENT' for this API to work properly. For more information, see https://wimsapi.readthedocs.io/adm-raw/""" def __init__(self, url, ident, passwd, **kwargs): self.params = {'module': 'adm/raw', 'ident': ident, 'passwd': passwd} if not url.endswith('/'): url += '/' self.url = url self.request_kwargs = kwargs @property def ident(self): """Returns the ident used by this instance of WimsAPI.""" return self.params['ident'] @property def passwd(self): """Returns the passwd used by this instance of WimsAPI.""" return self.params['passwd'] def addclass(self, rclass, class_info, supervisor_info, qclass=None, verbose=False, code=None, **kwargs): """Add a class on the receiving server. if provided, qclass will be the identifier of the newly created WIMS class. The identifier is randomly chosen if qclass is not provided. Parameters: rclass - (str) identifier of the class on the sending server. class_info - (dict) properties of the new class, following keys may be present: Mandatory: description - (str) name of the class institution - (str) name of the institution supervisor - (str) full name of the supervisor email - (str) contact email address password - (str) password for user registration lang - (str) class language (en, fr, es, it, etc) Optionnal: expiration - (str) class expiration date (yyyymmdd, defaults to one year later) limit - (str) limit of number of participants (defaults to 30) level - (str) level of the class (defaults to H4) Valid levels: E1, E2, ..., E6, H1, ..., H6, U1, ..., U5, G, R secure - (str) secure hosts bgcolor - (str) page background color refcolor - (str) menu background color css - (str) css file (must be existing css on the WIMS server) supervisor_info - (dict) properties of the supervisor account, folowing keys may be present: Mandatory lastname - (str) last name of the supervisor user firstname - (str) first name of the supervisor user password - (str) user's password, non-crypted. Optionnal: email - (str) supervisor email address comments - (str) any comments regnum - (str) registration number photourl - (str) url of an user picture participate - (str) list classes (if in a class group) where user participates agreecgu - (str) Boolean indicating if user accepted CGU regprop1, regprop2, ... regprop5 - (str) custom properties qclass - (int) if provided, identifier of the newly created WIMS class. The identifier is randomly chosen if not provided.""" params = { **self.params, **{ 'job': 'addclass', 'code': code if code else random_code(), 'rclass': rclass, 'data1': '\n'.join([str(k) + "=" + str(v) for k, v in class_info.items()]), 'data2': '\n'.join([str(k) + "=" + str(v) for k, v in supervisor_info.items()]), }, **({'qclass': qclass} if qclass is not None else {}) } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def addexam(self, qclass, rclass, exam_info, verbose=False, code=None, **kwargs): """Add an exam to the specified class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. exam_info - (dict) properties of the exam, following keys may be present: title - (str) title of the exam description - (str) description of the exam expiration - (str) exam expiration date (yyyymmdd) duration - (int) duration (in minutes) attempts - (int) number of attempts""" params = { **self.params, **{ 'job': 'addexam', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'data1': '\n'.join([str(k) + "=" + str(v) for k, v in exam_info.items()]), } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def addexo(self, qclass, rclass, qexo, exo_src, no_build=False, verbose=False, code=None, **kwargs): """Add an exercice to the specified class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qexo - (str) exo identifier on the receiving server. exo_src - (str) source of the exercice. no_build - (bool) Do not compile the exercise. Improves the speed when there is a lot of exercices to handle at the same time. Do not forget to call buildexos() to compile them at the end (defaults to False)""" params = { **self.params, **{ 'job': 'addexo', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qexo': qexo, 'data1': exo_src, } } if no_build: params['option'] = 'no_build' request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def addsheet(self, qclass, rclass, sheet_info, verbose=False, code=None, **kwargs): """Add a sheet to the specified class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. sheet_info - (dict) properties of the sheet, following keys may be present: Mandatory: / Optionnal: title - (str) name of the sheet (defaults to "sheet n#") description - (str) description of the sheet (defaults to "sheet n#") expiration - (str) expiration date (yyyymmdd) defaults to one year later sheetmode - (str) the mode of the sheet: 0 : pending (default) 1 : active 2 : expired 3 : expired + hidden weight - (int) weight of the sheet in the class score (default to 1), use 0 if you want this sheet's score to be ignored. formula - (str) How the score is calculated for this sheet (0 to 6, default to 2) 0 : Very lenient: maximum between percentage and quality. 1 : Quality is not taken into account. You get maximum of grade once all the required work is done. 2 : Quality has only slight effect over the grade. 3 : More effect of quality. 4 : To have a grade of 10, you must gather all require points (100%) without making any error (quality=10). 5 : Unfinished work is over-punished. 6 : Any mistake is over-punished. indicator - (str) what indicator will be used in the score formula (0 to 2, default to 1) contents - (str) the contents for the multi-line file to be created. The semicolons (;) in this parameter will be interpreted as new lines. Equal characters (=) must be replaced by the character AT (@). There is no check made, so the integrity of the contents is up to you only! (defaults to "")""" params = { **self.params, **{ 'job': 'addsheet', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'data1': '\n'.join([str(k) + "=" + str(v) for k, v in sheet_info.items()]), } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def adduser(self, qclass, rclass, quser, user_info, verbose=False, code=None, **kwargs): """Add an user to the specified class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server. user_info - (dict) properties of the user, following keys may be present: Mandatory lastname - (str) last name of the user firstname - (str) first name of the user password - (str) user's password, non-crypted. Optionnal: email - (str) email address comments - (str) any comments regnum - (str) registration number photourl - (str) url of user's photo participate - (str) list classes where user participates courses - (str) special for portal classes - (str) special for portal supervise - (str) List classes where teacher are administator supervisable - (str) yes/no ; give right to the user to supervise a class external_auth - (str) login for external_auth agreecgu - (str) if yes, the user will not be asked when he enters for the first time to agree the cgu regprop[1..5] - (str) custom variables""" params = { **self.params, **{ 'job': 'adduser', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'quser': quser, 'data1': '\n'.join([str(k) + "=" + str(v) for k, v in user_info.items()]), } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def authuser(self, qclass, rclass, quser, hashlogin=None, verbose=False, code=None, ip=None, **kwargs): """Get an authentification token for an user. User's password is not required. If parameter hashlogin is set to an hash function name, quser should be the external identification of user and the function hashlogin is called to convert such id to a WIMS login. If the user exists in class, it returns a session number as above. If the user does not exists, the WIMS login is returned in the error message. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server. ip - (str) IP of the user trying to authenticate trough WimsAPI. hashlogin - (str) hash function to use for an external authentification Return a session number under which the user can connect with no need of further authentification""" params = { **self.params, **{ 'job': 'authuser', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'quser': quser, 'data1': ip } } if hashlogin: params['hashlogin'] = hashlogin request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def buildexos(self, qclass, rclass, verbose=False, code=None, **kwargs): """Compile every exercises of the specified class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server.""" params = { **self.params, **{ 'job': 'buildexos', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def checkclass(self, qclass, rclass, verbose=False, code=None, **kwargs): """Check whether the class accepts connection. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server.""" params = { **self.params, **{ 'job': 'checkclass', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def checkexam(self, qclass, rclass, qexam, verbose=False, code=None, **kwargs): """Check whether the exam exists. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qexam - (str) exam identifier on the receiving server.""" params = { **self.params, **{ 'job': 'checkexam', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qexam': qexam, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def checkident(self, verbose=False, code=None, **kwargs): """Check whether the connection is accepted.""" params = { **self.params, **{'job': 'checkident', 'code': code if code else random_code()} } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def checksheet(self, qclass, rclass, qsheet, verbose=False, code=None, **kwargs): """Check whether the sheet exists. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (str) identifier of the sheet on the receiving server.""" params = { **self.params, **{ 'job': 'checksheet', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def checkuser(self, qclass, rclass, quser, verbose=False, code=None, **kwargs): """Check whether the user exists. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server.""" params = { **self.params, **{ 'job': 'checkuser', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'quser': quser, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def cleanclass(self, qclass, rclass, verbose=False, code=None, **kwargs): """Delete users (but supervisor) and all work done by students on the specified class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server.""" params = { **self.params, **{ 'job': 'cleanclass', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def copyclass(self, qclass, rclass, verbose=False, code=None, **kwargs): """Copy a class. Do not copy users or work done by students. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server.""" params = { **self.params, **{ 'job': 'copyclass', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def delclass(self, qclass, rclass, verbose=False, code=None, **kwargs): """Delete a class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server.""" params = { **self.params, **{ 'job': 'delclass', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def delexam(self, qclass, rclass, qexam, verbose=False, code=None, **kwargs): """Delete an exam. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qexam - (str) exam identifier on the receiving server.""" params = { **self.params, **{ 'job': 'delexam', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qexam': qexam, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def delexo(self, qclass, rclass, qexo, verbose=False, code=None, **kwargs): """Delete an exo. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qexo - (str) exo identifier on the receiving server.""" params = { **self.params, **{ 'job': 'delexo', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qexo': qexo, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def delsheet(self, qclass, rclass, qsheet, verbose=False, code=None, **kwargs): """Delete a sheet Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (int) identifier of the sheet on the receiving server. options - (list) names of fields queried.""" params = { **self.params, **{ 'job': 'delsheet', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def deluser(self, qclass, rclass, quser, verbose=False, code=None, **kwargs): """Delete an user. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server.""" params = { **self.params, **{ 'job': 'deluser', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'quser': quser, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getclass(self, qclass, rclass, options=None, verbose=False, code=None, **kwargs): """Get the properties of a class. Optionally, the parameter 'options' may contain the names of fields queried. In this case, only the queried properties are returned. Existing properties are: password, creator, secure, external_auth, mixed_external_auth, cas_auth, php_auth, authidp, supervisor, description, institution, lang, email, expiration, limit, topscores, superclass, type, level, parent, typename, bgcolor, bgimg, scorecolor, css, logo, logoside, refcolor, ref_menucolor, ref_button_color, ref_button_bgcolor, ref_button_help_color, ref_button_help_bgcolor, theme, theme_icon, connections, creation, userlist, usercount, examcount, sheetcount Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. options - (list) names of fields queried.""" params = { **self.params, **{ 'job': 'getclass', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, } } if options: params['option'] = ','.join(options) request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getclassesuser(self, rclass, quser, verbose=False, code=None, **kwargs): """List all the classes having connection with rclass where quser exists. Optionally, the parameter 'options' may contain the names of fields queried for each class. In this case, only the queried properties are returned. Existing properties are: password, creator, secure, external_auth, mixed_external_auth, cas_auth, php_auth, authidp, supervisor, description, institution, lang, email, expiration, limit, topscores, superclass, type, level, parent, typename, bgcolor, bgimg, scorecolor, css, logo, logoside, refcolor, ref_menucolor, ref_button_color, ref_button_bgcolor, ref_button_help_color, ref_button_help_bgcolor, theme, theme_icon, connections, creation, userlist, usercount, examcount, sheetcount Parameters: rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server. options - (list) names of fields queried.""" params = { **self.params, **{ 'job': 'getclassesuser', 'code': code if code else random_code(), 'rclass': rclass, 'quser': quser, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getclassfile(self, qclass, rclass, filename, code=None, **kwargs): """Download the file of the specified class. Parameters: qclass - (str) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. filename - (str) path to the file relative to 'log/classes/[qclass]/'. """ params = { **self.params, **{ 'job': 'getclassfile', 'code': code if code else random_code(), 'rclass': rclass, 'qclass': qclass, 'option': filename, } } request = post(self.url, data=params, stream=True, **{**self.request_kwargs, **kwargs}) response = parse_response(request, return_request=True) return ( response['status'] == 'OK' if isinstance(response, dict) else True, response if isinstance(response, dict) else request.content ) def getclassmodif(self, qclass, rclass, date, verbose=False, code=None, **kwargs): """List all the files modified on the specified class since . Parameters: rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server. date - (str) date (yyyymmdd)""" params = { **self.params, **{ 'job': 'getclassmodif', 'code': code if code else random_code(), 'rclass': rclass, 'qclass': qclass, 'data1': date, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getclasstgz(self, qclass, rclass, code=None, **kwargs): """Download the class in a compressed (tar-gzip) file. Parameters: rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server.""" params = { **self.params, **{ 'job': 'getclasstgz', 'code': code if code else random_code(), 'rclass': rclass, 'qclass': qclass, } } request = post(self.url, data=params, stream=True, **{**self.request_kwargs, **kwargs}) response = parse_response(request, return_request=True) return ( response['status'] == 'OK' if isinstance(response, dict) else True, response if isinstance(response, dict) else request.content ) def getcsv(self, qclass, rclass, options, frmt='csv', code=None, **kwargs): """Get data of the class, under the form of a csv/tsv/xls spreatsheet file. The parameter 'frmt' may be used to specify the desired output frmt (csv or tsv, defaults to csv). The parameter 'options' should contain a list of desired data columns. The following names can be included in 'option', with their respective meanings: login : user identifiers password : user passwords (uncrypted) name : user names (last name and first name) lastname : user family names firstname : user given names email : user email addresses regnum : user registration numbers allscore : all score fields (averages and details) averages : score averages (average0, average1, average2) average0 : global score average (as computed by WIMS) average1 : average of scores automatically attributed by WIMS average2 : average of teacher-entered scores exams : exam1+exam2+... exam1, exam2, ...: scores of each exam sheets : sheet1+sheet2+... sheet1, sheet2, ...: scores of each worksheet manuals : manual1+manual2+... manual1, manual2, ...: first, second, ... teacher-entered scores. The output content (below the status line in WIMS frmt) is a csv/tsv spreadsheet table. The first row of the table contains the names of the fields. The second row gives short descriptions of each field. The third row is blank. The rest is the table content, with one row for each user. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. options - (list) list of desired data columns. frmt - (str) output frmt ('csv', 'tsv' or 'xls', defaults to csv)""" params = { **self.params, **{ 'job': 'getcsv', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'frmt': frmt, } } if options: params['option'] = ','.join(options) request = post(self.url, data=params, stream=True, **{**self.request_kwargs, **kwargs}) response = parse_response(request, return_request=True) return ( response['status'] == 'OK' if isinstance(response, dict) else True, response if isinstance(response, dict) else request.content ) def getexam(self, qclass, rclass, qexam, verbose=False, code=None, **kwargs): """Get an exam from a class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qexam - (int) identifier of the exam on the receiving server.""" params = { **self.params, **{ 'job': 'getexam', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qexam': qexam, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getexamlog(self, qclass, rclass, quser, qexam, verbose=False, code=None, **kwargs): """Get the logs of on inside of a class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. quser - (int) identifier of the user on the receiving server. qexam - (int) identifier of the exam on the receiving server.""" params = { **self.params, **{ 'job': 'getexamlog', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'quser': quser, 'qexam': qexam, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getexamscores(self, qclass, rclass, qexam, verbose=False, code=None, **kwargs): """Get all scores from exam. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qexam - (int) identifier of the exam on the receiving server.""" params = { **self.params, **{ 'job': 'getexamscores', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qexam': qexam, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getexo(self, qclass, rclass, qsheet, qexo, verbose=False, code=None, **kwargs): """Get an exercice from a sheet. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (int) identifier of the sheet on the receiving server. qexo - (int) identifier of the exo on the receiving server.""" params = { **self.params, **{ 'job': 'getexo', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, 'qexo': qexo, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getexofile(self, qclass, rclass, qexo, code=None, **kwargs): """Download the source file of the specified class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qexo - (int) identifier of the exo on the receiving server.""" params = { **self.params, **{ 'job': 'getexofile', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qexo': qexo, } } request = post(self.url, data=params, stream=True, **{**self.request_kwargs, **kwargs}) response = parse_response(request, return_request=True) return ( response['status'] == 'OK' if isinstance(response, dict) else True, response if isinstance(response, dict) else request.content ) def getexosheet(self, qclass, rclass, qsheet, qexo, verbose=False, code=None, **kwargs): """Get informations of inside of of the specified class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (int) identifier of the sheet on the receiving server. qexo - (int) identifier of the exo on the receiving server.""" params = { **self.params, **{ 'job': 'getexosheet', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, 'qexo': qexo, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getinfoserver(self, verbose=False, code=None, **kwargs): """Get informations about the WIMS server.""" params = { **self.params, **{ 'job': 'getinfoserver', 'code': code if code else random_code(), } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getlog(self, qclass, rclass, quser, verbose=False, code=None, **kwargs): """Get the detailed activity registry of an user. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server.""" params = { **self.params, **{ 'job': 'getlog', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'quser': quser, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getmodule(self, module, verbose=False, code=None, **kwargs): """Get informations about . Parameters: module - (str) path of a module (i.e. 'E1/geometry/oefsquare.fr')""" params = { **self.params, **{ 'job': 'getmodule', 'code': code if code else random_code(), 'option': module, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getscore(self, qclass, rclass, quser, qsheet=None, verbose=False, code=None, **kwargs): """Get all scores from user. Can optionnal filter from a sheet. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server. qsheet - (int) identifier of the sheet on the receiving server. Used to filter the scores.""" params = { **self.params, **{ 'job': 'getscore', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'quser': quser, } } if qsheet is not None: params['qsheet'] = qsheet request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getscores(self, qclass, rclass, options, code=None, **kwargs): """Call getcsv() method with format='xls'. For more informations, see WimsAPI.getcsv() documentation.""" return self.getcsv(qclass, rclass, options, frmt='xls', code=code, **kwargs) def getsheet(self, qclass, rclass, qsheet, options=None, verbose=False, code=None, **kwargs): """Get the properties of a sheet (of a class). Optionally, the parameter 'options' may contain the names of fields queried. In this case, only the queried properties are returned. Existing properties are: exo_cnt, sheet_properties, sheet_status, sheet_expiration, sheet_title, sheet_description, exolist, title, params, points, weight, description Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (int) identifier of the sheet on the receiving server. options - (list) names of fields queried.""" params = { **self.params, **{ 'job': 'getsheet', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, } } if options: params['option'] = ','.join(options) request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getsheetscores(self, qclass, rclass, qsheet, verbose=False, code=None, **kwargs): """Get all scores from sheet. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (int) identifier of the sheet on the receiving server.""" params = { **self.params, **{ 'job': 'getsheetscores', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getsheetstats(self, qclass, rclass, qsheet, verbose=False, code=None, **kwargs): """Get stats about work of students for every exercise of . Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (int) identifier of the sheet on the receiving server.""" params = { **self.params, **{ 'job': 'getsheetstats', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def gettime(self, verbose=False, code=None, **kwargs): """Get the current time of the server. Can be used for synchronization purposes.""" params = { **self.params, **{ 'job': 'gettime', 'code': code if code else random_code(), } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def getuser(self, qclass, rclass, quser, options=None, verbose=False, code=None, **kwargs): """Get the properties of an user (of a class). Optionally, the parameter 'options' may contain the names of fields queried. In this case, only the queried properties are returned. Existing properties are: firstname, lastname, email, comments, regnum, photourl, participate, password, courses, classes, supervise, supervisable, external_auth, agreecgu, regprop1, regprop2, regprop3, regprop4, regprop5 Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server. options - (list) names of fields queried.""" params = { **self.params, **{ 'job': 'getuser', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'quser': quser, } } if options: params['option'] = ','.join(options) request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def lightpopup(self, qclass, rclass, quser, session, exercice, about=True, code=None, **kwargs): """Presents an exercise without the top, bottom, and left menu Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server. session - (str) session identifer returned by authuser(). exercice - (str) addresse of the exercice found in 'About this module / this exercice' on the page of the exercice eg: 'classes/en&exo=Exercise1&qnum=1&qcmlevel=1&scoredelay=700,700 ' about - (bool) If True (default), show "about" which gives author information about the exercise.""" params = { **self.params, **{ 'job': 'lightpopup', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'quser': quser, 'session': session, 'emod': exercice, 'option': 'about' if about else 'noabout', } } request = post(self.url, data=params, stream=True, **{**self.request_kwargs, **kwargs}) response = parse_response(request, return_request=True) return ( response['status'] == 'OK' if isinstance(response, dict) else True, response if isinstance(response, dict) else request.content ) def linkexo(self, qclass, rclass, qsheet, qexo, qexam, verbose=False, code=None, **kwargs): """Add exercise of the sheet to . Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (int) identifier of the sheet on the receiving server. qexam - (int) identifier of the exam on the receiving server. """ params = { **self.params, **{ 'job': 'linkexo', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, 'qexo': qexo, 'qexam': qexam, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def linksheet(self, qclass, rclass, qsheet, qexam, verbose=False, code=None, **kwargs): """Add all exercices from sheet to exam. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (int) identifier of the sheet on the receiving server. qexam - (int) identifier of the exam on the receiving server. """ params = { **self.params, **{ 'job': 'linksheet', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, 'qexam': qexam, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def listclasses(self, rclass, verbose=False, code=None, **kwargs): """List all the classes having connection with rclass. Optionally, the parameter 'options' may contain the names of fields queried for each class. In this case, only the queried properties are returned. Existing properties are: password, creator, secure, external_auth, mixed_external_auth, cas_auth, php_auth, authidp, supervisor, description, institution, lang, email, expiration, limit, topscores, superclass, type, level, parent, typename, bgcolor, bgimg, scorecolor, css, logo, logoside, refcolor, ref_menucolor, ref_button_color, ref_button_bgcolor, ref_button_help_color, ref_button_help_bgcolor, theme, theme_icon, connections, creation, userlist, usercount, examcount, sheetcount Parameters: rclass - (str) identifier of the class on the sending server. options - (list) names of fields queried.""" params = { **self.params, **{ 'job': 'listclasses', 'code': code if code else random_code(), 'rclass': rclass, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def listexams(self, qclass, rclass, verbose=False, code=None, **kwargs): """Lists all exams presents in class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server.""" params = { **self.params, **{ 'job': 'listexams', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def listexos(self, qclass, rclass, verbose=False, code=None, **kwargs): """Lists all exercices presents in class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server.""" params = { **self.params, **{ 'job': 'listexos', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def listlinks(self, qclass, rclass, qsheet, qexam, verbose=False, code=None, **kwargs): """Get the number of exercise of linked to . Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (int) identifier of the sheet on the receiving server. qexam - (int) identifier of the exam on the receiving server. """ params = { **self.params, **{ 'job': 'listlinks', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, 'qexam': qexam, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def listmodules(self, level='H4', verbose=False, code=None, **kwargs): """Get the number of exercise of linked to . Parameters: level - (str) level of the module (defaults to H4). Valid levels are E1, E2, ..., E6, H1, ..., H6, U1, ..., U5, G, R """ params = { **self.params, **{ 'job': 'listmodules', 'code': code if code else random_code(), 'option': level, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def listsheets(self, qclass, rclass, verbose=False, code=None, **kwargs): """List all the sheets of a class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server.""" params = { **self.params, **{ 'job': 'listsheets', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def modclass(self, qclass, rclass, class_info, verbose=False, code=None, **kwargs): """Modify the properties of a class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. class_info - (dict) Only modified properties need to be present, following keys may be present: description - (str) name of the class institution - (str) name of the institution supervisor - (str) full name of the supervisor email - (str) contact email address password - (str) password for user registration lang - (str) class language (en, fr, es, it, etc) expiration - (str) class expiration date (yyyymmdd, defaults to one year later) limit - (str) limit of number of participants (defaults to 30) level - (str) level of the class (defaults to H4) Valid levels: E1, E2, ..., E6, H1, ..., H6, U1, ..., U5, G, R secure - (str) secure hosts bgcolor - (str) page background color refcolor - (str) menu background color css - (str) css file (must be existing css on the WIMS server)""" params = { **self.params, **{ 'job': 'modclass', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'data1': '\n'.join([str(k) + "=" + str(v) for k, v in class_info.items()]), } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def modexam(self, qclass, rclass, qexam, exam_info, verbose=False, code=None, **kwargs): """Modify the property of an exam. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. exam_info - (dict) Only modified properties need to be present, following keys may be present: title - (str) title of the exam description - (str) description of the exam expiration - (str) exam expiration date (yyyymmdd) duration - (int) duration (in minutes) attempts - (int) number of attempts""" params = { **self.params, **{ 'job': 'modexam', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qexam': qexam, 'data1': '\n'.join([str(k) + "=" + str(v) for k, v in exam_info.items()]), } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def modexosheet(self, verbose=False, code=None, **kwargs): """Not yet implemented.""" pass # TODO def modsheet(self, qclass, rclass, qsheet, sheet_info, verbose=False, code=None, **kwargs): """Modify the properties of a sheet. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (int) identifier of the sheet on the receiving server. sheet_info - (dict) Only modified properties need to be present, following keys may be present: title - (str) name of the sheet (defaults to "sheet n#") description - (str) description of the sheet (defaults to "sheet n#") expiration - (str) expiration date (yyyymmdd) defaults to one year later status - (str) the mode of the sheet: 0 : pending (default) 1 : active 2 : expired 3 : expired + hidden weight - (str) weight of the sheet in class score formula - (str) How the score is calculated for this sheet (0 to 6) indicator - (str) what indicator will be used in the score formula (0 to 2) contents - (str) the contents for the multi-line file to be created. The semicolons (;) in this parameter will be interpreted as new lines. Equal characters (=) must be replaced by the character AT (@). There is no check made, so the integrity of the contents is up to you only! (defaults to "")""" params = { **self.params, **{ 'job': 'modsheet', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, 'data1': '\n'.join([str(k) + "=" + str(v) for k, v in sheet_info.items()]), } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def moduser(self, qclass, rclass, quser, user_info, verbose=False, code=None, **kwargs): """Modify the properties of an user. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server. user_info - (dict) Only modified properties need to be present, following keys may be present: lastname - (str) last name of the supervisor user firstname - (str) first name of the supervisor user password - (str) user's password, non-crypted. email - (str) email address comments - (str) any comments regnum - (str) registration number photourl - (str) url of user's photo participate - (str) list classes where user participates courses - (str) special for portal classes - (str) special for portal supervise - (str) List classes where teacher are administator supervisable - (str) yes/no ; give right to the user to supervise a class external_auth - (str) login for external_auth agreecgu - (str) if yes, the user will not be asked when he enters for the first time to agree the cgu regprop[1..5] - (str) custom variables""" params = { **self.params, **{ 'job': 'moduser', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'quser': quser, 'data1': '\n'.join([str(k) + "=" + str(v) for k, v in user_info.items()]), } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def movexo(self, qclass, qclass2, rclass, qsheet, copy=False, verbose=False, code=None, **kwargs): """Moves exercice from qclass to qclass2. Condition : Both 2 classes must be linked by. Parameters: qclass - (int) identifier of the class on the receiving server, source of the exo. qclass2 - (int) identifier of the class on the receiving server, destination of the exo. rclass - (str) identifier of the class on the sending server. qsheet - (int) identifier of the sheet on the receiving server. copy - (bool) If set to True, copy the exo instead of moving it. """ params = { **self.params, **{ 'job': 'movexo', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, 'data1': qclass2, } } if copy: params['option'] = 'copy' request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def movexos(self, qclass, qclass2, rclass, copy=False, verbose=False, code=None, **kwargs): """Moves ALL exercices from qclass to qclass2. Condition : Both 2 classes must be linked by. Parameters: qclass - (int) identifier of the class on the receiving server, source of the exos. qclass2 - (int) identifier of the class on the receiving server, destination of the exo. rclass - (str) identifier of the class on the sending server. copy - (bool) If set to True, copy the exo instead of moving it. """ params = { **self.params, **{ 'job': 'movexos', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'data1': qclass2, } } if copy: params['option'] = 'copy' request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def putcsv(self, qclass, rclass, csv, file=True, verbose=False, code=None, **kwargs): """Put data into the class. csv should respect this format: The first row of the table contains the names of the fields. The second row gives short descriptions of each field. The second row is blank. The rest is the table content, with one row for each user. The following data columns can be included in the csv, with their respective meanings: login : user identifiers password : user passwords (uncrypted) name : user names (last name and first name) lastname : user family names firstname : user given names email : user email addresses regnum : user registration numbers allscore : all score fields (averages and details) averages : score averages (average0, average1, average2) average0 : global score average (as computed by WIMS) average1 : average of scores automatically attributed by WIMS average2 : average of teacher-entered scores exams : exam1+exam2+... exam1, exam2, ...: scores of each exam sheets : sheet1+sheet2+... sheet1, sheet2, ...: scores of each worksheet manuals : manual1+manual2+... manual1, manual2, ...: first, second, ... teacher-entered scores. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. csv - (str) path to a csv if 'file' is True (default), content of a csv otherwise. file - (bool) Whether csv must be interpreted as a path to a csv or a .csv string""" if file: with open(csv) as f: csv = f.read() params = { **self.params, **{ 'job': 'putcsv', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'data1': csv, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def putexo(self, qclass, rclass, qsheet, module, options=None, verbose=False, code=None, **kwargs): """Add 's exercise to of a specified class. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. qsheet - (str) sheet identifier on the receiving server. module - (str) path of a module (i.e. 'E1/geometry/oefsquare.fr'). options - (dict) A dictionnary of optionnal parameters: params - (str) string of parameters to add to the module. points - (int) number of requested points. weight - (int) weight of the exercise. title - (str) title of the exercise in the sheet. description - (str) description of the exercise in the sheet.""" params = { **self.params, **{ 'job': 'putexo', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'qsheet': qsheet, 'data1': 'module=' + module, } } if options: params['data1'] += ('\nparams=' + '\n'.join([str(k) + "=" + str(v) for k, v in options.items()])) request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def recuser(self, qclass, rclass, quser, verbose=False, code=None, **kwargs): """Recover a deleted user. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server. quser - (str) user identifier on the receiving server.""" params = { **self.params, **{ 'job': 'recuser', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'quser': quser, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def repairclass(self, qclass, rclass, verbose=False, code=None, **kwargs): """Try to detect and correct eventual problems. Parameters: qclass - (int) identifier of the class on the receiving server. rclass - (str) identifier of the class on the sending server.""" params = { **self.params, **{ 'job': 'repairclass', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def search(self, verbose=False, code=None, **kwargs): """Not yet implemented.""" pass # TODO def sharecontent(self, qclass, qclass2, rclass, options=('exo',), verbose=False, code=None, **kwargs): """Declares neighbour classes, allowing class "qclass" to share content with class "data1". Condition : Both 2 classes must be linked by. Parameters: qclass - (int) identifier of the class on the receiving server. qclass2 - (int) identifier of the second class on the receiving server. rclass - (str) identifier of the class on the sending server. options - (list) content to share (currently, only the "exo" content type is supported). """ params = { **self.params, **{ 'job': 'sharecontent', 'code': code if code else random_code(), 'qclass': qclass, 'rclass': rclass, 'option': ','.join(options), 'data1': qclass2, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def testexo(self, exo_src, verbose=False, code=None, **kwargs): """Allow to test the compilation of an exercise without adding it to a class. Parameters: exo_src - (str) source of the exercice.""" params = { **self.params, **{ 'job': 'testexo', 'code': code if code else random_code(), 'data1': exo_src, } } request = post(self.url, data=params, **{**self.request_kwargs, **kwargs}) response = parse_response(request, verbose) return response['status'] == 'OK', response def update(self, verbose=False, code=None, **kwargs): """Not yet implemented.""" pass # TODO wimsapi-0.5.11/wimsapi/exam.py000066400000000000000000000230311407533276500162370ustar00rootroot00000000000000import datetime import sys from .exceptions import AdmRawError, NotSavedError from .item import ClassItemABC from .score import ExamScore from .user import User from .utils import one_year_later class Exam(ClassItemABC): """This class is used to represent a WIMS' Exam. Parameters: title - (str) name of the exam (defaults to "Examen #n") description - (str) description of the exam (defaults to "Vous êtes dans l'examen n") expiration - (str) expiration date (yyyymmdd) defaults to one year later duration - (int) duration of each attempt of the exam in minutes (defaults to 60) attempts - (int) number of possible attempts for this exam (defaults to 1)""" def __init__(self, title=None, description=None, expiration=None, duration=60, attempts=1, exammode=0, **kwargs): if expiration is not None: datetime.datetime.strptime(expiration, "%Y%m%d") super().__init__() self._class = None self._saved = False self.wclass = False self.qexam = sys.maxsize self.title = title self.description = description self.expiration = expiration if expiration is not None else one_year_later() self.attempts = attempts self.duration = duration self.exammode = exammode self.exos = [] @property def exo_count(self): """Returns the number of exercises in this exam.""" return len(self.exos) @property def infos(self): """Return all the informations hosted on the WIMS server about this exam.""" if not self._class: raise NotSavedError("infos is not defined until the exam has been saved once") status, exam_info = self._class._api.getexam( self._class.qclass, self._class.rclass, self.qexam, verbose=True) if not status: raise AdmRawError(exam_info['message']) for k in ['status', 'code', 'job']: del exam_info[k] return exam_info def __str__(self): return "" % (hex(id(self)), str(self.qexam)) __repr__ = __str__ def __eq__(self, other): """Exams have to come from the same server and have the same qexam to be equal.""" if isinstance(other, self.__class__): if not self.wclass or not other.wclass: raise NotSavedError("Cannot test equality between unsaved exams") return str(self.qexam) == str(other.qexam) and self._class == other._class return False def __hash__(self): if not self.wclass: raise NotSavedError("Unsaved User cannot be hashed") return hash((self._class.qclass, self.qexam)) def refresh(self): """Refresh this instance of a WIMS Exam from the server itself.""" if not self.wclass: raise NotSavedError("Can't refresh unsaved exam") new = Exam.get(self._class, self.qexam) self.__class__ = new.__class__ self.__dict__ = new.__dict__ return self def _to_payload(self): return {k: v for k, v in self.__dict__.items() if k not in ['qexam', '_saved', '_class']} def save(self, wclass=None, check_exists=True): """Save the Exam in the given class. wclass is an instance of wimsapi.Class. The argument is optionnal if the exam has already been saved or fetched from a Class, it will default to the last Class used to saved or the Class from which the exam was fetched. If check_exists is True, the api will check if a exam with the same ID exists on the WIMS' server. If it exists, save will instead modify this exam instead of trying to create new one.""" if not wclass and not self._class: raise NotSavedError("wclass must be provided if this exam has neither been imported " "from a WIMS class nor saved once yet") wclass = wclass or self._class if not wclass._saved: raise NotSavedError("Class must be saved before being able to save a exam") if wclass is not None and check_exists: self._saved = wclass.checkitem(self) if self._saved: status, response = wclass._api.modexam( wclass.qclass, wclass.rclass, self.qexam, self._to_payload(), verbose=True) else: status, response = wclass._api.addexam( wclass.qclass, wclass.rclass, self._to_payload(), verbose=True) if not status: raise AdmRawError(response['message']) self.qexam = response['exam_id'] if "exam_id" in response else response["queryexam"] self._class = wclass self.wclass = True def delete(self): """Delete the exam from its associated WIMS class on the server.""" if not self.wclass: raise NotSavedError("Cannot delete an unsaved exam") status, response = self._class._api.delexam(self._class.qclass, self._class.rclass, self.qexam, verbose=True) if not status: raise AdmRawError(response['message']) self.wclass = False self._class = None @classmethod def check(cls, wclass, exam): """Returns True if exam is in wclass, False otherwise. exam can be either an instance of Exam, or an int corresponding to the identifier (qexam) of the Exam in the WIMS class. E.G. either Exam.check(wclass, 1) or Exam.check(wclass, Exam(...))""" if not wclass._saved: raise NotSavedError("Class must be saved before being able to check whether a exam " "exists") exam = exam.qexam if isinstance(exam, cls) else exam status, response = wclass._api.checkexam(wclass.qclass, wclass.rclass, exam, verbose=True) msg = ('element #%s of type exam does not exist in this class (%s)' % (str(exam), str(wclass.qclass))) if not status and msg not in response['message']: # pragma: no cover raise AdmRawError(response['message']) return status @classmethod def remove(cls, wclass, exam): """Remove the exam from wclass. exam can be either an instance of Exam, or an int corresponding to the identifier (qexam) of the Exam in the WIMS class. E.G. either Exam.remove(wclass, 1) or Exam.remove(wclass, Exam(...))""" if not wclass._saved: raise NotSavedError("Class must be saved before being able to remove a exam") qexam = exam.qexam if isinstance(exam, cls) else exam status, response = wclass._api.delexam(wclass.qclass, wclass.rclass, qexam, verbose=True) if not status: raise AdmRawError(response['message']) @classmethod def get(cls, wclass, qexam): """Returns an instance of Exam corresponding to qexam in wclass.""" if not wclass._saved: raise NotSavedError("Class must be saved before being able to get a exam") status, exam_info = wclass._api.getexam(wclass.qclass, wclass.rclass, qexam, verbose=True) if not status: raise AdmRawError(exam_info['message']) duplicate = dict(exam_info) for k, v in duplicate.items(): if k.startswith("exam_"): exam_info[k[5:]] = v exam_info["exammode"] = exam_info["exam_status"] exam = Exam(**exam_info) exam.qexam = exam_info["query_exam"] exam._class = wclass exam.wclass = True return exam @classmethod def list(cls, wclass): """Returns a list of every Exam of wclass.""" status, response = wclass._api.listexams(wclass.qclass, wclass.rclass, verbose=True) if not status: raise AdmRawError(response['message']) return [cls.get(wclass, qexam) for qexam in response["examlist"] if qexam != ''] def scores(self, user=None): """Returns a list of ExamScore for every user. If user is given returns only its SheetScore. user can either be an instance of wimsapi.User or its quser.""" if not self.wclass: raise NotSavedError("Sheet must be saved before being able to retrieve scores") quser = user.quser if isinstance(user, User) else user if quser is not None and not self._class.checkitem(quser, User): # Checks that quser exists raise ValueError("User '%s' does not exists in class '%s'" % (user, self._class.qclass)) status, response = self._class._api.getexamscores(self._class.qclass, self._class.rclass, self.qexam, verbose=True) if not status: raise AdmRawError(response['message']) scores = [] for data in response["data_scores"]: # Ignore score of other users if quser is given if quser is not None and data['id'] != quser: continue user = self._class.getitem(data['id'], User) scores.append(ExamScore(self, user, data["score"], data["attempts"])) return scores[0] if quser is not None else scores wimsapi-0.5.11/wimsapi/exceptions.py000066400000000000000000000021631407533276500174710ustar00rootroot00000000000000class WimsAPIError(Exception): """Base exception for WimsAPI.""" pass class InvalidResponseError(WimsAPIError): # pragma: no cover """Raised when WIMS send a badly formatted response.""" def __init__(self, message, response): self.message = message self.response = response def __str__(self): return self.message class AdmRawError(WimsAPIError): """Raised when an error occurs while communicating with the WIMS server.""" def __init__(self, message): self.message = message def __str__(self): return "WIMS' server responded with an ERROR: " + self.message class NotSavedError(WimsAPIError): """Raised trying to use a method needing an object to be saved, without the object being actually saved (eg. deleting an unsaved class).""" pass class InvalidItemTypeError(WimsAPIError): """Raised when trying to add/get/delete an invalide type from a WIMS class.""" pass class InvalidIdentifier(WimsAPIError): """Raised when an identifier containing invalid character is sent to WIMS.""" pass wimsapi-0.5.11/wimsapi/item.py000066400000000000000000000034331407533276500162470ustar00rootroot00000000000000from abc import ABC, abstractmethod class ClassItemABC(ABC): # pragma: no cover """Allow to implement any kind of item of a WIMS class without the need of actually modifying wimsapi.class.Class.""" @abstractmethod def refresh(self): """Refresh this item from wclass.""" pass @classmethod @abstractmethod def check(cls, wclass, item): """Returns True if item is in wclass, False otherwise. Item can be either an instance of the corresponding item, or string corresponding to the identifier of the item in wclass. E.G. either SubClass.check(wclass, "identifier") or SubClass.check(wclass, SubClass(...))""" pass @classmethod @abstractmethod def remove(cls, wclass, item): """Deletes item from wclass. Item can be either an instance of the corresponding item, or string corresponding to the identifier of the item in wclass. E.G. either SubClass.remove(wclass, "identifier") or SubClass.remove(wclass, SubClass(...))""" pass @classmethod @abstractmethod def get(cls, wclass, identifier): """Returns an instance of cls corresponding to the item identified with identifier in wclass.""" pass @abstractmethod def save(self, wclass, check_exists=True): """Saves this item in wclass. If check_exists is True, the api will check if an item with the same ID exists on the WIMS' server. If it exists, save will instead modify this item instead of trying to create new one.""" pass @classmethod @abstractmethod def list(cls, wclass): """List every item from wclass.""" pass wimsapi-0.5.11/wimsapi/score.py000066400000000000000000000133571407533276500164320ustar00rootroot00000000000000class ExerciseScore: """Used to store every kind of score of a WIMS Exercise received from ADM/RAW. The following table give the correspondense between WIMS, ADM/RAW's getsheetscores job, and wimsapi value: +-----------------+----------------+----------+ | Exercise | ADM/RAW | WIMSAPI | +-----------------+----------------+----------+ | Points required | requires | required | | Weigth | exo_weights | weight | | Quality | mean_detail | quality | | Cumul | got_detail | cumul | | Best scores | best_detail | best | | Acquired | level_detail | acquired | | Last Result | last_detail | last | | Number of tries | try_detail | tries | +-----------------+----------------+----------+ Parameters: exo - (Exercise) Exercise corresponding to these scores. user - (User) User corresponding to these scores. quality - (float) Quality score ([0, 10]) as given by WIMS. cumul - (float) Cumul score ([0, 100]) as given by WIMS. best - (float) Level of success ([0, 100]) as given by WIMS. acquired - (float) Acquisition score ([0, required]) as given by WIMS. last - (float) Last score obtained ([0, required]) as given by WIMS. weight - (int) Weight of the sheet in the Class' score. tries - (int) Number of try as given by WIMS.""" def __init__(self, exo, user, quality, cumul, best, acquired, last, required, weight, tries, **kwargs): self.exo = exo self.user = user self.quality = quality self.cumul = cumul self.best = best self.acquired = acquired self.last = last self.weight = weight self.tries = tries self.required = required def __eq__(self, other): if isinstance(other, ExerciseScore): a = dict(self.__dict__) b = dict(other.__dict__) a["exo"] = a["exo"].qexo if a["exo"] is not None else None b["exo"] = b["exo"].qexo if b["exo"] is not None else None a["user"] = a["user"].quser if a["user"] is not None else None b["user"] = b["user"].quser if b["user"] is not None else None return a == b return False class SheetScore: """Used to store every kind of score of a WIMS Sheet received from ADM/RAW. The following table give the correspondense between WIMS, ADM/RAW's getsheetscores job, and wimsapi value: +-------------+--------------+----------+ | WIMS | ADM/RAW | WIMSAPI | +-------------+--------------+----------+ | Score | - | score | | Quality | user_quality | quality | | Cumul | user_percent | cumul | | Best scores | user_best | best | | Acquired | user_level | acquired | | Weigth | sheet_weight | weight | +-------------+--------------+----------+ Calculated through ADM/RAW's "sheet_formula". Parameters: sheet - (Sheet) Sheet corresponding to these scores. user - (User) User corresponding to these scores. score - (float) Global score ([0, 10]) as given by WIMS. quality - (float) Quality score ([0, 10]) as given by WIMS. cumul - (float) Cumul score ([0, 100]) as given by WIMS. best - (float) Level of success ([0, 100]) as given by WIMS. acquired - (float) Acquisition score ([0, 10]) as given by WIMS. weight - (int) Weight of the sheet in the Class' score. exercises - (List[ExerciseScore]) List of the scores obtained for each exercises.""" def __init__(self, sheet, user, score, quality, cumul, best, acquired, weight, exercises, **kwargs): self.sheet = sheet self.user = user self.score = score self.quality = quality self.cumul = cumul self.best = best self.acquired = acquired self.weight = weight self.exercises = exercises def __eq__(self, other): if isinstance(other, SheetScore): a = dict(self.__dict__) b = dict(other.__dict__) a["sheet"] = a["sheet"].qsheet if a["sheet"] is not None else None b["sheet"] = b["sheet"].qsheet if b["sheet"] is not None else None a["user"] = a["user"].quser if a["user"] is not None else None b["user"] = b["user"].quser if b["user"] is not None else None return a == b return False class ExamScore: """Used to store the score of a WIMS Exam as received from ADM/RAW. Parameters: exam - (Exam) Exam corresponding to these scores. user - (User) User corresponding to these scores. score - (float) Global score ([0, 10]) as given by WIMS. attempts - (int) Number of attempts at this exam.""" def __init__(self, exam, user, score, attempts): self.exam = exam self.user = user self.score = score self.attempts = attempts def __eq__(self, other): if isinstance(other, ExamScore): a = dict(self.__dict__) b = dict(other.__dict__) a["exam"] = a["exam"].qexam if a["exam"] is not None else None b["exam"] = b["exam"].qexam if b["exam"] is not None else None a["user"] = a["user"].quser if a["user"] is not None else None b["user"] = b["user"].quser if b["user"] is not None else None return a == b return False wimsapi-0.5.11/wimsapi/sheet.py000066400000000000000000000324101407533276500164160ustar00rootroot00000000000000import datetime import sys from .exceptions import AdmRawError, NotSavedError from .item import ClassItemABC from .score import ExerciseScore, SheetScore from .user import User from .utils import default, one_year_later class Sheet(ClassItemABC): """This class is used to represent a WIMS' Worksheet. Parameters: title - (str) name of the sheet (defaults to "sheet n#") description - (str) description of the sheet (defaults to "sheet n#") expiration - (str) expiration date (yyyymmdd) defaults to one year later sheetmode - (str) the mode of the sheet: 0 : pending (default) 1 : active 2 : expired 3 : expired + hidden weight - (int) weight of the sheet in the class score (default to 1), use 0 if you want this sheet's score to be ignored. formula - (str) How the score is calculated for this sheet (0 to 6, default to 2) 0 : Very lenient: maximum between percentage and quality. 1 : Quality is not taken into account. You get maximum of grade once all the required work is done. 2 : Quality has only slight effect over the grade. 3 : More effect of quality. 4 : To have a grade of 10, you must gather all required points (100%) without making any error (quality=10). 5 : Unfinished work is over-punished. 6 : Any mistake is over-punished. indicator - (str) what indicator will be used in the score formula (0 to 2, default to 1) contents - (str) the contents for the multi-line file to be created. The semicolons (;) in this parameter will be interpreted as new lines. Equal characters (=) must be replaced by the character AT (@). There is no check made, so the integrity of the contents is up to you only! (defaults to "")""" def __init__(self, title=None, description=None, expiration=None, sheetmode=0, weight=1, formula=2, indicator=1, contents="", **kwargs): if expiration is not None: datetime.datetime.strptime(expiration, "%Y%m%d") super().__init__() self._class = None self._saved = False self.wclass = False self.qsheet = sys.maxsize self.title = title self.description = description self.expiration = expiration if expiration is not None else one_year_later() self.sheetmode = sheetmode self.weight = weight self.formula = formula self.indicator = indicator self.contents = contents self.exos = [] @property def exo_count(self): """Returns the number of exercises in this sheet.""" return len(self.exos) @property def infos(self): """Return all the informations hosted on the WIMS server about this sheet.""" if not self._class: raise NotSavedError("infos is not defined until the sheet has been saved once") status, sheet_info = self._class._api.getsheet( self._class.qclass, self._class.rclass, self.qsheet, verbose=True) if not status: raise AdmRawError(sheet_info['message']) for k in ['status', 'code', 'job']: del sheet_info[k] return sheet_info def __str__(self): return "" % (hex(id(self)), str(self.qsheet)) __repr__ = __str__ def __eq__(self, other): """Sheets have to come from the same server and have the same qsheet to be equal.""" if isinstance(other, self.__class__): if not self.wclass or not other.wclass: raise NotSavedError("Cannot test equality between unsaved sheets") return str(self.qsheet) == str(other.qsheet) and self._class == other._class return False def __hash__(self): if not self.wclass: raise NotSavedError("Unsaved User cannot be hashed") return hash((self._class.qclass, self.qsheet)) def refresh(self): """Refresh this instance of a WIMS Sheet from the server itself.""" if not self.wclass: raise NotSavedError("Can't refresh unsaved sheet") new = Sheet.get(self._class, self.qsheet) self.__class__ = new.__class__ self.__dict__ = new.__dict__ return self def _to_payload(self): return {k: v for k, v in self.__dict__.items() if k not in ['qsheet', '_saved', '_class']} def save(self, wclass=None, check_exists=True): """Save the Sheet in the given class. wclass is an instance of wimsapi.Class. The argument is optionnal if the sheet has already been saved or fetched from a Class, it will default to the last Class used to saved or the Class from which the sheet was fetched. If check_exists is True, the api will check if a sheet with the same ID exists on the WIMS' server. If it exists, save will instead modify this sheet instead of trying to create new one.""" if not wclass and not self._class: raise NotSavedError("wclass must be provided if this sheet has neither been imported " "from a WIMS class nor saved once yet") wclass = wclass or self._class if not wclass._saved: raise NotSavedError("Class must be saved before being able to save a sheet") if wclass is not None and check_exists: self._saved = wclass.checkitem(self) if self._saved: status, response = wclass._api.modsheet( wclass.qclass, wclass.rclass, self.qsheet, self._to_payload(), verbose=True) else: status, response = wclass._api.addsheet( wclass.qclass, wclass.rclass, self._to_payload(), verbose=True) if not status: raise AdmRawError(response['message']) self.qsheet = response['sheet_id'] if "sheet_id" in response else response["querysheet"] self._class = wclass self.wclass = True def delete(self): """Delete the sheet from its associated WIMS class on the server.""" if not self.wclass: raise NotSavedError("Cannot delete an unsaved sheet") status, response = self._class._api.delsheet(self._class.qclass, self._class.rclass, self.qsheet, verbose=True) if not status: raise AdmRawError(response['message']) self.wclass = False self._class = None @classmethod def check(cls, wclass, sheet): """Returns True if sheet is in wclass, False otherwise. sheet can be either an instance of Sheet, or an int corresponding to the identifier (qsheet) of the Sheet in the WIMS class. E.G. either Sheet.check(wclass, 1) or Sheet.check(wclass, Sheet(...))""" if not wclass._saved: raise NotSavedError("Class must be saved before being able to check whether a sheet " "exists") sheet = sheet.qsheet if isinstance(sheet, cls) else sheet status, response = wclass._api.checksheet(wclass.qclass, wclass.rclass, sheet, verbose=True) msg = ('element #%s of type sheet does not exist in this class (%s)' % (str(sheet), str(wclass.qclass))) if not status and msg not in response['message']: # pragma: no cover raise AdmRawError(response['message']) return status @classmethod def remove(cls, wclass, sheet): """Remove the sheet from wclass. sheet can be either an instance of Sheet, or an int corresponding to the identifier (qsheet) of the Sheet in the WIMS class. E.G. either Sheet.remove(wclass, 1) or Sheet.remove(wclass, Sheet(...))""" if not wclass._saved: raise NotSavedError("Class must be saved before being able to remove a sheet") qsheet = sheet.qsheet if isinstance(sheet, cls) else sheet status, response = wclass._api.delsheet(wclass.qclass, wclass.rclass, qsheet, verbose=True) if not status: raise AdmRawError(response['message']) @classmethod def get(cls, wclass, qsheet): """Returns an instance of Sheet corresponding to qsheet in wclass.""" if not wclass._saved: raise NotSavedError("Class must be saved before being able to get a sheet") status, sheet_info = wclass._api.getsheet(wclass.qclass, wclass.rclass, qsheet, verbose=True) if not status: raise AdmRawError(sheet_info['message']) duplicate = dict(sheet_info) for k, v in duplicate.items(): if k.startswith("sheet_"): sheet_info[k[6:]] = v sheet_info["sheetmode"] = sheet_info["sheet_status"] sheet = Sheet(**sheet_info) sheet.qsheet = sheet_info["query_sheet"] sheet._class = wclass sheet.wclass = True return sheet @classmethod def list(cls, wclass): """Returns a list of every Sheet of wclass.""" status, response = wclass._api.listsheets(wclass.qclass, wclass.rclass, verbose=True) if not status: raise AdmRawError(response['message']) return [cls.get(wclass, qsheet) for qsheet in response["sheetlist"] if qsheet != ''] @staticmethod def _compute_grade(formula, i, Q, cumul, best, acquired): """Compute the grade of a sheet according to the formula and the chosen I. Formula contains both Q and I.""" i = int(i) if i == 0: I = cumul elif i == 1: I = best else: I = acquired Q /= 10 I /= 100 formula = "10 * (%s)" % formula return round(eval(formula.replace("^", "**")), 2) def scores(self, user=None): """Returns a list of SheetScore for every user. If user is given returns only its SheetScore. user can either be an instance of wimsapi.User or its quser. A value of -1 on some members mean that WIMS did not send the value. This may be caused by an outdated WIMS server.""" if not self.wclass: raise NotSavedError("Sheet must be saved before being able to retrieve scores") quser = user.quser if isinstance(user, User) else user if quser is not None and not self._class.checkitem(quser, User): # Checks that quser exists raise ValueError("User '%s' does not exists in class '%s'" % (user, self._class.qclass)) status, response = self._class._api.getsheetscores(self._class.qclass, self._class.rclass, self.qsheet, verbose=True) if not status: raise AdmRawError(response['message']) scores = [] exo_count = len(response["exo_weights" if "exo_weights" in response else "weights"]) for data in response["data_scores"]: if quser is not None and data['id'] != quser: # Searching for specific user score continue user = self._class.getitem(data['id'], User) try: score = self._compute_grade(response["sheet_formula"]["formula"], response["sheet_formula"]["I"], data["user_quality"], data["user_percent"], data["user_best"], data["user_level"]) except Exception: # pragma: no cover score = -1 sheet_params = { "sheet": self, "user": user, "score": score, "quality": data.get("user_quality", -1), "cumul": data.get("user_percent", -1), "best": data.get("user_best", -1), "acquired": data.get("user_level", -1), "weight": self.weight, "exercises": [], } for i in range(exo_count): exo_params = { "exo": None, "user": user, "quality": default(data, "mean_detail", i, -1), "cumul": default(data, "got_detail", i, -1), "best": default(data, "best_detail", i, -1), "acquired": default(data, "level_detail", i, -1), "last": default(data, "last_detail", i, -1), "tries": default(data, "try_detail", i, -1), "weight": default(response, "exo_weights", i, -1), "required": default(response, "requires", i, -1), } sheet_params["exercises"].append(ExerciseScore(**exo_params)) scores.append(SheetScore(**sheet_params)) return scores[0] if quser is not None else scores wimsapi-0.5.11/wimsapi/user.py000066400000000000000000000223321407533276500162660ustar00rootroot00000000000000from .exceptions import AdmRawError, InvalidIdentifier, NotSavedError from .item import ClassItemABC class User(ClassItemABC): """This class is used to represent a WIMS' user. Parameters: quser - (str) user identifier on the receiving server. lastname - (str) last name of the user. firstname - (str) first name of the user. password - (str) user's password, non-crypted. email - (str) email address. comments - (str) any comments. regnum - (str) registration number. photourl - (str) url of user's photo. participate - (str) list classes where user participates. courses - (str) special for portal. classes - (str) special for portal. supervise - (str) List classes where teacher are administator. supervisable - (str) yes/no ; give right to the user to supervise a class (default to 'no'). external_auth - (str) login for external_auth. agreecgu - (str) yes/ no ; if yes, the user will not be asked when he enters for the first time to agree the cgu (default to "yes"). regprop[1..5] - (str) custom variables.""" def __init__(self, quser, lastname, firstname, password, email="", comments="", regnum="", photourl="", participate="", courses="", classes="", supervise="", supervisable="no", external_auth="", agreecgu="yes", regprop1="", regprop2="", regprop3="", regprop4="", regprop5="", **kwargs): super().__init__() self._class = None self._saved = False self.wclass = False self.quser = quser self.lastname = lastname self.firstname = firstname self.password = password self.email = email self.comments = comments self.regnum = regnum self.photourl = photourl self.participate = participate self.courses = courses self.classes = classes self.supervise = supervise self.supervisable = supervisable self.external_auth = external_auth self.agreecgu = agreecgu self.regprop1 = regprop1 self.regprop2 = regprop2 self.regprop3 = regprop3 self.regprop4 = regprop4 self.regprop5 = regprop5 @property def fullname(self): """Return the titled firstname and lastname of this User""" return (self.firstname + " " + self.lastname).title() @property def infos(self): """Return all the informations hosted on the WIMS server about this user.""" if not self._class: raise NotSavedError("infos is not defined until the user has been saved once") status, user_info = self._class._api.getuser( self._class.qclass, self._class.rclass, self.quser, verbose=True) if not status: raise AdmRawError(user_info['message']) for k in ['status', 'code', 'job']: del user_info[k] return user_info def __str__(self): return "" % (hex(id(self)), str(self.quser)) __repr__ = __str__ def __eq__(self, other): """Users have to come from the same class and have the same quser to be equal.""" if isinstance(other, self.__class__): if not self.wclass or not other.wclass: raise NotSavedError("Cannot test equality between unsaved users") return self.quser == other.quser and self._class == other._class return False def __hash__(self): if not self.wclass: raise NotSavedError("Unsaved User cannot be hashed") return hash((self._class.qclass, self.quser)) def refresh(self): """Refresh this instance of a WIMS User from the server itself.""" if not self.wclass: raise NotSavedError("Can't refresh unsaved user") new = User.get(self._class, self.quser) self.__class__ = new.__class__ self.__dict__ = new.__dict__ return self def _to_payload(self): return {k: v for k, v in self.__dict__.items() if k not in ['quser', '_class', '_saved']} def save(self, wclass=None, check_exists=True, adapt=True): """Save the User in the given class. wclass is an instance of wimsapi.Class. The argument is optionnal if the user has already been saved or fetched from a Class, it will default to the last Class used to saved or the Class from which the user was fetched. If check_exists is True, the api will check if an user with the same ID exists on the WIMS' server. If it exists, save will instead modify this user instead of trying to create new one.""" if not wclass and not self._class: raise NotSavedError("wclass must be provided if this user has neither been imported " "from a WIMS class nor saved once yet") wclass = wclass or self._class if not wclass._saved: raise NotSavedError("Class must be saved before being able to save an user") if wclass is not None and check_exists: self._saved = wclass.checkitem(self) if self._saved: status, response = wclass._api.moduser( wclass.qclass, wclass.rclass, self.quser, self._to_payload(), verbose=True) else: status, response = wclass._api.adduser( wclass.qclass, wclass.rclass, self.quser, self._to_payload(), verbose=True) if status and response["user_id"] != self.quser: if adapt: self.quser = response["user_id"] else: # Removing the user with the invalid quser User.remove(wclass, response["user_id"]) raise InvalidIdentifier("quser '%s' contains invalid character" % self.quser) if not status: raise AdmRawError(response['message']) self._class = wclass self.wclass = True def delete(self): """Delete the user from its associated WIMS class on the server.""" if not self.wclass: raise NotSavedError("Cannot delete an unsaved user") status, response = self._class._api.deluser( self._class.qclass, self._class.rclass, self.quser, verbose=True) if not status: raise AdmRawError(response['message']) self.wclass = False self._class = None @classmethod def check(cls, wclass, user): """Returns True if item is in wclass, False otherwise. user can be either an instance of User, or string corresponding to the identifier (quser) of the User in the WIMS class. E.G. either User.check(wclass, "quser") or User.check(wclass, User(...))""" if not wclass._saved: raise NotSavedError("Class must be saved before being able to check whether an user " "exists") quser = user.quser if isinstance(user, cls) else user status, response = wclass._api.checkuser(wclass.qclass, wclass.rclass, quser, verbose=True) msg = 'user %s not in this class (%s)' % (str(quser), str(wclass.qclass)) if not status and msg not in response['message']: # pragma: no cover raise AdmRawError(response['message']) return status @classmethod def remove(cls, wclass, user): """Remove the user from wclass. user can be either an instance of User, or a string corresponding to the identifier (quser) of the User in the WIMS class. E.G. either User.remove(wclass, "quser") or User.remove(wclass, User(...))""" if not wclass._saved: raise NotSavedError("Class must be saved before being able to remove an user") quser = user.quser if isinstance(user, cls) else user status, response = wclass._api.deluser(wclass.qclass, wclass.rclass, quser, verbose=True) if not status: raise AdmRawError(response['message']) @classmethod def get(cls, wclass, quser): """Returns an instance of User corresponding to quser in wclass.""" if not wclass._saved: raise NotSavedError("Class must be saved before being able to get an user") status, user_info = wclass._api.getuser(wclass.qclass, wclass.rclass, quser, verbose=True) if not status: raise AdmRawError(user_info['message']) status, user_password = wclass._api.getuser(wclass.qclass, wclass.rclass, quser, ["password"], verbose=True) if not status: raise AdmRawError(user_password['message']) user_info['password'] = user_password['password'] user = User(quser, **user_info) user._class = wclass user.wclass = True return user @classmethod def list(cls, wclass): """Returns a list of every User of wclass.""" return [cls.get(wclass, quser) for quser in wclass.infos["userlist"] if quser != ''] wimsapi-0.5.11/wimsapi/utils.py000066400000000000000000000006351407533276500164520ustar00rootroot00000000000000import datetime def one_year_later(): """Give the date one year later from now in the format yyyymmdd.""" d = datetime.date.today() return d.replace(year=d.year + 1).strftime("%Y%m%d") def default(d, k, i, default=None): """Returns d[k][i], defaults to default if KeyError or IndexError is raised.""" try: return d[k][i] except (KeyError, IndexError): return default wimsapi-0.5.11/wimsapi/wclass.py000066400000000000000000000367701407533276500166170ustar00rootroot00000000000000import datetime from .api import WimsAPI from .exceptions import AdmRawError, InvalidItemTypeError, NotSavedError from .item import ClassItemABC from .user import User from .utils import one_year_later LANG = [ 'aa', 'ab', 'af', 'ak', 'sq', 'am', 'ar', 'an', 'hy', 'as', 'av', 'ae', 'ay', 'az', 'ba', 'bm', 'eu', 'be', 'bn', 'bh', 'bi', 'bo', 'bs', 'br', 'bg', 'my', 'ca', 'cs', 'ch', 'ce', 'zh', 'cu', 'cv', 'kw', 'co', 'cr', 'cy', 'cs', 'da', 'de', 'dv', 'nl', 'dz', 'el', 'en', 'eo', 'et', 'eu', 'ee', 'fo', 'fa', 'fj', 'fi', 'fr', 'fr', 'fy', 'ff', 'Ga', 'de', 'gd', 'ga', 'gl', 'gv', 'el', 'gn', 'gu', 'ht', 'ha', 'he', 'hz', 'hi', 'ho', 'hr', 'hu', 'hy', 'ig', 'is', 'io', 'ii', 'iu', 'ie', 'ia', 'id', 'ik', 'is', 'it', 'jv', 'ja', 'kl', 'kn', 'ks', 'ka', 'kr', 'kk', 'km', 'ki', 'rw', 'ky', 'kv', 'kg', 'ko', 'kj', 'ku', 'lo', 'la', 'lv', 'li', 'ln', 'lt', 'lb', 'lu', 'lg', 'mk', 'mh', 'ml', 'mi', 'mr', 'ms', 'Mi', 'mk', 'mg', 'mt', 'mn', 'mi', 'ms', 'my', 'na', 'nv', 'nr', 'nd', 'ng', 'ne', 'nl', 'nn', 'nb', 'no', 'oc', 'oj', 'or', 'om', 'os', 'pa', 'fa', 'pi', 'pl', 'pt', 'ps', 'qu', 'rm', 'ro', 'ro', 'rn', 'ru', 'sg', 'sa', 'si', 'sk', 'sk', 'sl', 'se', 'sm', 'sn', 'sd', 'so', 'st', 'es', 'sq', 'sc', 'sr', 'ss', 'su', 'sw', 'sv', 'ty', 'ta', 'tt', 'te', 'tg', 'tl', 'th', 'bo', 'ti', 'to', 'tn', 'ts', 'tk', 'tr', 'tw', 'ug', 'uk', 'ur', 'uz', 've', 'vi', 'vo', 'cy', 'wa', 'wo', 'xh', 'yi', 'yo', 'za', 'zh', 'zu', ] LEVEL = [ "K1", "K2", "K3", # Kindergarten "E1", "E2", "E3", "E4", "E5", "E6", # Elementary school "H1", "H2", "H3", "H4", "H5", "H6", # High school "U1", "U2", "U3", "U4", "U5", # University "G", "R", # Graduate, Researcher ] class Class: """This class is used to represent a WIMS' class. If provided, qclass will be the identifier of the newly created WIMS class when this instance is saved. The identifier is randomly chosen if qclass is not provided. Parameters: rclass - (str) identifier of the class on the sending server. name - (str) name of the class. institution - (str) name of the institution. email - (str) contact email address. password - (str) password for user registration. qclass - (int) identifier of the class on the receiving server. supervisor - (wimsapi.user.User) An WIMS user instance representing the supervisor. lang - (str) class language (en, fr, es, it, etc). expiration - (str) class expiration date (yyyymmdd, defaults to one year later). limit - (str) limit of number of participants (defaults to 30). level - (str) level of the class (defaults to H4) Valid levels: E1, E2, ..., E6, H1, ..., H6, U1, ..., U5, G, R secure - (str) secure hosts. bgcolor - (str) page background color. refcolor - (str) menu background color. css - (str) css file (must be existing css on the WIMS server).""" def __init__(self, rclass, name, institution, email, password, supervisor, qclass=None, lang="en", expiration=None, limit=30, level="H4", secure='all', bgcolor='', refcolor='', css='', cloningpwd='', **kwargs): if lang not in LANG: raise ValueError("lang must be one of wimsapi.class.LANG") if level not in LEVEL: raise ValueError("level must be in wimsapi.class.LEVEL") if expiration is not None: datetime.datetime.strptime(expiration, "%Y%m%d") self._api = None self._saved = False self.qclass = qclass self.rclass = rclass self.name = name self.institution = institution self.email = email self.password = password self.supervisor = supervisor self.lang = lang self.expiration = expiration if expiration is not None else one_year_later() self.limit = int(limit) self.level = level self.secure = secure self.bgcolor = bgcolor self.refcolor = refcolor self.css = css self.cloningpwd = cloningpwd def __str__(self): return "" % (hex(id(self)), str(self.qclass)) __repr__ = __str__ def __contains__(self, item): """check if an item is in the WIMS class. Item must be a subclass of wimsapi.item.ClassItemABC.""" if issubclass(type(item), ClassItemABC): if not self._saved: raise NotSavedError("Cannot use 'in' operator with an unsaved class.") return self.checkitem(item) return NotImplemented # pragma: no cover def __eq__(self, other): """Classes have to come from the same server and have the same qclass to be equal.""" if isinstance(other, self.__class__): if not self._api or not other._api: raise NotSavedError("Cannot test equality between unsaved classes") return str(self.qclass) == str(other.qclass) and self.url == other.url return False def __hash__(self): if not self._api: raise NotSavedError("Unsaved classes cannot be hashed") return hash((self.qclass, self.url)) def _to_payload(self): """Return a dictionnary representing this class as defined in adm/raw.""" d = {k: v for k, v in self.__dict__.items() if k not in ['qclass', 'rclass', 'supervisor', '_api', 'wclass']} d['description'] = d['name'] d['supervisor'] = self.supervisor.fullname del d['name'] return d @property def url(self): """Return the url of the server hosting this WIMS class. Raise ValueError if the class has not been saved yet.""" if not self._api: raise NotSavedError("url is not defined until the WIMS class is saved once") return self._api.url @property def ident(self): """Return the ident used on the server hosting this WIMS class. Raise ValueError if the class has not been saved yet.""" if not self._api: raise NotSavedError("ident is not defined until the WIMS class is saved once") return self._api.ident @property def passwd(self): """Return the passwd used on the server hosting this WIMS class. Raise ValueError if the class has not been saved yet.""" if not self._api: raise NotSavedError("passwd is not defined until the WIMS class is saved once") return self._api.passwd @property def infos(self): """Return all the informations hosted on the WIMS server about this class.""" if not self._api: raise NotSavedError("infos is not defined until the WIMS class is saved once") status, class_info = self._api.getclass(self.qclass, self.rclass, verbose=True) if not status: raise AdmRawError(class_info['message']) for k in ['status', 'code', 'job']: del class_info[k] return class_info @classmethod def check(cls, url, ident, passwd, qclass, rclass, **kwargs): """Returns True if the class exists and allows connection with ident and rclass, False otherwise.""" w = WimsAPI(url, ident, passwd, **kwargs) status, response = w.checkclass(qclass, rclass, verbose=True) msg1 = 'class %s not existing' % str(qclass) msg2 = 'connection refused by requested class (%s)' % str(qclass) if not status and response['message'] not in [msg1, msg2]: # pragma: no cover raise AdmRawError(response['message']) return status def save(self, url=None, ident=None, passwd=None, **kwargs): """Save this the modification done on this class on the WIMS server. If the class has not been obtain through the get() method and has never been saved yet, arguments url, ident and passwd must be provided. Use the method refresh() on any other instance representing this class to reflect the change saved.""" if url and ident and passwd: self._api = WimsAPI(url, ident, passwd, **kwargs) if not self._api: raise NotSavedError("url, ident and passwd must be provided when saving for the first " "time.") payload = self._to_payload() if self._saved: status, response = self._api.modclass(self.qclass, self.rclass, payload, verbose=True) if not status: raise AdmRawError(response['message']) else: status, response = self._api.addclass( self.rclass, payload, self.supervisor._to_payload(), self.qclass, verbose=True ) if not status: raise AdmRawError(response['message']) self._saved = True self.qclass = response['class_id'] self.supervisor.quser = "supervisor" self.supervisor._saved = True self.supervisor._class = self def delete(self): """Delete the class from the WIMS server.""" if not self._saved: raise NotSavedError("Can't delete unsaved class") status, response = self._api.delclass(self.qclass, self.rclass, verbose=True) if not status: raise AdmRawError(response['message']) self._saved = False self._api = None def refresh(self): """Refresh this instance of a WIMS class from the server itself.""" if not self._saved: raise NotSavedError("Can't refresh unsaved class") new = Class.get(self.url, self.ident, self.passwd, self.qclass, self.rclass) self.__class__ = new.__class__ self.__dict__ = new.__dict__ return self @classmethod def get(cls, url, ident, passwd, qclass, rclass, **kwargs): """Return an instance of a WIMS class corresponding to the class 'qclass' on the WIMS server pointed by 'url'.""" api = WimsAPI(url, ident, passwd, **kwargs) status, class_info = api.getclass(qclass, rclass, verbose=True) if not status: raise AdmRawError(class_info['message']) status, class_password = api.getclass(qclass, rclass, verbose=True) if not status: raise AdmRawError(class_password['message']) status, supervisor_info = api.getuser(qclass, rclass, "supervisor", verbose=True) if not status: raise AdmRawError(supervisor_info['message']) status, password_info = api.getuser(qclass, rclass, "supervisor", ["password"], verbose=True) if not status: raise AdmRawError(password_info['message']) supervisor_info['password'] = password_info['password'] supervisor = User("supervisor", **supervisor_info) class_info['supervisor'] = supervisor class_info['name'] = class_info['description'] class_info['password'] = class_password['password'] class_info['qclass'] = qclass c = cls(**class_info) c._api = api c._saved = True c.supervisor._saved = True c.supervisor._class = c return c @classmethod def list(cls, url, ident, passwd, rclass, **kwargs): """Return all the instances of Class of the given WIMS server (url) using ident and rclass.""" api = WimsAPI(url, ident, passwd, **kwargs) status, response = api.listclasses(rclass, verbose=True) if not status: if "there is no class allowed for this server" in response['message']: return [] raise AdmRawError(response['message']) # pragma: no cover qclasses = [c['qclass'] for c in response["classes_list"]] return [cls.get(url, ident, passwd, qclass, rclass) for qclass in qclasses] def additem(self, item): """Add an item to the WIMS class. Item must be a subclass of wimsapi.item.ClassItemABC.""" if not issubclass(type(item), ClassItemABC): raise InvalidItemTypeError( "Item of type %s cannot be deleted from the WIMS class." % str(type(item))) if not self._saved: raise NotSavedError("Class must be saved before being able to add an item") item.save(self, check_exists=False) def delitem(self, item, cls=None): """Remove an item from the WIMS class. Item must be either a subclass of wimsapi.item.ClassItemABC or a string. If item is a string, cls must be provided and be a subclass of ClassItemABC which correspond to the item. E.G. for an user : delitem(User(...)) or delitem("quser", User).""" test = ((not isinstance(item, str) and not issubclass(type(item), ClassItemABC)) or (cls is not None and not issubclass(cls, ClassItemABC))) if test: raise InvalidItemTypeError( "Item of type %s cannot be deleted from the WIMS class" % str(type(item) if not isinstance(item, str) else cls)) if not self._saved: raise NotSavedError("Class must be saved before being able to remove an item") cls = cls or type(item) cls.remove(self, item) def checkitem(self, item, cls=None): """check if an item is in the WIMS class. Item must be either a subclass of wimsapi.item.ClassItemABC or a string. If item is a string, cls must be provided and be a subclass of ClassItemABC which correspond to the item. E.G. for an user : checkitem(User(...)) or checkitem("quser", User).""" test = ((not isinstance(item, str) and not issubclass(type(item), ClassItemABC)) or (cls is not None and not issubclass(cls, ClassItemABC))) if test: raise InvalidItemTypeError( "Cannot check if an item of type %s is in a WIMS class" % str(type(item) if not isinstance(item, str) else cls)) if not self._saved: raise NotSavedError("Class must be saved before being able to check whether an item " "exists") cls = cls or type(item) return cls.check(self, item) def getitem(self, identifier, cls): """Return the instance of cls corresponding to identifier in the WIMS class. cls must be a subclass of ClassItemABC which correspond to the item identified by identier.""" if not issubclass(cls, ClassItemABC): raise InvalidItemTypeError("Cannot get element of type %s from a WIMS class" % str(cls)) if not self._saved: raise NotSavedError("Class must be saved before being able to get an item") return cls.get(self, identifier) def listitem(self, cls): """Return all the instances of cls in, this WIMS class. cls must be a subclass of ClassItemABC""" if not issubclass(cls, ClassItemABC): raise InvalidItemTypeError( "Cannot list element of type %s from a WIMS class" % str(cls)) if not self._saved: raise NotSavedError("Class must be saved before being able to list items") return cls.list(self)