├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples ├── README.md ├── __init__.py ├── copy_paste_course.py ├── create_content.py ├── data.php ├── download_random_leader_avatar.py ├── export_course.py ├── external-reports │ ├── README.md │ ├── cache │ │ └── test │ ├── dropout_report.py │ ├── item_report.py │ ├── latex │ │ ├── default-dropout │ │ │ ├── common │ │ │ │ └── introduction.tex │ │ │ ├── course-dropout-report.tex │ │ │ ├── cover.eps │ │ │ ├── generated │ │ │ │ └── test │ │ │ ├── logo.eps │ │ │ ├── makefile.sh │ │ │ └── stepik.sty │ │ ├── default-item │ │ │ ├── common │ │ │ │ ├── introduction.tex │ │ │ │ ├── methodology.tex │ │ │ │ ├── multiplecorrect.png │ │ │ │ ├── multiplewrong.png │ │ │ │ ├── singlecorrect.png │ │ │ │ └── singlewrong.png │ │ │ ├── course-item-report.tex │ │ │ ├── cover.eps │ │ │ ├── generated │ │ │ │ └── test │ │ │ ├── logo.eps │ │ │ ├── makefile.sh │ │ │ └── stepik.sty │ │ ├── default-video │ │ │ ├── common │ │ │ │ ├── introduction.tex │ │ │ │ └── methodology.tex │ │ │ ├── course-video-report.tex │ │ │ ├── cover.eps │ │ │ ├── generated │ │ │ │ └── test │ │ │ ├── logo.eps │ │ │ ├── makefile.sh │ │ │ └── stepik.sty │ │ └── default │ │ │ └── test │ ├── library │ │ ├── api.py │ │ ├── api_keys.py │ │ ├── models.py │ │ ├── settings.py │ │ └── utils.py │ ├── pdf │ │ └── test │ └── video_report.py ├── get_active_courses.py ├── get_certificates_urls_example.py ├── get_countries_all_count.py ├── get_courses_authors.py ├── get_courses_by_params.py ├── get_enrolled_courses.py ├── get_info_all_courses_titles.py ├── get_leaders_social_profiles.py ├── get_learn_events.py ├── get_ten_users_with_highest_reputation.py ├── get_top_lessons_by_reactions.py ├── get_user_courses.py ├── get_user_name.py ├── google-scripts │ ├── featured_courses.gs │ └── stepik.gs ├── monitor.php ├── oauth_auth_example.py ├── plot_course_viewed_by.py ├── plot_lesson_stats.py ├── popular_courses.py ├── recommendations_top_example.py ├── save_course_slides.py ├── save_course_source.py ├── save_course_steps.py ├── top_lessons_to_html.py └── videos_downloader.py ├── requirements.txt └── tests └── test_oauth_auth_example.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | .idea 63 | .DS_Store 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | 5 | # command to install dependencies 6 | install: "pip3 install -r requirements.txt" 7 | # command to run tests 8 | script: py.test 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | :rocket: We'll be glad to merge your pull requests with more examples, docs and ideas! :rocket: 4 | 5 | # Examples ideas: 6 | 7 | 0. Download all your submissions for the course 8 | 0. ... 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2016 Stepik.Org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stepik.org API Readme 2 | 3 | Stepik.org has REST API in JSON format. API endpoints are listed on https://stepik.org/api/docs, and you can also make API call there (but this page is limited to `GET` requests). 4 | 5 | Stepik.org use the same API for its web front-end (JS app) and its iOS/Android applications. Therefore, almost all the platform features are supported in this API. 6 | 7 | All API examples are up to date and working if the build status is `passing`: [![Build Status](https://travis-ci.org/StepicOrg/Stepik-API.svg?branch=master)](https://travis-ci.org/StepicOrg/Stepik-API) 8 | 9 | ## Flat 10 | 11 | Stepik API schema is flat, i.e. there are no nested end-points. 12 | 13 | Every request returns a list of objects, even if only one object was requested. This list can contain 0 or more elements. 14 | 15 | For example: `https://stepik.org/api/courses/1` returns not a single course, but a list with single course. 16 | 17 | ## Pagination 18 | 19 | All responses to `GET` requests are paginated. They contain extra `meta` object with the information about pagination. It may look like this: 20 | ``` 21 | { 22 | meta: { 23 | page: 1, 24 | has_next: true, 25 | has_previous: false 26 | }, 27 | requested_objects: [...] 28 | } 29 | ``` 30 | 31 | If the next page exists, then it can be requested using get parameter `?page=...`. By default, if no parameter is given, it’s equal to 1. 32 | 33 | For example: `https://stepik.org/api/courses` is equal to `https://stepik.org/api/courses?page=1`. Next page: `https://stepik.org/api/courses?page=2` and so on. 34 | 35 | Usual page size is equal to 20 elements, but it can vary due to API endpoint, user’s permissions etc. We do not recommend to rely on a constant page size. 36 | 37 | ## Side-Loading 38 | 39 | Response may also contain multiple objects, related to the requested object. 40 | 41 | For example: for registered user, response from `https://stepik.org/api/courses` also includes user’s course `enrollments`. 42 | 43 | ## OAuth 2 44 | 45 | In order to call Stepik.org API as a registered user, you can use this user’s OAuth2 keys. 46 | You can get your keys by creating an application on https://stepik.org/oauth2/applications/ (while being logged in), and you can also set `redirect_uri`, `Client type` and `Authorization grant type` there. 47 | 48 | Authorization endpoint (Authorization code, Implicit grant; redirect_uri needed): `https://stepik.org/oauth2/authorize/.` 49 | 50 | Token endpoint (Authorization code, Password and Client credentials): `https://stepik.org/oauth2/token/`. 51 | 52 | #### Client credentials flow 53 | 54 | You can then obtain access token using the following client credential flow: 55 | 56 | `curl -X POST -d "grant_type=client_credentials" -u"CLIENT_ID:CLIENT_SECRET" https://stepik.org/oauth2/token/` 57 | 58 | Response: 59 | 60 | `{"access_token": "ACCESS_TOKEN", "scope": "read write", "expires_in": 36000, "token_type": "Bearer"}` 61 | 62 | Example with access token: 63 | 64 | `curl -H "Authorization: Bearer ACCESS_TOKEN" "https://stepik.org/api/social-accounts?provider=github&uid=1216"` 65 | 66 | Response: 67 | 68 | `{"meta": {"page": 1, "has_next": false, "has_previous": false}, "social-accounts": []}` 69 | 70 | #### Authorization code flow 71 | 72 | - Set `grant type = autorization_code` and set `redirect_uri` in your application; 73 | - Redirect user to `https://stepik.org/oauth2/authorize/?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI`; 74 | - User should authenticate or register, and grant permissions to application; 75 | - It redirects to `redirect_uri` and receives the `CODE`; 76 | - Application asks for `ACCESS_TOKEN`: `curl -X POST -d "grant_type=authorization_code&code=CODE&redirect_uri=REDIRECT_URI" -u"CLIENT_ID:SECRET_ID" https://stepik.org/oauth2/token/`; 77 | - Application behaves as user, adding `Authorization: Bearer ACCESS_TOKEN;` to request headers. 78 | - Request to `https://stepik.org/api/stepics/1` returns the current user. 79 | 80 | ## Multiple IDs Calls 81 | 82 | You can request multiple objects using the single API call by using `?ids[]=2&ids[]=3...`. 83 | 84 | For example: to get courses with IDs = `2`, `67`, `76` and `70`; you can to call `https://stepik.org/api/courses?ids[]=2&ids[]=67&ids[]=76&ids[]=70`. 85 | 86 | This syntax is supported by all API endpoints. 87 | 88 | Don’t make calls with large size of `ids[]`. Such calls may be rejected by the server because of a large HTTP header. 89 | 90 | ## Examples 91 | 92 | #### Simple operations: 93 | 94 | - OAuth authorization example: [oauth_auth_example.py](/examples/oauth_auth_example.py) 95 | 96 | #### Complete examples: 97 | 98 | * VideoDownloader: [stepik-oauth2-videodownloader](https://github.com/StepicOrg/stepic-oauth2-videodownloader) 99 | * iOS app: [Stepik iOS app](https://github.com/StepicOrg/stepic-ios) 100 | * Android app: [Stepik Android app](https://github.com/StepicOrg/stepic-android) 101 | * Code Challenge submitter: [submitter.py](https://github.com/StepicOrg/SubmissionUtility/blob/master/submitter.py) 102 | * Video uploading (C#): [CoursePublishing](https://github.com/okulovsky/CoursePublishing/tree/master/Publishing/Stepic) 103 | * PyCharm Edu & IntelliJ integration: [intellij-community](https://github.com/JetBrains/intellij-community/tree/7e16c042a19767d5f548c84f88cc5edd5f9d1721/python/educational-core/student/src/com/jetbrains/edu/learning/stepic) 104 | * IntelliJ Plugin for Code Challenges: [intellij-plugins](https://github.com/StepicOrg/intellij-plugins) 105 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples overview 2 | 3 | |example|description| 4 | |---|---| 5 | |[`create_content.py`](create_content.py)|Example on how to create a step, a lesson and a course using API.| 6 | |[`download_random_leader_avatar.py`](download_random_leader_avatar.py)|Get leader user randomly from 100 leaders and download his avatar.| 7 | |[`export_course.py`](export_course.py)|Save course (all step sources) as in a foldered structure. Requires instructor access.| 8 | |[`get_certificates_urls_example.py`](get_certificates_urls_example.py)|Create HTML file with all user certificates.| 9 | |[`get_countries_all_count.py`](get_countries_all_count.py)|Count the number of countries known. Another example of how to use multiple api pages if size of response is unknown.| 10 | |[`get_courses_authors.py`](get_courses_authors.py)|Get list of all courses and create list of authors, ever involved in course creation process.Then print data aggregated by author name.| 11 | |[`get_courses_by_params.py`](get_courses_by_params.py)|Select active courses by specifing parameters such as: amount of units, language, number of overall discussions.| 12 | |[`get_enrolled_courses.py`](get_enrolled_courses.py)|Generate HTML-report with all enrolled courses for user. Example of using multiple IDs in one API call.| 13 | |[`get_info_all_courses_titles.py`](get_info_all_courses_titles.py)|Receive all courses titles and count courses with "Python" word in title.| 14 | |[`get_leaders_social_profiles.py`](get_leaders_social_profiles.py)|Get social profiles for leaders, if available.| 15 | |[`get_learn_events.py`](get_learn_events.py)|Get all learning events for user and prints them.| 16 | |[`get_ten_users_with_highest_reputation.py`](get_ten_users_with_highest_reputation.py)|Prints list of ten highest users by reputation.| 17 | |[`get_top_lessons_by_reactions.py`](get_top_lessons_by_reactions.py)|Get top lessons by reaction on recommendations.| 18 | |[`get_user_courses.py`](get_user_courses.py)|Example of getting all user courses using course-subscriptions endpoint and some info about them.| 19 | |[`get_user_name.py`](get_user_name.py)|Get username using auth token. Simple version.| 20 | |[`oauth_auth_example.py`](oauth_auth_example.py)|Example of OAuth authentification, used pretty all around all other examples.| 21 | |[`plot_course_viewed_by.py`](plot_course_viewed_by.py)|Script gets view statistics for all of the steps in some course and then displays it. The trend is usually (naturally) declining. By default uses course 187.| 22 | |[`plot_lesson_stats.py`](plot_lesson_stats.py)|Generates HTML, download lessons' data one by one, then we make plots to see how much the loss of the people depends on the lesson time.| 23 | |[`popular_courses.py`](popular_courses.py)| Calculate sum of all enrollments for popular courses.| 24 | |[`recommendations_top_example.py`](recommendations_top_example.py)|Prints list of top 10 recommended lessons for user. Warning! Get list of ALL lessons from stepik, so it might take a long time.| 25 | |[`save_course_steps.py`](save_course_steps.py)|Saves all step texts from course into single HTML file. Example of using multiple IDs in one API call.| 26 | |[`top_lessons_to_html.py`](top_lessons_to_html.py)|Generate HTML with top lessons sorted by number of views.| 27 | |[`videos_downloader.py`](videos_downloader.py)|Downloads all video files from a module (week) of a course or the whole course.| 28 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/__init__.py -------------------------------------------------------------------------------- /examples/copy_paste_course.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | # Clone course from one Stepik instance (domain) into another 3 | import re 4 | import os 5 | import csv 6 | import ast 7 | import json 8 | import requests 9 | import datetime 10 | 11 | # Enter parameters below: 12 | # 1. Get your keys at https://stepik.org/oauth2/applications/ 13 | # (client type = confidential, authorization grant type = client credentials) 14 | 15 | client_id = '...' 16 | client_secret = '...' 17 | api_host = 'https://stepik.org' 18 | 19 | # client_id = '...' 20 | # client_secret = '...' 21 | # api_host = 'http://127.0.0.1' # save to localhost 22 | 23 | course_id = 401 24 | mode = 'SAVE' # IMPORTANT: use SAVE first, then use PASTE with uncommented (or changed) lines above (client keys and host) 25 | 26 | cross_domain = True # to re-upload videos 27 | 28 | # 2. Get a token 29 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 30 | response = requests.post('{}/oauth2/token/'.format(api_host), 31 | data={'grant_type': 'client_credentials'}, 32 | auth=auth) 33 | token = response.json().get('access_token', None) 34 | if not token: 35 | print('Unable to authorize with provided credentials') 36 | exit(1) 37 | 38 | 39 | # 3. Call API (https://stepik.org/api/docs/) using this token. 40 | def fetch_object(obj_class, obj_id): 41 | api_url = '{}/api/{}s/{}'.format(api_host, obj_class, obj_id) 42 | response = requests.get(api_url, 43 | headers={'Authorization': 'Bearer ' + token}).json() 44 | return response['{}s'.format(obj_class)][0] 45 | 46 | 47 | def fetch_objects(obj_class, obj_ids): 48 | objs = [] 49 | # Fetch objects by 30 items, 50 | # so we won't bump into HTTP request length limits 51 | step_size = 30 52 | for i in range(0, len(obj_ids), step_size): 53 | obj_ids_slice = obj_ids[i:i + step_size] 54 | api_url = '{}/api/{}s?{}'.format(api_host, obj_class, 55 | '&'.join('ids[]={}'.format(obj_id) 56 | for obj_id in obj_ids_slice)) 57 | response = requests.get(api_url, 58 | headers={'Authorization': 'Bearer ' + token} 59 | ).json() 60 | objs += response['{}s'.format(obj_class)] 61 | return objs 62 | 63 | 64 | print('Mode:', mode) 65 | print('Course ID:', course_id) 66 | print() 67 | 68 | if mode == 'SAVE': # SAVE TO DATA 69 | 70 | course = fetch_object('course', course_id) 71 | sections = fetch_objects('section', course['sections']) 72 | unit_ids = [unit for section in sections for unit in section['units']] 73 | units = fetch_objects('unit', unit_ids) 74 | lessons_ids = [unit['lesson'] for unit in units] 75 | lessons = fetch_objects('lesson', lessons_ids) 76 | step_ids = [step for lesson in lessons for step in lesson['steps']] 77 | steps = fetch_objects('step-source', step_ids) 78 | 79 | data = [] 80 | 81 | idd = course['id'] 82 | course = { key: course[key] for key in ['title', 'summary', 'course_format', 'language', 'requirements', 'workload', 'is_public', 'description', 'certificate', 'target_audience'] } 83 | row = ['course', idd, course] 84 | data.append(row) 85 | 86 | for section in sections: 87 | idd = section['id'] 88 | section = { key: section[key] for key in ['title', 'position', 'course'] } 89 | row = ['section', idd, section] 90 | data.append(row) 91 | 92 | for unit in units: 93 | idd = unit['id'] 94 | unit = { key: unit[key] for key in ['position', 'section', 'lesson'] } 95 | row = ['unit', idd, unit] 96 | data.append(row) 97 | 98 | for lesson in lessons: 99 | idd = lesson['id'] 100 | lesson = { key: lesson[key] for key in ['title', 'is_public', 'language'] } 101 | row = ['lesson', idd, lesson] 102 | data.append(row) 103 | 104 | for step in steps: 105 | idd = step['id'] 106 | step = { key: step[key] for key in ['lesson', 'position', 'block', 'cost'] } 107 | row = ['step-source', idd, step] 108 | data.append(row) 109 | 110 | # write data to file 111 | csv_file = open('course-{}-dump.csv'.format(course_id), 'w') 112 | csv_writer = csv.writer(csv_file) 113 | csv_writer.writerows(data) 114 | csv_file.close() 115 | 116 | print('Objects:', len(data)) 117 | 118 | else: #PASTE FROM DATA 119 | 120 | # read data to file 121 | csv_file = open('course-{}-dump.csv'.format(course_id), 'r') 122 | csv_reader = csv.reader(csv_file) 123 | data = [row for row in csv_reader] 124 | for row in data: 125 | row[2] = ast.literal_eval(row[2]) # cast str to dict 126 | row[1] = int(row[1]) 127 | csv_file.close() 128 | 129 | print('Objects:', len(data)) 130 | print() 131 | 132 | courses_map = {} 133 | sections_map = {} 134 | lessons_map = {} 135 | 136 | for row in data: 137 | if row[0] != 'lesson': 138 | continue 139 | api_url = '{}/api/lessons'.format(api_host) 140 | api_data = { 'lesson' : row[2] } 141 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, json=api_data) 142 | new_id = r.json()['lessons'][0]['id'] 143 | old_id = row[1] 144 | lessons_map[old_id] = new_id 145 | print('Lesson ID:', old_id, '-->', new_id) 146 | 147 | for row in data: 148 | if row[0] != 'step-source': 149 | continue 150 | # reupload video if needed 151 | if cross_domain and row[2]['block']['name'] == 'video': 152 | # find best video from the old step to upload it by url 153 | video_urls = row[2]['block']['video']['urls'] 154 | best_quality = 0 155 | best_url = '' 156 | for url_and_quality in video_urls: 157 | q = int(url_and_quality['quality']) 158 | if q > best_quality: 159 | best_quality = q 160 | best_url = url_and_quality['url'] 161 | api_url = '{}/api/videos'.format(api_host) 162 | api_data = {'source_url': best_url, 'lesson': lessons_map[row[2]['lesson']]} 163 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, data=api_data) 164 | new_id = r.json()['videos'][0]['id'] 165 | old_id = row[2]['block']['video']['id'] 166 | row[2]['block']['video']['id'] = new_id 167 | print('Video ID:', old_id, '-->', new_id) 168 | 169 | api_url = '{}/api/step-sources'.format(api_host) 170 | row[2]['lesson'] = lessons_map[row[2]['lesson']] # fix lesson id to new ones 171 | if cross_domain: # hack for attachements, only works for when source domain is stepik.org 172 | row[2]['block']['text'] = row[2]['block']['text'].replace('href="/media/attachments/', 'href="https://stepik.org/media/attachments/') 173 | api_data = { 'stepSource' : row[2] } 174 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, json=api_data) 175 | new_id = r.json()['step-sources'][0]['id'] 176 | old_id = row[1] 177 | print('Step ID:', old_id, '-->', new_id) 178 | 179 | for row in data: 180 | if row[0] != 'course': 181 | continue 182 | api_url = '{}/api/courses'.format(api_host) 183 | api_data = { 'course' : row[2] } 184 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, json=api_data) 185 | new_id = r.json()['courses'][0]['id'] 186 | old_id = row[1] 187 | courses_map[old_id] = new_id 188 | print('Course ID:', old_id, '-->', new_id) 189 | 190 | for row in data: 191 | if row[0] != 'section': 192 | continue 193 | api_url = '{}/api/sections'.format(api_host) 194 | row[2]['course'] = courses_map[row[2]['course']] # fix course id to new ones 195 | api_data = { 'section' : row[2] } 196 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, json=api_data) 197 | new_id = r.json()['sections'][0]['id'] 198 | old_id = row[1] 199 | sections_map[old_id] = new_id 200 | print('Section ID:', old_id, '-->', new_id) 201 | 202 | for row in data: 203 | if row[0] != 'unit': 204 | continue 205 | api_url = '{}/api/units'.format(api_host) 206 | row[2]['section'] = sections_map[row[2]['section']] # fix section id to new ones 207 | row[2]['lesson'] = lessons_map[row[2]['lesson']] # fix lesson id to new ones 208 | api_data = { 'units' : row[2] } 209 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, json=api_data) 210 | new_id = r.json()['units'][0]['id'] 211 | old_id = row[1] 212 | print('Unit ID:', old_id, '-->', new_id) 213 | 214 | print() 215 | print(courses_map) 216 | 217 | print() 218 | print('Done!') 219 | -------------------------------------------------------------------------------- /examples/create_content.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | 3 | import json 4 | import requests 5 | 6 | # 1. Get your keys at https://stepik.org/oauth2/applications/ (client type = confidential, authorization grant type = client credentials) 7 | client_id = '...' 8 | client_secret = '...' 9 | 10 | # 2. Get a token 11 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 12 | resp = requests.post('https://stepik.org/oauth2/token/', data={'grant_type': 'client_credentials'}, auth=auth) 13 | token = json.loads(resp.text)['access_token'] 14 | 15 | # 3. Call API (https://stepik.org/api/docs/) using this token. 16 | # Example: 17 | 18 | # 3.1. Create a new lesson 19 | 20 | api_url = 'https://stepik.org/api/lessons' 21 | data = { 22 | 'lesson': { 23 | 'title': 'My Lesson' 24 | } 25 | } 26 | # Use POST to create new objects 27 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, json=data) 28 | lesson_id = r.json()['lessons'][0]['id'] 29 | print('Lesson ID:', lesson_id) 30 | # You can also debug using: 31 | # print(r.status_code) – should be 201 (HTTP Created) 32 | # print(r.text) – should print the lesson's json (with lots of properties) 33 | 34 | # 3.2. Add new theory step to this lesson 35 | 36 | api_url = 'https://stepik.org/api/step-sources' 37 | data = { 38 | 'stepSource': { 39 | 'block': { 40 | 'name': 'text', 41 | 'text': 'Hello World!' 42 | }, 43 | 'lesson': lesson_id, 44 | 'position': 1 45 | } 46 | } 47 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, json=data) 48 | step_id = r.json()['step-sources'][0]['id'] 49 | print('Step ID:', step_id) 50 | 51 | # 3.3. Update existing theory step 52 | 53 | api_url = 'https://stepik.org/api/step-sources/{}'.format(step_id) 54 | data = { 55 | 'stepSource': { 56 | 'block': { 57 | 'name': 'text', 58 | 'text': 'Hi World!' # <-- changed here :) 59 | }, 60 | 'lesson': lesson_id, 61 | 'position': 1 62 | } 63 | } 64 | # Use PUT to update existing objects 65 | r = requests.put(api_url, headers={'Authorization': 'Bearer '+ token}, json=data) 66 | step_id = r.json()['step-sources'][0]['id'] 67 | print('Step ID (update):', step_id) 68 | 69 | # 3.4. Add new multiple (single) choice step to this lesson 70 | 71 | api_url = 'https://stepik.org/api/step-sources' 72 | data = { 73 | 'stepSource': { 74 | 'block': { 75 | 'name': 'choice', 76 | 'text': 'Pick one!', 77 | 'source': { 78 | 'options': [ 79 | {'is_correct': False, 'text': '2+2=3', 'feedback': ''}, 80 | {'is_correct': True, 'text': '2+2=4', 'feedback': ''}, 81 | {'is_correct': False, 'text': '2+2=5', 'feedback': ''}, 82 | ], 83 | 'is_always_correct': False, 84 | 'is_html_enabled': True, 85 | 'sample_size': 3, 86 | 'is_multiple_choice': False, 87 | 'preserve_order': False, 88 | 'is_options_feedback': False 89 | } 90 | }, 91 | 'lesson': lesson_id, 92 | 'position': 2 93 | } 94 | } 95 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, json=data) 96 | step_id = r.json()['step-sources'][0]['id'] 97 | print('Step ID:', step_id) 98 | 99 | ### 100 | 101 | # Your lesson is ready! 102 | print('--> Check https://stepik.org/lesson/{}'.format(lesson_id)) 103 | 104 | ### 105 | 106 | # 3.4. Create a new course 107 | 108 | api_url = 'https://stepik.org/api/courses' 109 | data = { 110 | 'course': { 111 | 'title': 'My Course' 112 | } 113 | } 114 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, json=data) 115 | course_id = r.json()['courses'][0]['id'] 116 | print('Course ID:', course_id) 117 | 118 | # 3.5. Add new module (section) to this course 119 | 120 | api_url = 'https://stepik.org/api/sections' 121 | data = { 122 | 'section': { 123 | 'title': 'My Section', 124 | 'course': course_id, 125 | 'position': 1 126 | } 127 | } 128 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, json=data) 129 | section_id = r.json()['sections'][0]['id'] 130 | print('Section ID:', section_id) 131 | 132 | # 3.6. Add your existing lesson to this section (it is called unit) 133 | 134 | api_url = 'https://stepik.org/api/units' 135 | data = { 136 | 'unit': { 137 | 'section': section_id, 138 | 'lesson': lesson_id, 139 | 'position': 1 140 | } 141 | } 142 | r = requests.post(api_url, headers={'Authorization': 'Bearer '+ token}, json=data) 143 | unit_id = r.json()['units'][0]['id'] 144 | print('Unit ID:', unit_id) 145 | 146 | ### 147 | 148 | # Your course is ready 149 | print('--> Check https://stepik.org/course/{}'.format(course_id)) 150 | 151 | ### 152 | -------------------------------------------------------------------------------- /examples/data.php: -------------------------------------------------------------------------------- 1 | OAUTH2_CLIENT_ID, 32 | 'redirect_uri' => $scriptPath, 33 | 'scope' => 'read', 34 | 'state' => $_SESSION['state'], 35 | 'response_type' => 'code' 36 | ); 37 | 38 | # Redirect the user to Github's authorization page 39 | header('Location: ' . $authorizeURL . '?' . http_build_query($params)); 40 | die(); 41 | } 42 | 43 | # When Github redirects the user back here, there will be a "code" and "state" parameter in the query string 44 | 45 | if (get('code')) { 46 | # Verify the state matches our stored state 47 | if (!get('state') || $_SESSION['state'] != get('state')) { 48 | header('Location: ' . $scriptPath); 49 | die(); 50 | } 51 | 52 | # Exchange the auth code for a token 53 | $token = apiRequest($tokenURL, array( 54 | 'client_id' => OAUTH2_CLIENT_ID, 55 | 'client_secret' => OAUTH2_CLIENT_SECRET, 56 | 'redirect_uri' => $scriptPath, 57 | 'state' => $_SESSION['state'], 58 | 'code' => get('code'), 59 | 'grant_type' => 'authorization_code' 60 | )); 61 | $_SESSION['access_token'] = $token->access_token; 62 | 63 | header('Location: ' . $scriptPath); 64 | } 65 | 66 | if (session('access_token')) { 67 | 68 | # ========= 69 | # WELCOME! 70 | # ========= 71 | 72 | $stepiks = apiRequest($apiURLBase . 'stepics/1'); # should be stepic with "c"! 73 | $user_id = $stepiks->profiles[0]->id; 74 | $first_name = $stepiks->profiles[0]->first_name; 75 | $last_name = $stepiks->profiles[0]->last_name; 76 | echo ''; 77 | echo 'Личный кабинет онлайн-программы «Анализ данных»'; 78 | echo ''; 79 | echo ''; 80 | echo ''; 81 | echo '

Личный кабинет онлайн-программы «Анализ данных» на Stepik.org

'; 82 | echo '

Добро пожаловать, ' . $first_name . ' ' . $last_name . '!

'; 83 | echo '
'; 84 | 85 | # ============= 86 | # COURSES LIST 87 | # ============= 88 | 89 | $courses = array( 90 | array('id' => 217, 'required' => True, 'sections' => array(2, 4), 'need_to_pass' => 85, 'exams' => 1536), # Алгоритмы: теория и практика. Методы 91 | array('id' => 129, 'required' => True, 'sections' => array(1, 2, 3), 'need_to_pass' => 85, 'exams' => 1425), # Анализ данных в R 92 | array('id' => 724, 'required' => True, 'sections' => array(1, 2, 3), 'need_to_pass' => 85, 'exams' => null), # Анализ данных в R. Часть 2 93 | array('id' => 1240, 'required' => True, 'sections' => array(1, 2, 5, 6), 'need_to_pass' => 85, 'exams' => 1534), # Введение в базы данных 94 | array('id' => 253, 'required' => True, 'sections' => array(1, 3, 4, 5), 'need_to_pass' => 85, 'exams' => 1535), # Введение в архитектуру ЭВМ. Элементы операционных систем. 95 | array('id' => 902, 'required' => True, 'sections' => array(1, 2, 3, 4), 'need_to_pass' => 90, 'exams' => 1424), # Дискретная математика 96 | array('id' => 73, 'required' => True, 'sections' => array(1, 2, 3), 'need_to_pass' => 90, 'exams' => 1427), # Введение в Linux 97 | array('id' => 497, 'required' => True, 'sections' => array(1, 2, 3), 'need_to_pass' => 85, 'exams' => 1426), # Основы программирования на R 98 | array('id' => 76, 'required' => True, 'sections' => array(1, 2, 3), 'need_to_pass' => 90, 'exams' => 1423), # Основы статистики 99 | array('id' => 524, 'required' => True, 'sections' => array(1, 2, 3), 'need_to_pass' => 85, 'exams' => null), # Основы статистики. Часть 2 100 | array('id' => 67, 'required' => True, 'sections' => array(1, 2, 3), 'need_to_pass' => 90, 'exams' => 1428), # Программирование на Python 101 | array('id' => 512, 'required' => True, 'sections' => array(1, 2, 3), 'need_to_pass' => 90, 'exams' => null), # Python: основы и применение 102 | array('id' => 1612, 'required' => True, 'sections' => array(2, 3, 4), 'need_to_pass' => 85, 'exams' => 2458), # Управление вычислениями 103 | array('id' => 217, 'required' => False, 'sections' => array(6, 8), 'need_to_pass' => 85, 'exams' => 1537), # Алгоритмы: теория и практика. Методы 104 | array('id' => 253, 'required' => False, 'sections' => array(2, 6, 7), 'need_to_pass' => 85, 'exams' => 1538), # Введение в архитектуру ЭВМ. Элементы операционных систем. 105 | array('id' => 1240, 'required' => False, 'sections' => array(3, 4, 7), 'need_to_pass' => 85, 'exams' => 1544), # Введение в базы данных 106 | array('id' => 95, 'required' => False, 'sections' => array(1, 2, 3, 4), 'need_to_pass' => 85, 'exams' => 1429), # Введение в математический анализ 107 | array('id' => 401, 'required' => False, 'sections' => array(1, 2, 3, 4), 'need_to_pass' => 85, 'exams' => 1432), # Нейронные сети 108 | array('id' => 7, 'required' => False, 'sections' => array(1, 2, 3, 4, 5, 6), 'need_to_pass' => 85, 'exams' => 1430), # Программирование на языке C++ 109 | array('id' => 187, 'required' => False, 'sections' => array(1, 2, 3, 4, 5, 6), 'need_to_pass' => 85, 'exams' => 1431), # Java. Базовый курс 110 | array('id' => 2152, 'required' => False, 'sections' => array(1, 2, 3), 'need_to_pass' => 85, 'exams' => 2387) # Основы статистики. ч.3 111 | # TO ADD WHEN READY: Machine Learning (not required) 112 | ); 113 | 114 | echo '

Курсы программы (взаимосвязи):

'; 115 | echo ''; 116 | echo ''; 117 | echo ''; 118 | 119 | $previousCourse = null; 120 | foreach ($courses as $course) { 121 | $info = apiRequest($apiURLBase . 'courses/' . $course['id'])->courses[0]; 122 | $section_ids = array(); 123 | foreach ($course['sections'] as $i) { 124 | $section_ids[] = $info->sections[$i - 1]; 125 | } 126 | $sections = apiRequest($apiURLBase . 'sections?ids[]=' . implode('&ids[]=', $section_ids))->sections; 127 | $progress_ids = array(); 128 | foreach ($sections as $section) { 129 | $progress_ids[] = $section->progress; 130 | } 131 | $progresses = apiRequest($apiURLBase . 'progresses?ids[]=' . implode('&ids[]=', $progress_ids))->progresses; 132 | $score = 0; 133 | $cost = 0; 134 | foreach ($progresses as $progress) { 135 | $score += $progress->score; 136 | $cost += $progress->cost; 137 | } 138 | 139 | $percentage = $cost ? round(100 * $score / $cost, 2) : 0; 140 | if ($percentage >= $course['need_to_pass']) { 141 | try { 142 | $exam = apiRequest($apiURLBase . 'courses/' . $course['exams'])->courses[0]; 143 | $progress = apiRequest($apiURLBase . 'progresses/' . $exam->progress)->progresses[0]; 144 | $exam_message = $progress->score . ' / ' . $progress->cost; 145 | } catch (Exception $e) { 146 | $exam_message = 'можно начать'; 147 | } 148 | } else { 149 | $exam_message = 'нет доступа (нужно ' . $course['need_to_pass'] . '% за курс)'; 150 | } 151 | if ($previousCourse['required'] and !$course['required']) { 152 | echo ''; 153 | echo ''; 154 | } 155 | 156 | echo ''; 157 | echo ''; 158 | echo ''; 159 | echo ''; 160 | 161 | if ($cost) { 162 | echo ''; 163 | echo ''; 164 | } else { 165 | echo ''; 166 | } 167 | 168 | echo ''; 169 | echo ''; 170 | $previousCourse = $course; 171 | } 172 | 173 | echo '
Обязательные курсы:
КурсКоличество
модулей
МодулиВаши баллыВаш процентДопуск к экзамену
Курсы по выбору:
КурсКоличество
модулей
МодулиВаши баллыВаш процентЭкзамен
' . $info->title . '' . count($course['sections']) . '' . implode(", ", $course['sections']) . '' . $score . ' / ' . $cost . '' . $percentage . '%Запишитесь на курс' . $exam_message . '
'; 174 | 175 | # ============= 176 | # PAYMENTS LOG 177 | # ============= 178 | 179 | $subscription_plans = array(10 => 0, 1999 => 1, 5397 => 3, 6000 => 3, 9595 => 6, 16791 => 12); 180 | $format = 'M d, Y (H:i)'; 181 | $page = 1; 182 | $payments_list = array(); 183 | $payment_start = 0; 184 | $payment_valid_until = 0; 185 | do { 186 | $payments = apiRequest($apiURLBase . 'payments?page=' . $page); 187 | foreach ($payments->payments as $payment) { 188 | if ($payment->destination_type != 'au_data') { 189 | continue; 190 | } 191 | 192 | $payment_date = strtotime($payment->payment_date); 193 | $payment_start = max($payment_date, $payment_start); 194 | $months = $subscription_plans[intval($payment->amount)]; 195 | $payment_valid_until = paymentDue($payment_start, $months); 196 | $payments_list[] = array( 197 | intval($payment->amount), 198 | date($format, $payment_date), 199 | date($format, $payment_start), 200 | date($format, $payment_valid_until) 201 | ); 202 | $payment_start = $payment_valid_until; 203 | } 204 | $page += 1; 205 | } while ($payments->meta->has_next); 206 | 207 | if ($payment_valid_until >= time()) { 208 | $color = ($payment_valid_until - time() >= 60 * 60 * 24 * 7) ? "#66cc66" : "cccc00"; # yellow if less than one week left 209 | echo '

Ваша подписка на программу  активна до ' . date($format, $payment_valid_until) . ' 

'; 210 | } else { 211 | echo '

Ваша подписка на программу  не активна 

'; 212 | } 213 | 214 | echo '

Ваши платежи:

'; 215 | echo ''; 216 | echo ''; 217 | foreach (array_reverse($payments_list) as $payment) { 218 | echo ''; 221 | } 222 | echo '
Сумма (руб)Дата и время платежа (UTC)Действует отДействует до
'; 219 | echo implode('', $payment); 220 | echo '
'; 223 | 224 | # LOGOUT 225 | echo '

[Выйти из кабинета]

'; 226 | echo ''; 227 | } else { 228 | echo '

Личный кабинет онлайн-программы «Анализ данных» на Stepik.org

'; 229 | echo '

Вы не вошли

'; 230 | echo '

Войти через Stepik.org

'; 231 | } 232 | 233 | # ================= 234 | # HELPER FUNCTIONS 235 | # ================= 236 | 237 | function apiRequest($url, $post = FALSE, $headers = array()) 238 | { 239 | $ch = curl_init($url); 240 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 241 | 242 | if ($post) 243 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post)); 244 | 245 | $headers[] = 'Accept: application/json'; 246 | 247 | if (session('access_token')) 248 | $headers[] = 'Authorization: Bearer ' . session('access_token'); 249 | 250 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 251 | 252 | $response = curl_exec($ch); 253 | return json_decode($response); 254 | } 255 | 256 | function get($key, $default = NULL) 257 | { 258 | return array_key_exists($key, $_GET) ? $_GET[$key] : $default; 259 | } 260 | 261 | function session($key, $default = NULL) 262 | { 263 | return array_key_exists($key, $_SESSION) ? $_SESSION[$key] : $default; 264 | } 265 | 266 | # Based on http://stackoverflow.com/a/24014541/92396 but fixed and simplified: 267 | function paymentDue($timestamp, $months) 268 | { 269 | $date = new DateTime('@' . $timestamp); 270 | $next = clone $date; 271 | $next->modify('last day of +' . $months . ' month'); 272 | if ($date->format('d') > $next->format('d')) { 273 | $add = $date->diff($next); 274 | } else { 275 | $add = new DateInterval('P' . $months . 'M'); 276 | } 277 | $newDate = $date->add($add); 278 | return $newDate->getTimestamp(); 279 | } 280 | -------------------------------------------------------------------------------- /examples/download_random_leader_avatar.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | import requests 3 | from random import randint 4 | from os.path import splitext 5 | import math 6 | import json 7 | 8 | 9 | # 1. Get your keys at https://stepik.org/oauth2/applications/ (client type = confidential, 10 | # authorization grant type = client credentials) 11 | client_id = "..." 12 | client_secret = "..." 13 | 14 | # 2. Get a token 15 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 16 | resp = requests.post('https://stepik.org/oauth2/token/', 17 | data={'grant_type': 'client_credentials'}, 18 | auth=auth 19 | ) 20 | token = resp.json()['access_token'] 21 | 22 | # 3. Call API (https://stepik.org/api/docs/) using this token. 23 | 24 | 25 | # Get leaders by count 26 | def get_leaders(count): 27 | pages = math.ceil(count / 20) 28 | leaders = [] 29 | for page in range(1, pages + 1): 30 | api_url = 'https://stepik.org/api/users?order=knowledge_rank&page={}'.format(page) 31 | response = json.loads(requests.get(api_url, headers={'Authorization': 'Bearer ' + token}).text) 32 | leaders += response['users'] 33 | if not response['meta']['has_next']: 34 | break 35 | 36 | return leaders 37 | 38 | 39 | # Get leader user randomly from 100 leaders and download his avatar 40 | avatar_url = get_leaders(100)[randint(0, 99)]['avatar'] 41 | response = requests.get(avatar_url, stream=True) 42 | extension = splitext(avatar_url)[1] 43 | 44 | with open('leader{}'.format(extension), 'wb') as out_file: 45 | out_file.write(response.content) 46 | -------------------------------------------------------------------------------- /examples/export_course.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | # Saves all step sources into foldered structure 3 | import os 4 | import json 5 | import re 6 | import requests 7 | import datetime 8 | 9 | # Enter parameters below: 10 | # 1. Get your keys at https://stepik.org/oauth2/applications/ 11 | # (client type = confidential, authorization grant type = client credentials) 12 | client_id = '...' 13 | client_secret = '...' 14 | api_host = 'https://stepik.org' 15 | course_id = 1 16 | 17 | # 2. Get a token 18 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 19 | response = requests.post('https://stepik.org/oauth2/token/', 20 | data={'grant_type': 'client_credentials'}, 21 | auth=auth) 22 | token = response.json().get('access_token', None) 23 | if not token: 24 | print('Unable to authorize with provided credentials') 25 | exit(1) 26 | 27 | # 3. Call API (https://stepik.org/api/docs/) using this token. 28 | def get_valid_filename(s): 29 | return re.sub(r'(?u)[^-\w. ]', '', str(s)).strip() 30 | 31 | def fetch_object(obj_class, obj_id): 32 | api_url = '{}/api/{}s/{}'.format(api_host, obj_class, obj_id) 33 | response = requests.get(api_url, 34 | headers={'Authorization': 'Bearer ' + token}).json() 35 | return response['{}s'.format(obj_class)][0] 36 | 37 | 38 | def fetch_objects(obj_class, obj_ids): 39 | objs = [] 40 | # Fetch objects by 30 items, 41 | # so we won't bump into HTTP request length limits 42 | step_size = 30 43 | for i in range(0, len(obj_ids), step_size): 44 | obj_ids_slice = obj_ids[i:i + step_size] 45 | api_url = '{}/api/{}s?{}'.format(api_host, obj_class, 46 | '&'.join('ids[]={}'.format(obj_id) 47 | for obj_id in obj_ids_slice)) 48 | response = requests.get(api_url, 49 | headers={'Authorization': 'Bearer ' + token} 50 | ).json() 51 | objs += response['{}s'.format(obj_class)] 52 | return objs 53 | 54 | 55 | course = fetch_object('course', course_id) 56 | sections = fetch_objects('section', course['sections']) 57 | 58 | for section in sections: 59 | 60 | unit_ids = section['units'] 61 | units = fetch_objects('unit', unit_ids) 62 | 63 | for unit in units: 64 | 65 | lesson_id = unit['lesson'] 66 | lesson = fetch_object('lesson', lesson_id) 67 | 68 | step_ids = lesson['steps'] 69 | steps = fetch_objects('step', step_ids) 70 | 71 | for step in steps: 72 | step_source = fetch_object('step-source', step['id']) 73 | path = [ 74 | '{} {}'.format(str(course['id']).zfill(2), get_valid_filename(course['title'])), 75 | '{} {}'.format(str(section['position']).zfill(2), get_valid_filename(section['title'])), 76 | '{} {}'.format(str(unit['position']).zfill(2), get_valid_filename(lesson['title'])), 77 | '{}_{}_{}.step'.format(lesson['id'], str(step['position']).zfill(2), step['block']['name']) 78 | ] 79 | try: 80 | os.makedirs(os.path.join(os.curdir, *path[:-1])) 81 | except: 82 | pass 83 | filename = os.path.join(os.curdir, *path) 84 | f = open(filename, 'w') 85 | data = { 86 | 'block': step_source['block'], 87 | 'id': str(step['id']), 88 | 'time': datetime.datetime.now().isoformat() 89 | } 90 | f.write(json.dumps(data)) 91 | f.close() 92 | print(filename) 93 | -------------------------------------------------------------------------------- /examples/external-reports/README.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | * Python 3: pypandoc, shutil, argparse 4 | * PdfLaTeX 5 | 6 | We recommend to install TeX Live for compilation latex project to pdf if you use Linux. 7 | 8 | sudo apt-get install texlive-full 9 | 10 | If you need to edit latex projects, you can install [TeXmaker](http://www.xm1math.net/texmaker/download.html). 11 | 12 | If you use Windows, you can install [MikTeX](https://miktex.org/). 13 | 14 | # API keys 15 | Get your keys at https://stepik.org/oauth2/applications/ 16 | (client type = confidential, authorization grant type = client credentials) 17 | 18 | # Usage 19 | 20 | ## Item Report 21 | To build the item report for course with course_id, you should launch in the command line. 22 | 23 | python item_report.py --course course_id 24 | 25 | or 26 | 27 | python item_report.py -c course_id 28 | 29 | Report will be put in ``./pdf/course-{course_id}-item-report.pdf`` 30 | 31 | **Note:** Since course submissions can be extremely large, it it recommended to put the file ``course-{course_id}-submissions.csv`` in the folder ``./cache/`` 32 | 33 | 34 | ## Video Report 35 | To build the video report for course with course_id, you should launch in the command line. 36 | 37 | python video_report.py --course course_id 38 | 39 | or 40 | 41 | python video_report.py -c course_id 42 | 43 | Report will be put in ``./pdf/course-{course_id}-video-report.pdf`` 44 | 45 | 46 | ## Dropout Report 47 | To build the dropout report for course with course_id, you should launch in the command line. 48 | 49 | python dropout_report.py --course course_id 50 | 51 | or 52 | 53 | python dropout_report.py -c course_id 54 | 55 | Report will be put in ``./pdf/course-{course_id}-dropout-report.pdf`` -------------------------------------------------------------------------------- /examples/external-reports/cache/test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/cache/test -------------------------------------------------------------------------------- /examples/external-reports/dropout_report.py: -------------------------------------------------------------------------------- 1 | from library.models import DropoutReport 2 | 3 | report = DropoutReport() 4 | report.build() 5 | -------------------------------------------------------------------------------- /examples/external-reports/item_report.py: -------------------------------------------------------------------------------- 1 | from library.models import ItemReport 2 | 3 | report = ItemReport() 4 | report.build() -------------------------------------------------------------------------------- /examples/external-reports/latex/default-dropout/common/introduction.tex: -------------------------------------------------------------------------------- 1 | \chapter{Введение} 2 | \section*{Назначение} 3 | С помощью данного отчета можно посмотреть сводную статистику по шагам в разрезе каждого урока \addcoursetexttointro{} и выявить проблемные шаги. 4 | \section*{Методология} 5 | В данном отчете по каждому уроку и для каждого шага находятся следующие статистические характеристики: 6 | \begin{itemize} 7 | \item Число просмотров. Показывает число учащихся, которые просмотрели данный шаг. 8 | \item Процент завершения. Рассчитывается как отношение числа учащихся, которые успешно завершили данный шаг, к общему числу просмотревших. Для теоретических шагов всегда равно 100\%. 9 | \item Процент ухода. Рассчитывается как отношение числа учащихся, для которых решение данного шага было последним, к числу учащихся, решавших что-то на курсе, но не набравших достаточно баллов для сертификата. Только для практических шагов. 10 | \end{itemize} 11 | 12 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-dropout/course-dropout-report.tex: -------------------------------------------------------------------------------- 1 | \documentclass[12pt]{report} 2 | \usepackage{stepik} 3 | \usepackage{ifsym} 4 | 5 | \input{generated/info} 6 | 7 | \title{Отчет по~прохождению обучения на~курсе} 8 | \subtitle{<<\coursetitle>>} 9 | \cover{cover.eps} 10 | \addcoursetexttointro{в~курсе \textbf{<<\coursetitle>>} (\url{\courseurl})} 11 | 12 | \begin{document} 13 | 14 | \maketitle 15 | \tableofcontents 16 | 17 | \input{common/introduction} 18 | 19 | \chapter{Анализ прохождения обучения} 20 | \input{generated/map} 21 | 22 | \end{document} 23 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-dropout/cover.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/latex/default-dropout/cover.eps -------------------------------------------------------------------------------- /examples/external-reports/latex/default-dropout/generated/test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/latex/default-dropout/generated/test -------------------------------------------------------------------------------- /examples/external-reports/latex/default-dropout/logo.eps: -------------------------------------------------------------------------------- 1 | %!PS-Adobe-3.0 EPSF-3.0 2 | %%Creator: cairo 1.14.0 (http://cairographics.org) 3 | %%CreationDate: Fri Sep 9 13:24:44 2016 4 | %%Pages: 1 5 | %%DocumentData: Clean7Bit 6 | %%LanguageLevel: 2 7 | %%BoundingBox: 0 -1 448 80 8 | %%EndComments 9 | %%BeginProlog 10 | save 11 | 50 dict begin 12 | /q { gsave } bind def 13 | /Q { grestore } bind def 14 | /cm { 6 array astore concat } bind def 15 | /w { setlinewidth } bind def 16 | /J { setlinecap } bind def 17 | /j { setlinejoin } bind def 18 | /M { setmiterlimit } bind def 19 | /d { setdash } bind def 20 | /m { moveto } bind def 21 | /l { lineto } bind def 22 | /c { curveto } bind def 23 | /h { closepath } bind def 24 | /re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto 25 | 0 exch rlineto 0 rlineto closepath } bind def 26 | /S { stroke } bind def 27 | /f { fill } bind def 28 | /f* { eofill } bind def 29 | /n { newpath } bind def 30 | /W { clip } bind def 31 | /W* { eoclip } bind def 32 | /BT { } bind def 33 | /ET { } bind def 34 | /pdfmark where { pop globaldict /?pdfmark /exec load put } 35 | { globaldict begin /?pdfmark /pop load def /pdfmark 36 | /cleartomark load def end } ifelse 37 | /BDC { mark 3 1 roll /BDC pdfmark } bind def 38 | /EMC { mark /EMC pdfmark } bind def 39 | /cairo_store_point { /cairo_point_y exch def /cairo_point_x exch def } def 40 | /Tj { show currentpoint cairo_store_point } bind def 41 | /TJ { 42 | { 43 | dup 44 | type /stringtype eq 45 | { show } { -0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse 46 | } forall 47 | currentpoint cairo_store_point 48 | } bind def 49 | /cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore 50 | cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def 51 | /Tf { pop /cairo_font exch def /cairo_font_matrix where 52 | { pop cairo_selectfont } if } bind def 53 | /Td { matrix translate cairo_font_matrix matrix concatmatrix dup 54 | /cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point 55 | /cairo_font where { pop cairo_selectfont } if } bind def 56 | /Tm { 2 copy 8 2 roll 6 array astore /cairo_font_matrix exch def 57 | cairo_store_point /cairo_font where { pop cairo_selectfont } if } bind def 58 | /g { setgray } bind def 59 | /rg { setrgbcolor } bind def 60 | /d1 { setcachedevice } bind def 61 | %%EndProlog 62 | %%BeginSetup 63 | %%EndSetup 64 | %%Page: 1 1 65 | %%BeginPageSetup 66 | %%PageBoundingBox: 0 -1 448 80 67 | %%EndPageSetup 68 | q 0 -1 448 81 rectclip q 69 | 0.8 g 70 | 72.078 58.959 m 58.961 72.08 l 53.922 77.201 47.121 79.92 40 79.92 c 32.801 71 | 79.92 26.078 77.119 21.039 72.08 c 7.84 58.959 l 2.719 53.92 0 47.119 0 72 | 39.998 c 0 32.799 2.801 26.08 7.84 21.041 c 20.961 7.92 l 26.078 2.799 73 | 32.801 -0.002 39.922 -0.002 c 46.879 -0.002 53.52 2.639 58.559 7.522 c 72 74 | 20.959 l 77.121 25.998 79.84 32.799 79.84 39.92 c 80 47.201 77.199 53.92 75 | 72.078 58.959 c h 76 | 63.359 30.479 m 50 17.119 l 47.52 14.721 44.078 13.201 40.398 13.201 c 77 | 36.559 13.201 33.121 14.721 30.559 17.279 c 37.039 23.76 l 37.922 22.881 78 | 39.039 22.4 40.398 22.4 c 41.762 22.4 42.879 22.959 43.762 23.76 c 56.961 79 | 36.881 l 57.762 37.76 58.32 38.881 58.32 40.24 c 58.32 41.522 57.762 42.721 80 | 56.961 43.6 c 56.078 44.479 54.961 44.959 53.602 44.959 c 52.32 44.959 81 | 51.121 44.4 50.238 43.6 c 37.121 30.479 l 34.641 27.998 31.121 26.4 27.281 82 | 26.4 c 23.441 26.4 20 27.998 17.441 30.479 c 14.961 32.959 13.359 36.479 83 | 13.359 40.318 c 13.359 44.158 14.879 47.6 17.441 50.158 c 30.559 63.279 84 | l 33.039 65.76 36.559 67.361 40.398 67.361 c 44.238 67.361 47.68 65.76 85 | 50.238 63.279 c 43.762 56.799 l 42.879 57.682 41.762 58.158 40.398 58.158 86 | c 39.039 58.158 37.922 57.682 37.039 56.799 c 23.922 43.6 l 23.039 42.721 87 | 22.559 41.6 22.559 40.24 c 22.559 38.959 23.121 37.76 23.922 36.881 c 24.801 88 | 35.998 25.922 35.522 27.281 35.522 c 28.559 35.522 29.762 36.08 30.641 89 | 36.881 c 43.762 49.998 l 46.32 52.479 49.762 54.08 53.602 54.08 c 57.441 90 | 54.08 60.879 52.561 63.441 49.998 c 65.922 47.44 67.52 43.998 67.52 40.158 91 | c 67.441 36.479 65.84 33.041 63.359 30.479 c h 92 | 63.359 30.479 m f 93 | 268.48 20.318 m 266.879 20.318 265.52 21.682 265.52 23.279 c 265.52 59.682 94 | l 265.52 61.279 266.801 62.639 268.48 62.639 c 270.078 62.639 271.441 61.361 95 | 271.441 59.682 c 271.441 23.279 l 271.441 21.682 270.16 20.318 268.48 20.318 96 | c h 97 | 268.48 20.318 m f 98 | 167.762 28.721 m 166.559 29.682 164.801 29.522 163.84 28.318 c 161.199 99 | 25.041 154.641 25.842 152.961 26.561 c 150.48 27.522 149.281 29.6 149.281 100 | 32.639 c 149.281 52.561 l 162.32 52.561 l 163.922 52.561 165.281 53.842 101 | 165.281 55.522 c 165.281 57.119 164 58.479 162.32 58.479 c 149.281 58.479 102 | l 149.281 73.361 l 149.281 74.881 148.078 76.158 146.48 76.158 c 144.961 103 | 76.158 143.762 74.959 143.762 73.361 c 143.762 32.639 l 143.762 27.361 104 | 146.32 23.279 150.879 21.44 c 152.32 20.881 154.559 20.4 157.039 20.4 c 105 | 160.961 20.4 165.441 21.522 168.16 24.881 c 169.121 26.08 168.961 27.76 106 | 167.762 28.721 c h 107 | 167.762 28.721 m f 108 | 122.559 20.158 m 121.68 20.158 120.719 20.24 119.84 20.318 c 110.238 21.522 109 | 104.48 25.998 104.48 32.158 c 104.48 33.76 105.762 35.119 107.441 35.119 110 | c 109.039 35.119 110.398 33.76 110.398 32.158 c 110.398 29.279 114.398 111 | 26.959 120.641 26.158 c 126.879 25.361 131.281 28.479 131.922 30.881 c 132.32 112 | 32.318 132.16 33.6 131.52 34.639 c 130.801 35.76 129.602 36.479 127.762 113 | 36.881 c 115.199 39.119 106.879 41.201 105.68 49.119 c 105.281 51.998 106.078 114 | 54.881 108 57.119 c 110.719 60.318 115.441 62.158 120.961 62.158 c 131.039 115 | 62.158 137.602 57.682 137.602 50.639 c 137.602 49.041 136.238 47.682 134.641 116 | 47.682 c 133.039 47.682 131.68 49.041 131.68 50.639 c 131.68 54.799 125.922 117 | 56.24 120.961 56.24 c 117.281 56.24 114.078 55.119 112.48 53.279 c 111.68 118 | 52.318 111.281 51.201 111.52 49.998 c 112 47.041 114.641 45.201 128.879 119 | 42.721 c 128.961 42.721 l 133.281 41.842 135.52 39.522 136.559 37.842 c 120 | 138.078 35.44 138.48 32.479 137.68 29.44 c 136.238 24.561 130.398 20.158 121 | 122.559 20.158 c h 122 | 122.559 20.158 m f 123 | 190.398 20.158 m 178.801 20.158 169.359 29.6 169.359 41.119 c 169.359 52.721 124 | 178.801 62.08 190.398 62.08 c 200.961 62.08 210 54.158 211.281 43.682 c 125 | 211.441 42.318 210.719 41.201 210.238 40.639 c 209.52 39.842 208.559 39.361 126 | 207.602 39.361 c 175.359 39.361 l 176.16 31.842 182.641 25.92 190.398 25.92 127 | c 195.602 25.92 200.879 28.639 203.121 32.4 c 203.922 33.842 205.762 34.24 128 | 207.121 33.44 c 208.559 32.639 209.039 30.799 208.16 29.44 c 204.879 23.998 129 | 197.68 20.158 190.398 20.158 c h 130 | 175.84 45.361 m 204.961 45.361 l 203.121 51.682 197.199 56.24 190.398 56.24 131 | c 183.441 56.24 177.68 51.682 175.84 45.361 c h 132 | 175.84 45.361 m f 133 | 237.762 62.158 m 231.84 62.158 226.48 59.682 222.641 55.76 c 222.641 59.279 134 | l 222.641 60.881 221.281 62.24 219.68 62.24 c 218.078 62.24 216.719 60.959 135 | 216.719 59.279 c 216.719 8.799 l 216.719 7.201 218 5.842 219.68 5.842 c 136 | 221.281 5.842 222.641 7.201 222.641 8.799 c 222.641 26.561 l 226.48 22.561 137 | 231.84 20.158 237.762 20.158 c 249.359 20.158 258.719 29.6 258.719 41.119 138 | c 258.801 52.721 249.359 62.158 237.762 62.158 c h 139 | 237.762 26.08 m 229.441 26.08 222.641 32.881 222.641 41.201 c 222.641 49.522 140 | 229.441 56.318 237.762 56.318 c 246.078 56.318 252.879 49.522 252.879 41.201 141 | c 252.879 32.881 246.078 26.08 237.762 26.08 c h 142 | 237.762 26.08 m f 143 | 310.559 20.479 m 307.68 20.479 304.801 21.76 302.16 24.24 c 300.16 26.158 144 | 287.359 39.041 286.879 39.6 c 286.32 40.158 286 40.881 286 41.682 c 286 145 | 42.479 286.32 43.201 286.879 43.76 c 304.48 60.959 l 305.68 62.08 307.52 146 | 62.08 308.641 60.881 c 309.762 59.682 309.762 57.842 308.559 56.721 c 293.039 147 | 41.6 l 296.801 37.76 304.641 29.92 306.078 28.479 c 307.68 26.959 309.199 148 | 26.24 310.641 26.318 c 312.801 26.318 l 314.398 26.318 315.762 24.959 315.762 149 | 23.361 c 315.762 21.76 314.398 20.479 312.801 20.4 c 310.801 20.4 l 310.719 150 | 20.479 310.641 20.479 310.559 20.479 c h 151 | 310.559 20.479 m f 152 | 282 20.4 m 280.398 20.4 279.039 21.682 279.039 23.361 c 279.039 73.44 l 153 | 279.039 75.041 280.398 76.4 282 76.4 c 283.602 76.4 284.961 75.119 284.961 154 | 73.44 c 284.961 23.361 l 284.961 21.682 283.68 20.4 282 20.4 c h 155 | 282 20.4 m f 156 | 268.559 68.639 m 270.641 68.639 272.32 70.318 272.32 72.4 c 272.32 74.479 157 | 270.641 76.158 268.559 76.158 c 266.48 76.158 264.801 74.479 264.801 72.4 158 | c 264.801 70.318 266.48 68.639 268.559 68.639 c h 159 | 268.559 68.639 m f* 160 | 325.199 20.4 m 327.281 20.4 328.961 22.08 328.961 24.158 c 328.961 26.24 161 | 327.281 27.92 325.199 27.92 c 323.121 27.92 321.441 26.24 321.441 24.158 162 | c 321.441 22.158 323.121 20.4 325.199 20.4 c h 163 | 325.199 20.4 m f* 164 | 405.121 59.119 m 405.121 60.721 403.922 61.92 402.398 62.08 c 401.68 62.158 165 | 400.961 62.158 400.238 62.158 c 394.32 62.158 388.961 59.682 385.121 55.76 166 | c 385.121 59.279 l 385.121 60.881 383.762 62.24 382.16 62.24 c 380.559 167 | 62.24 379.199 60.881 379.199 59.279 c 379.199 23.279 l 379.199 21.682 380.559 168 | 20.318 382.16 20.318 c 383.762 20.318 385.121 21.682 385.121 23.279 c 385.121 169 | 41.279 l 385.199 49.6 391.922 56.318 400.238 56.318 c 400.641 56.318 401.039 170 | 56.318 401.52 56.24 c 401.762 56.158 401.922 56.158 402.16 56.158 c 402.398 171 | 56.158 l 402.398 56.24 l 403.922 56.318 405.121 57.522 405.121 59.119 c 172 | h 173 | 405.121 59.119 m f* 174 | 351.84 20.158 m 340.238 20.158 330.801 29.6 330.801 41.119 c 330.801 52.721 175 | 340.238 62.08 351.84 62.08 c 363.441 62.08 372.801 52.639 372.801 41.119 176 | c 372.879 29.6 363.441 20.158 351.84 20.158 c h 177 | 351.84 56.24 m 343.52 56.24 336.719 49.44 336.719 41.119 c 336.719 32.799 178 | 343.52 25.998 351.84 25.998 c 360.16 25.998 366.961 32.799 366.961 41.119 179 | c 366.961 49.522 360.16 56.24 351.84 56.24 c h 180 | 351.84 56.24 m f 181 | 447.039 41.201 m 447.039 41.6 447.039 41.92 446.961 42.318 c 446.961 59.279 182 | l 446.961 60.881 445.68 62.24 444 62.24 c 442.398 62.24 441.039 60.959 183 | 441.039 59.279 c 441.039 55.76 l 437.199 59.682 431.84 62.158 426 62.158 184 | c 414.398 62.158 404.961 52.721 404.961 41.201 c 404.961 29.6 414.398 20.24 185 | 426 20.24 c 431.922 20.24 437.281 22.721 441.039 26.639 c 441.039 23.682 186 | l 441.039 17.522 437.359 12.24 432.078 9.92 c 430.238 9.119 428.16 8.639 187 | 425.922 8.639 c 420.719 8.639 415.441 11.279 413.199 15.201 c 412.398 16.639 188 | 410.559 17.041 409.199 16.24 c 407.762 15.44 407.281 13.6 408.16 12.24 189 | c 411.441 6.639 418.641 2.799 425.922 2.799 c 437.52 2.799 446.879 12.24 190 | 446.879 23.76 c 446.879 40.24 l 447.039 40.4 447.039 40.799 447.039 41.201 191 | c h 192 | 426.078 26.08 m 417.762 26.08 410.961 32.881 410.961 41.201 c 410.961 49.522 193 | 417.762 56.318 426.078 56.318 c 434.16 56.318 440.801 49.92 441.121 41.842 194 | c 441.121 40.561 l 440.801 32.479 434.16 26.08 426.078 26.08 c h 195 | 426.078 26.08 m f 196 | Q Q 197 | showpage 198 | %%Trailer 199 | end restore 200 | %%EOF 201 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-dropout/makefile.sh: -------------------------------------------------------------------------------- 1 | pdflatex -synctex=1 -interaction=nonstopmode course-dropout-report.tex 2 | pdflatex -synctex=1 -interaction=nonstopmode course-dropout-report.tex 3 | pdflatex -synctex=1 -interaction=nonstopmode course-dropout-report.tex 4 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-dropout/stepik.sty: -------------------------------------------------------------------------------- 1 | \NeedsTeXFormat{LaTeX2e} 2 | \ProvidesPackage{stepik}[2016/08/17 Stepik.org style file] 3 | 4 | \RequirePackage{cmap} 5 | 6 | \RequirePackage[T2A]{fontenc} 7 | \RequirePackage[utf8]{inputenc} 8 | \RequirePackage[russian]{babel} 9 | \RequirePackage[default]{opensans} 10 | 11 | \RequirePackage[pdftex]{graphicx} 12 | \RequirePackage[pdftex]{color} 13 | \RequirePackage{colortbl} 14 | 15 | \RequirePackage[paper=a4paper, margin=2.5cm, bottom=3cm]{geometry} 16 | \RequirePackage[absolute]{textpos} 17 | \RequirePackage{wallpaper} 18 | \RequirePackage{fancyhdr} 19 | 20 | \RequirePackage{tabularx} 21 | \RequirePackage{titlesec} 22 | \RequirePackage{titletoc} 23 | \RequirePackage{hyperref} 24 | \RequirePackage{verbatim} 25 | 26 | 27 | \textblockorigin{2.5cm}{2.5cm} 28 | 29 | 30 | % Define Stepik pallette 31 | \definecolor{Black}{rgb}{0, 0, 0} 32 | \definecolor{White}{rgb}{1, 1, 1} 33 | \definecolor{PastelGreen}{rgb}{0.4, 0.8, 0.4} 34 | \definecolor{BlueRibbon}{rgb}{0, 0.4, 1} 35 | \definecolor{NeonCarrot}{rgb}{1, 0.6, 0.2} 36 | \definecolor{Silver}{rgb}{0.8, 0.8, 0.8} 37 | \definecolor{GrannySmithApple}{rgb}{0.6471, 0.8981, 0.6471} 38 | % Additional text color 39 | \definecolor{GreyTextColor}{rgb}{0.367,0.367,0.367} 40 | 41 | 42 | % Change section header style 43 | \titleformat{\chapter} 44 | {\normalfont\LARGE\color{black}}{\thechapter.}{1em}{} 45 | \titleformat{\section} 46 | {\normalfont\Large\color{black}}{\thesection.}{1em}{} 47 | \renewcommand{\thesection}{\arabic{section}} 48 | \titleformat{\subsection} 49 | {\normalfont\large\color{black}}{\thesubsection.}{1em}{} 50 | 51 | \titlecontents{chapter} 52 | [2em] 53 | {} 54 | {\contentslabel{2em}} 55 | {\hspace*{-2em}} 56 | {\titlerule*[1pc]{}\contentspage} 57 | [\vspace*{0.7em}] 58 | 59 | % Indent first 60 | \let\@afterindentfalse\@afterindenttrue 61 | \@afterindenttrue 62 | 63 | 64 | % Make titlepage 65 | \def\cover#1{ 66 | \def\cover@background{#1}} 67 | 68 | \def\subtitle#1{ 69 | \def\@subtitle{#1}} 70 | 71 | \def\maketitle{ 72 | \linespread{1} 73 | \begin{titlepage} 74 | \ifdefined\cover@background{\ThisLRCornerWallPaper{1}{\cover@background}} 75 | \begin{flushleft} 76 | \vspace*{0.5em} 77 | \fontsize{40px}{48px}\selectfont\@title 78 | \end{flushleft} 79 | \begin{flushleft} 80 | \color[rgb]{0.5,0.5,0.5}\fontsize{20px}{24px}\selectfont\@subtitle 81 | \end{flushleft} 82 | \end{titlepage} 83 | \setcounter{page}{2} 84 | } 85 | 86 | % Footer 87 | % Redefine 'plain' page style to be used for chapter pages 88 | \fancypagestyle{plain}{ 89 | \renewcommand{\headrulewidth}{0pt} 90 | \renewcommand{\footrulewidth}{0pt} 91 | \fancyhf{} 92 | \fancyfoot[L]{ 93 | \includegraphics[scale=0.2]{logo.eps} 94 | } 95 | \fancyfoot[R]{\raisebox{0.35em}{\thepage}}% 96 | } 97 | \pagestyle{plain} 98 | 99 | 100 | 101 | 102 | % Hyperref options 103 | \hypersetup{ 104 | colorlinks = true, %Colours links instead of ugly boxes 105 | urlcolor = PastelGreen, %Colour for external hyperlinks 106 | linkcolor = GreyTextColor, %Colour of internal links 107 | citecolor = GreyTextColor %Colour of citations 108 | } 109 | 110 | % Change TOC title with babel 111 | \addto\captionsrussian{% 112 | \renewcommand{\contentsname}{Содержание}% 113 | } 114 | 115 | 116 | % Item description 117 | \newcommand{\stepurl}[1]{ 118 | \begin{textblock*}{\textwidth}(0pt,-12pt) 119 | \begin{flushright} 120 | \small\url{#1} 121 | \end{flushright} 122 | \end{textblock*} 123 | } 124 | 125 | 126 | \newcommand{\stepstatistics}[6]{ 127 | \section*{Задание~#1} 128 | \begin{flushright} 129 | \begin{tabular}{p{0.2\textwidth}rr} 130 | &\textcolor{black}{Тип задания}: & #6\\ 131 | &\textcolor{black}{Число ответивших}: & #5\\ 132 | &\textcolor{black}{Сложность}: & #2\\ 133 | &\textcolor{black}{Дискриминативность}: & #3\\ 134 | &\textcolor{black}{Корреляция с общим баллом}: & #4\\ 135 | \end{tabular} 136 | \end{flushright} 137 | } 138 | 139 | \def\multiplecorrect{\raisebox{-2px}{\includegraphics[scale=0.5]{common/multiplecorrect.png}}} 140 | \def\multiplewrong{\raisebox{-2px}{\includegraphics[scale=0.5]{common/multiplewrong.png}}} 141 | \def\singlecorrect{\raisebox{-2px}{\includegraphics[scale=0.5]{common/singlecorrect.png}}} 142 | \def\singlewrong{\raisebox{-2px}{\includegraphics[scale=0.5]{common/singlewrong.png}}} 143 | 144 | 145 | \newenvironment{question}{%\subsection*{Текст задания} 146 | }{} 147 | \newenvironment{code}{\endgraf\verbatim}{\endverbatim} 148 | 149 | 150 | \newenvironment{options}{%\subsection*{Варианты опций} 151 | \begin{flushright}\begin{tabular}{lp{0.8\textwidth}r} 152 | &\multicolumn{2}{r}{\parbox{0.3\textwidth} 153 | {\raggedleft 154 | \scriptsize \textcolor{black}{Сложность опции} 155 | }}\\ 156 | } 157 | {\end{tabular}\end{flushright}} 158 | \newcommand{\option}[3]{ 159 | 160 | #1 161 | & 162 | \ifnum\pdfstrcmp{#3}{0.05}=1 \relax\else\cellcolor[rgb]{1, 0.95, 0.9}\fi 163 | #2 164 | & 165 | \ifnum\pdfstrcmp{#3}{0.05}=1 \relax\else\cellcolor[rgb]{1, 0.95, 0.9}\fi 166 | %\ifnum\pdfstrcmp{#3}{0.05}=1 #3 \else\textcolor{NeonCarrot}{#3}\fi 167 | #3 168 | \\ 169 | } 170 | 171 | 172 | \newenvironment{recommendations}{\subsection*{Рекомендации}}{} 173 | 174 | % Peaks 175 | \newenvironment{peaks}{\subsection*{Пики}\begin{tabular}{ccccc} 176 | Начало пика&Конец пика&Ширина&Высота&Площадь\\} 177 | {\end{tabular}} 178 | \newcommand{\peak}[5]{ 179 | #1 & #2 & #3 & #4 & #5 \\ 180 | } 181 | 182 | \newenvironment{totalpeaks}{\par\begin{tabular}{cccc} 183 | Номер видео&Начало пика&Конец пика&Площадь\\} 184 | {\end{tabular}} 185 | \newcommand{\totalpeak}[5]{ 186 | \href{#2}{#1} & #3 & #4 & #5 \\ 187 | } 188 | 189 | 190 | % dropout report 191 | \newcommand{\lessoninfo}[2]{ 192 | \stepurl{#2} 193 | \section*{Урок~#1} 194 | } 195 | \newenvironment{lessonstatistics}{\begin{flushleft}\begin{tabular}{cccll} 196 | Шаг&Тип&Число просмотров&Процент завершения&Процент ухода\\} 197 | {\end{tabular}\end{flushleft}} 198 | \newcommand{\lessonstepstatistics}[6]{ 199 | \href{#2}{#1} & #3 & #4 & 200 | \def\temp{#5}\ifx\temp\empty\relax\else\coloredrule{BlueRibbon}{#5}\fi & 201 | \def\temp{#6}\ifx\temp\empty\relax\else\coloredrule{NeonCarrot}{#6}\fi\\ 202 | } 203 | 204 | \newlength\onepercentbox 205 | \setlength{\onepercentbox}{0.3mm} 206 | \newcommand{\coloredrule}[2]{ 207 | {\color{#1}\rule{#2\onepercentbox}{1em}~#2\% } 208 | } 209 | 210 | % Course text to intro 211 | \let\@addcoursetexttointro\relax 212 | \def\addcoursetexttointro#1{\def\@addcoursetexttointro{#1}} 213 | \def\coursetexttointro{\@addcoursetexttointro\ } 214 | 215 | 216 | % Commands for whole report 217 | \linespread{1.5} 218 | \color{GreyTextColor} 219 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/common/introduction.tex: -------------------------------------------------------------------------------- 1 | \chapter{Введение} 2 | \section*{Назначение} 3 | С помощью данного отчета можно оценить качество заданий \coursetexttointro на основе их статистических характеристик, выявить проблемные задания и внести в них изменения. В отчете выделены следующие характеристики заданий: сложность задания, его дискриминативность и корреляция с общим баллом. Также проведен анализ опций для выявления нефункциональных дистракторов~--- неверных опций, на которые правильно отвечают почти все учащиеся. 4 | 5 | \section*{Сложность задания} 6 | Сложность задания определяется как процент учащихся, которые при первой попытке неправильно ответили на данное задание. Чем больше учащихся верно ответили на задание с первой попытки, тем менее трудным оно является. Оптимальная сложность задания~--- около 0.5, т.е. около 50\% учащихся верно ответили на задание с первой попытки. 7 | 8 | \begin{flushleft} 9 | \noindent\begin{tabularx}{\textwidth}{c@{\hspace*{5em}}X} 10 | \hline 11 | Сложность задания & \multicolumn{1}{c}{Интерпретация}\\ 12 | \hline 13 | 0.75--1.00 & Сложное задание\\ 14 | 0.25--0.75 & Среднее по сложности\\ 15 | 0.00--0.25 & Легкое задание\\ 16 | \hline 17 | \end{tabularx} 18 | \end{flushleft} 19 | 20 | \section*{Дискриминативность задания} 21 | Дискриминативность (или различительная способность) задания показывает, насколько сильно различаются сильно подготовленные и слабо подготовленные учащиеся. В данном отчете дискриминативность вычисляется с применением метода крайних групп, т.е. при расчете учитываются результаты учащихся, наиболее и наименее успешно справившихся со всеми тестовыми заданиями (в каждой из этих групп~--- по 30\% учащихся). 22 | 23 | Низкая дискриминативность означает, что данное задание одинаково\linebreak успешно выполняют как сильно подготовленные учащиеся, так и слабо подготовленные. Типичными недостатками задач с низкой дискриминативностью являются: 24 | \begin{itemize} 25 | \item излишняя сложная и запутанная формулировка задания; 26 | \item неоднозначная формулировка задания; 27 | \item очевидное решение задания; 28 | \item зависимость результата от памяти или других индивидуальных особенностей учащегося, а не от уровня развития тех умений и навыков, для оценки которых разрабатывалось задание; 29 | \item абсурдность некоторых опций; 30 | \item существование опций, отмеченные как неправильные, но которые могут быть верными при некоторых обстоятельствах. 31 | \end{itemize} 32 | 33 | Если дискриминативность отрицательна, то это означает, что сильно подготовленные учащиеся отвечают на такое задание хуже, чем слабо подготовленные. Такие задания требуют более тщательной проверки: возможно, что в таких заданиях верная опция была отмечена как неверная (или наоборот). 34 | 35 | 36 | \begin{flushleft} 37 | \noindent\begin{tabularx}{\textwidth}{c@{\hspace*{2em}}X} 38 | \hline 39 | Дискриминативность & Интерпретация\\ 40 | \hline 41 | 0.30--1.00 & Высокая дискриминативность\\ 42 | 0.10--0.30 & Средняя дискриминативность\\ 43 | 0.00--0.10 & Низкая дискриминативность. Задание рекомендуется заменить\\ 44 | меньше 0.00& Отрицательная дискриминативность. Задание требует проверки или исключения\\ 45 | \hline 46 | \end{tabularx} 47 | \end{flushleft} 48 | 49 | 50 | 51 | \section*{Корреляция с общим баллом} 52 | Корреляция с общим баллом может принимать значения от -1 до 1 и показывает, насколько задание согласуется с остальными. Низкое значение корреляции (меньше 0.2) часто свидетельствует о том, что данное задание измеряет другую характеристику, нежели остальные задания. 53 | 54 | Если корреляция с общим баллом отрицательная, то это может также свидетельствовать о низкой дискриминативности задания. 55 | 56 | 57 | \section*{Анализ дистракторов} 58 | Дистрактором называется неправильная, но правдоподобная опция в заданиях с выбором одного или нескольких правильных вариантов. При анализе дистракторов следует обращать внимание на те из них, которые были выбраны менее чем 5\% учащихся, так называемые нефункциональные дистракторы, так как при удалении их из задания, его характеристики практически не меняются. 59 | 60 | Общие рекомендации при написании опций (в том числе, и дистракторов) таковы: 61 | \begin{itemize} 62 | \item верная опция не должна быть самой длинной или самой подробной, 63 | \item необходимо избегать <<частично правильных опций>>, т.е. таких опций, которые могут оказаться верными в некоторых ситуациях, 64 | \item в качестве дистракторов рекомендуется использовать типичные ошибки, 65 | \item не должно быть грамматических подсказок, например, согласование в роде и/или числе, между заданием и опциями, 66 | \item опции <<все из выше перечисленного>> и <<ничего из выше перечисленного>> не рекомендуются. 67 | \end{itemize} 68 | 69 | 70 | \chapter{Рекомендации по составлению заданий} 71 | 72 | При составлении текста задания общие рекоммендации следующие: 73 | \begin{itemize} 74 | \item Задания должны быть связаны с учебными целями, т.е. со знаниями и навыками, которыми должен обладать учащийся после окончания курса. 75 | \item Текст задания должен исключать всякую двусмысленность и неясность формулировок. 76 | \item Текст задания формулируется предельно кратко, но без ущерба для понимания. 77 | \item Используемая в заданиях терминология не должна выходить за рамки курса. 78 | \item Если задание содержит отрицание, то частицу НЕ или слово, выражающее отрицание, необходимо выделить (например, заглавными буквами или жирным текстом с подчеркиванием). 79 | \item Текст задания должен исключать сложные синтаксические обороты, в том числе, двойное отрицание. 80 | \item В тексте задании не используются слова, которые могут вызвать различное понимание учащихся, а также слова <<иногда>>, <<часто>>, <<всегда>>, <<все>>, <<никогда>>. 81 | \end{itemize} 82 | 83 | При составлении текста ответа (опций) следует руководствоваться следующими рекомендациями: 84 | \begin{itemize} 85 | \item По возможности, не следует делать текст ответов длинным. 86 | \item Из ответов, как правило, исключаются все повторяющиеся слова путем ввода их в основной текст задания. 87 | \item Все ответы должны быть похожими как по внешнему виду, так и по грамматической структуре. 88 | \item Все ответы должны быть грамматически согласованными с основной частью задания. 89 | \item Ответы должны быть примерно равны по длине: не следует формулировать правильный ответ заметно длиннее или короче, чем неправильные. 90 | \item Одно и то же слово (или словосочетание, или однокоренное слово) не должно находиться и в тексте задания, и в правильном ответе. 91 | \item Каждый неправильный ответ должен быть правдоподобным, внушающим доверие и убедительным для учащихся. 92 | \item В неправильных ответах не следует использовать слова или термины, которые учащийся не может или не должен знать, в том числе, не относящиеся к содержанию курса. 93 | \item Исключаются ответы, вытекающие один из другого или дополняющие друг друга. 94 | \item При формулировке ответов не следует использовать выражения <<все перечисленные>>, <<ни один из перечисленных>>, <<все, кроме...>> или их аналоги. 95 | \item Если ответы выражены в виде чисел, то рекомендуется располагать эти числа от меньшего к большему или наоборот. 96 | \item В задании на выбор нескольких правильных ответов не должны быть все ответы правильные или только один правильный (учащиеся ожидают два и более правильных ответа, но не все из них). 97 | \end{itemize} 98 | 99 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/common/methodology.tex: -------------------------------------------------------------------------------- 1 | Далее приведены результаты анализа заданий на выбор из нескольких вариантов. Карточка задания состоит из номера задания, ссылки на задание, статистических характеристик, текста задания и вариантов опций с указанной сложностью опций. 2 | 3 | Если задание является проблемным, то приводятся рекомендации относительно того, изменить ли задание (например, заменой нескольких опций) или полностью его исключить. -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/common/multiplecorrect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/latex/default-item/common/multiplecorrect.png -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/common/multiplewrong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/latex/default-item/common/multiplewrong.png -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/common/singlecorrect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/latex/default-item/common/singlecorrect.png -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/common/singlewrong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/latex/default-item/common/singlewrong.png -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/course-item-report.tex: -------------------------------------------------------------------------------- 1 | \documentclass[12pt]{report} 2 | \usepackage{stepik} 3 | \usepackage{ifsym} 4 | 5 | \input{generated/info} 6 | 7 | \title{Анализ заданий по~курсу} 8 | \subtitle{<<\coursetitle>>} 9 | \cover{cover.eps} 10 | \addcoursetexttointro{в курсе \textbf{<<\coursetitle>>} (\url{\courseurl})} 11 | 12 | \begin{document} 13 | 14 | \maketitle 15 | \tableofcontents 16 | 17 | 18 | \input{common/introduction} 19 | 20 | \chapter{Анализ заданий} 21 | \input{common/methodology} 22 | 23 | \parindent=0pt 24 | \input{generated/map} 25 | 26 | 27 | \end{document} 28 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/cover.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/latex/default-item/cover.eps -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/generated/test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/latex/default-item/generated/test -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/logo.eps: -------------------------------------------------------------------------------- 1 | %!PS-Adobe-3.0 EPSF-3.0 2 | %%Creator: cairo 1.14.0 (http://cairographics.org) 3 | %%CreationDate: Fri Sep 9 13:24:44 2016 4 | %%Pages: 1 5 | %%DocumentData: Clean7Bit 6 | %%LanguageLevel: 2 7 | %%BoundingBox: 0 -1 448 80 8 | %%EndComments 9 | %%BeginProlog 10 | save 11 | 50 dict begin 12 | /q { gsave } bind def 13 | /Q { grestore } bind def 14 | /cm { 6 array astore concat } bind def 15 | /w { setlinewidth } bind def 16 | /J { setlinecap } bind def 17 | /j { setlinejoin } bind def 18 | /M { setmiterlimit } bind def 19 | /d { setdash } bind def 20 | /m { moveto } bind def 21 | /l { lineto } bind def 22 | /c { curveto } bind def 23 | /h { closepath } bind def 24 | /re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto 25 | 0 exch rlineto 0 rlineto closepath } bind def 26 | /S { stroke } bind def 27 | /f { fill } bind def 28 | /f* { eofill } bind def 29 | /n { newpath } bind def 30 | /W { clip } bind def 31 | /W* { eoclip } bind def 32 | /BT { } bind def 33 | /ET { } bind def 34 | /pdfmark where { pop globaldict /?pdfmark /exec load put } 35 | { globaldict begin /?pdfmark /pop load def /pdfmark 36 | /cleartomark load def end } ifelse 37 | /BDC { mark 3 1 roll /BDC pdfmark } bind def 38 | /EMC { mark /EMC pdfmark } bind def 39 | /cairo_store_point { /cairo_point_y exch def /cairo_point_x exch def } def 40 | /Tj { show currentpoint cairo_store_point } bind def 41 | /TJ { 42 | { 43 | dup 44 | type /stringtype eq 45 | { show } { -0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse 46 | } forall 47 | currentpoint cairo_store_point 48 | } bind def 49 | /cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore 50 | cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def 51 | /Tf { pop /cairo_font exch def /cairo_font_matrix where 52 | { pop cairo_selectfont } if } bind def 53 | /Td { matrix translate cairo_font_matrix matrix concatmatrix dup 54 | /cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point 55 | /cairo_font where { pop cairo_selectfont } if } bind def 56 | /Tm { 2 copy 8 2 roll 6 array astore /cairo_font_matrix exch def 57 | cairo_store_point /cairo_font where { pop cairo_selectfont } if } bind def 58 | /g { setgray } bind def 59 | /rg { setrgbcolor } bind def 60 | /d1 { setcachedevice } bind def 61 | %%EndProlog 62 | %%BeginSetup 63 | %%EndSetup 64 | %%Page: 1 1 65 | %%BeginPageSetup 66 | %%PageBoundingBox: 0 -1 448 80 67 | %%EndPageSetup 68 | q 0 -1 448 81 rectclip q 69 | 0.8 g 70 | 72.078 58.959 m 58.961 72.08 l 53.922 77.201 47.121 79.92 40 79.92 c 32.801 71 | 79.92 26.078 77.119 21.039 72.08 c 7.84 58.959 l 2.719 53.92 0 47.119 0 72 | 39.998 c 0 32.799 2.801 26.08 7.84 21.041 c 20.961 7.92 l 26.078 2.799 73 | 32.801 -0.002 39.922 -0.002 c 46.879 -0.002 53.52 2.639 58.559 7.522 c 72 74 | 20.959 l 77.121 25.998 79.84 32.799 79.84 39.92 c 80 47.201 77.199 53.92 75 | 72.078 58.959 c h 76 | 63.359 30.479 m 50 17.119 l 47.52 14.721 44.078 13.201 40.398 13.201 c 77 | 36.559 13.201 33.121 14.721 30.559 17.279 c 37.039 23.76 l 37.922 22.881 78 | 39.039 22.4 40.398 22.4 c 41.762 22.4 42.879 22.959 43.762 23.76 c 56.961 79 | 36.881 l 57.762 37.76 58.32 38.881 58.32 40.24 c 58.32 41.522 57.762 42.721 80 | 56.961 43.6 c 56.078 44.479 54.961 44.959 53.602 44.959 c 52.32 44.959 81 | 51.121 44.4 50.238 43.6 c 37.121 30.479 l 34.641 27.998 31.121 26.4 27.281 82 | 26.4 c 23.441 26.4 20 27.998 17.441 30.479 c 14.961 32.959 13.359 36.479 83 | 13.359 40.318 c 13.359 44.158 14.879 47.6 17.441 50.158 c 30.559 63.279 84 | l 33.039 65.76 36.559 67.361 40.398 67.361 c 44.238 67.361 47.68 65.76 85 | 50.238 63.279 c 43.762 56.799 l 42.879 57.682 41.762 58.158 40.398 58.158 86 | c 39.039 58.158 37.922 57.682 37.039 56.799 c 23.922 43.6 l 23.039 42.721 87 | 22.559 41.6 22.559 40.24 c 22.559 38.959 23.121 37.76 23.922 36.881 c 24.801 88 | 35.998 25.922 35.522 27.281 35.522 c 28.559 35.522 29.762 36.08 30.641 89 | 36.881 c 43.762 49.998 l 46.32 52.479 49.762 54.08 53.602 54.08 c 57.441 90 | 54.08 60.879 52.561 63.441 49.998 c 65.922 47.44 67.52 43.998 67.52 40.158 91 | c 67.441 36.479 65.84 33.041 63.359 30.479 c h 92 | 63.359 30.479 m f 93 | 268.48 20.318 m 266.879 20.318 265.52 21.682 265.52 23.279 c 265.52 59.682 94 | l 265.52 61.279 266.801 62.639 268.48 62.639 c 270.078 62.639 271.441 61.361 95 | 271.441 59.682 c 271.441 23.279 l 271.441 21.682 270.16 20.318 268.48 20.318 96 | c h 97 | 268.48 20.318 m f 98 | 167.762 28.721 m 166.559 29.682 164.801 29.522 163.84 28.318 c 161.199 99 | 25.041 154.641 25.842 152.961 26.561 c 150.48 27.522 149.281 29.6 149.281 100 | 32.639 c 149.281 52.561 l 162.32 52.561 l 163.922 52.561 165.281 53.842 101 | 165.281 55.522 c 165.281 57.119 164 58.479 162.32 58.479 c 149.281 58.479 102 | l 149.281 73.361 l 149.281 74.881 148.078 76.158 146.48 76.158 c 144.961 103 | 76.158 143.762 74.959 143.762 73.361 c 143.762 32.639 l 143.762 27.361 104 | 146.32 23.279 150.879 21.44 c 152.32 20.881 154.559 20.4 157.039 20.4 c 105 | 160.961 20.4 165.441 21.522 168.16 24.881 c 169.121 26.08 168.961 27.76 106 | 167.762 28.721 c h 107 | 167.762 28.721 m f 108 | 122.559 20.158 m 121.68 20.158 120.719 20.24 119.84 20.318 c 110.238 21.522 109 | 104.48 25.998 104.48 32.158 c 104.48 33.76 105.762 35.119 107.441 35.119 110 | c 109.039 35.119 110.398 33.76 110.398 32.158 c 110.398 29.279 114.398 111 | 26.959 120.641 26.158 c 126.879 25.361 131.281 28.479 131.922 30.881 c 132.32 112 | 32.318 132.16 33.6 131.52 34.639 c 130.801 35.76 129.602 36.479 127.762 113 | 36.881 c 115.199 39.119 106.879 41.201 105.68 49.119 c 105.281 51.998 106.078 114 | 54.881 108 57.119 c 110.719 60.318 115.441 62.158 120.961 62.158 c 131.039 115 | 62.158 137.602 57.682 137.602 50.639 c 137.602 49.041 136.238 47.682 134.641 116 | 47.682 c 133.039 47.682 131.68 49.041 131.68 50.639 c 131.68 54.799 125.922 117 | 56.24 120.961 56.24 c 117.281 56.24 114.078 55.119 112.48 53.279 c 111.68 118 | 52.318 111.281 51.201 111.52 49.998 c 112 47.041 114.641 45.201 128.879 119 | 42.721 c 128.961 42.721 l 133.281 41.842 135.52 39.522 136.559 37.842 c 120 | 138.078 35.44 138.48 32.479 137.68 29.44 c 136.238 24.561 130.398 20.158 121 | 122.559 20.158 c h 122 | 122.559 20.158 m f 123 | 190.398 20.158 m 178.801 20.158 169.359 29.6 169.359 41.119 c 169.359 52.721 124 | 178.801 62.08 190.398 62.08 c 200.961 62.08 210 54.158 211.281 43.682 c 125 | 211.441 42.318 210.719 41.201 210.238 40.639 c 209.52 39.842 208.559 39.361 126 | 207.602 39.361 c 175.359 39.361 l 176.16 31.842 182.641 25.92 190.398 25.92 127 | c 195.602 25.92 200.879 28.639 203.121 32.4 c 203.922 33.842 205.762 34.24 128 | 207.121 33.44 c 208.559 32.639 209.039 30.799 208.16 29.44 c 204.879 23.998 129 | 197.68 20.158 190.398 20.158 c h 130 | 175.84 45.361 m 204.961 45.361 l 203.121 51.682 197.199 56.24 190.398 56.24 131 | c 183.441 56.24 177.68 51.682 175.84 45.361 c h 132 | 175.84 45.361 m f 133 | 237.762 62.158 m 231.84 62.158 226.48 59.682 222.641 55.76 c 222.641 59.279 134 | l 222.641 60.881 221.281 62.24 219.68 62.24 c 218.078 62.24 216.719 60.959 135 | 216.719 59.279 c 216.719 8.799 l 216.719 7.201 218 5.842 219.68 5.842 c 136 | 221.281 5.842 222.641 7.201 222.641 8.799 c 222.641 26.561 l 226.48 22.561 137 | 231.84 20.158 237.762 20.158 c 249.359 20.158 258.719 29.6 258.719 41.119 138 | c 258.801 52.721 249.359 62.158 237.762 62.158 c h 139 | 237.762 26.08 m 229.441 26.08 222.641 32.881 222.641 41.201 c 222.641 49.522 140 | 229.441 56.318 237.762 56.318 c 246.078 56.318 252.879 49.522 252.879 41.201 141 | c 252.879 32.881 246.078 26.08 237.762 26.08 c h 142 | 237.762 26.08 m f 143 | 310.559 20.479 m 307.68 20.479 304.801 21.76 302.16 24.24 c 300.16 26.158 144 | 287.359 39.041 286.879 39.6 c 286.32 40.158 286 40.881 286 41.682 c 286 145 | 42.479 286.32 43.201 286.879 43.76 c 304.48 60.959 l 305.68 62.08 307.52 146 | 62.08 308.641 60.881 c 309.762 59.682 309.762 57.842 308.559 56.721 c 293.039 147 | 41.6 l 296.801 37.76 304.641 29.92 306.078 28.479 c 307.68 26.959 309.199 148 | 26.24 310.641 26.318 c 312.801 26.318 l 314.398 26.318 315.762 24.959 315.762 149 | 23.361 c 315.762 21.76 314.398 20.479 312.801 20.4 c 310.801 20.4 l 310.719 150 | 20.479 310.641 20.479 310.559 20.479 c h 151 | 310.559 20.479 m f 152 | 282 20.4 m 280.398 20.4 279.039 21.682 279.039 23.361 c 279.039 73.44 l 153 | 279.039 75.041 280.398 76.4 282 76.4 c 283.602 76.4 284.961 75.119 284.961 154 | 73.44 c 284.961 23.361 l 284.961 21.682 283.68 20.4 282 20.4 c h 155 | 282 20.4 m f 156 | 268.559 68.639 m 270.641 68.639 272.32 70.318 272.32 72.4 c 272.32 74.479 157 | 270.641 76.158 268.559 76.158 c 266.48 76.158 264.801 74.479 264.801 72.4 158 | c 264.801 70.318 266.48 68.639 268.559 68.639 c h 159 | 268.559 68.639 m f* 160 | 325.199 20.4 m 327.281 20.4 328.961 22.08 328.961 24.158 c 328.961 26.24 161 | 327.281 27.92 325.199 27.92 c 323.121 27.92 321.441 26.24 321.441 24.158 162 | c 321.441 22.158 323.121 20.4 325.199 20.4 c h 163 | 325.199 20.4 m f* 164 | 405.121 59.119 m 405.121 60.721 403.922 61.92 402.398 62.08 c 401.68 62.158 165 | 400.961 62.158 400.238 62.158 c 394.32 62.158 388.961 59.682 385.121 55.76 166 | c 385.121 59.279 l 385.121 60.881 383.762 62.24 382.16 62.24 c 380.559 167 | 62.24 379.199 60.881 379.199 59.279 c 379.199 23.279 l 379.199 21.682 380.559 168 | 20.318 382.16 20.318 c 383.762 20.318 385.121 21.682 385.121 23.279 c 385.121 169 | 41.279 l 385.199 49.6 391.922 56.318 400.238 56.318 c 400.641 56.318 401.039 170 | 56.318 401.52 56.24 c 401.762 56.158 401.922 56.158 402.16 56.158 c 402.398 171 | 56.158 l 402.398 56.24 l 403.922 56.318 405.121 57.522 405.121 59.119 c 172 | h 173 | 405.121 59.119 m f* 174 | 351.84 20.158 m 340.238 20.158 330.801 29.6 330.801 41.119 c 330.801 52.721 175 | 340.238 62.08 351.84 62.08 c 363.441 62.08 372.801 52.639 372.801 41.119 176 | c 372.879 29.6 363.441 20.158 351.84 20.158 c h 177 | 351.84 56.24 m 343.52 56.24 336.719 49.44 336.719 41.119 c 336.719 32.799 178 | 343.52 25.998 351.84 25.998 c 360.16 25.998 366.961 32.799 366.961 41.119 179 | c 366.961 49.522 360.16 56.24 351.84 56.24 c h 180 | 351.84 56.24 m f 181 | 447.039 41.201 m 447.039 41.6 447.039 41.92 446.961 42.318 c 446.961 59.279 182 | l 446.961 60.881 445.68 62.24 444 62.24 c 442.398 62.24 441.039 60.959 183 | 441.039 59.279 c 441.039 55.76 l 437.199 59.682 431.84 62.158 426 62.158 184 | c 414.398 62.158 404.961 52.721 404.961 41.201 c 404.961 29.6 414.398 20.24 185 | 426 20.24 c 431.922 20.24 437.281 22.721 441.039 26.639 c 441.039 23.682 186 | l 441.039 17.522 437.359 12.24 432.078 9.92 c 430.238 9.119 428.16 8.639 187 | 425.922 8.639 c 420.719 8.639 415.441 11.279 413.199 15.201 c 412.398 16.639 188 | 410.559 17.041 409.199 16.24 c 407.762 15.44 407.281 13.6 408.16 12.24 189 | c 411.441 6.639 418.641 2.799 425.922 2.799 c 437.52 2.799 446.879 12.24 190 | 446.879 23.76 c 446.879 40.24 l 447.039 40.4 447.039 40.799 447.039 41.201 191 | c h 192 | 426.078 26.08 m 417.762 26.08 410.961 32.881 410.961 41.201 c 410.961 49.522 193 | 417.762 56.318 426.078 56.318 c 434.16 56.318 440.801 49.92 441.121 41.842 194 | c 441.121 40.561 l 440.801 32.479 434.16 26.08 426.078 26.08 c h 195 | 426.078 26.08 m f 196 | Q Q 197 | showpage 198 | %%Trailer 199 | end restore 200 | %%EOF 201 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/makefile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | pdflatex -synctex=1 -interaction=nonstopmode course-item-report.tex 3 | pdflatex -synctex=1 -interaction=nonstopmode course-item-report.tex 4 | pdflatex -synctex=1 -interaction=nonstopmode course-item-report.tex 5 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-item/stepik.sty: -------------------------------------------------------------------------------- 1 | \NeedsTeXFormat{LaTeX2e} 2 | \ProvidesPackage{stepik}[2016/08/17 Stepik.org style file] 3 | 4 | \RequirePackage{cmap} 5 | 6 | \RequirePackage[T2A]{fontenc} 7 | \RequirePackage[utf8]{inputenc} 8 | \RequirePackage[russian]{babel} 9 | \RequirePackage[default]{opensans} 10 | 11 | \RequirePackage[pdftex]{graphicx} 12 | \RequirePackage[pdftex]{color} 13 | \RequirePackage{colortbl} 14 | 15 | \RequirePackage[paper=a4paper, margin=2.5cm, bottom=3cm]{geometry} 16 | \RequirePackage[absolute]{textpos} 17 | \RequirePackage{wallpaper} 18 | \RequirePackage{fancyhdr} 19 | 20 | \RequirePackage{tabularx} 21 | \RequirePackage{titlesec} 22 | \RequirePackage{titletoc} 23 | \RequirePackage{hyperref} 24 | \RequirePackage{verbatim} 25 | 26 | 27 | \textblockorigin{2.5cm}{2.5cm} 28 | 29 | 30 | % Define Stepik pallette 31 | \definecolor{Black}{rgb}{0, 0, 0} 32 | \definecolor{White}{rgb}{1, 1, 1} 33 | \definecolor{PastelGreen}{rgb}{0.4, 0.8, 0.4} 34 | \definecolor{BlueRibbon}{rgb}{0, 0.4, 1} 35 | \definecolor{NeonCarrot}{rgb}{1, 0.6, 0.2} 36 | \definecolor{Silver}{rgb}{0.8, 0.8, 0.8} 37 | \definecolor{GrannySmithApple}{rgb}{0.6471, 0.8981, 0.6471} 38 | % Additional text color 39 | \definecolor{GreyTextColor}{rgb}{0.367,0.367,0.367} 40 | 41 | 42 | % Change section header style 43 | \titleformat{\chapter} 44 | {\normalfont\LARGE\color{black}}{\thechapter.}{1em}{} 45 | \titleformat{\section} 46 | {\normalfont\Large\color{black}}{\thesection.}{1em}{} 47 | \renewcommand{\thesection}{\arabic{section}} 48 | \titleformat{\subsection} 49 | {\normalfont\large\color{black}}{\thesubsection.}{1em}{} 50 | 51 | \titlecontents{chapter} 52 | [2em] 53 | {} 54 | {\contentslabel{2em}} 55 | {\hspace*{-2em}} 56 | {\titlerule*[1pc]{}\contentspage} 57 | [\vspace*{0.7em}] 58 | 59 | % Indent first 60 | \let\@afterindentfalse\@afterindenttrue 61 | \@afterindenttrue 62 | 63 | 64 | % Make titlepage 65 | \def\cover#1{ 66 | \def\cover@background{#1}} 67 | 68 | \def\subtitle#1{ 69 | \def\@subtitle{#1}} 70 | 71 | \def\maketitle{ 72 | \linespread{1} 73 | \begin{titlepage} 74 | \ifdefined\cover@background{\ThisLRCornerWallPaper{1}{\cover@background}} 75 | \begin{flushleft} 76 | \vspace*{0.5em} 77 | \fontsize{40px}{48px}\selectfont\@title 78 | \end{flushleft} 79 | \begin{flushleft} 80 | \color[rgb]{0.5,0.5,0.5}\fontsize{20px}{24px}\selectfont\@subtitle 81 | \end{flushleft} 82 | \end{titlepage} 83 | \setcounter{page}{2} 84 | } 85 | 86 | % Footer 87 | % Redefine 'plain' page style to be used for chapter pages 88 | \fancypagestyle{plain}{ 89 | \renewcommand{\headrulewidth}{0pt} 90 | \renewcommand{\footrulewidth}{0pt} 91 | \fancyhf{} 92 | \fancyfoot[L]{ 93 | \includegraphics[scale=0.2]{logo.eps} 94 | } 95 | \fancyfoot[R]{\raisebox{0.35em}{\thepage}}% 96 | } 97 | \pagestyle{plain} 98 | 99 | 100 | 101 | 102 | % Hyperref options 103 | \hypersetup{ 104 | colorlinks = true, %Colours links instead of ugly boxes 105 | urlcolor = PastelGreen, %Colour for external hyperlinks 106 | linkcolor = GreyTextColor, %Colour of internal links 107 | citecolor = GreyTextColor %Colour of citations 108 | } 109 | 110 | % Change TOC title with babel 111 | \addto\captionsrussian{% 112 | \renewcommand{\contentsname}{Содержание}% 113 | } 114 | 115 | 116 | % Item description 117 | \newcommand{\stepurl}[1]{ 118 | \begin{textblock*}{\textwidth}(0pt,-12pt) 119 | \begin{flushright} 120 | \small\url{#1} 121 | \end{flushright} 122 | \end{textblock*} 123 | } 124 | 125 | 126 | \newcommand{\stepstatistics}[6]{ 127 | \section*{Задание~#1} 128 | \begin{flushright} 129 | \begin{tabular}{p{0.2\textwidth}rr} 130 | &\textcolor{black}{Тип задания}: & #6\\ 131 | &\textcolor{black}{Число ответивших}: & #5\\ 132 | &\textcolor{black}{Сложность}: & #2\\ 133 | &\textcolor{black}{Дискриминативность}: & #3\\ 134 | &\textcolor{black}{Корреляция с общим баллом}: & #4\\ 135 | \end{tabular} 136 | \end{flushright} 137 | } 138 | 139 | \def\multiplecorrect{\raisebox{-2px}{\includegraphics[scale=0.5]{common/multiplecorrect.png}}} 140 | \def\multiplewrong{\raisebox{-2px}{\includegraphics[scale=0.5]{common/multiplewrong.png}}} 141 | \def\singlecorrect{\raisebox{-2px}{\includegraphics[scale=0.5]{common/singlecorrect.png}}} 142 | \def\singlewrong{\raisebox{-2px}{\includegraphics[scale=0.5]{common/singlewrong.png}}} 143 | 144 | 145 | \newenvironment{question}{%\subsection*{Текст задания} 146 | }{} 147 | \newenvironment{code}{\endgraf\verbatim}{\endverbatim} 148 | 149 | 150 | \newenvironment{options}{%\subsection*{Варианты опций} 151 | \begin{flushright}\begin{tabular}{lp{0.8\textwidth}r} 152 | &\multicolumn{2}{r}{\parbox{0.3\textwidth} 153 | {\raggedleft 154 | \scriptsize \textcolor{black}{Сложность опции} 155 | }}\\ 156 | } 157 | {\end{tabular}\end{flushright}} 158 | \newcommand{\option}[3]{ 159 | 160 | #1 161 | & 162 | \ifnum\pdfstrcmp{#3}{0.05}=1 \relax\else\cellcolor[rgb]{1, 0.95, 0.9}\fi 163 | #2 164 | & 165 | \ifnum\pdfstrcmp{#3}{0.05}=1 \relax\else\cellcolor[rgb]{1, 0.95, 0.9}\fi 166 | %\ifnum\pdfstrcmp{#3}{0.05}=1 #3 \else\textcolor{NeonCarrot}{#3}\fi 167 | #3 168 | \\ 169 | } 170 | 171 | 172 | \newenvironment{recommendations}{\subsection*{Рекомендации}}{} 173 | 174 | % Peaks 175 | \newenvironment{peaks}{\subsection*{Пики}\begin{tabular}{ccccc} 176 | Начало пика&Конец пика&Ширина&Высота&Площадь\\} 177 | {\end{tabular}} 178 | \newcommand{\peak}[5]{ 179 | #1 & #2 & #3 & #4 & #5 \\ 180 | } 181 | 182 | \newenvironment{totalpeaks}{\par\begin{tabular}{cccc} 183 | Номер видео&Начало пика&Конец пика&Площадь\\} 184 | {\end{tabular}} 185 | \newcommand{\totalpeak}[5]{ 186 | \href{#2}{#1} & #3 & #4 & #5 \\ 187 | } 188 | 189 | 190 | % dropout report 191 | \newcommand{\lessoninfo}[2]{ 192 | \stepurl{#2} 193 | \section*{Урок~#1} 194 | } 195 | \newenvironment{lessonstatistics}{\begin{flushleft}\begin{tabular}{cccll} 196 | Шаг&Тип&Число просмотров&Процент завершения&Процент ухода\\} 197 | {\end{tabular}\end{flushleft}} 198 | \newcommand{\lessonstepstatistics}[6]{ 199 | \href{#2}{#1} & #3 & #4 & 200 | \def\temp{#5}\ifx\temp\empty\relax\else\coloredrule{BlueRibbon}{#5}\fi & 201 | \def\temp{#6}\ifx\temp\empty\relax\else\coloredrule{NeonCarrot}{#6}\fi\\ 202 | } 203 | 204 | \newlength\onepercentbox 205 | \setlength{\onepercentbox}{0.3mm} 206 | \newcommand{\coloredrule}[2]{ 207 | {\color{#1}\rule{#2\onepercentbox}{1em}~#2\% } 208 | } 209 | 210 | % Course text to intro 211 | \let\@addcoursetexttointro\relax 212 | \def\addcoursetexttointro#1{\def\@addcoursetexttointro{#1}} 213 | \def\coursetexttointro{\@addcoursetexttointro\ } 214 | 215 | 216 | % Commands for whole report 217 | \linespread{1.5} 218 | \color{GreyTextColor} 219 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-video/common/introduction.tex: -------------------------------------------------------------------------------- 1 | \chapter{Введение} 2 | \section*{Назначение} 3 | С помощью данного отчета можно оценить взаимодействие учащихся с учебным видео \coursetexttointro, в частности, когда достаточно большое количество учащихся показывают аналогичные модели взаимодействия в течение короткого промежутка времени (так называемые пики взаимодействия). 4 | 5 | \section*{Методология} 6 | 7 | Методология для данного отчета основана на статье [Kim, J. et al., 2014]. 8 | 9 | Для анализа взаимодействия учащихся с видео используется два типа данных: 10 | \begin{enumerate} 11 | \item Повторные просмотры видео (rewatching). При анализе данных исключается первый просмотр видео. Отметим, что эта метрика не считается за уникального учащегося: если учащийся пересматривал некоторую часть видео три раза, то и метрика увеличивается на три за этот временной период. 12 | \item Нажатие на Play (play events). При этом, игнорируется автозапуск в начале видео, так как он либо возникает автоматически, либо из-за необходимости начать смотреть видео, а не за счет выбора учащихся. 13 | \end{enumerate} 14 | 15 | Исходные данные о взаимодействии учащихся с видео содержит шумы. Идентификация пиков в зашумленных данных как вручную, так и автоматически становится затруднительным из-за локальных максимумов и ложных пиков. Поэтому сначала производится сглаживание данных с помощью метода LOWESS (locally weighted scatterplot smoothing) со специально подобранными параметрами сглаживания после тестирования различных значений. 16 | 17 | После сглаживания, применяется алгоритм обнаружения пиков к обоим типам данных: повторные просмотры видео и нажатие на Play. Используемый алгоритм является вариантом алгоритма TwitInfo [Marcus, A. et al., 2011]. Он использует взвешенное скользящее среднее и дисперсию для обнаружения необычно большого количества событий в данных временного ряда, что хорошо согласуется с данными для видео. 18 | 19 | Одной из причин, почему используется оба типа данных, является то, что они могут обнаружить различные модели поведения учащихся: нажатие на Play может обнаружить краткосрочные, импульсивные модели поведения, в то время как повторному просмотру соответствуют более длительные промежутки времени. В случае, если пики для повторного просмотра и для нажатия на Play перекрываются, считается, что они указывают на одно и то же событие. При этом, выбирается пик для повторного просмотра, так как он более информативный и его легче интерпретировать. 20 | 21 | % TODO: peak features 22 | %Особенности пика, такие как ширина, высота и площадь, может указывать на прочность коллектива, времени конкретных интересов студентов. Мы сравниваем эти функции между типами видео и студенческих контекстах. Предыдущая работа рассматриваются аналогичные конструкции в моделировании временных профилей поисковых запросов [12]. пик характеризуется описательных свойствами, как показано на рисунке 4. Она включает в себя как запуск и маркеры окончания времени, которые определяют продолжительность ширины или время пика. точка пик это самая высокая точка между [начала , конец] диапазон, который определяет высоту. И, наконец, площадь под пиком является суммой отсчетов событий во время пика временного окна, которое обозначает относительную значимость пика против всего видео. 23 | 24 | %Несколько пиков различной профили могут появиться в видеоклипе. В отчетности высота, ширина, и область, нормировать значения путем масштабирования между 0 и 1 для решения высокой изменчивости подсчета событий и длительности через видео. Для ширины, высоты и области , мы возьмем нормированный диапазон от длительности видео, максимальное количество событий, и сумма всех отсчетов событий, соответственно. -------------------------------------------------------------------------------- /examples/external-reports/latex/default-video/common/methodology.tex: -------------------------------------------------------------------------------- 1 | Далее приведены результаты анализа заданий на выбор из нескольких вариантов. Карточка задания состоит из номера задания, ссылки на задание, статистических характеристик, текста задания и вариантов опций с указанной сложностью опций. 2 | 3 | Если задание является проблемным, то приводятся рекомендации относительно того, изменить ли задание (например, заменой нескольких опций) или полностью его исключить. -------------------------------------------------------------------------------- /examples/external-reports/latex/default-video/course-video-report.tex: -------------------------------------------------------------------------------- 1 | \documentclass[12pt]{report} 2 | \usepackage{stepik} 3 | \usepackage{ifsym} 4 | 5 | \input{generated/info} 6 | 7 | \title{Анализ видео на~курсе} 8 | \subtitle{<<\coursetitle>>} 9 | \cover{cover.eps} 10 | \addcoursetexttointro{в курсе \textbf{<<\coursetitle>>} (\url{\courseurl})} 11 | 12 | \begin{document} 13 | 14 | \maketitle 15 | \tableofcontents 16 | 17 | \input{common/introduction} 18 | 19 | \chapter{Анализ видео} 20 | \input{generated/total} 21 | \input{generated/map} 22 | 23 | \end{document} 24 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-video/cover.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/latex/default-video/cover.eps -------------------------------------------------------------------------------- /examples/external-reports/latex/default-video/generated/test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/latex/default-video/generated/test -------------------------------------------------------------------------------- /examples/external-reports/latex/default-video/logo.eps: -------------------------------------------------------------------------------- 1 | %!PS-Adobe-3.0 EPSF-3.0 2 | %%Creator: cairo 1.14.0 (http://cairographics.org) 3 | %%CreationDate: Fri Sep 9 13:24:44 2016 4 | %%Pages: 1 5 | %%DocumentData: Clean7Bit 6 | %%LanguageLevel: 2 7 | %%BoundingBox: 0 -1 448 80 8 | %%EndComments 9 | %%BeginProlog 10 | save 11 | 50 dict begin 12 | /q { gsave } bind def 13 | /Q { grestore } bind def 14 | /cm { 6 array astore concat } bind def 15 | /w { setlinewidth } bind def 16 | /J { setlinecap } bind def 17 | /j { setlinejoin } bind def 18 | /M { setmiterlimit } bind def 19 | /d { setdash } bind def 20 | /m { moveto } bind def 21 | /l { lineto } bind def 22 | /c { curveto } bind def 23 | /h { closepath } bind def 24 | /re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto 25 | 0 exch rlineto 0 rlineto closepath } bind def 26 | /S { stroke } bind def 27 | /f { fill } bind def 28 | /f* { eofill } bind def 29 | /n { newpath } bind def 30 | /W { clip } bind def 31 | /W* { eoclip } bind def 32 | /BT { } bind def 33 | /ET { } bind def 34 | /pdfmark where { pop globaldict /?pdfmark /exec load put } 35 | { globaldict begin /?pdfmark /pop load def /pdfmark 36 | /cleartomark load def end } ifelse 37 | /BDC { mark 3 1 roll /BDC pdfmark } bind def 38 | /EMC { mark /EMC pdfmark } bind def 39 | /cairo_store_point { /cairo_point_y exch def /cairo_point_x exch def } def 40 | /Tj { show currentpoint cairo_store_point } bind def 41 | /TJ { 42 | { 43 | dup 44 | type /stringtype eq 45 | { show } { -0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse 46 | } forall 47 | currentpoint cairo_store_point 48 | } bind def 49 | /cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore 50 | cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def 51 | /Tf { pop /cairo_font exch def /cairo_font_matrix where 52 | { pop cairo_selectfont } if } bind def 53 | /Td { matrix translate cairo_font_matrix matrix concatmatrix dup 54 | /cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point 55 | /cairo_font where { pop cairo_selectfont } if } bind def 56 | /Tm { 2 copy 8 2 roll 6 array astore /cairo_font_matrix exch def 57 | cairo_store_point /cairo_font where { pop cairo_selectfont } if } bind def 58 | /g { setgray } bind def 59 | /rg { setrgbcolor } bind def 60 | /d1 { setcachedevice } bind def 61 | %%EndProlog 62 | %%BeginSetup 63 | %%EndSetup 64 | %%Page: 1 1 65 | %%BeginPageSetup 66 | %%PageBoundingBox: 0 -1 448 80 67 | %%EndPageSetup 68 | q 0 -1 448 81 rectclip q 69 | 0.8 g 70 | 72.078 58.959 m 58.961 72.08 l 53.922 77.201 47.121 79.92 40 79.92 c 32.801 71 | 79.92 26.078 77.119 21.039 72.08 c 7.84 58.959 l 2.719 53.92 0 47.119 0 72 | 39.998 c 0 32.799 2.801 26.08 7.84 21.041 c 20.961 7.92 l 26.078 2.799 73 | 32.801 -0.002 39.922 -0.002 c 46.879 -0.002 53.52 2.639 58.559 7.522 c 72 74 | 20.959 l 77.121 25.998 79.84 32.799 79.84 39.92 c 80 47.201 77.199 53.92 75 | 72.078 58.959 c h 76 | 63.359 30.479 m 50 17.119 l 47.52 14.721 44.078 13.201 40.398 13.201 c 77 | 36.559 13.201 33.121 14.721 30.559 17.279 c 37.039 23.76 l 37.922 22.881 78 | 39.039 22.4 40.398 22.4 c 41.762 22.4 42.879 22.959 43.762 23.76 c 56.961 79 | 36.881 l 57.762 37.76 58.32 38.881 58.32 40.24 c 58.32 41.522 57.762 42.721 80 | 56.961 43.6 c 56.078 44.479 54.961 44.959 53.602 44.959 c 52.32 44.959 81 | 51.121 44.4 50.238 43.6 c 37.121 30.479 l 34.641 27.998 31.121 26.4 27.281 82 | 26.4 c 23.441 26.4 20 27.998 17.441 30.479 c 14.961 32.959 13.359 36.479 83 | 13.359 40.318 c 13.359 44.158 14.879 47.6 17.441 50.158 c 30.559 63.279 84 | l 33.039 65.76 36.559 67.361 40.398 67.361 c 44.238 67.361 47.68 65.76 85 | 50.238 63.279 c 43.762 56.799 l 42.879 57.682 41.762 58.158 40.398 58.158 86 | c 39.039 58.158 37.922 57.682 37.039 56.799 c 23.922 43.6 l 23.039 42.721 87 | 22.559 41.6 22.559 40.24 c 22.559 38.959 23.121 37.76 23.922 36.881 c 24.801 88 | 35.998 25.922 35.522 27.281 35.522 c 28.559 35.522 29.762 36.08 30.641 89 | 36.881 c 43.762 49.998 l 46.32 52.479 49.762 54.08 53.602 54.08 c 57.441 90 | 54.08 60.879 52.561 63.441 49.998 c 65.922 47.44 67.52 43.998 67.52 40.158 91 | c 67.441 36.479 65.84 33.041 63.359 30.479 c h 92 | 63.359 30.479 m f 93 | 268.48 20.318 m 266.879 20.318 265.52 21.682 265.52 23.279 c 265.52 59.682 94 | l 265.52 61.279 266.801 62.639 268.48 62.639 c 270.078 62.639 271.441 61.361 95 | 271.441 59.682 c 271.441 23.279 l 271.441 21.682 270.16 20.318 268.48 20.318 96 | c h 97 | 268.48 20.318 m f 98 | 167.762 28.721 m 166.559 29.682 164.801 29.522 163.84 28.318 c 161.199 99 | 25.041 154.641 25.842 152.961 26.561 c 150.48 27.522 149.281 29.6 149.281 100 | 32.639 c 149.281 52.561 l 162.32 52.561 l 163.922 52.561 165.281 53.842 101 | 165.281 55.522 c 165.281 57.119 164 58.479 162.32 58.479 c 149.281 58.479 102 | l 149.281 73.361 l 149.281 74.881 148.078 76.158 146.48 76.158 c 144.961 103 | 76.158 143.762 74.959 143.762 73.361 c 143.762 32.639 l 143.762 27.361 104 | 146.32 23.279 150.879 21.44 c 152.32 20.881 154.559 20.4 157.039 20.4 c 105 | 160.961 20.4 165.441 21.522 168.16 24.881 c 169.121 26.08 168.961 27.76 106 | 167.762 28.721 c h 107 | 167.762 28.721 m f 108 | 122.559 20.158 m 121.68 20.158 120.719 20.24 119.84 20.318 c 110.238 21.522 109 | 104.48 25.998 104.48 32.158 c 104.48 33.76 105.762 35.119 107.441 35.119 110 | c 109.039 35.119 110.398 33.76 110.398 32.158 c 110.398 29.279 114.398 111 | 26.959 120.641 26.158 c 126.879 25.361 131.281 28.479 131.922 30.881 c 132.32 112 | 32.318 132.16 33.6 131.52 34.639 c 130.801 35.76 129.602 36.479 127.762 113 | 36.881 c 115.199 39.119 106.879 41.201 105.68 49.119 c 105.281 51.998 106.078 114 | 54.881 108 57.119 c 110.719 60.318 115.441 62.158 120.961 62.158 c 131.039 115 | 62.158 137.602 57.682 137.602 50.639 c 137.602 49.041 136.238 47.682 134.641 116 | 47.682 c 133.039 47.682 131.68 49.041 131.68 50.639 c 131.68 54.799 125.922 117 | 56.24 120.961 56.24 c 117.281 56.24 114.078 55.119 112.48 53.279 c 111.68 118 | 52.318 111.281 51.201 111.52 49.998 c 112 47.041 114.641 45.201 128.879 119 | 42.721 c 128.961 42.721 l 133.281 41.842 135.52 39.522 136.559 37.842 c 120 | 138.078 35.44 138.48 32.479 137.68 29.44 c 136.238 24.561 130.398 20.158 121 | 122.559 20.158 c h 122 | 122.559 20.158 m f 123 | 190.398 20.158 m 178.801 20.158 169.359 29.6 169.359 41.119 c 169.359 52.721 124 | 178.801 62.08 190.398 62.08 c 200.961 62.08 210 54.158 211.281 43.682 c 125 | 211.441 42.318 210.719 41.201 210.238 40.639 c 209.52 39.842 208.559 39.361 126 | 207.602 39.361 c 175.359 39.361 l 176.16 31.842 182.641 25.92 190.398 25.92 127 | c 195.602 25.92 200.879 28.639 203.121 32.4 c 203.922 33.842 205.762 34.24 128 | 207.121 33.44 c 208.559 32.639 209.039 30.799 208.16 29.44 c 204.879 23.998 129 | 197.68 20.158 190.398 20.158 c h 130 | 175.84 45.361 m 204.961 45.361 l 203.121 51.682 197.199 56.24 190.398 56.24 131 | c 183.441 56.24 177.68 51.682 175.84 45.361 c h 132 | 175.84 45.361 m f 133 | 237.762 62.158 m 231.84 62.158 226.48 59.682 222.641 55.76 c 222.641 59.279 134 | l 222.641 60.881 221.281 62.24 219.68 62.24 c 218.078 62.24 216.719 60.959 135 | 216.719 59.279 c 216.719 8.799 l 216.719 7.201 218 5.842 219.68 5.842 c 136 | 221.281 5.842 222.641 7.201 222.641 8.799 c 222.641 26.561 l 226.48 22.561 137 | 231.84 20.158 237.762 20.158 c 249.359 20.158 258.719 29.6 258.719 41.119 138 | c 258.801 52.721 249.359 62.158 237.762 62.158 c h 139 | 237.762 26.08 m 229.441 26.08 222.641 32.881 222.641 41.201 c 222.641 49.522 140 | 229.441 56.318 237.762 56.318 c 246.078 56.318 252.879 49.522 252.879 41.201 141 | c 252.879 32.881 246.078 26.08 237.762 26.08 c h 142 | 237.762 26.08 m f 143 | 310.559 20.479 m 307.68 20.479 304.801 21.76 302.16 24.24 c 300.16 26.158 144 | 287.359 39.041 286.879 39.6 c 286.32 40.158 286 40.881 286 41.682 c 286 145 | 42.479 286.32 43.201 286.879 43.76 c 304.48 60.959 l 305.68 62.08 307.52 146 | 62.08 308.641 60.881 c 309.762 59.682 309.762 57.842 308.559 56.721 c 293.039 147 | 41.6 l 296.801 37.76 304.641 29.92 306.078 28.479 c 307.68 26.959 309.199 148 | 26.24 310.641 26.318 c 312.801 26.318 l 314.398 26.318 315.762 24.959 315.762 149 | 23.361 c 315.762 21.76 314.398 20.479 312.801 20.4 c 310.801 20.4 l 310.719 150 | 20.479 310.641 20.479 310.559 20.479 c h 151 | 310.559 20.479 m f 152 | 282 20.4 m 280.398 20.4 279.039 21.682 279.039 23.361 c 279.039 73.44 l 153 | 279.039 75.041 280.398 76.4 282 76.4 c 283.602 76.4 284.961 75.119 284.961 154 | 73.44 c 284.961 23.361 l 284.961 21.682 283.68 20.4 282 20.4 c h 155 | 282 20.4 m f 156 | 268.559 68.639 m 270.641 68.639 272.32 70.318 272.32 72.4 c 272.32 74.479 157 | 270.641 76.158 268.559 76.158 c 266.48 76.158 264.801 74.479 264.801 72.4 158 | c 264.801 70.318 266.48 68.639 268.559 68.639 c h 159 | 268.559 68.639 m f* 160 | 325.199 20.4 m 327.281 20.4 328.961 22.08 328.961 24.158 c 328.961 26.24 161 | 327.281 27.92 325.199 27.92 c 323.121 27.92 321.441 26.24 321.441 24.158 162 | c 321.441 22.158 323.121 20.4 325.199 20.4 c h 163 | 325.199 20.4 m f* 164 | 405.121 59.119 m 405.121 60.721 403.922 61.92 402.398 62.08 c 401.68 62.158 165 | 400.961 62.158 400.238 62.158 c 394.32 62.158 388.961 59.682 385.121 55.76 166 | c 385.121 59.279 l 385.121 60.881 383.762 62.24 382.16 62.24 c 380.559 167 | 62.24 379.199 60.881 379.199 59.279 c 379.199 23.279 l 379.199 21.682 380.559 168 | 20.318 382.16 20.318 c 383.762 20.318 385.121 21.682 385.121 23.279 c 385.121 169 | 41.279 l 385.199 49.6 391.922 56.318 400.238 56.318 c 400.641 56.318 401.039 170 | 56.318 401.52 56.24 c 401.762 56.158 401.922 56.158 402.16 56.158 c 402.398 171 | 56.158 l 402.398 56.24 l 403.922 56.318 405.121 57.522 405.121 59.119 c 172 | h 173 | 405.121 59.119 m f* 174 | 351.84 20.158 m 340.238 20.158 330.801 29.6 330.801 41.119 c 330.801 52.721 175 | 340.238 62.08 351.84 62.08 c 363.441 62.08 372.801 52.639 372.801 41.119 176 | c 372.879 29.6 363.441 20.158 351.84 20.158 c h 177 | 351.84 56.24 m 343.52 56.24 336.719 49.44 336.719 41.119 c 336.719 32.799 178 | 343.52 25.998 351.84 25.998 c 360.16 25.998 366.961 32.799 366.961 41.119 179 | c 366.961 49.522 360.16 56.24 351.84 56.24 c h 180 | 351.84 56.24 m f 181 | 447.039 41.201 m 447.039 41.6 447.039 41.92 446.961 42.318 c 446.961 59.279 182 | l 446.961 60.881 445.68 62.24 444 62.24 c 442.398 62.24 441.039 60.959 183 | 441.039 59.279 c 441.039 55.76 l 437.199 59.682 431.84 62.158 426 62.158 184 | c 414.398 62.158 404.961 52.721 404.961 41.201 c 404.961 29.6 414.398 20.24 185 | 426 20.24 c 431.922 20.24 437.281 22.721 441.039 26.639 c 441.039 23.682 186 | l 441.039 17.522 437.359 12.24 432.078 9.92 c 430.238 9.119 428.16 8.639 187 | 425.922 8.639 c 420.719 8.639 415.441 11.279 413.199 15.201 c 412.398 16.639 188 | 410.559 17.041 409.199 16.24 c 407.762 15.44 407.281 13.6 408.16 12.24 189 | c 411.441 6.639 418.641 2.799 425.922 2.799 c 437.52 2.799 446.879 12.24 190 | 446.879 23.76 c 446.879 40.24 l 447.039 40.4 447.039 40.799 447.039 41.201 191 | c h 192 | 426.078 26.08 m 417.762 26.08 410.961 32.881 410.961 41.201 c 410.961 49.522 193 | 417.762 56.318 426.078 56.318 c 434.16 56.318 440.801 49.92 441.121 41.842 194 | c 441.121 40.561 l 440.801 32.479 434.16 26.08 426.078 26.08 c h 195 | 426.078 26.08 m f 196 | Q Q 197 | showpage 198 | %%Trailer 199 | end restore 200 | %%EOF 201 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-video/makefile.sh: -------------------------------------------------------------------------------- 1 | pdflatex -synctex=1 -interaction=nonstopmode course-video-report.tex 2 | pdflatex -synctex=1 -interaction=nonstopmode course-video-report.tex 3 | pdflatex -synctex=1 -interaction=nonstopmode course-video-report.tex 4 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default-video/stepik.sty: -------------------------------------------------------------------------------- 1 | \NeedsTeXFormat{LaTeX2e} 2 | \ProvidesPackage{stepik}[2016/08/17 Stepik.org style file] 3 | 4 | \RequirePackage{cmap} 5 | 6 | \RequirePackage[T2A]{fontenc} 7 | \RequirePackage[utf8]{inputenc} 8 | \RequirePackage[russian]{babel} 9 | \RequirePackage[default]{opensans} 10 | 11 | \RequirePackage[pdftex]{graphicx} 12 | \RequirePackage[pdftex]{color} 13 | \RequirePackage{colortbl} 14 | 15 | \RequirePackage[paper=a4paper, margin=2.5cm, bottom=3cm]{geometry} 16 | \RequirePackage[absolute]{textpos} 17 | \RequirePackage{wallpaper} 18 | \RequirePackage{fancyhdr} 19 | 20 | \RequirePackage{tabularx} 21 | \RequirePackage{titlesec} 22 | \RequirePackage{titletoc} 23 | \RequirePackage{hyperref} 24 | \RequirePackage{verbatim} 25 | 26 | 27 | \textblockorigin{2.5cm}{2.5cm} 28 | 29 | 30 | % Define Stepik pallette 31 | \definecolor{Black}{rgb}{0, 0, 0} 32 | \definecolor{White}{rgb}{1, 1, 1} 33 | \definecolor{PastelGreen}{rgb}{0.4, 0.8, 0.4} 34 | \definecolor{BlueRibbon}{rgb}{0, 0.4, 1} 35 | \definecolor{NeonCarrot}{rgb}{1, 0.6, 0.2} 36 | \definecolor{Silver}{rgb}{0.8, 0.8, 0.8} 37 | \definecolor{GrannySmithApple}{rgb}{0.6471, 0.8981, 0.6471} 38 | % Additional text color 39 | \definecolor{GreyTextColor}{rgb}{0.367,0.367,0.367} 40 | 41 | 42 | % Change section header style 43 | \titleformat{\chapter} 44 | {\normalfont\LARGE\color{black}}{\thechapter.}{1em}{} 45 | \titleformat{\section} 46 | {\normalfont\Large\color{black}}{\thesection.}{1em}{} 47 | \renewcommand{\thesection}{\arabic{section}} 48 | \titleformat{\subsection} 49 | {\normalfont\large\color{black}}{\thesubsection.}{1em}{} 50 | 51 | \titlecontents{chapter} 52 | [2em] 53 | {} 54 | {\contentslabel{2em}} 55 | {\hspace*{-2em}} 56 | {\titlerule*[1pc]{}\contentspage} 57 | [\vspace*{0.7em}] 58 | 59 | % Indent first 60 | \let\@afterindentfalse\@afterindenttrue 61 | \@afterindenttrue 62 | 63 | 64 | % Make titlepage 65 | \def\cover#1{ 66 | \def\cover@background{#1}} 67 | 68 | \def\subtitle#1{ 69 | \def\@subtitle{#1}} 70 | 71 | \def\maketitle{ 72 | \linespread{1} 73 | \begin{titlepage} 74 | \ifdefined\cover@background{\ThisLRCornerWallPaper{1}{\cover@background}} 75 | \begin{flushleft} 76 | \vspace*{0.5em} 77 | \fontsize{40px}{48px}\selectfont\@title 78 | \end{flushleft} 79 | \begin{flushleft} 80 | \color[rgb]{0.5,0.5,0.5}\fontsize{20px}{24px}\selectfont\@subtitle 81 | \end{flushleft} 82 | \end{titlepage} 83 | \setcounter{page}{2} 84 | } 85 | 86 | % Footer 87 | % Redefine 'plain' page style to be used for chapter pages 88 | \fancypagestyle{plain}{ 89 | \renewcommand{\headrulewidth}{0pt} 90 | \renewcommand{\footrulewidth}{0pt} 91 | \fancyhf{} 92 | \fancyfoot[L]{ 93 | \includegraphics[scale=0.2]{logo.eps} 94 | } 95 | \fancyfoot[R]{\raisebox{0.35em}{\thepage}}% 96 | } 97 | \pagestyle{plain} 98 | 99 | 100 | 101 | 102 | % Hyperref options 103 | \hypersetup{ 104 | colorlinks = true, %Colours links instead of ugly boxes 105 | urlcolor = PastelGreen, %Colour for external hyperlinks 106 | linkcolor = GreyTextColor, %Colour of internal links 107 | citecolor = GreyTextColor %Colour of citations 108 | } 109 | 110 | % Change TOC title with babel 111 | \addto\captionsrussian{% 112 | \renewcommand{\contentsname}{Содержание}% 113 | } 114 | 115 | 116 | % Item description 117 | \newcommand{\stepurl}[1]{ 118 | \begin{textblock*}{\textwidth}(0pt,-12pt) 119 | \begin{flushright} 120 | \small\url{#1} 121 | \end{flushright} 122 | \end{textblock*} 123 | } 124 | 125 | 126 | \newcommand{\stepstatistics}[6]{ 127 | \section*{Задание~#1} 128 | \begin{flushright} 129 | \begin{tabular}{p{0.2\textwidth}rr} 130 | &\textcolor{black}{Тип задания}: & #6\\ 131 | &\textcolor{black}{Число ответивших}: & #5\\ 132 | &\textcolor{black}{Сложность}: & #2\\ 133 | &\textcolor{black}{Дискриминативность}: & #3\\ 134 | &\textcolor{black}{Корреляция с общим баллом}: & #4\\ 135 | \end{tabular} 136 | \end{flushright} 137 | } 138 | 139 | \def\multiplecorrect{\raisebox{-2px}{\includegraphics[scale=0.5]{common/multiplecorrect.png}}} 140 | \def\multiplewrong{\raisebox{-2px}{\includegraphics[scale=0.5]{common/multiplewrong.png}}} 141 | \def\singlecorrect{\raisebox{-2px}{\includegraphics[scale=0.5]{common/singlecorrect.png}}} 142 | \def\singlewrong{\raisebox{-2px}{\includegraphics[scale=0.5]{common/singlewrong.png}}} 143 | 144 | 145 | \newenvironment{question}{%\subsection*{Текст задания} 146 | }{} 147 | \newenvironment{code}{\endgraf\verbatim}{\endverbatim} 148 | 149 | 150 | \newenvironment{options}{%\subsection*{Варианты опций} 151 | \begin{flushright}\begin{tabular}{lp{0.8\textwidth}r} 152 | &\multicolumn{2}{r}{\parbox{0.3\textwidth} 153 | {\raggedleft 154 | \scriptsize \textcolor{black}{Сложность опции} 155 | }}\\ 156 | } 157 | {\end{tabular}\end{flushright}} 158 | \newcommand{\option}[3]{ 159 | 160 | #1 161 | & 162 | \ifnum\pdfstrcmp{#3}{0.05}=1 \relax\else\cellcolor[rgb]{1, 0.95, 0.9}\fi 163 | #2 164 | & 165 | \ifnum\pdfstrcmp{#3}{0.05}=1 \relax\else\cellcolor[rgb]{1, 0.95, 0.9}\fi 166 | %\ifnum\pdfstrcmp{#3}{0.05}=1 #3 \else\textcolor{NeonCarrot}{#3}\fi 167 | #3 168 | \\ 169 | } 170 | 171 | 172 | \newenvironment{recommendations}{\subsection*{Рекомендации}}{} 173 | 174 | % Peaks 175 | \newenvironment{peaks}{\subsection*{Пики}\begin{tabular}{ccccc} 176 | Начало пика&Конец пика&Ширина&Высота&Площадь\\} 177 | {\end{tabular}} 178 | \newcommand{\peak}[5]{ 179 | #1 & #2 & #3 & #4 & #5 \\ 180 | } 181 | 182 | \newenvironment{totalpeaks}{\par\begin{tabular}{cccc} 183 | Номер видео&Начало пика&Конец пика&Площадь\\} 184 | {\end{tabular}} 185 | \newcommand{\totalpeak}[5]{ 186 | \href{#2}{#1} & #3 & #4 & #5 \\ 187 | } 188 | 189 | 190 | % dropout report 191 | \newcommand{\lessoninfo}[2]{ 192 | \stepurl{#2} 193 | \section*{Урок~#1} 194 | } 195 | \newenvironment{lessonstatistics}{\begin{flushleft}\begin{tabular}{cccll} 196 | Шаг&Тип&Число просмотров&Процент завершения&Процент ухода\\} 197 | {\end{tabular}\end{flushleft}} 198 | \newcommand{\lessonstepstatistics}[6]{ 199 | \href{#2}{#1} & #3 & #4 & 200 | \def\temp{#5}\ifx\temp\empty\relax\else\coloredrule{BlueRibbon}{#5}\fi & 201 | \def\temp{#6}\ifx\temp\empty\relax\else\coloredrule{NeonCarrot}{#6}\fi\\ 202 | } 203 | 204 | \newlength\onepercentbox 205 | \setlength{\onepercentbox}{0.3mm} 206 | \newcommand{\coloredrule}[2]{ 207 | {\color{#1}\rule{#2\onepercentbox}{1em}~#2\% } 208 | } 209 | 210 | % Course text to intro 211 | \let\@addcoursetexttointro\relax 212 | \def\addcoursetexttointro#1{\def\@addcoursetexttointro{#1}} 213 | \def\coursetexttointro{\@addcoursetexttointro\ } 214 | 215 | 216 | % Commands for whole report 217 | \linespread{1.5} 218 | \color{GreyTextColor} 219 | -------------------------------------------------------------------------------- /examples/external-reports/latex/default/test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/latex/default/test -------------------------------------------------------------------------------- /examples/external-reports/library/api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | from urllib.parse import urlencode 5 | 6 | # 1. Get your keys at https://stepik.org/oauth2/applications/ 7 | # (client type = confidential, authorization grant type = client credentials) 8 | # and write them to api_keys.py 9 | from library.api_keys import API_HOST, CLIENT_ID, CLIENT_SECRET 10 | 11 | 12 | # 2. Get a token 13 | def get_token(client_id=CLIENT_ID, client_secret=CLIENT_SECRET): 14 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 15 | response = requests.post('{}/oauth2/token/'.format(API_HOST), data={'grant_type': 'client_credentials'}, auth=auth) 16 | # status code should be 200 (HTTP OK) 17 | assert(response.status_code == 200) 18 | token = json.loads(response.text)['access_token'] 19 | return token 20 | 21 | 22 | # 3. Call API ({API_HOST}/api/docs/) using this token. 23 | def get_api_response(api_url, token=None): 24 | if not token: 25 | token = get_token() 26 | response = requests.get(api_url, headers={'Authorization': 'Bearer ' + token}) 27 | # status code should be 200 (HTTP OK) 28 | assert (response.status_code == 200) 29 | 30 | return response.json() 31 | 32 | 33 | # Additional API functions 34 | 35 | def fetch_objects_by_id(object_name, object_id, token=None): 36 | if not token: 37 | token = get_token() 38 | 39 | if type(object_id) is not list: 40 | # if it is not a list, we create an one-element list 41 | object_id = [object_id] 42 | 43 | # Fetch objects by 30 items, 44 | # so we will not bump into HTTP request length limits 45 | slice_size = 30 46 | objects_from_api = [] 47 | for i in range(0, len(object_id), slice_size): 48 | obj_ids_slice = object_id[i:i + slice_size] 49 | api_url = '{}/api/{}?{}'.format( 50 | API_HOST, object_name, '&'.join('ids[]={}'.format(obj_id) for obj_id in obj_ids_slice)) 51 | response = get_api_response(api_url, token) 52 | objects_from_api += response[object_name] 53 | return objects_from_api 54 | 55 | 56 | def fetch_objects_by_pk(object_name, object_pk, token=None): 57 | if not token: 58 | token = get_token() 59 | 60 | api_url = '{}/api/{}/{}'.format(API_HOST, object_name, object_pk) 61 | response = get_api_response(api_url, token) 62 | objects_from_api = response[object_name] 63 | return objects_from_api 64 | 65 | 66 | def fetch_objects(object_name, token=None, **kwargs): 67 | if not token: 68 | token = get_token() 69 | 70 | if 'pk' in kwargs: 71 | # fetch objects by pk 72 | return fetch_objects_by_pk(object_name, kwargs['pk'], token) 73 | elif 'id' in kwargs: 74 | # fetch objects by ids 75 | return fetch_objects_by_id(object_name, kwargs['id'], token) 76 | else: 77 | # fetch objects by other params 78 | params = kwargs 79 | 80 | # can be pagination 81 | if 'page' not in kwargs: 82 | params = dict(params, page=1) 83 | 84 | objects_from_api = [] 85 | while True: 86 | query = urlencode(params, doseq=True) 87 | api_url = '{}/api/{}?{}'.format(API_HOST, object_name, query) 88 | 89 | response = get_api_response(api_url, token) 90 | if object_name not in response: 91 | # if not success (e.g., error 404), then return collected objects 92 | return objects_from_api 93 | 94 | objects_from_api += response[object_name] 95 | has_next = response['meta']['has_next'] 96 | if not has_next: 97 | break 98 | params['page'] += 1 99 | 100 | return objects_from_api 101 | 102 | 103 | def create_object(object_name, data, token=None): 104 | if not token: 105 | token = get_token() 106 | 107 | api_url = '{}/api/{}'.format(API_HOST, object_name) 108 | # use POST to create new objects 109 | response = requests.post(api_url, headers={'Authorization': 'Bearer ' + token}, json=data) 110 | # status code should be 201 (HTTP Created) 111 | assert(response.status_code == 201) 112 | object_id = response.json()[object_name][0]['id'] 113 | return object_id 114 | 115 | 116 | def update_object(object_name, object_id, data, token=None): 117 | if not token: 118 | token = get_token() 119 | 120 | api_url = '{}/api/{}/{}'.format(API_HOST, object_name, object_id) 121 | # use PUT to update existing objects 122 | response = requests.put(api_url, headers={'Authorization': 'Bearer ' + token}, json=data) 123 | # status code should be 200 (HTTP OK) 124 | assert(response.status_code == 200) 125 | object_id = response.json()[object_name][0]['id'] 126 | return object_id 127 | -------------------------------------------------------------------------------- /examples/external-reports/library/api_keys.py: -------------------------------------------------------------------------------- 1 | # Change your CLIENT_ID and CLIENT_SECRET: 2 | # Get your keys at https://stepik.org/oauth2/applications/ 3 | # (client type = confidential, authorization grant type = client credentials) 4 | CLIENT_ID = '...' 5 | CLIENT_SECRET = '...' 6 | API_HOST = 'https://stepik.org' 7 | -------------------------------------------------------------------------------- /examples/external-reports/library/settings.py: -------------------------------------------------------------------------------- 1 | ITEM_FORMAT = ''' 2 | \\newpage 3 | \\stepurl{{{item[step_url]}}} 4 | \\stepstatistics{{{item[step_id]:.0f}}}{{{item[difficulty]:.2f}}}{{{item[discrimination]:.2f}}}{{{item[item_total_corr]:.2f}}}{{{item[n_people]:.0f}}}{{{item[step_type]}}} 5 | 6 | \\begin{{question}} 7 | {item[question]} 8 | \\end{{question}} 9 | ''' 10 | 11 | OPTION_FORMAT = '\\option{{{label}}}{{{name}}}{{{difficulty}}} \n' 12 | 13 | 14 | STEP_FORMAT = ''' 15 | \\newpage 16 | \\stepurl{{{step_url}}} 17 | \\section*{{Видео~{step_id:.0f}}} 18 | 19 | \\includegraphics[scale=0.8]{{generated/step_{step_id:.0f}.png}} 20 | ''' 21 | MIN_VIDEO_LENGTH = 5 22 | 23 | STEP_STAT_FORMAT = """ 24 | \\lessonstepstatistics{{{stat[step_id]:.0f}}}{{{stat[step_url]}}}{{{stat[step_type]}}}{{{stat[viewed_by]}}}{{{stat[completion_rate]}}}{{{stat[dropout_rate]}}} 25 | """ 26 | -------------------------------------------------------------------------------- /examples/external-reports/library/utils.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import datetime 3 | import time 4 | import math 5 | import pypandoc 6 | import os 7 | 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | import numpy.ma as ma 11 | import pandas as pd 12 | import statsmodels.api as sm 13 | 14 | from library.api import API_HOST, fetch_objects, fetch_objects_by_id, get_token 15 | from library.settings import MIN_VIDEO_LENGTH 16 | 17 | 18 | def get_unix_date(date): 19 | if date: 20 | timestamp = time.mktime(datetime.datetime.strptime(date.split('+')[0], "%Y-%m-%dT%H:%M:%SZ").timetuple()) 21 | return int(timestamp) 22 | return None 23 | 24 | 25 | def html2latex(text): 26 | output = pypandoc.convert(text, 'latex', format='html', extra_args=['-f', 'html+tex_math_dollars']) 27 | return output 28 | 29 | 30 | def process_step_url(row): 31 | if ('max_step_variation' not in row.index) or (row.max_step_variation == 1): 32 | # no step variations 33 | return '{}/lesson/{}/step/{}'.format(API_HOST, row.lesson_id, row.step_position) 34 | return '{}/lesson/{}/step/{}?alternative={}'.format(API_HOST, 35 | row.lesson_id, row.step_position, row.step_variation) 36 | 37 | 38 | # API functions 39 | 40 | def get_course_structure(course_id, cached=True, token=None): 41 | # use cache 42 | course_structure_filename = 'cache/course-{}-structure.csv'.format(course_id) 43 | if os.path.isfile(course_structure_filename) and cached: 44 | course_structure = pd.read_csv(course_structure_filename) 45 | return course_structure 46 | 47 | if not token: 48 | token = get_token() 49 | course = fetch_objects_by_id('courses', course_id, token=token)[0] 50 | sections = fetch_objects('sections', token=token, id=course['sections']) 51 | 52 | unit_ids = [unit for section in sections for unit in section['units']] 53 | units = fetch_objects('units', token=token, id=unit_ids) 54 | 55 | lesson_ids = [unit['lesson'] for unit in units] 56 | lessons = fetch_objects('lessons', token=token, id=lesson_ids) 57 | 58 | step_ids = [step for lesson in lessons for step in lesson['steps']] 59 | steps = fetch_objects('steps', token=token, id=step_ids) 60 | step_id = [step['id'] for step in steps] 61 | step_position = [step['position'] for step in steps] 62 | step_type = [step['block']['name'] for step in steps] 63 | step_lesson = [step['lesson'] for step in steps] 64 | step_correct_ratio = [step['correct_ratio'] for step in steps] 65 | 66 | course_structure = pd.DataFrame({'course_id': course_id, 67 | 'lesson_id': step_lesson, 68 | 'step_id': step_id, 69 | 'step_position': step_position, 70 | 'step_type': step_type, 71 | 'step_correct_ratio': step_correct_ratio}) 72 | 73 | module_position = [[section['position']]*len(section['units']) for section in sections] 74 | module_position = [value for small_list in module_position for value in small_list] 75 | 76 | module_id = [[section['id']]*len(section['units']) for section in sections] 77 | module_id = [value for small_list in module_id for value in small_list] 78 | 79 | module_hard_deadline = [[section['hard_deadline']]*len(section['units']) for section in sections] 80 | module_hard_deadline = [value for small_list in module_hard_deadline for value in small_list] 81 | 82 | module_begin_date = [[section['begin_date']]*len(section['units']) for section in sections] 83 | module_begin_date = [value for small_list in module_begin_date for value in small_list] 84 | 85 | lesson_position = [unit['position'] for unit in units] 86 | 87 | module_structure = pd.DataFrame({'lesson_id': lesson_ids, 88 | 'lesson_position': lesson_position, 89 | 'module_id': module_id, 90 | 'module_position': module_position, 91 | 'hard_deadline': module_hard_deadline, 92 | 'begin_date': module_begin_date}) 93 | 94 | course_structure = course_structure.merge(module_structure) 95 | course_structure = course_structure.sort_values(['module_position', 'lesson_position', 'step_position']) 96 | course_structure.to_csv(course_structure_filename, index=False) 97 | return course_structure 98 | 99 | 100 | def get_course_submissions(course_id, course_structure=pd.DataFrame(), cached=True, token=None): 101 | header = ['submission_id', 'step_id', 'user_id', 'attempt_time', 'submission_time', 'status'] 102 | 103 | # use cache 104 | course_submissions_filename = 'cache/course-{}-submissions.csv'.format(course_id) 105 | if os.path.isfile(course_submissions_filename) and cached: 106 | course_submissions = pd.read_csv(course_submissions_filename) 107 | course_submissions = course_submissions[header] 108 | return course_submissions 109 | 110 | if not token: 111 | token = get_token() 112 | 113 | if course_structure.empty: 114 | course_structure = get_course_structure(course_id, token) 115 | 116 | course_submissions = pd.DataFrame() 117 | for step in course_structure.step_id.unique().tolist(): 118 | step_submissions = pd.DataFrame(fetch_objects('submissions', token=token, step=step)) 119 | if step_submissions.empty: 120 | continue 121 | 122 | step_submissions = step_submissions.rename(columns={'id': 'submission_id', 123 | 'time': 'submission_time', 124 | 'attempt': 'attempt_id'}) 125 | attempt_ids = step_submissions['attempt_id'].unique().tolist() 126 | step_attempts = pd.DataFrame(fetch_objects_by_id('attempts', attempt_ids, token=token)) 127 | step_attempts = step_attempts.rename(columns={'id': 'attempt_id', 128 | 'time': 'attempt_time', 129 | 'status': 'attempt_status'}) 130 | step_submissions = pd.merge(step_submissions, step_attempts, on='attempt_id') 131 | step_submissions['step_id'] = step 132 | course_submissions = course_submissions.append(step_submissions) 133 | 134 | if course_submissions.empty: 135 | return pd.DataFrame(columns=header) 136 | 137 | course_submissions['submission_time'] = course_submissions['submission_time'].apply(get_unix_date) 138 | course_submissions['attempt_time'] = course_submissions['attempt_time'].apply(get_unix_date) 139 | 140 | course_submissions = course_submissions.rename(columns={'user': 'user_id'}) 141 | course_submissions = course_submissions[header] 142 | course_submissions.to_csv(course_submissions_filename, index=False) 143 | return course_submissions 144 | 145 | 146 | def get_course_grades(course_id, cached=True, token=None): 147 | header = ['user_id', 'step_id', 'is_passed', 'score', 'total_score', 'date_joined', 'last_viewed'] 148 | 149 | # use cache 150 | course_grades_filename = 'cache/course-{}-grades.csv'.format(course_id) 151 | if os.path.isfile(course_grades_filename) and cached: 152 | course_grades = pd.read_csv(course_grades_filename) 153 | course_grades = course_grades[header] 154 | return course_grades 155 | 156 | if not token: 157 | token = get_token() 158 | 159 | course_grades = pd.DataFrame() 160 | grades = fetch_objects('course-grades', course=course_id, token=token) 161 | 162 | for grade in grades: 163 | user_grade = pd.DataFrame(grade['results']).transpose() 164 | user_grade['user_id'] = grade['user'] 165 | user_grade['total_score'] = grade['score'] 166 | user_grade['date_joined'] = grade['date_joined'] 167 | user_grade['last_viewed'] = grade['last_viewed'] 168 | course_grades = course_grades.append(user_grade) 169 | 170 | course_grades['date_joined'] = course_grades['date_joined'].apply(get_unix_date) 171 | course_grades['last_viewed'] = course_grades['last_viewed'].apply(get_unix_date) 172 | course_grades = course_grades.reset_index(drop=True) 173 | course_grades = course_grades[header] 174 | course_grades.to_csv(course_grades_filename, index=False) 175 | return course_grades 176 | 177 | 178 | def get_enrolled_users(course_id, token=None): 179 | if not token: 180 | token = get_token() 181 | 182 | learner_group = fetch_objects('courses', token=token, pk=course_id)[0]['learners_group'] 183 | users = fetch_objects('groups', token=token, pk=learner_group)[0]['users'] 184 | return users 185 | 186 | 187 | def process_options_with_name(data, reply, option_names): 188 | data = ast.literal_eval(data) 189 | reply = ast.literal_eval(reply)['choices'] 190 | 191 | is_multiple = data['is_multiple_choice'] 192 | options = data['options'] 193 | 194 | option_id = [] 195 | clue = [] 196 | for op in options: 197 | if op in option_names.option_name.tolist(): 198 | val = option_names.loc[option_names.option_name == op, 'option_id'].values[0] 199 | clue_val = option_names.loc[option_names.option_name == op, 'is_correct'].values[0] 200 | else: 201 | val = np.nan 202 | clue_val = np.nan 203 | option_id += [val] 204 | clue += [clue_val] 205 | 206 | answer = [(c == r) for c, r in zip(clue, reply)] 207 | 208 | options = pd.DataFrame({'is_multiple': is_multiple, 209 | 'option_id': option_id, 210 | 'answer': answer, 211 | 'clue': clue}) 212 | options = options[['is_multiple', 'option_id', 'answer', 'clue']] 213 | return options 214 | 215 | 216 | def get_question(step_id): 217 | source = fetch_objects('step-sources', id=step_id) 218 | 219 | try: 220 | question = source[0]['block']['text'] 221 | except: 222 | question = '\n' 223 | 224 | question = html2latex(question) 225 | 226 | return question 227 | 228 | 229 | def get_step_options(step_id): 230 | source = fetch_objects('step-sources', id=step_id) 231 | 232 | try: 233 | options = source[0]['block']['source']['options'] 234 | options = pd.DataFrame(options) 235 | is_multiple = source[0]['block']['source']['is_multiple_choice'] 236 | except KeyError: 237 | options = pd.DataFrame(columns=['step_id', 'option_id', 'option_name', 'is_correct', 'is_multiple']) 238 | return options 239 | 240 | options['step_id'] = step_id 241 | options['is_multiple'] = is_multiple 242 | options = options.sort_values('text').reset_index() 243 | options = options.rename(columns={'text': 'option_name'}) 244 | options['option_id'] = options.index + 1 245 | options = options[['step_id', 'option_id', 'option_name', 'is_correct', 'is_multiple']] 246 | 247 | return options 248 | 249 | 250 | def get_step_info(step_id): 251 | info = pd.Series(fetch_objects('steps', pk=step_id)[0]) 252 | info = info.rename(columns={'id': 'step_id'}) 253 | return info 254 | 255 | 256 | # IRT functions 257 | 258 | def create_answer_matrix(data, user_column, item_column, value_column, aggfunc=np.mean, time_column=None): 259 | if time_column: 260 | # select only the first response 261 | data = data.loc[data.groupby([item_column, user_column])[time_column].idxmin()] 262 | data = data.drop_duplicates(subset=[item_column, user_column]) 263 | 264 | answers = pd.pivot_table(data, values=[value_column], index=[user_column], columns=[item_column], 265 | aggfunc=aggfunc) 266 | 267 | if not answers.empty: 268 | answers = answers[value_column] 269 | return answers 270 | 271 | 272 | # TODO: add Cronbach's alpha to item statistics 273 | # see http://stackoverflow.com/questions/20799403/improving-performance-of-cronbach-alpha-code-python-numpy 274 | def get_item_statistics(answers, discrimination_prop=0.3): 275 | total_people = answers.shape[0] 276 | 277 | n_people = answers.count(axis=0) 278 | 279 | # use mean (not sum) because of NA values 280 | item_difficulty = 1 - answers.mean(axis=0) 281 | total_score = answers.mean(axis=1) 282 | item_total_corr = answers.corrwith(total_score) 283 | 284 | n_top_people = int(discrimination_prop * total_people) 285 | low_performers = total_score.sort_values().index[:n_top_people] 286 | top_performers = total_score.sort_values().index[-n_top_people:] 287 | item_discrimination = answers.loc[top_performers].mean(axis=0) - answers.loc[low_performers].mean(axis=0) 288 | 289 | stats = pd.DataFrame({'item': item_difficulty.index, 290 | 'n_people': n_people, 291 | 'difficulty': item_difficulty, 292 | 'item_total_corr': item_total_corr, 293 | 'discrimination': item_discrimination}) 294 | stats.reset_index(drop=True, inplace=True) 295 | stats = stats[['item', 'n_people', 'difficulty', 'discrimination', 'item_total_corr']] 296 | return stats 297 | 298 | 299 | # Video report 300 | 301 | def get_video_stats(step_id, cached=True, token=None): 302 | if not token: 303 | token = get_token() 304 | 305 | cached_name = 'cache/step-{}-videostats.csv'.format(step_id) 306 | 307 | if cached and os.path.isfile(cached_name): 308 | stats = pd.read_csv(cached_name) 309 | return stats 310 | 311 | stats = pd.DataFrame(fetch_objects('video-stats', token=token, step=step_id)) 312 | 313 | if not stats.empty: 314 | stats.to_csv(cached_name, index=False) 315 | stats = pd.read_csv(cached_name) 316 | return stats 317 | 318 | 319 | def get_video_peaks(stats, plot=False, ax=None, ax2=None): 320 | header = ['start', 'peak', 'end', 'rise_rate', 'is_common', 321 | 'width', 'height', 'area'] 322 | 323 | if stats.empty: 324 | return pd.DataFrame(columns=header) 325 | row = stats.loc[stats.index[0]] 326 | 327 | try: 328 | watched_first = np.array(ast.literal_eval(row['watched_first'])) 329 | watched_total = np.array(ast.literal_eval(row['watched_total'])) 330 | play = np.array(ast.literal_eval(row['play'])) 331 | except ValueError: 332 | return pd.DataFrame(columns=header) 333 | 334 | # use only shortest data for analyses 335 | video_length = min(len(watched_first), len(watched_total), len(play)) 336 | if video_length < MIN_VIDEO_LENGTH: 337 | return pd.DataFrame(columns=header) 338 | 339 | watched_first = watched_first[:video_length] 340 | watched_total = watched_total[:video_length] 341 | play = play[:video_length] 342 | 343 | play[0] = play[1] # ignore auto-play in the beginning 344 | 345 | rewatching = watched_total - watched_first 346 | 347 | # To fight the noise, use smoothing technique before analysis 348 | rewatching = get_smoothing_data(rewatching, frac=0.05) 349 | play = get_smoothing_data(play, frac=0.1) 350 | 351 | rewatch_windows = detect_peaks(rewatching) 352 | play_windows = detect_peaks(play) 353 | 354 | rewatch_windows['is_common'] = False 355 | play_windows['is_common'] = False 356 | 357 | # find common windows 358 | for ind, row in rewatch_windows.iterrows(): 359 | start = row['start'] 360 | end = row['end'] 361 | if play_windows.loc[~((play_windows.end < start) | (end < play_windows.start))].shape[0] > 0: 362 | rewatch_windows.loc[ind, 'is_common'] = True 363 | 364 | common_windows = rewatch_windows[rewatch_windows.is_common].copy() 365 | 366 | if plot: 367 | peak_plot(rewatching, rewatch_windows, ax) 368 | if ax: 369 | ax.set_ylabel('Num rewatchers', fontsize=10) 370 | peak_plot(play, play_windows, ax2) 371 | if ax2: 372 | ax2.set_xlabel('Time in video (seconds)', fontsize=10) 373 | ax2.set_ylabel('Num play events', fontsize=10) 374 | 375 | # calculate peak features (normalized width, height, and area) 376 | total_length = len(rewatching) 377 | total_height = max(rewatching) 378 | total_area = sum(rewatching) 379 | 380 | if not common_windows.empty: 381 | common_windows['width'] = common_windows.apply(lambda x: (x['end']-x['start'])/total_length, axis=1) 382 | common_windows['height'] = common_windows.apply(lambda x: rewatching[x['peak']]/total_height, axis=1) 383 | common_windows['area'] = common_windows.apply( 384 | lambda x: rewatching[x['start']:x['end']].sum()/total_area, axis=1) 385 | else: 386 | common_windows = pd.DataFrame(columns=header) 387 | return common_windows 388 | 389 | 390 | def get_smoothing_data(data, frac=0.05): 391 | """ 392 | Return smoothing data based on LOWESS (Locally Weighted Scatterplot Smoothing) 393 | :param data: 1-D numpy array of values 394 | :param frac: Between 0 and 1. The fraction of the data used when estimating each value 395 | :return: 1-D numpy array of smoothing values 396 | """ 397 | smoothing_data = sm.nonparametric.lowess(data, np.arange(len(data)), frac=frac, return_sorted=False) 398 | return smoothing_data 399 | 400 | 401 | # TwitInfo Peak Detection Algorithm 402 | # Paper: 403 | # http://hci.stanford.edu/publications/2011/twitinfo/twitinfo-chi2011.pdf 404 | # Code source: 405 | # https://github.com/pmitros/LectureScapeBlock/blob/ac16ec00dc018e5b17a8c23a025f98e693695527/lecturescape/lecturescape/algorithms.py 406 | # updated for peaks finding 407 | def detect_peaks(data, tau=1.5): 408 | """ 409 | peak detection algorithm 410 | """ 411 | 412 | def detect_peaks_update(old_mean, old_mean_dev, update_value): 413 | ALPHA = 0.125 414 | diff = math.fabs(old_mean - update_value) 415 | new_mean_dev = ALPHA * diff + (1-ALPHA) * old_mean_dev 416 | new_mean = ALPHA * update_value + (1-ALPHA) * old_mean 417 | return [new_mean, new_mean_dev] 418 | 419 | bins = data 420 | P = 5 421 | TAU = tau 422 | 423 | # list of peaks - their start, end, and peak time 424 | windows = pd.DataFrame(columns=['start', 'peak', 'end', 'rise_rate']) 425 | if len(bins) <= 5: 426 | return windows 427 | 428 | if np.isnan(bins[5]) or bins[5] is ma.masked or bins[5] == 0: 429 | mean = np.mean(bins) 430 | else: 431 | mean = bins[5] 432 | if np.isnan(np.var(bins[5:5+P])) or np.var(bins[5:5+P]) is ma.masked or np.var(bins[5:5+P]) == 0: 433 | meandev = np.var(bins) 434 | else: 435 | meandev = np.var(bins[5:5+P]) 436 | 437 | i = 5 438 | while i < len(bins): 439 | if np.isnan(bins[i]) or bins[i] is ma.masked: 440 | i += 1 441 | continue 442 | rise_rate = math.fabs(bins[i] - mean) / meandev 443 | if rise_rate > TAU and bins[i] > bins[i-1]: 444 | start = i - 1 445 | while i < len(bins) and bins[i] > bins[i-1]: 446 | [mean, meandev] = detect_peaks_update(mean, meandev, bins[i]) 447 | i += 1 448 | peak = i - 1 449 | end = i 450 | 451 | # until the bin counts are back at the level they started 452 | while i < len(bins) and bins[i] > bins[start]: 453 | # UPDATE: the peak point should be maximum between start and end 454 | if bins[peak] < bins[i-1] and bins[i] < bins[i-1]: 455 | peak = i - 1 456 | 457 | if math.fabs(bins[i] - mean) / meandev > TAU and bins[i] > bins[i-1]: 458 | # another significant rise found, so going back and quit the downhill climbing 459 | while bins[i] > bins[i-1] and i > start: 460 | i -= 1 461 | end = i 462 | break 463 | 464 | [mean, meandev] = detect_peaks_update(mean, meandev, bins[i]) 465 | i += 1 466 | end = i 467 | 468 | windows = windows.append({'start': start, 469 | 'peak': peak, 470 | 'end': end, 471 | 'rise_rate': rise_rate}, ignore_index=True) 472 | else: 473 | [mean, meandev] = detect_peaks_update(mean, meandev, bins[i]) 474 | i += 1 475 | 476 | windows[['start', 'peak', 'end']] = windows[['start', 'peak', 'end']].astype(int) 477 | return windows 478 | 479 | 480 | def peak_plot(data, windows, ax): 481 | """Plot results of the detect_peaks function.""" 482 | 483 | peak_index = np.array(windows['peak'].tolist()) 484 | 485 | if not ax: 486 | _, ax = plt.subplots(1, 1, figsize=(8, 4)) 487 | 488 | ax.plot(data, '#66CC66', lw=1) 489 | 490 | if peak_index.size: 491 | #label = 'peak' 492 | #label = label + 's' if peak_index.size > 1 else label 493 | #ax.plot(peak_index, data[peak_index], '+', mfc=None, mec='r', mew=2, ms=8, 494 | # label='%d %s' % (peak_index.size, label)) 495 | 496 | for ind, row in windows.iterrows(): 497 | start = row['start'] 498 | end = row['end'] 499 | is_common = row['is_common'] 500 | width = end - start 501 | 502 | if is_common: 503 | fill_color = '#A5E5A5' 504 | else: 505 | fill_color = '#CCCCCC' 506 | ax.fill_between(np.arange(start, end), np.zeros(width), data[start:end], 507 | interpolate=True, facecolor=fill_color, linewidth=0.0) 508 | 509 | ax.legend(loc='best', framealpha=.5, numpoints=1) 510 | 511 | ax.set_xlim(-.02*data.size, data.size*1.02-1) 512 | ymin, ymax = data[np.isfinite(data)].min(), data[np.isfinite(data)].max() 513 | yrange = ymax - ymin if ymax > ymin else 1 514 | ax.set_ylim(0, ymax + 0.1*yrange) 515 | -------------------------------------------------------------------------------- /examples/external-reports/pdf/test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StepicOrg/Stepik-API/74ec5da54d4f7d9003b53bed2c3304ce265e2c12/examples/external-reports/pdf/test -------------------------------------------------------------------------------- /examples/external-reports/video_report.py: -------------------------------------------------------------------------------- 1 | from library.models import VideoReport 2 | 3 | report = VideoReport() 4 | report.build() -------------------------------------------------------------------------------- /examples/get_active_courses.py: -------------------------------------------------------------------------------- 1 | # Script that produces: 2 | # a) courses, started in the last six months, in which there are new lessons in the last month, 3 | # b) courses created over the past six months, in which more than 10 students 4 | # 5 | # Issues (stdout + csv file): 6 | # 0) the condition a) or b) or a) + b) 7 | # 1) a reference to the course 8 | # 2) the name of the course 9 | # 3) name of the author of the course 10 | # 4) the course author email 11 | 12 | import csv 13 | import json 14 | import requests 15 | import datetime 16 | from dateutil import parser 17 | 18 | def get_token(): 19 | # Get your keys at https://stepik.org/oauth2/applications/ 20 | # (client type = confidential, authorization grant type = client credentials) 21 | client_id = '...' 22 | client_secret = '...' 23 | 24 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 25 | resp = requests.post('https://stepik.org/oauth2/token/', 26 | data={'grant_type': 'client_credentials'}, 27 | auth=auth) 28 | token = json.loads(resp.text)['access_token'] 29 | return token 30 | 31 | 32 | def get_data(pageNum): 33 | api_url = 'https://stepik.org/api/courses?page={}'.format(pageNum) 34 | course = json.loads(requests.get(api_url, headers={'Authorization': 'Bearer ' + get_token()}).text) 35 | return course 36 | 37 | 38 | limit_6m = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=31*6) 39 | limit_1m = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=31) 40 | 41 | def get_courses(): 42 | page = 1 43 | while True: 44 | api_url = 'https://stepik.org/api/courses?page={}'.format(page) 45 | courses = json.loads(requests.get(api_url, headers={'Authorization': 'Bearer ' + get_token()}).text)['courses'] 46 | for course in courses: 47 | if parser.parse(course['create_date']) < limit_6m: 48 | return 49 | # a) courses, started in the last six months, in which there are new lessons in the last month 50 | a = '' 51 | api_url = 'https://stepik.org/api/lessons?course={}&order=-id'.format(course['id']) 52 | lessons = json.loads(requests.get(api_url, headers={'Authorization': 'Bearer ' + get_token()}).text)['lessons'] 53 | if lessons and parser.parse(lessons[0]['create_date']) > limit_1m: 54 | a = 'A' 55 | # b) courses created over the past six months, in which more than 10 students 56 | b = '' 57 | api_url = 'https://stepik.org/api/members?group={}'.format(course['learners_group']) 58 | members = json.loads(requests.get(api_url, headers={'Authorization': 'Bearer ' + get_token()}).text)['members'] 59 | if len(members) > 10: 60 | b = 'B' 61 | # Issues a row 62 | if a or b: 63 | owner = course['owner'] 64 | api_url = 'https://stepik.org/api/users/{}'.format(owner) 65 | user = json.loads(requests.get(api_url, headers={'Authorization': 'Bearer ' + get_token()}).text)['users'][0] 66 | owner_name = user['first_name'] + ' ' + user['last_name'] 67 | api_url = 'https://stepik.org/api/email-addresses?user={}&is_primary=true'.format(owner) 68 | owner_email = json.loads(requests.get(api_url, headers={'Authorization': 'Bearer ' + get_token()}).text)['email-addresses'][0]['email'] 69 | link = 'https://stepik.org/{}'.format(course['id']) 70 | row = [a, b, link, course['title'], owner_name, owner_email] 71 | yield row 72 | page += 1 73 | 74 | csv_file = open('get_active_courses-{}.csv'.format(datetime.date.today()), 'w') 75 | csv_writer = csv.writer(csv_file) 76 | 77 | header = ['A?', 'B?', 'Link', 'Title', 'Owner', 'OwnerEmail'] 78 | csv_writer.writerow(header) 79 | print('\t'.join(header)) 80 | 81 | for row in get_courses(): 82 | csv_writer.writerow(row) 83 | print('\t'.join(row)) 84 | 85 | csv_file.close() -------------------------------------------------------------------------------- /examples/get_certificates_urls_example.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | import requests 3 | 4 | # 1. Get your keys at https://stepik.org/oauth2/applications/ (client type = confidential, 5 | # authorization grant type = client credentials) 6 | client_id = "..." 7 | client_secret = "..." 8 | 9 | # 2. Get a token 10 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 11 | resp = requests.post('https://stepik.org/oauth2/token/', 12 | data={'grant_type': 'client_credentials'}, 13 | auth=auth 14 | ) 15 | token = resp.json()['access_token'] 16 | 17 | # 3. Call API (https://stepik.org/api/docs/) using this token. 18 | # Example: 19 | 20 | 21 | def get_user_id(): 22 | api_url = 'https://stepik.org/api/stepics/1' # should be stepic with "c"! 23 | user = requests.get(api_url, headers={'Authorization': 'Bearer ' + token}).json() 24 | return user['users'][0]['id'] 25 | 26 | 27 | def get_certificates(page_number): 28 | user_id = get_user_id() 29 | api_url = 'https://stepik.org/api/certificates?user={}&page={}'.format(user_id, page_number) 30 | page = requests.get(api_url, headers={'Authorization': 'Bearer ' + token}).json() 31 | 32 | for certificate in page['certificates']: 33 | links.append(certificate['url']) 34 | 35 | return page['meta']['has_next'] 36 | 37 | 38 | def get_certificate_links(): 39 | has_next = True 40 | page = 1 41 | 42 | while has_next: 43 | has_next = get_certificates(page) 44 | page += 1 45 | 46 | 47 | links = [] 48 | get_certificate_links() 49 | 50 | with open('certificates.html', 'w', encoding='utf-8') as f: 51 | f.write(' \n') 52 | f.write(' \n') 53 | f.write(' Certificates \n') 54 | f.write(' \n') 55 | f.write(' \n') 56 | f.write(' \n') 57 | f.write('

Certificates

\n') 58 | f.write('
    \n') 59 | 60 | for url in links: 61 | f.write('
  1. {}
  2. \n'.format(url, url)) 62 | 63 | f.write('
\n') 64 | f.write(' \n') 65 | f.write(' \n') 66 | -------------------------------------------------------------------------------- /examples/get_countries_all_count.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | # Count the number of countries known to Stepik! 3 | 4 | import requests 5 | 6 | # Copied from test_examples.py 7 | client_id = "..." 8 | client_secret = "..." 9 | 10 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 11 | resp = requests.post('https://stepik.org/oauth2/token/', 12 | data={'grant_type': 'client_credentials'}, 13 | auth=auth 14 | ) 15 | token = resp.json()['access_token'] 16 | 17 | def get_data(page_id): 18 | api_url = 'https://stepik.org:443/api/countries?page={}'.format(page_id) 19 | response = requests.get(api_url, headers={'Authorization': 'Bearer '+ token}).json() 20 | return response 21 | 22 | count = 0 23 | page_id = 0 24 | response = {'countries': [], 'meta': {'has_next': True}} 25 | 26 | # loop invariant: we've counted countries up to the current response including 27 | while response['meta']['has_next']: 28 | page_id += 1 29 | response = get_data(page_id) 30 | count += len(response['countries']) 31 | 32 | print('Seems like Stepik has knowledge about {} countries. Wow!'.format(count)) 33 | -------------------------------------------------------------------------------- /examples/get_courses_authors.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | client_id = "..." 4 | client_secret = "..." 5 | try: 6 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret); 7 | resp = requests.post('https://stepik.org/oauth2/token/', data={'grant_type': 'client_credentials'},auth=auth) 8 | token = resp.json()['access_token'] 9 | except: 10 | print('problems getting token') 11 | api_url = "https://stepik.org/api/"; 12 | 13 | author_courses = {} 14 | page_ind = 1 15 | 16 | print('Please, wait a bit while list of courses is being processed') 17 | # in this cycle we get all courses' titles and their authors 18 | while True: 19 | page = requests.get(api_url + 'courses' + '?page=' + str(page_ind) + '&language=ru', headers={'Authorization': 'Bearer ' + token}).json() 20 | for course in page['courses']: 21 | # ignore 'dead' courses 22 | if course['discussions_count'] == 0: 23 | continue 24 | for author in course['authors']: 25 | if not author in author_courses: 26 | author_courses.update({author : set()}) 27 | 28 | # for each author we will have all courses that were created with his participation 29 | author_courses[author].add(course['title']) 30 | if not page['meta']['has_next']: 31 | break 32 | page_ind += 1 33 | 34 | for user_id,titles in author_courses.items(): 35 | user = requests.get(api_url + 'users/' + str(user_id)).json()['users'][0] 36 | print() 37 | print(user['first_name'], user['last_name'], 'took part in creating ', end='') 38 | if len(titles) == 1: 39 | print('this course:') 40 | else: 41 | print('these courses:') 42 | for title in titles: 43 | print(title) 44 | -------------------------------------------------------------------------------- /examples/get_courses_by_params.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | 5 | def get_token(): 6 | client_id = "..." 7 | client_secret = "..." 8 | 9 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 10 | resp = requests.post('https://stepik.org/oauth2/token/', 11 | data={'grant_type': 'client_credentials'}, 12 | auth=auth) 13 | token = json.loads(resp.text)['access_token'] 14 | return token 15 | 16 | 17 | def get_data(pageNum): 18 | api_url = 'https://stepik.org/api/courses?page={}'.format(pageNum) 19 | course = json.loads(requests.get(api_url, headers={'Authorization': 'Bearer ' + get_token()}).text) 20 | return course 21 | 22 | 23 | def get_chosen_courses(amountOfUnits, courseLang, amountOfDiscuss): 24 | pageNum = 0 25 | hasNextPage = True 26 | listOfChoices = [] 27 | while hasNextPage: 28 | try: 29 | pageNum += 1 30 | pageContent = get_data(pageNum) 31 | hasNextPage = pageContent['meta']['has_next'] 32 | courses = pageContent['courses'] 33 | for course in courses: # Select only active courses (courses with active session) 34 | if ((course['total_units']) > amountOfUnits and (course['language'] == courseLang) 35 | and (course['is_active'] == True) and (course['discussions_count'] > amountOfDiscuss)): 36 | listOfChoices.append({ 37 | 'course_name': course['slug'], 38 | 'amount_of_units': course['total_units'], 39 | 'language': course['language'], 40 | 'create_date': course['create_date'], 41 | 'discussions_count': course['discussions_count'] 42 | }) 43 | except: 44 | print("Error exception: something was broken!") 45 | 46 | print(listOfChoices) 47 | 48 | 49 | def main(): 50 | # Choose values of parameters for a course choice 51 | # Example: 52 | amountOfUnits = 5 # amount of units in a course 53 | courseLang = 'ru' # language of the chosen course 54 | amountOfDiscuss = 30 # number of discussions in a course (as an indicator of the popularity) 55 | get_chosen_courses(amountOfUnits, courseLang, amountOfDiscuss) 56 | 57 | main() 58 | -------------------------------------------------------------------------------- /examples/get_enrolled_courses.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import requests 4 | 5 | # Enter parameters below: 6 | # 1. Get your keys at https://stepik.org/oauth2/applications/ (client type = confidential, 7 | # authorization grant type = client credentials) 8 | client_id = '...' 9 | client_secret = '...' 10 | api_host = 'https://stepik.org' 11 | 12 | # 2. Get a token 13 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 14 | resp = requests.post('https://stepik.org/oauth2/token/', 15 | data={'grant_type': 'client_credentials'}, 16 | auth=auth 17 | ) 18 | token = resp.json().get('access_token') 19 | if not token: 20 | raise RuntimeWarning('Client id/secret is probably incorrect') 21 | 22 | 23 | # 3. Call API (https://stepik.org/api/docs/) using this token. 24 | # Generator definition for iterating over pages 25 | def list_pages(api_url, obj_class): 26 | has_next = True 27 | page = 1 28 | if '?' in api_url: 29 | connector = '&' 30 | else: 31 | connector = '?' 32 | while has_next: 33 | response = requests.get(api_url + '{}page={}'.format(connector, page), 34 | headers={'Authorization': 'Bearer ' + token}).json() 35 | yield response[obj_class] 36 | page += 1 37 | has_next = response['meta']['has_next'] 38 | 39 | 40 | # Access to any API method 41 | def fetch_object(obj_class, query_string=''): 42 | api_url = '{}/api/{}{}'.format(api_host, obj_class, query_string) 43 | response = list_pages(api_url, obj_class) 44 | return [obj for page in response for obj in page] # Example of using generator 45 | 46 | 47 | # Information about course sections 48 | def get_sections(course_sections): 49 | qs = '?ids[]=' + '&ids[]='.join([str(cs) for cs in course_sections]) # Example of multiple IDs call 50 | sections = fetch_object('sections', qs) 51 | return sections 52 | 53 | 54 | # Information about enrolled courses 55 | def get_enrolled_courses(): 56 | courses = fetch_object('courses', '?enrolled=true') 57 | for course in courses: 58 | course['sections'] = get_sections(course['sections']) 59 | return courses 60 | 61 | 62 | # Retrieving course information 63 | courses = get_enrolled_courses() 64 | 65 | # and HTML-report generating 66 | with open('enrolled_courses.html', 'w', encoding='utf-8') as f: 67 | f.write('') 68 | f.write('') 69 | f.write('') 70 | f.write('Courses enrolled by user') 71 | f.write('') 72 | f.write('') 73 | for course in courses: 74 | f.write('

{1}

'.format(course['slug'], course['title'])) 75 | f.write('

{}

'.format(course['summary'])) 76 | if course['sections']: 77 | f.write('

Course sections:

') 78 | f.write('') 82 | f.write('
') 83 | f.write('') 84 | f.write('') 85 | -------------------------------------------------------------------------------- /examples/get_info_all_courses_titles.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | has_next = True 5 | page = 0 6 | titles = [] 7 | while has_next: 8 | page += 1 9 | url = 'https://stepik.org/api/courses?page={}'.format(page) 10 | courses = requests.get(url).json()['courses'] 11 | has_next = requests.get(url).json()['meta']['has_next'] 12 | 13 | page_titles = [course['title'] for course in courses] 14 | for title in page_titles: 15 | titles.append(title) 16 | 17 | python_titles = 0 18 | for title in titles: 19 | if 'python' in title.lower(): 20 | python_titles += 1 21 | 22 | if python_titles > len(titles) - python_titles: 23 | a = 'больше' 24 | elif python_titles == len(titles) - python_titles: 25 | a = 'cтолько же' 26 | else: 27 | a = 'меньше' 28 | 29 | print('Курсов по Python {}.'.format(a)) 30 | -------------------------------------------------------------------------------- /examples/get_leaders_social_profiles.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | import requests 3 | from pprint import pprint 4 | 5 | # Enter parameters below: 6 | # 1. Get your keys at https://stepik.org/oauth2/applications/ (client type = confidential, 7 | # authorization grant type = client credentials) 8 | CLIENT_ID = '...' 9 | CLIENT_SECRET = '...' 10 | API_HOST = 'https://stepik.org' 11 | 12 | # 2. Get a token 13 | AUTH = requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET) 14 | RESP = requests.post('https://stepik.org/oauth2/token/', 15 | data={'grant_type': 'client_credentials'}, 16 | auth=AUTH 17 | ) 18 | TOKEN = RESP.json()['access_token'] 19 | 20 | 21 | def prepare_ids(ids): 22 | request = '' 23 | for id in ids: 24 | request += 'ids[]={}&'.format(id) 25 | return request[:-1] 26 | 27 | 28 | def api_call(relative_url): 29 | api_url = API_HOST + ':443/api/' + relative_url 30 | data = requests.get(api_url, headers={'Authorization': 'Bearer ' + TOKEN}) 31 | return data 32 | 33 | 34 | def get_leaders(): 35 | data = api_call('leaders') 36 | return data.json()['leaders'] 37 | 38 | 39 | def get_users(user_ids): 40 | request = prepare_ids(user_ids) 41 | data = api_call('users?{}'.format(request)) 42 | return data.json()['users'] 43 | 44 | 45 | def get_social_profiles(social_ids): 46 | if not social_ids: 47 | return None 48 | request = prepare_ids(social_ids) 49 | data = api_call('social-profiles?{}'.format(request)) 50 | profiles = data.json()['social-profiles'] 51 | accounts = {} 52 | for profile in profiles: 53 | accounts[profile['provider']] = profile['url'] 54 | return accounts 55 | 56 | 57 | def main(): 58 | leaders = get_leaders() 59 | users = [] 60 | while leaders: 61 | IDS_PER_REQUEST = 10 62 | ids = [u['user'] for u in leaders[:IDS_PER_REQUEST]] 63 | leaders = leaders[IDS_PER_REQUEST:] 64 | users += get_users(ids) 65 | 66 | result = [] 67 | 68 | for index, user in enumerate(users): 69 | acc = get_social_profiles(user['social_profiles']) 70 | if acc: 71 | print('download data for ' + str(index + 1)) 72 | name = user['first_name'] 73 | if user['last_name']: 74 | name += ' ' + user['last_name'] 75 | result.append([index + 1, name, acc]) 76 | 77 | pprint(result) 78 | 79 | 80 | if __name__ == '__main__': 81 | main() 82 | -------------------------------------------------------------------------------- /examples/get_learn_events.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | import re 3 | import json 4 | import requests 5 | 6 | # 1. Get your keys at https://stepik.org/oauth2/applications/ (client type = confidential, 7 | # authorization grant type = client credentials) 8 | client_id = "..." 9 | client_secret = "..." 10 | 11 | # 2. Get a token 12 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 13 | resp = requests.post('https://stepik.org/oauth2/token/', 14 | data={'grant_type': 'client_credentials'}, 15 | auth=auth 16 | ) 17 | token = json.loads(resp.text)['access_token'] 18 | 19 | # 3. Get all learning events 20 | page = 1 21 | events = [] 22 | while True: 23 | api_url = 'https://stepik.org/api/events?type=learn' + '&page=' + str(page) 24 | data = json.loads(requests.get(api_url, headers={'Authorization': 'Bearer '+ token}).text) 25 | events += data['events'] 26 | if not data['meta']['has_next']: 27 | break 28 | page += 1 29 | 30 | # 4. Print them all 31 | print('\t\t\t'.join(['ID', 'Time', 'Action', 'URL'])) 32 | for event in events: 33 | match = re.search(r'href=[\'"]?([^\'" >]+)', event['html_text']) 34 | url = match.group(0) if match else 'none' 35 | print('\t\t'.join([str(event['id']), event['time'], event['action'], url])) 36 | -------------------------------------------------------------------------------- /examples/get_ten_users_with_highest_reputation.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import json 3 | import requests 4 | from operator import itemgetter 5 | 6 | # Enter parameters below: 7 | # 1. Get your keys at https://stepik.org/oauth2/applications/ (client type = confidential, 8 | # authorization grant type = client credentials) 9 | client_id = "..." 10 | client_secret = "..." 11 | 12 | class Getter(object): 13 | def __init__(self, client_id, secret_id): 14 | self.client_id = client_id 15 | self.secret_id = secret_id 16 | self.url = 'https://stepik.org/api/' 17 | try: 18 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 19 | resp = requests.post('https://stepik.org/oauth2/token/', 20 | data={'grant_type': 'client_credentials'}, 21 | auth=auth 22 | ).json() 23 | self.token = resp['access_token'] 24 | except: 25 | print("Unable to get token") 26 | 27 | def get(self, url): 28 | try: 29 | resp = requests.get(url, headers={'Authorization': 'Bearer ' + self.token}).json() 30 | except: 31 | print("Unable to get data") 32 | resp = None 33 | return resp 34 | 35 | 36 | def get_users(self): 37 | page = 0 38 | reputations = [] 39 | logins = [] 40 | has_next = True 41 | while(has_next): 42 | page += 1 43 | cur_url = ("{}/users?page={}").format(self.url, page); 44 | result = self.get(cur_url) 45 | profiles = result['users'] 46 | has_next = result['meta']['has_next'] 47 | for profile in profiles: 48 | reputations.append(profile['reputation']) 49 | name = profile['first_name'] + " " + profile['last_name'] 50 | logins.append(name) 51 | users = [list(c) for c in zip(reputations, logins)] 52 | return users 53 | 54 | 55 | getter = Getter(client_id, client_secret) 56 | users = getter.get_users() 57 | users.sort(key=itemgetter(0), reverse=True) 58 | for i in range(10): 59 | print(users[i]) 60 | -------------------------------------------------------------------------------- /examples/get_top_lessons_by_reactions.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | import collections 3 | import json 4 | import operator 5 | import requests 6 | import urllib 7 | 8 | # 1. Get your keys at https://stepik.org/oauth2/applications/ (client type = confidential, 9 | # authorization grant type = client credentials) 10 | client_id = "..." 11 | client_secret = "..." 12 | 13 | # 2. Get a token 14 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 15 | resp = requests.post( 16 | 'https://stepik.org/oauth2/token/', 17 | data={'grant_type': 'client_credentials'}, 18 | auth=auth 19 | ) 20 | response = json.loads(resp.text) 21 | token = response.get('access_token') 22 | 23 | if not token: 24 | print('Auth unsuccessful') 25 | exit(0) 26 | 27 | 28 | # 3. Call API (https://stepik.org/api/docs/) using this token. 29 | # Example: 30 | 31 | def load_api(method, **kwargs): 32 | api_url = 'https://stepik.org:443/api/' 33 | params = urllib.parse.urlencode(kwargs) 34 | return json.loads(requests.get( 35 | api_url + method + '/?' + params, 36 | headers={'Authorization': 'Bearer ' + token}).text 37 | ) 38 | 39 | # Get top lessons by reaction on recommendations 40 | 41 | NUM_OF_REACTION_PAGES = 100 42 | NUM_OF_REACTION_TOP = 10 43 | 44 | lessons_method = 'lessons' 45 | recommendation_reactions_method = 'recommendation-reactions' 46 | 47 | 48 | def print_status(message, done, total): 49 | print('{}: {}/{}'.format(message, done, total), end='\r' if done != total else '\n') 50 | 51 | course_reactons = collections.defaultdict(int) 52 | 53 | for i in range(NUM_OF_REACTION_PAGES): 54 | print_status('Loading', i+1, NUM_OF_REACTION_PAGES) 55 | response = load_api(recommendation_reactions_method, page=i+1) 56 | 57 | for reaction in response.get('recommendation-reactions'): 58 | value = reaction.get('reaction') 59 | course_reactons[reaction.get('lesson')] += value if value > 0 else -value - 1 60 | 61 | if not response.get('meta').get('has_next'): 62 | break 63 | 64 | 65 | sorted_lessons = sorted(course_reactons.items(), key=operator.itemgetter(1), reverse=True) 66 | 67 | # Add all equal to the last 68 | while len(sorted_lessons) > NUM_OF_REACTION_TOP and \ 69 | sorted_lessons[NUM_OF_REACTION_TOP][1] == sorted_lessons[NUM_OF_REACTION_TOP - 1][1]: 70 | NUM_OF_REACTION_TOP += 1 71 | 72 | print('Top lessons by reaction') 73 | print('(Title: reaction value)') 74 | print() 75 | 76 | for lesson_id in sorted_lessons[:min(NUM_OF_REACTION_TOP, len(sorted_lessons))]: 77 | response = response = load_api('{}/{}'.format(lessons_method, lesson_id[0])) 78 | lesson = response.get('lessons')[0] 79 | 80 | print('{}: {}'.format(lesson.get('title'), lesson_id[1])) 81 | -------------------------------------------------------------------------------- /examples/get_user_courses.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | client_id = "..." 5 | client_secret = "..." 6 | 7 | class StepikAPI(object): 8 | 9 | def __init__(self, client_id, client_secret): 10 | """ """ 11 | self.api_url = 'https://stepik.org/api/' 12 | self.client_id = client_id 13 | self.client_secret = client_secret 14 | 15 | try: 16 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 17 | resp = requests.post('https://stepik.org/oauth2/token/', 18 | data={'grant_type': 'client_credentials'}, 19 | auth=auth 20 | ).json() 21 | self.token = resp['access_token'] 22 | except: 23 | print("Error while obtaining token") 24 | 25 | def get(self, url): 26 | """ """ 27 | try: 28 | resp = requests.get(url, headers={'Authorization': 'Bearer ' + self.token}).json() 29 | except: 30 | print("Error while getting data") 31 | resp = None 32 | return resp 33 | 34 | def course_subscriptions(self, page = 1): 35 | url = self.api_url + 'course-subscriptions?page={}'.format(page) 36 | return self.get(url) 37 | 38 | def course(self, course_id): 39 | url = self.api_url + 'courses/{}'.format(course_id) 40 | return self.get(url) 41 | 42 | 43 | sapi = StepikAPI(client_id, client_secret) 44 | 45 | #Example of getting list of user's courses 46 | has_next = True 47 | page = 0 48 | course_ids = [] 49 | while has_next: 50 | page += 1 51 | courses = sapi.course_subscriptions(page) 52 | has_next = courses['meta']['has_next'] 53 | for el in courses['course-subscriptions']: 54 | course_ids.append(el['course']) 55 | print("pages: %d"%page) 56 | print("course ids: %s"%str(course_ids)) 57 | 58 | #For each course get its info 59 | data = [] 60 | for course_id in course_ids: 61 | course_data = sapi.course(course_id) 62 | summary = (course_data['courses'][0]['summary']) 63 | title = (course_data['courses'][0]['title']) 64 | data.append((course_id, title, summary)) 65 | print(data) 66 | -------------------------------------------------------------------------------- /examples/get_user_name.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | 5 | token = "..." 6 | 7 | api_url = 'https://stepik.org/api/stepics/1' # should be stepic with "c"! 8 | resp = json.loads(requests.get(api_url, headers={'Authorization': 'Bearer '+ token}).text) 9 | 10 | user = resp['users'] 11 | name = user[0]['first_name'] +' ' + user[0]['last_name'] 12 | 13 | print(name) 14 | -------------------------------------------------------------------------------- /examples/google-scripts/featured_courses.gs: -------------------------------------------------------------------------------- 1 | function createStepikMenu(e) { 2 | SpreadsheetApp.getUi() 3 | .createMenu('Stepik') 4 | .addItem('Featured Courses', 'collectFeaturedCoursesInfo') 5 | .addToUi(); 6 | } 7 | 8 | function collectFeaturedCoursesInfo() 9 | { 10 | newSheet = createOrReplaceSheet('Featured Courses') 11 | newSheet.appendRow(['ID', 'Title', 'Course Format', 'Target Audience', 'Workload']); 12 | newSheet.getRange(1, 1, 1, 5).setFontWeight('bold') 13 | page = 1 14 | do { 15 | response = getApiRequest('courses?is_featured=True&page=' + page); 16 | response_json = JSON.parse(response); 17 | data = response_json['courses']; 18 | for(id in data) { 19 | item = data[id] 20 | url = '=HYPERLINK("https://stepik.org/course/'+ item.id +'"; "'+ item.id +'")' 21 | newSheet.appendRow([url, item.title, item.course_format, item.target_audience, item.workload]); 22 | } 23 | page++; 24 | } while(response_json['meta']['has_next']); 25 | } 26 | 27 | function createOrReplaceSheet(sheetName){ 28 | app = SpreadsheetApp.getActive() 29 | oldSheet = app.getSheetByName(sheetName); 30 | if(oldSheet) { 31 | app.deleteSheet(oldSheet); 32 | } 33 | newSheet = app.insertSheet(sheetName); 34 | return newSheet 35 | } 36 | -------------------------------------------------------------------------------- /examples/google-scripts/stepik.gs: -------------------------------------------------------------------------------- 1 | var CLIENT_ID = '...' 2 | var CLIENT_SECRET = '...' 3 | var API_HOST = 'https://stepik.org/' 4 | 5 | var token = getStepikToken() 6 | 7 | function getStepikToken(){ 8 | tokenUrl = API_HOST + 'oauth2/token/'; 9 | options = { 10 | 'method': 'post', 11 | 'headers': { 12 | 'Authorization': 'Basic ' + Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET) 13 | }, 14 | 'payload' : { 15 | 'grant_type': 'client_credentials' 16 | }, 17 | }; 18 | response = UrlFetchApp.fetch(tokenUrl, options); 19 | return JSON.parse(response.getContentText()).access_token 20 | } 21 | 22 | function getApiRequest(url){ 23 | options = { 24 | 'headers': { 25 | 'Authorization': 'Bearer ' + token 26 | } 27 | }; 28 | response = UrlFetchApp.fetch(API_HOST + 'api/' + url, options); 29 | return response.getContentText() 30 | } 31 | 32 | function fetchObjectsByPK(objectName, objectPK){ 33 | objectUrl = objectName + '/' + objectPK; 34 | return getApiRequest(objectUrl) 35 | } 36 | 37 | -------------------------------------------------------------------------------- /examples/monitor.php: -------------------------------------------------------------------------------- 1 | /var/www/html/index.html 5 | # 3. Add it to crontab -e to run every minute 6 | 7 | date_default_timezone_set('UTC'); 8 | 9 | session_start(); 10 | 11 | # Get your app keys on https://stepik.org/oauth2/applications (Client Type = confidential; Authorization Grant Type: client credentials) 12 | $token = apiRequest( 13 | 'https://stepik.org/oauth2/token/', 14 | array( 15 | 'grant_type' => 'client_credentials', 16 | 'client_id' => '...', 17 | 'client_secret' => '...') 18 | ); 19 | $_SESSION['access_token'] = $token->access_token; 20 | 21 | if (session('access_token')) { 22 | ?> 23 | 24 | 25 | 26 | 27 | Bioinformatics Contest 2017 Final Round 28 | 42 | 43 | 44 |

Bioinformatics Contest 2017 Final Round (finished!)

45 |

46 | Round Started: 00:01:00 (UTC). 47 | Round Ended: 23:59:00 (UTC). 48 | Round Date: 18 Feb 2017. 49 |

50 |

Last Update: (UTC).

51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {'course-grades'} as $course_grade) { 104 | if (!$course_grade->rank) continue; 105 | echo ''; 106 | echo ''; 107 | $users = apiRequest('https://stepik.org/api/users/' . $course_grade->user); 108 | $full_name = $users->users[0]->first_name . ' ' . $users->users[0]->last_name; 109 | $full_name = transliterate($full_name); 110 | echo ''; 111 | echo ''; 112 | foreach ($steps as $step) { 113 | $ok = False; 114 | foreach ($course_grade->results as $result) { 115 | if ($result->step_id != $step) continue; 116 | $color = 'white'; 117 | if ($result->score) { 118 | $color = 'yellow'; 119 | } 120 | if ($result->is_passed) { 121 | $color = 'lightgreen'; 122 | } 123 | echo ''; 124 | $ok = True; 125 | break; 126 | } 127 | if (!$ok) { 128 | echo ''; 129 | } 130 | } 131 | echo ''; 132 | } 133 | $page++; 134 | } while ($grades->meta->has_next); 135 | echo '
#NameTotalGene ExpressionReconstruction of BacteriaGenetic Linkage MapLocating InsertionsPathway Dissection
EasyHardEasyHard#1#2#3#4#5#6#7#8#9#10#1#2#3#4#5#6#1#2#3#4#5#6#7
' . $course_grade->rank . '' . $full_name . '' . round($course_grade->score - 1, 2) . '' . round($result->score, 2) . ' 0
'; 136 | } 137 | 138 | // # ================= 139 | // # HELPER FUNCTIONS 140 | // # ================= 141 | 142 | function apiRequest($url, $post = FALSE, $headers = array()) 143 | { 144 | $ch = curl_init($url); 145 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 146 | 147 | if ($post) 148 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post)); 149 | 150 | $headers[] = 'Accept: application/json'; 151 | 152 | if (session('access_token')) 153 | $headers[] = 'Authorization: Bearer ' . session('access_token'); 154 | 155 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 156 | 157 | $response = curl_exec($ch); 158 | return json_decode($response); 159 | } 160 | 161 | function get($key, $default = NULL) 162 | { 163 | return array_key_exists($key, $_GET) ? $_GET[$key] : $default; 164 | } 165 | 166 | function session($key, $default = NULL) 167 | { 168 | return array_key_exists($key, $_SESSION) ? $_SESSION[$key] : $default; 169 | } 170 | 171 | // http://stackoverflow.com/a/8285804 172 | function transliterate($textcyr = null, $textlat = null) { 173 | $cyr = array( 174 | 'а','б','в','г','д','е','ё','ж','з','и','й','к','л','м','н','о','п', 175 | 'р','с','т','у','ф','х','ц','ч','ш','щ','ъ','ы','ь','э','ю','я', 176 | 'А','Б','В','Г','Д','Е','Ё','Ж','З','И','Й','К','Л','М','Н','О','П', 177 | 'Р','С','Т','У','Ф','Х','Ц','Ч','Ш','Щ','Ъ','Ы','Ь','Э','Ю','Я' 178 | ); 179 | $lat = array( 180 | 'a','b','v','g','d','e','io','zh','z','i','y','k','l','m','n','o','p', 181 | 'r','s','t','u','f','h','ts','ch','sh','sht','a','i','y','e','yu','ya', 182 | 'A','B','V','G','D','E','Io','Zh','Z','I','Y','K','L','M','N','O','P', 183 | 'R','S','T','U','F','H','Ts','Ch','Sh','Sht','A','I','Y','e','Yu','Ya' 184 | ); 185 | if($textcyr) return str_replace($cyr, $lat, $textcyr); 186 | else if($textlat) return str_replace($lat, $cyr, $textlat); 187 | else return null; 188 | } 189 | ?> 190 | -------------------------------------------------------------------------------- /examples/oauth_auth_example.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | import requests 3 | 4 | # 1. Get your keys at https://stepik.org/oauth2/applications/ 5 | # (client type = confidential, authorization grant type = client credentials) 6 | client_id = "..." 7 | client_secret = "..." 8 | 9 | # 2. Get a token 10 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 11 | response = requests.post('https://stepik.org/oauth2/token/', 12 | data={'grant_type': 'client_credentials'}, 13 | auth=auth) 14 | token = response.json().get('access_token', None) 15 | if not token: 16 | print('Unable to authorize with provided credentials') 17 | exit(1) 18 | 19 | # 3. Call API (https://stepik.org/api/docs/) using this token. 20 | api_url = 'https://stepik.org/api/courses/67' 21 | course = requests.get(api_url, 22 | headers={'Authorization': 'Bearer ' + token}).json() 23 | 24 | print(course) 25 | -------------------------------------------------------------------------------- /examples/plot_course_viewed_by.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | client_id = "..." 7 | client_secret = "..." 8 | 9 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 10 | resp = requests.post('https://stepik.org/oauth2/token/', 11 | data={'grant_type': 'client_credentials'}, 12 | auth=auth) 13 | token = json.loads(resp.text)["access_token"] 14 | 15 | 16 | def make_stepik_api_call_pk(name, pk, key): 17 | api_url = 'https://stepik.org/api/{}/{}'.format(name, pk) 18 | res = json.loads( 19 | requests.get(api_url, 20 | headers={'Authorization': 'Bearer '+ token}).text 21 | )[name][0][key] 22 | return res 23 | 24 | 25 | def get_all_steps_viewed_by(course): 26 | sections = make_stepik_api_call_pk("courses", course, "sections") 27 | units = [unit 28 | for section in sections 29 | for unit in 30 | make_stepik_api_call_pk("sections", section, "units")] 31 | assignments = [assignment 32 | for unit in units 33 | for assignment in 34 | make_stepik_api_call_pk("units", unit, "assignments")] 35 | steps = [make_stepik_api_call_pk("assignments", assignment, "step") 36 | for assignment in assignments] 37 | viewed_by = [make_stepik_api_call_pk("steps", step, "viewed_by") 38 | for step in steps] 39 | return viewed_by 40 | 41 | 42 | def main(): 43 | # This script gets view statistics for all of the steps in some course 44 | # and then displays it. The trend is usually (naturally) declining 45 | course = 187 46 | viewed_by = get_all_steps_viewed_by(course) 47 | plt.plot(viewed_by) 48 | plt.show() 49 | 50 | 51 | if __name__ == '__main__': 52 | main() 53 | -------------------------------------------------------------------------------- /examples/plot_lesson_stats.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | import requests 3 | import pandas as pd 4 | import math 5 | import copy 6 | 7 | '''This example demonstrates how to get lessons data via Stepik-API and why it can be useful.''' 8 | 9 | '''We download lessons' data one by one, 10 | then we make plots to see how much the loss of the people depends on the lesson time ''' 11 | 12 | plots_message = '

Plots describe how quantity of people who viewed, ' \ 13 | 'passed and left depends on lesson duration.' 14 | 15 | enable_russian = ' \n' 16 | 17 | welcome_message = 'Hi!

Click on public lessons to check them out. ' \ 18 | '

List of existing lessons with id from {} to {}:
' 19 | 20 | setting_css_style = '' 21 | 22 | start_lesson_id = 1 23 | finish_lesson_id = 100 24 | 25 | # 1. Get your keys at https://stepik.org/oauth2/applications/ (client type = confidential, 26 | # authorization grant type = client credentials) 27 | client_id = "..." 28 | client_secret = "..." 29 | 30 | 31 | # 2. Get a token 32 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 33 | resp = requests.post('https://stepik.org/oauth2/token/', 34 | data={'grant_type': 'client_credentials'}, 35 | auth=auth 36 | ) 37 | token = resp.json()['access_token'] 38 | 39 | 40 | # Class for drawing plots in text 41 | class Figure: 42 | def __init__(self, space_for_digits=10, rows=25, columns=120, bar_quantity_in_one_fifth_y=5, 43 | underscore_quantity_in_one_fifth_x=20, divider_value=5, save_file='plot.html'): 44 | """ 45 | :param space_for_digits: constant to make spare space for OY values 46 | :param rows: full quantity of bars (|) OY 47 | :param columns: full quantity of underscores (_) OX 48 | :param bar_quantity_in_one_fifth_y: how many bars (|) are in 1 of divider-value parts 49 | :param underscore_quantity_in_one_fifth_x: how many underscores (_) are in 1 of divider-value 50 | :param divider_value: how many parts axes are divided into 51 | :param save_file: html file where we save result 52 | """ 53 | self.figure_matrix = [] # canvas 54 | self.rows = rows 55 | self.columns = columns 56 | self.space_for_digits = space_for_digits 57 | self.bar_quantity_y = bar_quantity_in_one_fifth_y 58 | self.underscore_quantity_x = underscore_quantity_in_one_fifth_x 59 | self.divider = divider_value 60 | self.file = save_file 61 | self.plot_matrix_list = [] 62 | # creating empty canvas 63 | for r in range(self.rows): # rows 64 | self.figure_matrix.append([]) # add empty 65 | for c in range(self.columns + self.space_for_digits): # each column 66 | self.figure_matrix[r].append(' ') # axes 67 | # drawing axes 68 | self.figure_matrix.append(['_'] * (self.columns + self.space_for_digits)) 69 | self.figure_matrix.append([' '] * (self.columns + self.space_for_digits)) 70 | for row in self.figure_matrix: 71 | row[self.space_for_digits] = '|' 72 | 73 | def add_barplot(self, x_axe, y_axe, name="Plot"): 74 | """ 75 | adds new bar matrix to plot_matrix_list 76 | :param x_axe - list of values X to put on OX axe 77 | :param y_axe: - list of values Y to put on OY axe 78 | :param name - title of this plot 79 | """ 80 | if x_axe and y_axe: 81 | # calculating canvas params of current plot 82 | max_x = max(x_axe) 83 | max_y = max(y_axe) 84 | step_y = max_y // self.divider 85 | step_x = max_x // self.divider 86 | value_of_bar = step_y / self.bar_quantity_y 87 | value_of_underscore = step_x / self.underscore_quantity_x 88 | current_plot_matrix = copy.deepcopy(self.figure_matrix) 89 | # drawing bars on figure_matrix canvas 90 | for point in range(len(x_axe)): 91 | current_x = x_axe[point] 92 | current_y = y_axe[point] 93 | if value_of_bar == 0: 94 | y = max_y 95 | else: 96 | y = round((max_y - current_y) // value_of_bar) 97 | if value_of_underscore == 0: 98 | x = max_x 99 | else: 100 | x = round(self.space_for_digits + current_x // value_of_underscore) 101 | for row_index in range(y, 26): 102 | current_plot_matrix[row_index][x] = '*' 103 | i = 0 104 | # putting values on axe Y 105 | while max_y >= 0: 106 | for dig in range(len(str(max_y))): 107 | current_plot_matrix[i][dig] = str(max_y)[dig] 108 | i += self.bar_quantity_y 109 | if max_y == step_y: 110 | break 111 | max_y -= step_y 112 | # putting values on axe X 113 | i = self.space_for_digits 114 | x_value = 0 115 | while max_x >= x_value: 116 | for dig in range(len(str(x_value))): 117 | current_plot_matrix[-1][i + dig] = str(x_value)[dig] 118 | i += self.underscore_quantity_x 119 | x_value += step_x 120 | # storing current plot in Figure field of all plots 121 | self.plot_matrix_list.append({"matrix": current_plot_matrix, "name": name}) 122 | 123 | # saving plots given to html file 124 | def save_plots_to_html(self): 125 | f = open(self.file, 'a', encoding='utf-8') 126 | f.write('{}'.format(plots_message)) 127 | for i in self.plot_matrix_list: 128 | f.write('

{}

\n
'.format(i["name"]))
129 |             for row in i["matrix"]:
130 |                 for symbol in row:
131 |                     f.write(str(symbol))
132 |                 f.write('\n')
133 |             f.write('\n\n')
134 |             f.write('

') 135 | 136 | 137 | def introduce_lessons_in_html(start, finish, json_of_lessons, html_file='lessons.html'): 138 | """ 139 | :param start: first id of lesson downloaded via API 140 | :param finish: last id of lesson downloaded via API 141 | :param json_of_lessons: json file we made by concatenating API answers that gave one-lesson-answer 142 | :param html_file: file we write to 143 | """ 144 | with open(html_file, 'w', encoding='utf-8') as f: 145 | # enabling russian language and setting html style for two-columns lists 146 | f.write(enable_russian + setting_css_style) 147 | f.write('{}
    \n'.format(welcome_message.format(start, finish))) 148 | for lesson in json_of_lessons: 149 | if lesson['is_public']: 150 | url = '{}'.format(lesson['slug'], lesson["title"]) 151 | f.write('
  1. {}
  2. \n'.format(url)) 152 | else: 153 | f.write('
  3. {}
  4. \n'.format(lesson['title'])) 154 | f.write('
\n') 155 | f.close() 156 | 157 | 158 | # 3. Call API (https://stepik.org/api/docs/) using this token. 159 | # Example: 160 | def get_lessons_from_n_to_m(from_n, to_m, current_token): 161 | """ 162 | :param from_n: starting lesson id 163 | :param to_m: finish lesson id 164 | :param current_token: token given by API 165 | :return: json object with all existing lessons with id from from_n to to_m 166 | """ 167 | api_url = 'https://stepik.org/api/lessons/' 168 | json_of_n_lessons = [] 169 | for n in range(from_n, to_m + 1): 170 | try: 171 | current_answer = (requests.get(api_url + str(n), 172 | headers={'Authorization': 'Bearer ' + current_token}).json()) 173 | # check if lesson exists 174 | if not ("detail" in current_answer): 175 | json_of_n_lessons.append(current_answer['lessons'][0]) 176 | except: 177 | print("Failure on id {}".format(n)) 178 | return json_of_n_lessons 179 | 180 | 181 | def nan_to_zero(*args): 182 | """ 183 | :param args: lists with possible float-nan values 184 | :return: same list with all nans replaced by 0 185 | """ 186 | for current_list in args: 187 | for i in range(len(current_list)): 188 | if not math.isnan(current_list[i]): 189 | current_list[i] = round(current_list[i]) 190 | else: 191 | current_list[i] = 0 192 | 193 | 194 | if __name__ == '__main__': 195 | # downloading lessons using API 196 | json_of_lessons_being_analyzed = get_lessons_from_n_to_m(start_lesson_id, finish_lesson_id, token) 197 | 198 | # storing the result in pandas DataFrame 199 | lessons_data_frame = pd.DataFrame(json_of_lessons_being_analyzed) 200 | 201 | # extracting the data needed 202 | passed = lessons_data_frame['passed_by'].values 203 | time_to_complete = lessons_data_frame['time_to_complete'].values 204 | viewed = lessons_data_frame['viewed_by'].values 205 | left = viewed - passed 206 | 207 | # replacing data-slices by lists of their values 208 | time_to_complete = time_to_complete.tolist() 209 | viewed = viewed.tolist() 210 | passed = passed.tolist() 211 | left = left.tolist() 212 | 213 | # replacing nan-values with 0 and rounding values 214 | nan_to_zero(time_to_complete, viewed, passed, left) 215 | 216 | # creating new Figure to make plots 217 | figure1 = Figure(save_file='lessons.html') 218 | 219 | # adding bar diagrams to Figure f1 220 | figure1.add_barplot(time_to_complete, viewed, "X -- time to complete | Y - quantity of people who viewed") 221 | figure1.add_barplot(time_to_complete, passed, "X -- time to complete | Y - quantity of people who passed") 222 | figure1.add_barplot(time_to_complete, left, "X -- time to complete | Y - quantity of people who left") 223 | 224 | # creating html-file describing lessons 225 | introduce_lessons_in_html(start_lesson_id, finish_lesson_id, json_of_lessons_being_analyzed, 'lessons.html') 226 | # saving plots (file is linked with Figure object f1) 227 | figure1.save_plots_to_html() 228 | -------------------------------------------------------------------------------- /examples/popular_courses.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | import json 3 | import requests 4 | 5 | ### DATA ENDS 6 | # Enter parameters below: 7 | # 1. Get your keys at https://stepik.org/oauth2/applications/ (client type = confidential, 8 | # authorization grant type = client credentials) 9 | client_id = '...' 10 | client_secret = '...' 11 | api_host = 'https://stepik.org' 12 | 13 | # 2. Get a token 14 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 15 | resp = requests.post('https://stepik.org/oauth2/token/', 16 | data={'grant_type': 'client_credentials'}, 17 | auth=auth 18 | ) 19 | token = resp.json().get('access_token') 20 | if not token: 21 | raise RuntimeWarning('Client id/secret is probably incorrect') 22 | 23 | 24 | # 3. Call API (https://stepik.org/api/docs/) using this token. 25 | def fetch_object(obj_class, obj_id): 26 | api_url = '{}/api/{}s/{}'.format(api_host, obj_class, obj_id) 27 | response = requests.get(api_url, 28 | headers={'Authorization': 'Bearer ' + token}).json() 29 | return response['{}s'.format(obj_class)][0] 30 | 31 | 32 | def fetch_objects(obj_class, obj_ids): 33 | objs = [] 34 | # Fetch objects by 30 items, 35 | # so we won't bump into HTTP request length limits 36 | step_size = 30 37 | for i in range(0, len(obj_ids), step_size): 38 | obj_ids_slice = obj_ids[i:i + step_size] 39 | api_url = '{}/api/{}s?{}'.format(api_host, obj_class, 40 | '&'.join('ids[]={}'.format(obj_id) 41 | for obj_id in obj_ids_slice)) 42 | response = requests.get(api_url, 43 | headers={'Authorization': 'Bearer ' + token} 44 | ).json() 45 | objs += response['{}s'.format(obj_class)] 46 | return objs 47 | 48 | # SOMETHING MEANINGFUL: 49 | 50 | courses = fetch_objects('course', list(range(2000))) 51 | 52 | total_courses = 0 53 | total_enrollments = 0 54 | total_lessons = 0 55 | 56 | for course in courses: 57 | course_id = course['id'] 58 | learners_group_id = course['learners_group'] 59 | learners_group = fetch_object('group', learners_group_id) 60 | learners_count = len(learners_group['users']) 61 | sections = fetch_objects('section', course['sections']) 62 | units_count = sum(len(section['units']) for section in sections) 63 | lessons_count = units_count 64 | print(course_id, learners_count, lessons_count) 65 | if learners_count >= 10: 66 | total_courses += 1 67 | total_enrollments += learners_count 68 | total_lessons += lessons_count 69 | 70 | print('Number of courses with >= 10 learners:', total_courses) 71 | print('Total number of enrollments in such courses:', total_enrollments) 72 | print('Total number of lessons in such courses:', total_lessons) -------------------------------------------------------------------------------- /examples/recommendations_top_example.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import requests 4 | from collections import Counter 5 | 6 | 7 | api_url = 'https://stepik.org/api/' 8 | 9 | 10 | def get_api_requests(topic, pages_max_num=None): 11 | result = [] 12 | page_num = 1 13 | 14 | while True if pages_max_num is None else page_num <= pages_max_num: 15 | request_url = (api_url + topic + '?page={}').format(page_num) 16 | request_data = json.loads(requests.get(request_url, headers={'Authorization': 'Bearer '+ token}).text) 17 | request_result = request_data[topic] 18 | result.extend(request_result) 19 | 20 | if not request_data['meta']['has_next']: 21 | break 22 | else: 23 | page_num += 1 24 | 25 | return result 26 | 27 | 28 | def get_recommendation_reactions_lessons_count(pages_max_num=None): 29 | recommendation_reactions = get_api_requests('recommendation-reactions', pages_max_num) 30 | recommendation_reactions_lessons = [item['lesson'] for item in recommendation_reactions] 31 | return Counter(recommendation_reactions_lessons) 32 | 33 | 34 | def get_top_lessons(recommendations, n, titles=False): 35 | sorted_keys = [key for (key, value) in sorted(lessons_top.items(), key=lambda x: x[1], reverse=True)] 36 | 37 | top_n_keys = sorted_keys[:n] 38 | 39 | if titles: 40 | lessons = get_api_requests('lessons') 41 | top_lessons_list = [] 42 | 43 | for id in top_n_keys: 44 | 45 | for lesson in lessons: 46 | 47 | if lesson['id'] == id: 48 | top_lessons_list.append((id, lesson['title'], recommendations[id])) 49 | break 50 | 51 | else: 52 | top_lessons_list = [(id, recommendations[id]) for id in top_n_keys] 53 | 54 | return top_lessons_list 55 | 56 | 57 | def print_scores(top_lessons, titles=False): 58 | for lesson in top_lessons: 59 | lesson_id = lesson[0] 60 | num_recs = lesson[2] if titles else lesson[1] 61 | 62 | if titles: 63 | title = lesson[1] 64 | out_str = 'Course-ID: {0}, title: {1}, #recs: {2}'.format(lesson_id, title, num_recs) 65 | else: 66 | out_str = 'Course-ID: {0}, #recs: {1}'.format(lesson_id, num_recs) 67 | 68 | print(out_str) 69 | 70 | 71 | if __name__ == "__main__": 72 | 73 | client_id = "..." 74 | client_secret = "..." 75 | 76 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 77 | resp = requests.post('https://stepik.org/oauth2/token/', 78 | data={'grant_type': 'client_credentials'}, 79 | auth=auth 80 | ) 81 | token = json.loads(resp.text)['access_token'] 82 | 83 | lessons_top = get_recommendation_reactions_lessons_count(pages_max_num=50) 84 | 85 | lessons_top_10 = get_top_lessons(lessons_top, 10) 86 | 87 | print_scores(lessons_top_10) 88 | -------------------------------------------------------------------------------- /examples/save_course_slides.py: -------------------------------------------------------------------------------- 1 | # Run with Python3 2 | # Saves all presentations from course in pdfs and arranges them into folders 3 | # BeautifulSoup is required, just do: pip install beautifulsoup4 4 | 5 | import requests 6 | import sys 7 | import os 8 | from bs4 import BeautifulSoup 9 | 10 | client_id = "..." 11 | client_secret = "..." 12 | api_host = "https://stepik.org/" 13 | 14 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 15 | response = requests.post('https://stepik.org/oauth2/token/', 16 | data={'grant_type': 'client_credentials'}, 17 | auth=auth) 18 | token = response.json().get('access_token', None) 19 | if not token: 20 | print('Unable to authorize with provided credentials') 21 | exit(1) 22 | 23 | course_id = 0 24 | 25 | if len(sys.argv) == 2: 26 | course_id = sys.argv[1] 27 | else: 28 | print("Error, enter course_id") 29 | exit(0) 30 | 31 | 32 | # get 1 json object 33 | def fetch_object(obj_class, obj_id): 34 | api_url = '{}/api/{}s/{}'.format(api_host, obj_class, obj_id) 35 | response = requests.get(api_url, 36 | headers={'Authorization': 'Bearer ' + token}).json() 37 | return response['{}s'.format(obj_class)][0] 38 | 39 | 40 | # get json-objects with ids in right order 41 | def fetch_objects(obj_class, obj_ids, keep_order=True): 42 | objs = [] 43 | # Fetch objects by 30 items, 44 | # so we won't bump into HTTP request length limits 45 | step_size = 30 46 | for i in range(0, len(obj_ids), step_size): 47 | obj_ids_slice = obj_ids[i:i + step_size] 48 | api_url = '{}/api/{}s?{}'.format(api_host, obj_class, 49 | '&'.join('ids[]={}'.format(obj_id) 50 | for obj_id in obj_ids_slice)) 51 | 52 | response = requests.get(api_url,headers={'Authorization': 'Bearer ' + token}).json() 53 | 54 | objs += response['{}s'.format(obj_class)] 55 | if (keep_order): 56 | return sorted(objs, key=lambda x: obj_ids.index(x['id'])) 57 | return objs 58 | 59 | 60 | # convert name of section into proper name folder 61 | def replace_characters(text): 62 | text = text.replace(":", " -") 63 | text = text.replace("?", " ") 64 | return text 65 | 66 | 67 | # download pdf 68 | def download_file(link, path): 69 | slides = requests.get(link) 70 | file_name = link[link.rfind('/') + 1:] 71 | with open(os.path.join(path, file_name), "wb") as pdf: 72 | for chunk in slides.iter_content(chunk_size=128): 73 | pdf.write(chunk) 74 | 75 | 76 | # find all links with slides 77 | def find_slides(text, path): 78 | soup = BeautifulSoup(text, 'html.parser') 79 | for link in soup.find_all('a'): 80 | if link.get('href') and "slides" in link.get('href'): 81 | print("https://stepik.org" + link.get('href')) 82 | download_file("https://stepik.org" + link.get('href'), path) 83 | 84 | course = fetch_object("course", course_id) 85 | sections = fetch_objects("section", course['sections']) 86 | 87 | title = course['title'] 88 | workload = course['workload'] 89 | summary = course['summary'] 90 | 91 | #create info dir 92 | current_path = os.path.dirname(os.path.abspath(__file__)) 93 | current_path = os.path.join(current_path, title) 94 | 95 | if not os.path.exists(current_path): 96 | os.makedirs(current_path) 97 | with open(os.path.join(current_path, "readme.html"), "w") as info_file: 98 | print( 99 | "

" + title + "

" + "

Нагрузка: " + workload + "

" + "Коротко о курсе: " + summary, 100 | file=info_file) 101 | else: 102 | print("folder already exists") 103 | 104 | for section in sections: 105 | if not os.path.exists(os.path.join(current_path, replace_characters(section['title']))): 106 | os.makedirs(os.path.join(current_path, replace_characters(section['title']))) 107 | 108 | 109 | units_id = section['units'] 110 | units = fetch_objects('unit', units_id) 111 | 112 | for unit in units: 113 | lesson = fetch_object('lesson', unit['lesson']) 114 | steps = fetch_objects('step', lesson['steps']) 115 | for step in steps: 116 | text = step['block']['text'] 117 | path = os.path.join(current_path, section['title']) 118 | find_slides(text, path) 119 | -------------------------------------------------------------------------------- /examples/save_course_source.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | # Saves all step sources into foldered structure 3 | import os 4 | import json 5 | import requests 6 | import datetime 7 | 8 | # Enter parameters below: 9 | # 1. Get your keys at https://stepik.org/oauth2/applications/ 10 | # (client type = confidential, authorization grant type = client credentials) 11 | client_id = "..." 12 | client_secret = "..." 13 | api_host = 'https://stepik.org' 14 | course_id = 1 15 | 16 | # 2. Get a token 17 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 18 | response = requests.post('https://stepik.org/oauth2/token/', 19 | data={'grant_type': 'client_credentials'}, 20 | auth=auth) 21 | token = response.json().get('access_token', None) 22 | if not token: 23 | print('Unable to authorize with provided credentials') 24 | exit(1) 25 | 26 | 27 | # 3. Call API (https://stepik.org/api/docs/) using this token. 28 | def fetch_object(obj_class, obj_id): 29 | api_url = '{}/api/{}s/{}'.format(api_host, obj_class, obj_id) 30 | response = requests.get(api_url, 31 | headers={'Authorization': 'Bearer ' + token}).json() 32 | return response['{}s'.format(obj_class)][0] 33 | 34 | 35 | def fetch_objects(obj_class, obj_ids): 36 | objs = [] 37 | # Fetch objects by 30 items, 38 | # so we won't bump into HTTP request length limits 39 | step_size = 30 40 | for i in range(0, len(obj_ids), step_size): 41 | obj_ids_slice = obj_ids[i:i + step_size] 42 | api_url = '{}/api/{}s?{}'.format(api_host, obj_class, 43 | '&'.join('ids[]={}'.format(obj_id) 44 | for obj_id in obj_ids_slice)) 45 | response = requests.get(api_url, 46 | headers={'Authorization': 'Bearer ' + token} 47 | ).json() 48 | objs += response['{}s'.format(obj_class)] 49 | return objs 50 | 51 | 52 | course = fetch_object('course', course_id) 53 | sections = fetch_objects('section', course['sections']) 54 | 55 | for section in sections: 56 | 57 | unit_ids = section['units'] 58 | units = fetch_objects('unit', unit_ids) 59 | 60 | for unit in units: 61 | 62 | lesson_id = unit['lesson'] 63 | lesson = fetch_object('lesson', lesson_id) 64 | 65 | step_ids = lesson['steps'] 66 | steps = fetch_objects('step', step_ids) 67 | 68 | for step in steps: 69 | step_source = fetch_object('step-source', step['id']) 70 | path = [ 71 | '{} {}'.format(str(course['id']).zfill(2), course['title']), 72 | '{} {}'.format(str(section['position']).zfill(2), section['title']), 73 | '{} {}'.format(str(unit['position']).zfill(2), lesson['title']), 74 | '{}_{}_{}.step'.format(lesson['id'], str(step['position']).zfill(2), step['block']['name']) 75 | ] 76 | try: 77 | os.makedirs(os.path.join(os.curdir, *path[:-1])) 78 | except: 79 | pass 80 | filename = os.path.join(os.curdir, *path) 81 | f = open(filename, 'w') 82 | data = { 83 | 'block': step_source['block'], 84 | 'id': str(step['id']), 85 | 'time': datetime.datetime.now().isoformat() 86 | } 87 | f.write(json.dumps(data)) 88 | f.close() 89 | print(filename) 90 | -------------------------------------------------------------------------------- /examples/save_course_steps.py: -------------------------------------------------------------------------------- 1 | # Run with Python 3 2 | # Saves all step texts from course into single HTML file. 3 | import requests 4 | 5 | # Enter parameters below: 6 | # 1. Get your keys at https://stepik.org/oauth2/applications/ 7 | # (client type = confidential, authorization grant type = client credentials) 8 | client_id = "..." 9 | client_secret = "..." 10 | api_host = 'https://stepik.org' 11 | course_id = 1 12 | 13 | # 2. Get a token 14 | auth = requests.auth.HTTPBasicAuth(client_id, client_secret) 15 | response = requests.post('https://stepik.org/oauth2/token/', 16 | data={'grant_type': 'client_credentials'}, 17 | auth=auth) 18 | token = response.json().get('access_token', None) 19 | if not token: 20 | print('Unable to authorize with provided credentials') 21 | exit(1) 22 | 23 | 24 | # 3. Call API (https://stepik.org/api/docs/) using this token. 25 | def fetch_object(obj_class, obj_id): 26 | api_url = '{}/api/{}s/{}'.format(api_host, obj_class, obj_id) 27 | response = requests.get(api_url, 28 | headers={'Authorization': 'Bearer ' + token}).json() 29 | return response['{}s'.format(obj_class)][0] 30 | 31 | 32 | def fetch_objects(obj_class, obj_ids, keep_order=True): 33 | objs = [] 34 | # Fetch objects by 30 items, 35 | # so we won't bump into HTTP request length limits 36 | step_size = 30 37 | for i in range(0, len(obj_ids), step_size): 38 | obj_ids_slice = obj_ids[i:i + step_size] 39 | api_url = '{}/api/{}s?{}'.format(api_host, obj_class, 40 | '&'.join('ids[]={}'.format(obj_id) 41 | for obj_id in obj_ids_slice)) 42 | response = requests.get(api_url, 43 | headers={'Authorization': 'Bearer ' + token} 44 | ).json() 45 | 46 | objs += response['{}s'.format(obj_class)] 47 | if (keep_order): 48 | return sorted(objs, key=lambda x: obj_ids.index(x['id'])) 49 | return objs 50 | 51 | 52 | course = fetch_object('course', course_id) 53 | sections = fetch_objects('section', course['sections']) 54 | 55 | unit_ids = [unit for section in sections for unit in section['units']] 56 | units = fetch_objects('unit', unit_ids) 57 | 58 | lesson_ids = [unit['lesson'] for unit in units] 59 | lessons = fetch_objects('lesson', lesson_ids) 60 | 61 | step_ids = [step for lesson in lessons for step in lesson['steps']] 62 | steps = fetch_objects('step', step_ids) 63 | 64 | with open('course{}.html'.format(course_id), 'w', encoding='utf-8') as f: 65 | for step in steps: 66 | text = step['block']['text'] 67 | url = '{}'\ 68 | .format(step['lesson'], step['position'], step['id']) 69 | f.write('

{}

'.format(url)) 70 | f.write(text) 71 | f.write('
') 72 | -------------------------------------------------------------------------------- /examples/top_lessons_to_html.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def generate_html(top_lessons): 5 | rows = "" 6 | 7 | for lesson in top_lessons: 8 | rows += """ 9 | {title} 10 | {viewed_by} 11 | {passed_by} 12 | {vote_delta} 13 | """.format(**lesson) 14 | 15 | template = f""" 16 | 17 | 18 | 19 | 20 | Top lessons sorted by number of views 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {rows} 31 |
Course titleViewed byPassed byVote delta
32 | 33 | 34 | """ 35 | 36 | return template 37 | 38 | 39 | def main(): 40 | # To sort by votes we use "order" query parameter 41 | top_lessons = requests.get("https://stepik.org:443/api/lessons?order=-vote_delta").json()["lessons"] 42 | 43 | # Sort by number of views 44 | lessons = sorted(top_lessons, key=lambda x: x["viewed_by"], reverse=True) 45 | 46 | # Generate html and write it to file 47 | with open("top_lessons.html", "w+") as f: 48 | f.write(generate_html(lessons)) 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /examples/videos_downloader.py: -------------------------------------------------------------------------------- 1 | # 1. Go to https://stepik.org/oauth2/applications/ 2 | # 3 | # 2. Register your application with settings: 4 | # Client type: confidential 5 | # Authorization Grant Type: client-credentials 6 | # 7 | # 3. Install requests module 8 | # > pip install requests 9 | 10 | # 4. Run the script 11 | # > python3 downloader.py [-h] --course_id=COURSE_ID --client_id=CLIENT_ID --client_secret=CLIENT_SECRET [--week_id=WEEK_ID] [--quality=360|720|1080] [--output_dir=.] 12 | 13 | import argparse 14 | import json 15 | import os 16 | import urllib 17 | import urllib.request 18 | import requests 19 | import sys 20 | from requests.auth import HTTPBasicAuth 21 | 22 | 23 | def get_course_page(api_url, token): 24 | return json.loads(requests.get(api_url, headers={'Authorization': 'Bearer ' + token}).text) 25 | 26 | 27 | def get_all_weeks(stepik_resp): 28 | return stepik_resp['courses'][0]['sections'] 29 | 30 | 31 | def get_unit_list(section_list, token): 32 | resp = [json.loads(requests.get('https://stepik.org/api/sections/' + str(arr), 33 | headers={'Authorization': 'Bearer ' + token}).text) 34 | for arr in section_list] 35 | return [section['sections'][0]['units'] for section in resp] 36 | 37 | 38 | def get_steps_list(units_list, week, token): 39 | data = [json.loads(requests.get('https://stepik.org/api/units/' + str(unit_id), 40 | headers={'Authorization': 'Bearer ' + token}).text) 41 | for unit_id in units_list[week - 1]] 42 | lesson_lists = [elem['units'][0]['lesson'] for elem in data] 43 | data = [json.loads(requests.get('https://stepik.org/api/lessons/' + str(lesson_id), 44 | headers={'Authorization': 'Bearer ' + token}).text)['lessons'][ 45 | 0]['steps'] 46 | for lesson_id in lesson_lists] 47 | return [item for sublist in data for item in sublist] 48 | 49 | 50 | def get_only_video_steps(step_list, token): 51 | resp_list = list() 52 | for s in step_list: 53 | resp = json.loads(requests.get('https://stepik.org/api/steps/' + str(s), 54 | headers={'Authorization': 'Bearer ' + token}).text) 55 | if resp['steps'][0]['block']['video']: 56 | resp_list.append(resp['steps'][0]['block']) 57 | print('Only video:', len(resp_list)) 58 | return resp_list 59 | 60 | 61 | def parse_arguments(): 62 | """ 63 | Parse input arguments with help of argparse. 64 | """ 65 | 66 | parser = argparse.ArgumentParser( 67 | description='Stepik downloader') 68 | 69 | parser.add_argument('-c', '--client_id', 70 | help='your client_id from https://stepik.org/oauth2/applications/', 71 | required=True) 72 | 73 | parser.add_argument('-s', '--client_secret', 74 | help='your client_secret from https://stepik.org/oauth2/applications/', 75 | required=True) 76 | 77 | parser.add_argument('-i', '--course_id', 78 | help='course id', 79 | required=True) 80 | 81 | parser.add_argument('-w', '--week_id', 82 | help='week id starts from 1 (if not set then it will download the whole course)', 83 | type=int, 84 | default=None) 85 | 86 | parser.add_argument('-q', '--quality', 87 | help='quality of a video. Default is 720', 88 | choices=['360', '720', '1080'], 89 | default='720') 90 | 91 | parser.add_argument('-o', '--output_dir', 92 | help='output directory. Default is the current folder', 93 | default='.') 94 | 95 | args = parser.parse_args() 96 | 97 | return args 98 | 99 | 100 | def reporthook(blocknum, blocksize, totalsize): # progressbar 101 | readsofar = blocknum * blocksize 102 | if totalsize > 0: 103 | percent = readsofar * 1e2 / totalsize 104 | s = "\r%5.1f%% %*d / %d" % (percent, len(str(totalsize)), readsofar, totalsize) 105 | sys.stderr.write(s) 106 | if readsofar >= totalsize: # near the end 107 | sys.stderr.write("\n") 108 | else: # total size is unknown 109 | sys.stderr.write("read %d\n" % (readsofar,)) 110 | 111 | 112 | def main(): 113 | args = parse_arguments() 114 | 115 | """ 116 | Example how to receive token from Stepik.org 117 | Token should also been add to every request header 118 | example: requests.get(api_url, headers={'Authorization': 'Bearer '+ token}) 119 | """ 120 | 121 | auth = HTTPBasicAuth(args.client_id, args.client_secret) 122 | resp = requests.post('https://stepik.org/oauth2/token/', 123 | data={'grant_type': 'client_credentials'}, auth=auth) 124 | token = json.loads(resp.text)['access_token'] 125 | 126 | course_data = get_course_page('http://stepik.org/api/courses/' + args.course_id, token) 127 | 128 | weeks_num = get_all_weeks(course_data) 129 | 130 | all_units = get_unit_list(weeks_num, token) 131 | # Loop through all week in a course and 132 | # download all videos or 133 | # download only for the week_id is passed as an argument. 134 | for week in range(1, len(weeks_num) + 1): 135 | # Skip if week_id is passed as an argument 136 | args_week_id = str(args.week_id) 137 | if args_week_id != "None": 138 | # week_id starts from 1 and week counts from 0! 139 | if week != int(args_week_id): 140 | continue 141 | 142 | all_steps = get_steps_list(all_units, week, token) 143 | 144 | only_video_steps = get_only_video_steps(all_steps, token) 145 | 146 | url_list_with_q = [] 147 | 148 | # Loop through videos and store the url link and the quality. 149 | for video_step in only_video_steps: 150 | video_link = None 151 | msg = None 152 | 153 | # Check a video quality. 154 | for url in video_step['video']['urls']: 155 | if url['quality'] == args.quality: 156 | video_link = url['url'] 157 | 158 | # If the is no required video quality then download 159 | # with the best available quality. 160 | if video_link is None: 161 | msg = "The requested quality = {} is not available!".format(args.quality) 162 | 163 | video_link = video_step['video']['urls'][0]['url'] 164 | 165 | # Store link and quality. 166 | url_list_with_q.append({'url': video_link, 'msg': msg}) 167 | 168 | # Compose a folder name. 169 | folder_name = os.path.join(args.output_dir, args.course_id, 'week_' + str(week)) 170 | 171 | # Create a folder if needed. 172 | if not os.path.isdir(folder_name): 173 | try: 174 | # Create a directory for a particular week in the course. 175 | os.makedirs(folder_name) 176 | except PermissionError: 177 | print("Run the script from admin") 178 | exit(1) 179 | except FileExistsError: 180 | print("Please delete the folder " + folder_name) 181 | exit(1) 182 | 183 | print('Folder_name ', folder_name) 184 | 185 | for week, el in enumerate(url_list_with_q): 186 | # Print a message if something wrong. 187 | if el['msg']: 188 | print("{}".format(el['msg'])) 189 | 190 | filename = os.path.join(folder_name, 'Video_' + str(week) + '.mp4') 191 | if not os.path.isfile(filename): 192 | try: 193 | print('Downloading file ', filename) 194 | urllib.request.urlretrieve(el['url'], filename, reporthook) 195 | print('Done') 196 | except urllib.error.ContentTooShortError: 197 | os.remove(filename) 198 | print('Error while downloading. File {} deleted:'.format(filename)) 199 | except KeyboardInterrupt: 200 | if os.path.isfile(filename): 201 | os.remove(filename) 202 | print('\nAborted') 203 | exit(1) 204 | else: 205 | print('File {} already exist'.format(filename)) 206 | print("All steps downloaded") 207 | 208 | 209 | if __name__ == "__main__": 210 | main() 211 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==2.7.2 2 | requests==2.20.1 3 | codegen==1.0 4 | -------------------------------------------------------------------------------- /tests/test_oauth_auth_example.py: -------------------------------------------------------------------------------- 1 | def test_oauth_auth_example(): 2 | import ast 3 | import _ast 4 | import codegen 5 | 6 | client_id = "n4mnzQGfDEfOhFixwBvLV2mZJJLvf86pzfMMiPF5" 7 | client_secret = "40ON9IPJRDAngUkVbGBTEjCBAwc2wB7lV8e71jJUPKabdKq6KBTUBKb1xGkh82KtAI1AqISrL3Zi4sTfhCBVh27YvlV6Y5klpXXV5loUWvuhMSRiN3HRZzVDO0fLBibv" 8 | 9 | with open("examples/oauth_auth_example.py", "r") as f: 10 | data = f.read() 11 | p = ast.parse(data) 12 | for node in p.body: 13 | if type(node) == _ast.Assign: 14 | if node.targets[0].id == 'client_id': 15 | node.value.s = client_id 16 | if node.targets[0].id == 'client_secret': 17 | node.value.s = client_secret 18 | ls = {} 19 | exec(codegen.to_source(p), ls) 20 | assert ls['course']['courses'][0]['id'] == 67 21 | --------------------------------------------------------------------------------