56bd969aa9f8cc2fac30a6a41860aa62f3ae1cf3
[idea/community.git] / python / educational / src / com / jetbrains / edu / stepic / EduStepicConnector.java
1 package com.jetbrains.edu.stepic;
2
3 import com.google.gson.FieldNamingPolicy;
4 import com.google.gson.Gson;
5 import com.google.gson.GsonBuilder;
6 import com.google.gson.annotations.Expose;
7 import com.intellij.openapi.application.ApplicationManager;
8 import com.intellij.openapi.diagnostic.Logger;
9 import com.intellij.openapi.editor.Document;
10 import com.intellij.openapi.project.Project;
11 import com.intellij.openapi.vfs.VirtualFile;
12 import com.intellij.util.io.HttpRequests;
13 import com.intellij.util.net.ssl.CertificateManager;
14 import com.jetbrains.edu.EduUtils;
15 import com.jetbrains.edu.courseFormat.Course;
16 import com.jetbrains.edu.courseFormat.Lesson;
17 import com.jetbrains.edu.courseFormat.Task;
18 import com.jetbrains.edu.courseFormat.TaskFile;
19 import org.apache.http.Header;
20 import org.apache.http.StatusLine;
21 import org.apache.http.client.methods.CloseableHttpResponse;
22 import org.apache.http.client.methods.HttpPost;
23 import org.apache.http.entity.ContentType;
24 import org.apache.http.entity.StringEntity;
25 import org.apache.http.impl.client.BasicCookieStore;
26 import org.apache.http.impl.client.CloseableHttpClient;
27 import org.apache.http.impl.client.HttpClientBuilder;
28 import org.apache.http.impl.client.HttpClients;
29 import org.apache.http.impl.cookie.BasicClientCookie;
30 import org.apache.http.message.BasicHeader;
31 import org.apache.http.util.EntityUtils;
32 import org.jetbrains.annotations.NotNull;
33
34 import java.io.BufferedReader;
35 import java.io.IOException;
36 import java.util.*;
37
38 public class EduStepicConnector {
39   private static final String stepicApiUrl = "https://stepic.org/api/";
40   private static final Logger LOG = Logger.getInstance(EduStepicConnector.class.getName());
41   private static final String ourDomain = "stepic.org";
42   private static String ourSessionId = "524iethiwju2tjywaqmf7tbwx0p0jk1b";
43   private static String ourCSRFToken = "LJ9n6OyLVA7hxU94dlYWUu65MF51Nx37";
44   //this prefix indicates that course can be opened by educational plugin
45   public static final String PYCHARM_PREFIX = "pycharm ";
46
47   private EduStepicConnector() {
48   }
49
50   @NotNull
51   public static List<CourseInfo> getCourses() {
52     try {
53       return HttpRequests.request(stepicApiUrl + "courses/99").connect(new HttpRequests.RequestProcessor<List<CourseInfo>>() {
54
55         @Override
56         public List<CourseInfo> process(@NotNull HttpRequests.Request request) throws IOException {
57           final BufferedReader reader = request.getReader();
58           Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
59           return gson.fromJson(reader, CoursesContainer.class).courses;
60         }
61       });
62     }
63     catch (IOException e) {
64       LOG.error("IOException " + e.getMessage());
65     }
66     return Collections.emptyList();
67     /*try {                             // TODO: uncomment
68       return HttpRequests.request(stepicApiUrl + "courses").connect(new HttpRequests.RequestProcessor<List<CourseInfo>>() {
69
70         @Override
71         public List<CourseInfo> process(@NotNull HttpRequests.Request request) throws IOException {
72           final BufferedReader reader = request.getReader();
73           Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
74           return gson.fromJson(reader, CoursesContainer.class).courses;
75         }
76       });
77     }
78     catch (IOException e) {
79       LOG.error("IOException " + e.getMessage());
80     }
81     return null;*/
82   }
83
84   public static Course getCourse(@NotNull final CourseInfo info) {
85     final Course course = new Course();
86     course.setAuthor(info.getAuthor());
87     course.setDescription(info.getDescription());
88     course.setName(info.getName());
89     String courseType = info.getType();
90     course.setLanguage(courseType.substring(PYCHARM_PREFIX.length()));
91     course.setUpToDate(true);  // TODO: get from stepic
92     try {
93       for (Integer section : info.sections) {
94         course.addLessons(getLessons(section));
95       }
96       return course;
97     }
98     catch (IOException e) {
99       LOG.error("IOException " + e.getMessage());
100     }
101     return null;
102   }
103
104   public static List<Lesson> getLessons(int sectionId) throws IOException {
105
106     final SectionWrapper sectionWrapper = HttpRequests.request(stepicApiUrl + "sections/" + String.valueOf(sectionId))
107       .connect(new HttpRequests.RequestProcessor<SectionWrapper>() {
108
109         @Override
110         public SectionWrapper process(@NotNull HttpRequests.Request request) throws IOException {
111           final BufferedReader reader = request.getReader();
112           Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
113           return gson.fromJson(reader, SectionWrapper.class);
114         }
115       });
116     final List<Lesson> lessons = getSortedLessons(sectionWrapper);
117     for (Lesson lesson : lessons) {
118       lesson.taskList = new ArrayList<Task>();
119       for (Integer s : lesson.steps) {
120         final Step step = getStep(s);
121         final Task task = new Task();
122         task.setName(step.name);
123         task.setText(step.text);
124         for (TestFileWrapper wrapper : step.options.test) {
125           task.setTestsTexts(wrapper.name, wrapper.text);
126         }
127
128         task.taskFiles = new HashMap<String, TaskFile>();      // TODO: it looks like we don't need taskFiles as map anymore
129         if (step.options.files != null) {
130           for (TaskFile taskFile : step.options.files) {
131             task.taskFiles.put(taskFile.name, taskFile);
132           }
133         }
134         lesson.taskList.add(task);
135       }
136     }
137     return lessons;
138   }
139
140   @NotNull
141   private static List<Lesson> getSortedLessons(SectionWrapper sectionWrapper) {
142     final List<Lesson> lessons = sectionWrapper.lessons;
143
144     final List<Integer> units = sectionWrapper.sections.get(0).units;
145     final List<SectionWrapper.Unit> wrapperUnits = sectionWrapper.units;
146
147     final HashMap<Integer, Integer> unitsMap = new HashMap<Integer, Integer>();
148     for (SectionWrapper.Unit unit : wrapperUnits) {
149       unitsMap.put(unit.lesson, unit.id);
150     }
151     Collections.sort(lessons, new Comparator<Lesson>() {
152       @Override
153       public int compare(Lesson l1, Lesson l2) {
154
155         return units.indexOf(unitsMap.get(l1.id)) - units.indexOf(unitsMap.get(l2.id));
156       }
157     });
158     return lessons;
159   }
160
161   public static Step getStep(Integer step) throws IOException {
162     return HttpRequests.request(stepicApiUrl + "steps/" + String.valueOf(step)).connect(new HttpRequests.RequestProcessor<Step>() {
163
164       @Override
165       public Step process(@NotNull HttpRequests.Request request) throws IOException {
166         final BufferedReader reader = request.getReader();
167         Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
168         return gson.fromJson(reader, StepContainer.class).steps.get(0).block;
169       }
170     });
171   }
172
173
174
175   public static boolean postLesson(Project project, @NotNull final Lesson lesson) {
176     final HttpPost request = new HttpPost(stepicApiUrl + "lessons");
177     final ArrayList<Header> headers = getHeaders(request);
178     HttpClientBuilder builder = HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext());
179     final BasicCookieStore cookieStore = getCookies();
180     final CloseableHttpClient client = builder.setDefaultHeaders(headers).setDefaultCookieStore(cookieStore).build();
181
182     String requestBody = new Gson().toJson(new LessonWrapper(lesson));
183     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
184
185     try {
186       final CloseableHttpResponse response = client.execute(request);
187       final String responseString = EntityUtils.toString(response.getEntity());
188       final StatusLine line = response.getStatusLine();
189       if (line.getStatusCode() != 201) {
190         LOG.error("Failed to push " + EntityUtils.toString(response.getEntity()));
191         return false;
192       }
193       final Lesson postedLesson = new Gson().fromJson(responseString, Course.class).getLessons().get(0);
194       for (Task task : lesson.getTaskList()) {
195         postTask(project, task, postedLesson.id);
196       }
197     }
198     catch (IOException e) {
199       LOG.error(e.getMessage());
200     }
201     return false;
202   }
203
204   public static boolean postTask(Project project, @NotNull final Task task, int id) {
205     final HttpPost request = new HttpPost(stepicApiUrl + "step-sources");
206     final ArrayList<Header> headers = getHeaders(request);
207     HttpClientBuilder builder = HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext());
208     final BasicCookieStore cookieStore = getCookies();
209     final CloseableHttpClient client = builder.setDefaultHeaders(headers).setDefaultCookieStore(cookieStore).build();
210     final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().create();
211     String requestBody = gson.toJson(new StepSourceWrapper(project, task, id));
212     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
213
214     try {
215       final CloseableHttpResponse response = client.execute(request);
216       final StatusLine line = response.getStatusLine();
217       if (line.getStatusCode() != 201) {
218         LOG.error("Failed to push " + EntityUtils.toString(response.getEntity()));
219       }
220       return line.getStatusCode() == 201;
221     }
222     catch (IOException e) {
223       LOG.error(e.getMessage());
224     }
225
226     return false;
227   }
228
229   private static BasicCookieStore getCookies() {
230     final BasicCookieStore cookieStore = new BasicCookieStore();
231     final BasicClientCookie sessionid = new BasicClientCookie("sessionid", ourSessionId);
232     sessionid.setDomain(ourDomain);
233     sessionid.setPath("/");
234
235     cookieStore.addCookie(sessionid);
236     final BasicClientCookie csrfToken = new BasicClientCookie("csrftoken", ourCSRFToken);
237     csrfToken.setDomain(ourDomain);
238     csrfToken.setPath("/");
239
240     cookieStore.addCookie(csrfToken);
241     return cookieStore;
242   }
243
244   private static ArrayList<Header> getHeaders(HttpPost request) {
245     final ArrayList<Header> headers = new ArrayList<Header>();
246     headers.add(new BasicHeader("referer", "https://stepic.org"));
247     headers.add(new BasicHeader("X-CSRFToken", ourCSRFToken));
248     headers.add(new BasicHeader("content-type", "application/json"));
249     request.setHeaders(headers.toArray(new Header[headers.size()]));
250     return headers;
251   }
252
253   private static class StepContainer {
254     List<StepSource> steps;
255   }
256
257   private static class Step {
258     @Expose StepOptions options;
259     @Expose String text;
260     @Expose String name = "pycharm";
261     @Expose StepOptions source;
262
263     public static Step fromTask(Project project, @NotNull final Task task) {
264       final Step step = new Step();
265       step.text = task.getTaskText(project);
266       step.source = StepOptions.fromTask(project, task);
267       return step;
268     }
269   }
270
271   private static class StepOptions {
272     @Expose List<TestFileWrapper> test;
273     @Expose String title;  //HERE
274     @Expose List<TaskFile> files;
275     @Expose String text;
276
277     public static StepOptions fromTask(final Project project, @NotNull final Task task) {
278       final StepOptions source = new StepOptions();
279
280       final String text = task.getTestsText(project);
281       source.test = Collections.singletonList(new TestFileWrapper("tests.py", text));
282       source.files = new ArrayList<TaskFile>();
283       source.title = task.getName();
284       for (final Map.Entry<String, TaskFile> entry : task.getTaskFiles().entrySet()) {
285         ApplicationManager.getApplication().runWriteAction(new Runnable() {
286           @Override
287           public void run() {
288             final VirtualFile taskDir = task.getTaskDir(project);
289             EduUtils.createStudentFileFromAnswer(project, taskDir, taskDir, entry);
290           }
291         });
292         final TaskFile taskFile = entry.getValue();
293         taskFile.name = entry.getKey();
294         final Document document = task.getDocument(project, taskFile.name);
295         if (document != null) {
296           source.text = document.getImmutableCharSequence().toString();
297           taskFile.text = document.getImmutableCharSequence().toString();
298         }
299         source.files.add(taskFile);
300       }
301       return source;
302     }
303   }
304
305   private static class CoursesContainer {
306     public List<CourseInfo> courses;
307   }
308
309   static class StepSourceWrapper {
310     @Expose
311     StepSource stepSource;
312
313     public StepSourceWrapper(Project project, Task task, int id) {
314       stepSource = new StepSource(project, task, id);
315     }
316   }
317
318   static class LessonWrapper {
319     Lesson lesson;
320
321     public LessonWrapper(Lesson lesson) {
322       this.lesson = new Lesson();
323       this.lesson.setName(lesson.getName());
324     }
325   }
326
327   static class StepSource {
328     @Expose Step block;
329     @Expose int position = 0;
330     @Expose int lesson = 0;
331
332     public StepSource(Project project, Task task, int id) {
333       lesson = id;
334       position = task.getIndex();
335       block = Step.fromTask(project, task);
336     }
337   }
338
339   static class TestFileWrapper {
340     @Expose private final String name;
341     @Expose private final String text;
342
343     public TestFileWrapper(String name, String text) {
344       this.name = name;
345       this.text = text;
346     }
347   }
348
349   static class SectionWrapper {
350     static class Section {
351       List<Integer> units;
352     }
353
354     List<Section> sections;
355     List<Lesson> lessons;
356
357     static class Unit {
358       int id;
359       int lesson;
360     }
361
362     List<Unit> units;
363
364   }
365 }