1 package com.jetbrains.edu.learning.stepic;
3 import com.google.gson.Gson;
4 import com.google.gson.GsonBuilder;
5 import com.intellij.openapi.diagnostic.Logger;
6 import com.intellij.openapi.project.Project;
7 import com.intellij.openapi.util.text.StringUtil;
8 import com.jetbrains.edu.learning.courseFormat.Course;
9 import com.jetbrains.edu.learning.courseFormat.Lesson;
10 import com.jetbrains.edu.learning.courseFormat.Task;
11 import com.jetbrains.edu.learning.courseFormat.TaskFile;
12 import org.apache.http.HttpEntity;
13 import org.apache.http.HttpStatus;
14 import org.apache.http.StatusLine;
15 import org.apache.http.client.methods.CloseableHttpResponse;
16 import org.apache.http.client.methods.HttpPost;
17 import org.apache.http.client.utils.URIBuilder;
18 import org.apache.http.entity.ContentType;
19 import org.apache.http.entity.StringEntity;
20 import org.apache.http.impl.client.CloseableHttpClient;
21 import org.apache.http.util.EntityUtils;
22 import org.jetbrains.annotations.NotNull;
24 import java.io.IOException;
26 import java.net.URISyntaxException;
29 public class EduStepicConnector {
30 private static final Logger LOG = Logger.getInstance(EduStepicConnector.class.getName());
32 public static final int CURRENT_VERSION = 2;
33 //this prefix indicates that course can be opened by educational plugin
34 public static final String PYCHARM_PREFIX = "pycharm";
35 private static final String ADAPTIVE_NOTE =
36 "\n\nInitially, the adaptive system may behave somewhat randomly, but the more problems you solve, the smarter it become!";
38 private EduStepicConnector() {
41 public static boolean enrollToCourse(final int courseId, final StepicUser stepicUser) {
42 HttpPost post = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.ENROLLMENTS);
44 final StepicWrappers.EnrollmentWrapper enrollment = new StepicWrappers.EnrollmentWrapper(String.valueOf(courseId));
45 post.setEntity(new StringEntity(new GsonBuilder().create().toJson(enrollment)));
46 final CloseableHttpClient client = EduStepicAuthorizedClient.getHttpClient(stepicUser);
47 CloseableHttpResponse response = client.execute(post);
48 StatusLine line = response.getStatusLine();
49 return line.getStatusCode() == HttpStatus.SC_CREATED;
51 catch (IOException e) {
52 LOG.warn(e.getMessage());
58 public static List<CourseInfo> getCourses() {
60 List<CourseInfo> result = new ArrayList<>();
62 while (addCoursesFromStepic(result, pageNumber)) {
67 catch (IOException e) {
68 LOG.error("Cannot load course list " + e.getMessage());
70 return Collections.singletonList(CourseInfo.INVALID_COURSE);
73 public static Date getCourseUpdateDate(final int courseId) {
74 final String url = EduStepicNames.COURSES + "/" + courseId;
76 final List<CourseInfo> courses = EduStepicClient.getFromStepic(url, StepicWrappers.CoursesContainer.class).courses;
77 if (!courses.isEmpty()) {
78 return courses.get(0).getUpdateDate();
81 catch (IOException e) {
82 LOG.warn("Could not retrieve course with id=" + courseId);
88 public static Date getLessonUpdateDate(final int lessonId) {
89 final String url = EduStepicNames.LESSONS + "/" + lessonId;
91 List<Lesson> lessons = EduStepicClient.getFromStepic(url, StepicWrappers.LessonContainer.class).lessons;
92 if (!lessons.isEmpty()) {
93 return lessons.get(0).getUpdateDate();
96 catch (IOException e) {
97 LOG.warn("Could not retrieve course with id=" + lessonId);
103 public static Date getTaskUpdateDate(final int taskId) {
104 final String url = EduStepicNames.STEPS + "/" + String.valueOf(taskId);
106 List<StepicWrappers.StepSource> steps = EduStepicClient.getFromStepic(url, StepicWrappers.StepContainer.class).steps;
107 if (!steps.isEmpty()) {
108 return steps.get(0).update_date;
111 catch (IOException e) {
112 LOG.warn("Could not retrieve course with id=" + taskId);
118 private static boolean addCoursesFromStepic(List<CourseInfo> result, int pageNumber) throws IOException {
121 url = new URIBuilder(EduStepicNames.COURSES).addParameter("is_idea_compatible", "true").
122 addParameter("page", String.valueOf(pageNumber)).build();
124 catch (URISyntaxException e) {
125 LOG.error(e.getMessage());
128 final StepicWrappers.CoursesContainer coursesContainer = EduStepicClient.getFromStepic(url.toString(), StepicWrappers.CoursesContainer.class);
129 addAvailableCourses(result, coursesContainer);
130 return coursesContainer.meta.containsKey("has_next") && coursesContainer.meta.get("has_next") == Boolean.TRUE;
133 static void addAvailableCourses(List<CourseInfo> result, StepicWrappers.CoursesContainer coursesContainer) throws IOException {
134 final List<CourseInfo> courseInfos = coursesContainer.courses;
135 for (CourseInfo info : courseInfos) {
136 if (!info.isAdaptive() && StringUtil.isEmptyOrSpaces(info.getType())) continue;
137 if (canBeOpened(info)) {
138 for (Integer instructor : info.instructors) {
139 final StepicUser author = EduStepicClient.getFromStepic(EduStepicNames.USERS + "/" + String.valueOf(instructor),
140 StepicWrappers.AuthorWrapper.class).users.get(0);
141 info.addAuthor(author);
144 if (info.isAdaptive()) {
145 info.setDescription("This is a Stepik Adaptive course.\n\n" + info.getDescription() + ADAPTIVE_NOTE);
153 static boolean canBeOpened(CourseInfo courseInfo) {
154 if (courseInfo.isAdaptive) {
157 String courseType = courseInfo.getType();
158 final List<String> typeLanguage = StringUtil.split(courseType, " ");
159 String prefix = typeLanguage.get(0);
160 if (typeLanguage.size() != 2 || !prefix.startsWith(PYCHARM_PREFIX)) {
163 String versionString = prefix.substring(PYCHARM_PREFIX.length());
164 if (versionString.isEmpty()) {
168 Integer version = Integer.valueOf(versionString);
169 return version <= CURRENT_VERSION;
170 } catch (NumberFormatException e) {
171 LOG.info("Wrong version format", e);
176 public static Course getCourse(@NotNull final Project project, @NotNull final CourseInfo info) {
177 final Course course = new Course();
178 course.setAuthors(info.getAuthors());
179 course.setDescription(info.getDescription());
180 course.setAdaptive(info.isAdaptive());
181 course.setId(info.getId());
182 course.setUpdateDate(getCourseUpdateDate(info.getId()));
184 if (!course.isAdaptive()) {
185 String courseType = info.getType();
186 course.setName(info.getName());
187 String language = courseType.split(" ")[1];
188 course.setLanguage(language);
190 for (Integer section : info.sections) {
191 course.addLessons(getLessons(section));
195 catch (IOException e) {
196 LOG.error("IOException " + e.getMessage());
200 final Lesson lesson = new Lesson();
201 course.setName(info.getName());
202 //TODO: more specific name?
203 lesson.setName("Adaptive");
204 course.addLesson(lesson);
205 final Task recommendation = EduAdaptiveStepicConnector.getNextRecommendation(project, course);
206 if (recommendation != null) {
207 lesson.addTask(recommendation);
217 public static List<Lesson> getLessons(int sectionId) throws IOException {
218 final StepicWrappers.SectionContainer
219 sectionContainer = EduStepicClient.getFromStepic(EduStepicNames.SECTIONS + String.valueOf(sectionId), StepicWrappers.SectionContainer.class);
220 List<Integer> unitIds = sectionContainer.sections.get(0).units;
221 final List<Lesson> lessons = new ArrayList<>();
222 for (Integer unitId : unitIds) {
223 StepicWrappers.UnitContainer
224 unit = EduStepicClient.getFromStepic(EduStepicNames.UNITS + "/" + String.valueOf(unitId), StepicWrappers.UnitContainer.class);
225 int lessonID = unit.units.get(0).lesson;
226 StepicWrappers.LessonContainer
227 lessonContainer = EduStepicClient.getFromStepic(EduStepicNames.LESSONS + String.valueOf(lessonID), StepicWrappers.LessonContainer.class);
228 Lesson lesson = lessonContainer.lessons.get(0);
229 lesson.taskList = new ArrayList<>();
230 for (Integer s : lesson.steps) {
231 createTask(lesson, s);
233 if (!lesson.taskList.isEmpty())
240 private static void createTask(Lesson lesson, Integer stepicId) throws IOException {
241 final StepicWrappers.StepSource step = getStep(stepicId);
242 final StepicWrappers.Step block = step.block;
243 if (!block.name.startsWith(PYCHARM_PREFIX)) return;
244 final Task task = new Task();
245 task.setStepId(stepicId);
246 task.setUpdateDate(step.update_date);
247 task.setName(block.options != null ? block.options.title : (PYCHARM_PREFIX + CURRENT_VERSION));
248 task.setText(block.text);
249 for (StepicWrappers.TestFileWrapper wrapper : block.options.test) {
250 task.addTestsTexts(wrapper.name, wrapper.text);
253 task.taskFiles = new HashMap<>(); // TODO: it looks like we don't need taskFiles as map anymore
254 if (block.options.files != null) {
255 for (TaskFile taskFile : block.options.files) {
256 task.taskFiles.put(taskFile.name, taskFile);
259 lesson.taskList.add(task);
262 public static StepicWrappers.StepSource getStep(Integer step) throws IOException {
263 return EduStepicClient.getFromStepic(EduStepicNames.STEPS + "/" + String.valueOf(step), StepicWrappers.StepContainer.class).steps.get(0);
266 public static void postAttempt(@NotNull final Task task, boolean passed, @NotNull final Project project) {
267 if (task.getStepId() <= 0) {
271 final HttpPost attemptRequest = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.ATTEMPTS);
272 String attemptRequestBody = new Gson().toJson(new StepicWrappers.AttemptWrapper(task.getStepId()));
273 attemptRequest.setEntity(new StringEntity(attemptRequestBody, ContentType.APPLICATION_JSON));
276 final CloseableHttpClient client = EduStepicAuthorizedClient.getHttpClient(project);
277 final CloseableHttpResponse attemptResponse = client.execute(attemptRequest);
278 final HttpEntity responseEntity = attemptResponse.getEntity();
279 final String attemptResponseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
280 final StatusLine statusLine = attemptResponse.getStatusLine();
281 EntityUtils.consume(responseEntity);
282 if (statusLine.getStatusCode() != HttpStatus.SC_CREATED) {
283 LOG.warn("Failed to make attempt " + attemptResponseString);
285 final StepicWrappers.AttemptWrapper.Attempt attempt = new Gson().fromJson(attemptResponseString, StepicWrappers.AttemptContainer.class).attempts.get(0);
287 final Map<String, TaskFile> taskFiles = task.getTaskFiles();
288 final ArrayList<StepicWrappers.SolutionFile> files = new ArrayList<>();
289 for (TaskFile fileEntry : taskFiles.values()) {
290 files.add(new StepicWrappers.SolutionFile(fileEntry.name, fileEntry.text));
292 postSubmission(passed, attempt, project, files);
294 catch (IOException e) {
295 LOG.error(e.getMessage());
299 private static void postSubmission(boolean passed, StepicWrappers.AttemptWrapper.Attempt attempt,
300 Project project, ArrayList<StepicWrappers.SolutionFile> files) throws IOException {
301 final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.SUBMISSIONS);
303 String requestBody = new Gson().toJson(new StepicWrappers.SubmissionWrapper(attempt.id, passed ? "1" : "0", files));
304 request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
305 final CloseableHttpClient client = EduStepicAuthorizedClient.getHttpClient(project);
306 final CloseableHttpResponse response = client.execute(request);
307 final HttpEntity responseEntity = response.getEntity();
308 final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
309 final StatusLine line = response.getStatusLine();
310 EntityUtils.consume(responseEntity);
311 if (line.getStatusCode() != HttpStatus.SC_CREATED) {
312 LOG.error("Failed to make submission " + responseString);