2698f74871776443d794aeff231fbf645c16ca7d
[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.FieldNamingPolicy;
4 import com.google.gson.Gson;
5 import com.google.gson.GsonBuilder;
6 import com.intellij.openapi.application.ApplicationManager;
7 import com.intellij.openapi.application.ModalityState;
8 import com.intellij.openapi.diagnostic.Logger;
9 import com.intellij.openapi.progress.ProgressIndicator;
10 import com.intellij.openapi.progress.ProgressManager;
11 import com.intellij.openapi.project.Project;
12 import com.intellij.openapi.ui.DialogWrapper;
13 import com.intellij.openapi.util.io.FileUtil;
14 import com.intellij.openapi.util.text.StringUtil;
15 import com.intellij.openapi.vfs.VfsUtil;
16 import com.intellij.openapi.vfs.VirtualFile;
17 import com.intellij.openapi.vfs.VirtualFileFilter;
18 import com.intellij.util.net.ssl.CertificateManager;
19 import com.jetbrains.edu.learning.StudySerializationUtils;
20 import com.jetbrains.edu.learning.StudyTaskManager;
21 import com.jetbrains.edu.learning.core.EduNames;
22 import com.jetbrains.edu.learning.core.EduUtils;
23 import com.jetbrains.edu.learning.courseFormat.*;
24 import org.apache.commons.codec.binary.Base64;
25 import org.apache.http.*;
26 import org.apache.http.client.entity.UrlEncodedFormEntity;
27 import org.apache.http.client.methods.*;
28 import org.apache.http.client.utils.URIBuilder;
29 import org.apache.http.cookie.Cookie;
30 import org.apache.http.entity.ContentType;
31 import org.apache.http.entity.StringEntity;
32 import org.apache.http.impl.DefaultConnectionReuseStrategy;
33 import org.apache.http.impl.client.BasicCookieStore;
34 import org.apache.http.impl.client.CloseableHttpClient;
35 import org.apache.http.impl.client.HttpClientBuilder;
36 import org.apache.http.impl.client.HttpClients;
37 import org.apache.http.message.BasicHeader;
38 import org.apache.http.message.BasicNameValuePair;
39 import org.apache.http.util.EntityUtils;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42
43 import javax.net.ssl.SSLContext;
44 import javax.net.ssl.TrustManager;
45 import javax.net.ssl.X509TrustManager;
46 import java.io.IOException;
47 import java.net.URI;
48 import java.net.URISyntaxException;
49 import java.security.KeyManagementException;
50 import java.security.NoSuchAlgorithmException;
51 import java.security.SecureRandom;
52 import java.security.cert.X509Certificate;
53 import java.util.*;
54
55 public class EduStepicConnector {
56   private static final Logger LOG = Logger.getInstance(EduStepicConnector.class.getName());
57   private static final String stepicUrl = "https://stepic.org/";
58   private static String ourCSRFToken = "";
59   private static CloseableHttpClient ourClient;
60
61   //this prefix indicates that course can be opened by educational plugin
62   public static final String PYCHARM_PREFIX = "pycharm";
63   private static BasicCookieStore ourCookieStore;
64
65   private EduStepicConnector() {
66   }
67
68   public static StepicUser login(@NotNull final String username, @NotNull final String password) {
69     initializeClient();
70     if (postCredentials(username, password)) {
71       final StepicWrappers.AuthorWrapper stepicUserWrapper = getCurrentUser();
72       if (stepicUserWrapper != null && stepicUserWrapper.users.size() == 1) {
73         return stepicUserWrapper.users.get(0);
74       }
75     }
76     return null;
77   }
78   
79   @NotNull
80   public static List<Integer> getEnrolledCoursesIds() {
81     try {
82       final URI enrolledCoursesUri = new URIBuilder(EduStepicNames.COURSES).addParameter("enrolled", "true").build();
83       final List<CourseInfo> courses = getFromStepic(enrolledCoursesUri.toString(), StepicWrappers.CoursesContainer.class).courses;
84       final ArrayList<Integer> ids = new ArrayList<>();
85       for (CourseInfo course : courses) {
86         ids.add(course.getId());
87       }
88       return ids;
89     }
90     catch (IOException e) {
91       LOG.warn(e.getMessage());
92     }
93     catch (URISyntaxException e) {
94       LOG.warn(e.getMessage());
95     }
96     return Collections.emptyList();
97   }
98
99   @Nullable
100   public static StepicWrappers.AuthorWrapper getCurrentUser() {
101     try {
102       return getFromStepic(EduStepicNames.CURRENT_USER, StepicWrappers.AuthorWrapper.class);
103     }
104     catch (IOException e) {
105       LOG.warn("Couldn't get author info");
106     }
107     return null;
108   }
109
110   public static boolean createUser(@NotNull final String user, @NotNull final String password) {
111     final HttpPost userRequest = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.USERS);
112     initializeClient();
113     setHeaders(userRequest, "application/json");
114     String requestBody = new Gson().toJson(new StepicWrappers.UserWrapper(user, password));
115     userRequest.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
116
117     try {
118       final CloseableHttpResponse response = ourClient.execute(userRequest);
119       final HttpEntity responseEntity = response.getEntity();
120       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
121       final StatusLine statusLine = response.getStatusLine();
122       if (statusLine.getStatusCode() != HttpStatus.SC_CREATED) {
123         LOG.error("Failed to create user " + responseString);
124         return false;
125       }
126     }
127     catch (IOException e) {
128       LOG.error(e.getMessage());
129     }
130     return true;
131   }
132
133   public static void initializeClient() {
134     if (ourClient == null) {
135       final HttpGet request = new HttpGet(EduStepicNames.STEPIC_URL);
136       request.addHeader(new BasicHeader("referer", EduStepicNames.STEPIC_URL));
137       request.addHeader(new BasicHeader("content-type", EduStepicNames.CONTENT_TYPE_APPL_JSON));
138
139
140       HttpClientBuilder builder =
141         HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext()).setMaxConnPerRoute(100000).
142           setConnectionReuseStrategy(DefaultConnectionReuseStrategy.INSTANCE);
143       ourCookieStore = new BasicCookieStore();
144
145       try {
146         // Create a trust manager that does not validate certificate for this connection
147         TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
148           public X509Certificate[] getAcceptedIssuers() {
149             return null;
150           }
151
152           public void checkClientTrusted(X509Certificate[] certs, String authType) {
153           }
154
155           public void checkServerTrusted(X509Certificate[] certs, String authType) {
156           }
157         }};
158         SSLContext sslContext = SSLContext.getInstance("TLS");
159         sslContext.init(null, trustAllCerts, new SecureRandom());
160         ourClient = builder.setDefaultCookieStore(ourCookieStore).setSslcontext(sslContext).build();
161
162         ourClient.execute(request);
163         saveCSRFToken();
164       }
165       catch (IOException e) {
166         LOG.error(e.getMessage());
167       }
168       catch (NoSuchAlgorithmException e) {
169         LOG.error(e.getMessage());
170       }
171       catch (KeyManagementException e) {
172         LOG.error(e.getMessage());
173       }
174     }
175   }
176
177   private static void saveCSRFToken() {
178     if (ourCookieStore == null) return;
179     final List<Cookie> cookies = ourCookieStore.getCookies();
180     for (Cookie cookie : cookies) {
181       if (cookie.getName().equals("csrftoken")) {
182         ourCSRFToken = cookie.getValue();
183       }
184     }
185   }
186
187   private static boolean postCredentials(String user, String password) {
188     String url = EduStepicNames.STEPIC_URL + EduStepicNames.LOGIN;
189     final HttpPost request = new HttpPost(url);
190     List <NameValuePair> nvps = new ArrayList <NameValuePair>();
191     nvps.add(new BasicNameValuePair("csrfmiddlewaretoken", ourCSRFToken));
192     nvps.add(new BasicNameValuePair("login", user));
193     nvps.add(new BasicNameValuePair("next", "/"));
194     nvps.add(new BasicNameValuePair("password", password));
195     nvps.add(new BasicNameValuePair("remember", "on"));
196
197     request.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8));
198
199     setHeaders(request, "application/x-www-form-urlencoded");
200
201     try {
202       final CloseableHttpResponse response = ourClient.execute(request);
203       saveCSRFToken();
204       final StatusLine line = response.getStatusLine();
205       if (line.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) {
206         final HttpEntity responseEntity = response.getEntity();
207         final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
208         LOG.warn("Failed to login: " + line.getStatusCode() + line.getReasonPhrase());
209         LOG.debug("Failed to login " + responseString);
210         ourClient = null;
211         return false;
212       }
213     }
214     catch (IOException e) {
215       LOG.warn(e.getMessage());
216       ourClient = null;
217       return false;
218     }
219     return true;
220   }
221
222   static <T> T getFromStepic(String link, final Class<T> container) throws IOException {
223     if (!link.startsWith("/")) link = "/" + link;
224     final HttpGet request = new HttpGet(EduStepicNames.STEPIC_API_URL + link);
225     if (ourClient == null) {
226       initializeClient();
227     }
228     setHeaders(request, EduStepicNames.CONTENT_TYPE_APPL_JSON);
229
230     final CloseableHttpResponse response = ourClient.execute(request);
231     final StatusLine statusLine = response.getStatusLine();
232     final HttpEntity responseEntity = response.getEntity();
233     final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
234     if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
235       throw new IOException("Stepic returned non 200 status code " + responseString);
236     }
237     Gson gson = new GsonBuilder().registerTypeAdapter(TaskFile.class, new StudySerializationUtils.Json.StepicTaskFileAdapter()).
238       registerTypeAdapter(AnswerPlaceholder.class, new StudySerializationUtils.Json.StepicAnswerPlaceholderAdapter()).
239       setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
240     return gson.fromJson(responseString, container);
241   }
242
243   @NotNull
244   public static CloseableHttpClient getHttpClient(@NotNull final Project project) {
245     if (ourClient == null) {
246       login(project);
247       initializeClient();
248     }
249     return ourClient;
250   }
251
252   public static boolean enrollToCourse(final int courseId) {
253     HttpPost post = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.ENROLLMENTS);
254     try {
255       final StepicWrappers.EnrollmentWrapper enrollment = new StepicWrappers.EnrollmentWrapper(String.valueOf(courseId));
256       post.setEntity(new StringEntity(new GsonBuilder().create().toJson(enrollment)));
257       setHeaders(post, EduStepicNames.CONTENT_TYPE_APPL_JSON);
258       if (ourClient == null) {
259         initializeClient();
260       }
261       CloseableHttpResponse response = ourClient.execute(post);
262       StatusLine line = response.getStatusLine();
263       return line.getStatusCode() == HttpStatus.SC_CREATED;
264     }
265     catch (IOException e) {
266       LOG.warn(e.getMessage());
267     }
268     return false;
269   }
270
271   @NotNull
272   public static List<CourseInfo> getCourses() {
273     try {
274       List<CourseInfo> result = new ArrayList<CourseInfo>();
275       int pageNumber = 1;
276       while (addCoursesFromStepic(result, pageNumber)) {
277         pageNumber += 1;
278       }
279       return result;
280     }
281     catch (IOException e) {
282       LOG.error("Cannot load course list " + e.getMessage());
283     }
284     return Collections.singletonList(CourseInfo.INVALID_COURSE);
285   }
286
287   private static boolean addCoursesFromStepic(List<CourseInfo> result, int pageNumber) throws IOException {
288     final String url = pageNumber == 0 ? EduStepicNames.COURSES : EduStepicNames.COURSES_FROM_PAGE + String.valueOf(pageNumber);
289     final StepicWrappers.CoursesContainer coursesContainer = getFromStepic(url, StepicWrappers.CoursesContainer.class);
290     final List<CourseInfo> courseInfos = coursesContainer.courses;
291     for (CourseInfo info : courseInfos) {
292       final String courseType = info.getType();
293       if (!info.isAdaptive() && StringUtil.isEmptyOrSpaces(courseType)) continue;
294       final List<String> typeLanguage = StringUtil.split(courseType, " ");
295       // TODO: should adaptive course be of PyCharmType ?
296       if (info.isAdaptive() || (typeLanguage.size() == 2 && PYCHARM_PREFIX.equals(typeLanguage.get(0)))) {
297         for (Integer instructor : info.instructors) {
298           final StepicUser author = getFromStepic(EduStepicNames.USERS + "/" + String.valueOf(instructor), StepicWrappers.AuthorWrapper.class).users.get(0);
299           info.addAuthor(author);
300         }
301
302         String name = info.getName().replaceAll("[^a-zA-Z0-9\\s]", "");
303         info.setName(name.trim());
304         
305         result.add(info);
306       }
307     }
308     return coursesContainer.meta.containsKey("has_next") && coursesContainer.meta.get("has_next") == Boolean.TRUE;
309   }
310
311   public static Course getCourse(@NotNull final Project project, @NotNull final CourseInfo info) {
312     final Course course = new Course();
313     course.setAuthors(info.getAuthors());
314     course.setDescription(info.getDescription());
315     course.setAdaptive(info.isAdaptive());
316     course.setId(info.id);
317     course.setUpToDate(true);  // TODO: get from stepic
318     
319     if (!course.isAdaptive()) {
320       String courseType = info.getType();
321       course.setName(info.getName());
322       course.setLanguage(courseType.substring(PYCHARM_PREFIX.length() + 1));
323       try {
324         for (Integer section : info.sections) {
325           course.addLessons(getLessons(section));
326         }
327         return course;
328       }
329       catch (IOException e) {
330         LOG.error("IOException " + e.getMessage());
331       }
332     }
333     else {
334       final Lesson lesson = new Lesson();
335       course.setName(info.getName());
336       //TODO: more specific name?
337       lesson.setName("Adaptive");
338       course.addLesson(lesson);
339       final Task recommendation = EduAdaptiveStepicConnector.getNextRecommendation(project, course);
340       if (recommendation != null) {
341         lesson.addTask(recommendation);
342         return course;
343       }
344       else {
345         return null;
346       }
347     }
348     return null;
349   }
350
351   public static List<Lesson> getLessons(int sectionId) throws IOException {
352     final StepicWrappers.SectionContainer
353       sectionContainer = getFromStepic(EduStepicNames.SECTIONS + String.valueOf(sectionId), StepicWrappers.SectionContainer.class);
354     List<Integer> unitIds = sectionContainer.sections.get(0).units;
355     final List<Lesson> lessons = new ArrayList<Lesson>();
356     for (Integer unitId : unitIds) {
357       StepicWrappers.UnitContainer
358         unit = getFromStepic(EduStepicNames.UNITS + "/" + String.valueOf(unitId), StepicWrappers.UnitContainer.class);
359       int lessonID = unit.units.get(0).lesson;
360       StepicWrappers.LessonContainer
361         lesson = getFromStepic(EduStepicNames.LESSONS + String.valueOf(lessonID), StepicWrappers.LessonContainer.class);
362       Lesson realLesson = lesson.lessons.get(0);
363       realLesson.taskList = new ArrayList<Task>();
364       for (Integer s : realLesson.steps) {
365         createTask(realLesson, s);
366       }
367       if (!realLesson.taskList.isEmpty())
368         lessons.add(realLesson);
369     }
370
371     return lessons;
372   }
373
374   private static void createTask(Lesson lesson, Integer stepicId) throws IOException {
375     final StepicWrappers.Step step = getStep(stepicId);
376     if (!step.name.equals(PYCHARM_PREFIX)) return;
377     final Task task = new Task();
378     task.setStepicId(stepicId);
379     task.setName(step.options != null ? step.options.title : PYCHARM_PREFIX);
380     task.setText(step.text);
381     for (StepicWrappers.TestFileWrapper wrapper : step.options.test) {
382       task.addTestsTexts(wrapper.name, wrapper.text);
383     }
384
385     task.taskFiles = new HashMap<String, TaskFile>();      // TODO: it looks like we don't need taskFiles as map anymore
386     if (step.options.files != null) {
387       for (TaskFile taskFile : step.options.files) {
388         task.taskFiles.put(taskFile.name, taskFile);
389       }
390     }
391     lesson.taskList.add(task);
392   }
393
394   public static StepicWrappers.Step getStep(Integer step) throws IOException {
395     return getFromStepic(EduStepicNames.STEPS + "/" + String.valueOf(step), StepicWrappers.StepContainer.class).steps.get(0).block;
396   }
397
398
399   public static boolean showLoginDialog() {
400     final boolean[] logged = {false};
401     ApplicationManager.getApplication().invokeAndWait(() -> {
402       final LoginDialog dialog = new LoginDialog();
403       dialog.show();
404       logged[0] = dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE;
405     }, ModalityState.defaultModalityState());
406     return logged[0];
407   }
408
409   public static void postAttempt(@NotNull final Task task, boolean passed, @Nullable String login, @Nullable String password) {
410     if (task.getStepicId() <= 0) {
411       return;
412     }
413     if (ourClient == null) {
414       if (StringUtil.isEmptyOrSpaces(login) || StringUtil.isEmptyOrSpaces(password)) {
415         return;
416       }
417       else {
418         if (login(login, password) == null) return;
419       }
420     }
421
422     final HttpPost attemptRequest = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.ATTEMPTS);
423     setHeaders(attemptRequest, "application/json");
424     String attemptRequestBody = new Gson().toJson(new StepicWrappers.AttemptWrapper(task.getStepicId()));
425     attemptRequest.setEntity(new StringEntity(attemptRequestBody, ContentType.APPLICATION_JSON));
426
427     try {
428       final CloseableHttpResponse attemptResponse = ourClient.execute(attemptRequest);
429       final HttpEntity responseEntity = attemptResponse.getEntity();
430       final String attemptResponseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
431       final StatusLine statusLine = attemptResponse.getStatusLine();
432       if (statusLine.getStatusCode() != HttpStatus.SC_CREATED) {
433         LOG.error("Failed to make attempt " + attemptResponseString);
434       }
435       final StepicWrappers.AttemptWrapper.Attempt attempt = new Gson().fromJson(attemptResponseString, StepicWrappers.AttemptContainer.class).attempts.get(0);
436
437       final Map<String, TaskFile> taskFiles = task.getTaskFiles();
438       final ArrayList<StepicWrappers.SolutionFile> files = new ArrayList<StepicWrappers.SolutionFile>();
439       for (TaskFile fileEntry : taskFiles.values()) {
440         files.add(new StepicWrappers.SolutionFile(fileEntry.name, fileEntry.text));
441       }
442       postSubmission(passed, attempt, files);
443     }
444     catch (IOException e) {
445       LOG.error(e.getMessage());
446     }
447   }
448
449   private static void postSubmission(boolean passed, StepicWrappers.AttemptWrapper.Attempt attempt, ArrayList<StepicWrappers.SolutionFile> files) throws IOException {
450     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.SUBMISSIONS);
451     setHeaders(request, EduStepicNames.CONTENT_TYPE_APPL_JSON);
452
453     String requestBody = new Gson().toJson(new StepicWrappers.SubmissionWrapper(attempt.id, passed ? "1" : "0", files));
454     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
455     final CloseableHttpResponse response = ourClient.execute(request);
456     final HttpEntity responseEntity = response.getEntity();
457     final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
458     final StatusLine line = response.getStatusLine();
459     if (line.getStatusCode() != HttpStatus.SC_CREATED) {
460       LOG.error("Failed to make submission " + responseString);
461     }
462   }
463
464   public static void postCourseWithProgress(final Project project, @NotNull final Course course) {
465     postCourseWithProgress(project, course, false);
466   }
467
468   public static void postCourseWithProgress(final Project project, @NotNull final Course course, final boolean relogin) {
469     ProgressManager.getInstance().run(new com.intellij.openapi.progress.Task.Modal(project, "Uploading Course", true) {
470       @Override
471       public void run(@NotNull final ProgressIndicator indicator) {
472         postCourse(project, course, relogin, indicator);
473       }
474     });
475   }
476
477   private static void postCourse(final Project project, @NotNull Course course, boolean relogin, @NotNull final ProgressIndicator indicator) {
478     indicator.setText("Uploading course to " + stepicUrl);
479     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + "/courses");
480     if (ourClient == null || !relogin) {
481       if (!login(project)) return;
482     }
483     final StepicWrappers.AuthorWrapper user = getCurrentUser();
484     if (user != null) {
485       course.setAuthors(user.users);
486     }
487
488     setHeaders(request, EduStepicNames.CONTENT_TYPE_APPL_JSON);
489     String requestBody = new Gson().toJson(new StepicWrappers.CourseWrapper(course));
490     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
491
492     try {
493       final CloseableHttpResponse response = ourClient.execute(request);
494       final HttpEntity responseEntity = response.getEntity();
495       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
496       final StatusLine line = response.getStatusLine();
497       if (line.getStatusCode() != HttpStatus.SC_CREATED) {
498         if (!relogin) {
499           login(project);
500           postCourse(project, course, true, indicator);
501         }
502         LOG.error("Failed to push " + responseString);
503         return;
504       }
505       final CourseInfo postedCourse = new Gson().fromJson(responseString, StepicWrappers.CoursesContainer.class).courses.get(0);
506
507       final int sectionId = postModule(postedCourse.id, 1, String.valueOf(postedCourse.getName()));
508       int position = 1;
509       for (Lesson lesson : course.getLessons()) {
510         indicator.checkCanceled();
511         final int lessonId = postLesson(project, lesson, indicator);
512         postUnit(lessonId, position, sectionId);
513         position += 1;
514       }
515       ApplicationManager.getApplication().runReadAction(() -> postAdditionalFiles(project, postedCourse.id, indicator));
516     }
517     catch (IOException e) {
518       LOG.error(e.getMessage());
519     }
520   }
521
522   private static boolean login(@NotNull final Project project) {
523     final StepicUser user = StudyTaskManager.getInstance(project).getUser();
524     final String login =  user.getEmail();
525     if (StringUtil.isEmptyOrSpaces(login)) {
526       return showLoginDialog();
527     }
528     else {
529       if (login(login, user.getPassword()) == null) {
530         return showLoginDialog();
531       }
532     }
533     return true;
534   }
535
536   private static void postAdditionalFiles(@NotNull final Project project, int id, ProgressIndicator indicator) {
537     final VirtualFile baseDir = project.getBaseDir();
538     final List<VirtualFile> files = VfsUtil.getChildren(baseDir, new VirtualFileFilter() {
539       @Override
540       public boolean accept(VirtualFile file) {
541         final String name = file.getName();
542         return !name.contains(EduNames.LESSON) && !name.equals(EduNames.COURSE_META_FILE) && !name.equals(EduNames.HINTS) &&
543           !"pyc".equals(file.getExtension()) && !file.isDirectory() && !name.equals(EduNames.TEST_HELPER) && !name.startsWith("");
544       }
545     });
546
547     if (!files.isEmpty()) {
548       final int sectionId = postModule(id, 2, EduNames.PYCHARM_ADDITIONAL);
549       final Lesson lesson = new Lesson();
550       lesson.setName(EduNames.PYCHARM_ADDITIONAL);
551       final Task task = new Task();
552       task.setLesson(lesson);
553       task.setName(EduNames.PYCHARM_ADDITIONAL);
554       task.setIndex(1);
555       task.setText(EduNames.PYCHARM_ADDITIONAL);
556       for (VirtualFile file : files) {
557         try {
558           if (file != null) {
559             if (EduUtils.isImage(file.getName())) {
560               task.addTestsTexts(file.getName(), Base64.encodeBase64URLSafeString(FileUtil.loadBytes(file.getInputStream())));
561             }
562             else {
563               task.addTestsTexts(file.getName(), FileUtil.loadTextAndClose(file.getInputStream()));
564             }
565           }
566         }
567         catch (IOException e) {
568           LOG.error("Can't find file " + file.getPath());
569         }
570       }
571       lesson.addTask(task);
572       lesson.setIndex(1);
573       final int lessonId = postLesson(project, lesson, indicator);
574       postUnit(lessonId, 1, sectionId);
575     }
576   }
577
578   private static void postUnit(int lessonId, int position, int sectionId) {
579     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.UNITS);
580     setHeaders(request, EduStepicNames.CONTENT_TYPE_APPL_JSON);
581     final StepicWrappers.UnitWrapper unitWrapper = new StepicWrappers.UnitWrapper();
582     unitWrapper.unit = new StepicWrappers.Unit();
583     unitWrapper.unit.lesson = lessonId;
584     unitWrapper.unit.position = position;
585     unitWrapper.unit.section = sectionId;
586
587     String requestBody = new Gson().toJson(unitWrapper);
588     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
589
590     try {
591       final CloseableHttpResponse response = ourClient.execute(request);
592       final HttpEntity responseEntity = response.getEntity();
593       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
594       final StatusLine line = response.getStatusLine();
595       if (line.getStatusCode() != HttpStatus.SC_CREATED) {
596         LOG.error("Failed to push " + responseString);
597       }
598     }
599     catch (IOException e) {
600       LOG.error(e.getMessage());
601     }
602   }
603
604   private static int postModule(int courseId, int position, @NotNull final String title) {
605     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + "/sections");
606     setHeaders(request, "application/json");
607     final StepicWrappers.Section section = new StepicWrappers.Section();
608     section.course = courseId;
609     section.title = title;
610     section.position = position;
611     final StepicWrappers.SectionWrapper sectionContainer = new StepicWrappers.SectionWrapper();
612     sectionContainer.section = section;
613     String requestBody = new Gson().toJson(sectionContainer);
614     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
615
616     try {
617       final CloseableHttpResponse response = ourClient.execute(request);
618       final HttpEntity responseEntity = response.getEntity();
619       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
620       final StatusLine line = response.getStatusLine();
621       if (line.getStatusCode() != HttpStatus.SC_CREATED) {
622         LOG.error("Failed to push " + responseString);
623       }
624       final StepicWrappers.Section
625         postedSection = new Gson().fromJson(responseString, StepicWrappers.SectionContainer.class).sections.get(0);
626       return postedSection.id;
627     }
628     catch (IOException e) {
629       LOG.error(e.getMessage());
630     }
631     return -1;
632   }
633
634   public static int updateLesson(@NotNull final Project project, @NotNull final Lesson lesson, ProgressIndicator indicator) {
635     final HttpPut request = new HttpPut(EduStepicNames.STEPIC_API_URL + EduStepicNames.LESSONS + String.valueOf(lesson.getId()));
636     if (ourClient == null) {
637       if (!login(project)) {
638         LOG.error("Failed to push lesson");
639         return 0;
640       }
641     }
642
643     setHeaders(request, "application/json");
644     String requestBody = new Gson().toJson(new StepicWrappers.LessonWrapper(lesson));
645     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
646
647     try {
648       final CloseableHttpResponse response = ourClient.execute(request);
649       final HttpEntity responseEntity = response.getEntity();
650       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
651       final StatusLine line = response.getStatusLine();
652       if (line.getStatusCode() != HttpStatus.SC_OK) {
653         LOG.error("Failed to push " + responseString);
654         return 0;
655       }
656       final Lesson postedLesson = new Gson().fromJson(responseString, Course.class).getLessons().get(0);
657       for (Integer step : postedLesson.steps) {
658         deleteTask(step);
659       }
660
661       for (Task task : lesson.getTaskList()) {
662         indicator.checkCanceled();
663         postTask(project, task, lesson.getId());
664       }
665       return lesson.getId();
666     }
667     catch (IOException e) {
668       LOG.error(e.getMessage());
669     }
670     return -1;
671   }
672
673   public static int postLesson(@NotNull final Project project, @NotNull final Lesson lesson, ProgressIndicator indicator) {
674     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + "/lessons");
675     if (ourClient == null) {
676       login(project);
677     }
678
679     setHeaders(request, "application/json");
680     String requestBody = new Gson().toJson(new StepicWrappers.LessonWrapper(lesson));
681     request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
682
683     try {
684       final CloseableHttpResponse response = ourClient.execute(request);
685       final HttpEntity responseEntity = response.getEntity();
686       final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
687       final StatusLine line = response.getStatusLine();
688       if (line.getStatusCode() != HttpStatus.SC_CREATED) {
689         LOG.error("Failed to push " + responseString);
690         return 0;
691       }
692       final Lesson postedLesson = new Gson().fromJson(responseString, Course.class).getLessons().get(0);
693       lesson.setId(postedLesson.getId());
694       for (Task task : lesson.getTaskList()) {
695         indicator.checkCanceled();
696         postTask(project, task, postedLesson.getId());
697       }
698       return postedLesson.getId();
699     }
700     catch (IOException e) {
701       LOG.error(e.getMessage());
702     }
703     return -1;
704   }
705
706   public static void deleteTask(@NotNull final Integer task) {
707     final HttpDelete request = new HttpDelete(EduStepicNames.STEPIC_API_URL + EduStepicNames.STEP_SOURCES + task);
708     setHeaders(request, "application/json");
709     ApplicationManager.getApplication().invokeLater(() -> {
710       try {
711         final CloseableHttpResponse response = ourClient.execute(request);
712         final StatusLine line = response.getStatusLine();
713         if (line.getStatusCode() != HttpStatus.SC_NO_CONTENT) {
714           final HttpEntity responseEntity = response.getEntity();
715           final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
716           LOG.error("Failed to delete task " + responseString);
717         }
718       }
719       catch (IOException e) {
720         LOG.error(e.getMessage());
721       }
722     });
723   }
724
725   public static void postTask(final Project project, @NotNull final Task task, final int lessonId) {
726     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + "/step-sources");
727     setHeaders(request, "application/json");
728     //TODO: register type adapter for task files here?
729     final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().
730       registerTypeAdapter(AnswerPlaceholder.class, new StudySerializationUtils.Json.StepicAnswerPlaceholderAdapter()).create();
731     ApplicationManager.getApplication().invokeLater(() -> {
732       final String requestBody = gson.toJson(new StepicWrappers.StepSourceWrapper(project, task, lessonId));
733       request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
734
735       try {
736         final CloseableHttpResponse response = ourClient.execute(request);
737         final StatusLine line = response.getStatusLine();
738         if (line.getStatusCode() != HttpStatus.SC_CREATED) {
739           final HttpEntity responseEntity = response.getEntity();
740           final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : "";
741           LOG.error("Failed to push " + responseString);
742         }
743       }
744       catch (IOException e) {
745         LOG.error(e.getMessage());
746       }
747     });
748   }
749
750   static void setHeaders(@NotNull final HttpRequestBase request, String contentType) {
751     request.addHeader(new BasicHeader("referer", stepicUrl));
752     request.addHeader(new BasicHeader("X-CSRFToken", ourCSRFToken));
753     request.addHeader(new BasicHeader("content-type", contentType));
754   }
755 }