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