243e302130000884a58a22ca09401f090b80a7ba
[idea/community.git] / python / educational-core / student / src / com / jetbrains / edu / learning / stepic / EduStepicConnector.java
1 package com.jetbrains.edu.learning.stepic;
2
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;
23
24 import java.io.IOException;
25 import java.net.URI;
26 import java.net.URISyntaxException;
27 import java.util.*;
28
29 public class EduStepicConnector {
30   private static final Logger LOG = Logger.getInstance(EduStepicConnector.class.getName());
31
32   //this prefix indicates that course can be opened by educational plugin
33   public static final String PYCHARM_PREFIX = "pycharm";
34   private static final String ADAPTIVE_NOTE =
35     "\n\nInitially, the adaptive system may behave somewhat randomly, but the more problems you solve, the smarter it become!";
36
37   private EduStepicConnector() {
38   }
39
40   public static boolean enrollToCourse(final int courseId, final StepicUser stepicUser) {
41     HttpPost post = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.ENROLLMENTS);
42     try {
43       final StepicWrappers.EnrollmentWrapper enrollment = new StepicWrappers.EnrollmentWrapper(String.valueOf(courseId));
44       post.setEntity(new StringEntity(new GsonBuilder().create().toJson(enrollment)));
45       final CloseableHttpClient client = EduStepicAuthorizedClient.getHttpClient(stepicUser);
46       CloseableHttpResponse response = client.execute(post);
47       StatusLine line = response.getStatusLine();
48       return line.getStatusCode() == HttpStatus.SC_CREATED;
49     }
50     catch (IOException e) {
51       LOG.warn(e.getMessage());
52     }
53     return false;
54   }
55
56   @NotNull
57   public static List<CourseInfo> getCourses() {
58     try {
59       List<CourseInfo> result = new ArrayList<>();
60       int pageNumber = 1;
61       while (addCoursesFromStepic(result, pageNumber)) {
62         pageNumber += 1;
63       }
64       return result;
65     }
66     catch (IOException e) {
67       LOG.error("Cannot load course list " + e.getMessage());
68     }
69     return Collections.singletonList(CourseInfo.INVALID_COURSE);
70   }
71
72   public static Date getCourseUpdateDate(final int courseId) {
73     final String url = EduStepicNames.COURSES + "/" + courseId;
74     try {
75       final List<CourseInfo> courses = EduStepicClient.getFromStepic(url, StepicWrappers.CoursesContainer.class).courses;
76       if (!courses.isEmpty()) {
77         return courses.get(0).getUpdateDate();
78       }
79     }
80     catch (IOException e) {
81       LOG.warn("Could not retrieve course with id=" + courseId);
82     }
83
84     return null;
85   }
86
87   public static Date getLessonUpdateDate(final int lessonId) {
88     final String url = EduStepicNames.LESSONS + "/" + lessonId;
89     try {
90       List<Lesson> lessons = EduStepicClient.getFromStepic(url, StepicWrappers.LessonContainer.class).lessons;
91       if (!lessons.isEmpty()) {
92         return lessons.get(0).getUpdateDate();
93       }
94     }
95     catch (IOException e) {
96       LOG.warn("Could not retrieve course with id=" + lessonId);
97     }
98
99     return null;
100   }
101
102   public static Date getTaskUpdateDate(final int taskId) {
103     final String url = EduStepicNames.STEPS + "/" + String.valueOf(taskId);
104     try {
105       List<StepicWrappers.StepSource> steps = EduStepicClient.getFromStepic(url, StepicWrappers.StepContainer.class).steps;
106       if (!steps.isEmpty()) {
107         return steps.get(0).update_date;
108       }
109     }
110     catch (IOException e) {
111       LOG.warn("Could not retrieve course with id=" + taskId);
112     }
113
114     return null;
115   }
116
117   private static boolean addCoursesFromStepic(List<CourseInfo> result, int pageNumber) throws IOException {
118     final URI url;
119     try {
120       url = new URIBuilder(EduStepicNames.COURSES).addParameter("is_idea_compatible", "true").
121           addParameter("page", String.valueOf(pageNumber)).build();
122     }
123     catch (URISyntaxException e) {
124       LOG.error(e.getMessage());
125       return false;
126     }
127     final StepicWrappers.CoursesContainer coursesContainer = EduStepicClient.getFromStepic(url.toString(), StepicWrappers.CoursesContainer.class);
128     final List<CourseInfo> courseInfos = coursesContainer.courses;
129     for (CourseInfo info : courseInfos) {
130       final String courseType = info.getType();
131       if (!info.isAdaptive() && StringUtil.isEmptyOrSpaces(courseType)) continue;
132       final List<String> typeLanguage = StringUtil.split(courseType, " ");
133       if (info.isAdaptive() || (typeLanguage.size() == 2 && PYCHARM_PREFIX.equals(typeLanguage.get(0)))) {
134         for (Integer instructor : info.instructors) {
135           final StepicUser author = EduStepicClient.getFromStepic(EduStepicNames.USERS + "/" + String.valueOf(instructor),
136                                                   StepicWrappers.AuthorWrapper.class).users.get(0);
137           info.addAuthor(author);
138         }
139         
140         if (info.isAdaptive()) {
141           info.setDescription("This is a Stepik Adaptive course.\n\n" + info.getDescription() + ADAPTIVE_NOTE);
142         }
143         
144         result.add(info);
145       }
146     }
147     return coursesContainer.meta.containsKey("has_next") && coursesContainer.meta.get("has_next") == Boolean.TRUE;
148   }
149
150   public static Course getCourse(@NotNull final Project project, @NotNull final CourseInfo info) {
151     final Course course = new Course();
152     course.setAuthors(info.getAuthors());
153     course.setDescription(info.getDescription());
154     course.setAdaptive(info.isAdaptive());
155     course.setId(info.getId());
156     course.setUpdateDate(getCourseUpdateDate(info.getId()));
157     
158     if (!course.isAdaptive()) {
159       String courseType = info.getType();
160       course.setName(info.getName());
161       course.setLanguage(courseType.substring(PYCHARM_PREFIX.length() + 1));
162       try {
163         for (Integer section : info.sections) {
164           course.addLessons(getLessons(section));
165         }
166         return course;
167       }
168       catch (IOException e) {
169         LOG.error("IOException " + e.getMessage());
170       }
171     }
172     else {
173       final Lesson lesson = new Lesson();
174       course.setName(info.getName());
175       //TODO: more specific name?
176       lesson.setName("Adaptive");
177       course.addLesson(lesson);
178       final Task recommendation = EduAdaptiveStepicConnector.getNextRecommendation(project, course);
179       if (recommendation != null) {
180         lesson.addTask(recommendation);
181         return course;
182       }
183       else {
184         return null;
185       }
186     }
187     return null;
188   }
189
190   public static List<Lesson> getLessons(int sectionId) throws IOException {
191     final StepicWrappers.SectionContainer
192       sectionContainer = EduStepicClient.getFromStepic(EduStepicNames.SECTIONS + String.valueOf(sectionId), StepicWrappers.SectionContainer.class);
193     List<Integer> unitIds = sectionContainer.sections.get(0).units;
194     final List<Lesson> lessons = new ArrayList<>();
195     for (Integer unitId : unitIds) {
196       StepicWrappers.UnitContainer
197         unit = EduStepicClient.getFromStepic(EduStepicNames.UNITS + "/" + String.valueOf(unitId), StepicWrappers.UnitContainer.class);
198       int lessonID = unit.units.get(0).lesson;
199       StepicWrappers.LessonContainer
200         lessonContainer = EduStepicClient.getFromStepic(EduStepicNames.LESSONS + String.valueOf(lessonID), StepicWrappers.LessonContainer.class);
201       Lesson lesson = lessonContainer.lessons.get(0);
202       lesson.taskList = new ArrayList<>();
203       for (Integer s : lesson.steps) {
204         createTask(lesson, s);
205       }
206       if (!lesson.taskList.isEmpty())
207         lessons.add(lesson);
208     }
209
210     return lessons;
211   }
212
213   private static void createTask(Lesson lesson, Integer stepicId) throws IOException {
214     final StepicWrappers.StepSource step = getStep(stepicId);
215     final StepicWrappers.Step block = step.block;
216     if (!block.name.equals(PYCHARM_PREFIX)) return;
217     final Task task = new Task();
218     task.setStepId(stepicId);
219     task.setUpdateDate(step.update_date);
220     task.setName(block.options != null ? block.options.title : PYCHARM_PREFIX);
221     task.setText(block.text);
222     for (StepicWrappers.TestFileWrapper wrapper : block.options.test) {
223       task.addTestsTexts(wrapper.name, wrapper.text);
224     }
225
226     task.taskFiles = new HashMap<>();      // TODO: it looks like we don't need taskFiles as map anymore
227     if (block.options.files != null) {
228       for (TaskFile taskFile : block.options.files) {
229         task.taskFiles.put(taskFile.name, taskFile);
230       }
231     }
232     lesson.taskList.add(task);
233   }
234
235   public static StepicWrappers.StepSource getStep(Integer step) throws IOException {
236     return EduStepicClient.getFromStepic(EduStepicNames.STEPS + "/" + String.valueOf(step), StepicWrappers.StepContainer.class).steps.get(0);
237   }
238
239   public static void postAttempt(@NotNull final Task task, boolean passed, @NotNull final Project project) {
240     if (task.getStepId() <= 0) {
241       return;
242     }
243
244     final HttpPost attemptRequest = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.ATTEMPTS);
245     String attemptRequestBody = new Gson().toJson(new StepicWrappers.AttemptWrapper(task.getStepId()));
246     attemptRequest.setEntity(new StringEntity(attemptRequestBody, ContentType.APPLICATION_JSON));
247
248     try {
249       final CloseableHttpClient client = EduStepicAuthorizedClient.getHttpClient(project);
250       final CloseableHttpResponse attemptResponse = client.execute(attemptRequest);
251       final HttpEntity responseEntity = attemptResponse.getEntity();
252       final String attemptResponseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
253       final StatusLine statusLine = attemptResponse.getStatusLine();
254       EntityUtils.consume(responseEntity);
255       if (statusLine.getStatusCode() != HttpStatus.SC_CREATED) {
256         LOG.warn("Failed to make attempt " + attemptResponseString);
257       }
258       final StepicWrappers.AttemptWrapper.Attempt attempt = new Gson().fromJson(attemptResponseString, StepicWrappers.AttemptContainer.class).attempts.get(0);
259
260       final Map<String, TaskFile> taskFiles = task.getTaskFiles();
261       final ArrayList<StepicWrappers.SolutionFile> files = new ArrayList<>();
262       for (TaskFile fileEntry : taskFiles.values()) {
263         files.add(new StepicWrappers.SolutionFile(fileEntry.name, fileEntry.text));
264       }
265       postSubmission(passed, attempt, project, files);
266     }
267     catch (IOException e) {
268       LOG.error(e.getMessage());
269     }
270   }
271
272   private static void postSubmission(boolean passed, StepicWrappers.AttemptWrapper.Attempt attempt,
273                                      Project project, ArrayList<StepicWrappers.SolutionFile> files) throws IOException {
274     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.SUBMISSIONS);
275
276     String requestBody = new Gson().toJson(new StepicWrappers.SubmissionWrapper(attempt.id, passed ? "1" : "0", files));
277     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
278     final CloseableHttpClient client = EduStepicAuthorizedClient.getHttpClient(project);
279     final CloseableHttpResponse response = client.execute(request);
280     final HttpEntity responseEntity = response.getEntity();
281     final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
282     final StatusLine line = response.getStatusLine();
283     EntityUtils.consume(responseEntity);
284     if (line.getStatusCode() != HttpStatus.SC_CREATED) {
285       LOG.error("Failed to make submission " + responseString);
286     }
287   }
288 }