add format version to stepic format
[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   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!";
37
38   private EduStepicConnector() {
39   }
40
41   public static boolean enrollToCourse(final int courseId, final StepicUser stepicUser) {
42     HttpPost post = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.ENROLLMENTS);
43     try {
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;
50     }
51     catch (IOException e) {
52       LOG.warn(e.getMessage());
53     }
54     return false;
55   }
56
57   @NotNull
58   public static List<CourseInfo> getCourses() {
59     try {
60       List<CourseInfo> result = new ArrayList<>();
61       int pageNumber = 1;
62       while (addCoursesFromStepic(result, pageNumber)) {
63         pageNumber += 1;
64       }
65       return result;
66     }
67     catch (IOException e) {
68       LOG.error("Cannot load course list " + e.getMessage());
69     }
70     return Collections.singletonList(CourseInfo.INVALID_COURSE);
71   }
72
73   public static Date getCourseUpdateDate(final int courseId) {
74     final String url = EduStepicNames.COURSES + "/" + courseId;
75     try {
76       final List<CourseInfo> courses = EduStepicClient.getFromStepic(url, StepicWrappers.CoursesContainer.class).courses;
77       if (!courses.isEmpty()) {
78         return courses.get(0).getUpdateDate();
79       }
80     }
81     catch (IOException e) {
82       LOG.warn("Could not retrieve course with id=" + courseId);
83     }
84
85     return null;
86   }
87
88   public static Date getLessonUpdateDate(final int lessonId) {
89     final String url = EduStepicNames.LESSONS + "/" + lessonId;
90     try {
91       List<Lesson> lessons = EduStepicClient.getFromStepic(url, StepicWrappers.LessonContainer.class).lessons;
92       if (!lessons.isEmpty()) {
93         return lessons.get(0).getUpdateDate();
94       }
95     }
96     catch (IOException e) {
97       LOG.warn("Could not retrieve course with id=" + lessonId);
98     }
99
100     return null;
101   }
102
103   public static Date getTaskUpdateDate(final int taskId) {
104     final String url = EduStepicNames.STEPS + "/" + String.valueOf(taskId);
105     try {
106       List<StepicWrappers.StepSource> steps = EduStepicClient.getFromStepic(url, StepicWrappers.StepContainer.class).steps;
107       if (!steps.isEmpty()) {
108         return steps.get(0).update_date;
109       }
110     }
111     catch (IOException e) {
112       LOG.warn("Could not retrieve course with id=" + taskId);
113     }
114
115     return null;
116   }
117
118   private static boolean addCoursesFromStepic(List<CourseInfo> result, int pageNumber) throws IOException {
119     final URI url;
120     try {
121       url = new URIBuilder(EduStepicNames.COURSES).addParameter("is_idea_compatible", "true").
122           addParameter("page", String.valueOf(pageNumber)).build();
123     }
124     catch (URISyntaxException e) {
125       LOG.error(e.getMessage());
126       return false;
127     }
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;
131   }
132
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);
142         }
143
144         if (info.isAdaptive()) {
145           info.setDescription("This is a Stepik Adaptive course.\n\n" + info.getDescription() + ADAPTIVE_NOTE);
146         }
147
148         result.add(info);
149       }
150     }
151   }
152
153   static boolean canBeOpened(CourseInfo courseInfo) {
154     if (courseInfo.isAdaptive) {
155       return true;
156     }
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)) {
161       return false;
162     }
163     String versionString = prefix.substring(PYCHARM_PREFIX.length());
164     if (versionString.isEmpty()) {
165       return true;
166     }
167     try {
168       Integer version = Integer.valueOf(versionString);
169       return version <= CURRENT_VERSION;
170     } catch (NumberFormatException e) {
171       LOG.info("Wrong version format", e);
172       return false;
173     }
174   }
175
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()));
183     
184     if (!course.isAdaptive()) {
185       String courseType = info.getType();
186       course.setName(info.getName());
187       String language = courseType.split(" ")[1];
188       course.setLanguage(language);
189       try {
190         for (Integer section : info.sections) {
191           course.addLessons(getLessons(section));
192         }
193         return course;
194       }
195       catch (IOException e) {
196         LOG.error("IOException " + e.getMessage());
197       }
198     }
199     else {
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);
208         return course;
209       }
210       else {
211         return null;
212       }
213     }
214     return null;
215   }
216
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);
232       }
233       if (!lesson.taskList.isEmpty())
234         lessons.add(lesson);
235     }
236
237     return lessons;
238   }
239
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);
251     }
252
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);
257       }
258     }
259     lesson.taskList.add(task);
260   }
261
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);
264   }
265
266   public static void postAttempt(@NotNull final Task task, boolean passed, @NotNull final Project project) {
267     if (task.getStepId() <= 0) {
268       return;
269     }
270
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));
274
275     try {
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);
284       }
285       final StepicWrappers.AttemptWrapper.Attempt attempt = new Gson().fromJson(attemptResponseString, StepicWrappers.AttemptContainer.class).attempts.get(0);
286
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));
291       }
292       postSubmission(passed, attempt, project, files);
293     }
294     catch (IOException e) {
295       LOG.error(e.getMessage());
296     }
297   }
298
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);
302
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);
313     }
314   }
315 }