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