├── .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`: [](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 '';
82 | echo '';
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 '' . $info->title . ' ';
158 | echo '' . count($course['sections']) . ' ';
159 | echo '' . implode(", ", $course['sections']) . ' ';
160 |
161 | if ($cost) {
162 | echo '' . $score . ' / ' . $cost . ' ';
163 | echo '' . $percentage . '% ';
164 | } else {
165 | echo 'Запишитесь на курс ';
166 | }
167 |
168 | echo '' . $exam_message . ' ';
169 | echo ' ';
170 | $previousCourse = $course;
171 | }
172 |
173 | echo '
';
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 'Сумма (руб) Дата и время платежа (UTC) Действует от Действует до ';
217 | foreach (array_reverse($payments_list) as $payment) {
218 | echo '';
219 | echo implode(' ', $payment);
220 | echo ' ';
221 | }
222 | echo '
';
223 |
224 | # LOGOUT
225 | echo '[Выйти из кабинета]
';
226 | echo '';
227 | } else {
228 | echo '';
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('{} \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(''.format(course['slug'], course['title']))
75 | f.write('{}
'.format(course['summary']))
76 | if course['sections']:
77 | f.write('Course sections:
')
78 | f.write('')
79 | for section in course['sections']:
80 | f.write('{} '.format(section['title']))
81 | 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 | Name
55 | Total
56 | Gene Expression
57 | Reconstruction of Bacteria
58 | Genetic Linkage Map
59 | Locating Insertions
60 | Pathway Dissection
61 |
62 |
63 | Easy
64 | Hard
65 | Easy
66 | Hard
67 | #1
68 | #2
69 | #3
70 | #4
71 | #5
72 | #6
73 | #7
74 | #8
75 | #9
76 | #10
77 | #1
78 | #2
79 | #3
80 | #4
81 | #5
82 | #6
83 | #1
84 | #2
85 | #3
86 | #4
87 | #5
88 | #6
89 | #7
90 |
91 |
92 | {'course-grades'} as $course_grade) {
104 | if (!$course_grade->rank) continue;
105 | echo '';
106 | echo '' . $course_grade->rank . ' ';
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 '' . $full_name . ' ';
111 | echo '' . round($course_grade->score - 1, 2) . ' ';
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 '' . round($result->score, 2) . ' ';
124 | $ok = True;
125 | break;
126 | }
127 | if (!$ok) {
128 | echo ' 0 ';
129 | }
130 | }
131 | echo ' ';
132 | }
133 | $page++;
134 | } while ($grades->meta->has_next);
135 | echo '
';
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('{} \n'.format(url))
152 | else:
153 | f.write('{} \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 | Course title
26 | Viewed by
27 | Passed by
28 | Vote delta
29 |
30 | {rows}
31 |
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 |
--------------------------------------------------------------------------------