add format version to stepic format
authorliana.bakradze <liana.bakradze@jetbrains.com>
Tue, 8 Nov 2016 18:03:33 +0000 (21:03 +0300)
committerliana.bakradze <liana.bakradze@jetbrains.com>
Thu, 17 Nov 2016 14:08:40 +0000 (17:08 +0300)
13 files changed:
python/educational-core/student/src/com/jetbrains/edu/learning/StudySerializationUtils.java
python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/AnswerPlaceholder.java
python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/AnswerPlaceholderSubtaskInfo.java
python/educational-core/student/src/com/jetbrains/edu/learning/stepic/CCStepicConnector.java
python/educational-core/student/src/com/jetbrains/edu/learning/stepic/CourseInfo.java
python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduStepicClient.java
python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java
python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java
python/educational-core/student/testData/stepic/1.json [new file with mode: 0644]
python/educational-core/student/testData/stepic/2.json [new file with mode: 0644]
python/educational-core/student/testData/stepic/3.json [new file with mode: 0644]
python/educational-core/student/testData/stepic/courses.json [new file with mode: 0644]
python/educational-core/student/testSrc/com/jetbrains/edu/learning/stepic/StudyStepicFormatTest.java [new file with mode: 0644]

index d2e7d54dd2156bc5784c79aa31e7ab82bb0c4f18..c54a7d5fac265ee120f005fcfb6735213b5c275e 100644 (file)
@@ -12,10 +12,10 @@ import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.util.containers.ContainerUtil;
 import com.intellij.util.containers.hash.HashMap;
 import com.jetbrains.edu.learning.core.EduNames;
-import com.jetbrains.edu.learning.courseFormat.AnswerPlaceholder;
 import com.jetbrains.edu.learning.courseFormat.Course;
 import com.jetbrains.edu.learning.courseFormat.StudyStatus;
-import com.jetbrains.edu.learning.courseFormat.TaskFile;
+import com.jetbrains.edu.learning.stepic.EduStepicConnector;
+import com.jetbrains.edu.learning.stepic.StepicWrappers;
 import org.jdom.Attribute;
 import org.jdom.Element;
 import org.jdom.output.XMLOutputter;
@@ -387,6 +387,10 @@ public class StudySerializationUtils {
 
     public static final String TASK_LIST = "task_list";
     public static final String TASK_FILES = "task_files";
+    public static final String FILES = "files";
+    public static final String HINTS = "hints";
+    public static final String SUBTASK_INFOS = "subtask_infos";
+    public static final String FORMAT_VERSION = "format_version";
 
     private Json() {
     }
@@ -438,74 +442,97 @@ public class StudySerializationUtils {
       }
     }
 
-    public static class StepicTaskFileAdapter implements JsonDeserializer<TaskFile> {
-
+    public static class StepicStepOptionsAdapter implements JsonDeserializer<StepicWrappers.StepOptions> {
       @Override
-      public TaskFile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
-        final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
-        JsonObject taskFileObject = json.getAsJsonObject();
-        JsonArray placeholders = taskFileObject.getAsJsonArray(PLACEHOLDERS);
-        for (JsonElement placeholder : placeholders) {
-          JsonObject placeholderObject = placeholder.getAsJsonObject();
-          int line = placeholderObject.getAsJsonPrimitive(LINE).getAsInt();
-          int start = placeholderObject.getAsJsonPrimitive(START).getAsInt();
-          if (line == -1) {
-            placeholderObject.addProperty(OFFSET, start);
-          }
-          else {
-            Document document = EditorFactory.getInstance().createDocument(taskFileObject.getAsJsonPrimitive(TEXT).getAsString());
-            placeholderObject.addProperty(OFFSET, document.getLineStartOffset(line) + start);
+      public StepicWrappers.StepOptions deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+        JsonObject stepOptionsJson = json.getAsJsonObject();
+        JsonPrimitive versionJson = stepOptionsJson.getAsJsonPrimitive(FORMAT_VERSION);
+        int version = 1;
+        if (versionJson != null) {
+          version = versionJson.getAsInt();
+        }
+        switch (version) {
+          case 1:
+            stepOptionsJson = convertToSecondVersion(stepOptionsJson);
+            // uncomment for future versions
+            //case 2:
+            //  stepOptionsJson = convertToThirdVersion(stepOptionsJson);
+        }
+        StepicWrappers.StepOptions stepOptions =
+          new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create()
+            .fromJson(stepOptionsJson, StepicWrappers.StepOptions.class);
+        stepOptions.formatVersion = EduStepicConnector.CURRENT_VERSION;
+        return stepOptions;
+      }
+
+      private static JsonObject convertToSecondVersion(JsonObject stepOptionsJson) {
+        Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+        for (JsonElement taskFileElement : stepOptionsJson.getAsJsonArray(FILES)) {
+          JsonObject taskFileObject = taskFileElement.getAsJsonObject();
+          JsonArray placeholders = taskFileObject.getAsJsonArray(PLACEHOLDERS);
+          for (JsonElement placeholder : placeholders) {
+            JsonObject placeholderObject = placeholder.getAsJsonObject();
+            convertToAbsoluteOffset(taskFileObject, placeholderObject);
+            convertMultipleHints(gson, placeholderObject);
+            convertToSubtaskInfo(placeholderObject);
           }
-          final String hintString = placeholderObject.getAsJsonPrimitive(HINT).getAsString();
-          final JsonArray hintsArray = new JsonArray();
-
-          try {
-            final Type listType = new TypeToken<List<String>>() {}.getType();
-            final List<String> hints = gson.fromJson(hintString, listType);
-            if (hints != null && !hints.isEmpty()) {
-              for (int i = 0; i < hints.size(); i++) {
-                if (i == 0) {
-                  placeholderObject.addProperty(HINT, hints.get(0));
-                  continue;
-                }
-                hintsArray.add(hints.get(i));
+        }
+        return stepOptionsJson;
+      }
+
+      private static void convertToSubtaskInfo(JsonObject placeholderObject) {
+        JsonObject subtaskInfosObject = new JsonObject();
+        placeholderObject.add(SUBTASK_INFOS, subtaskInfosObject);
+        JsonObject subtaskInfo = new JsonObject();
+        subtaskInfosObject.add("0", subtaskInfo);
+        JsonArray hintsArray = new JsonArray();
+        hintsArray.add(placeholderObject.getAsJsonPrimitive(HINT).getAsString());
+        JsonArray additionalHints = placeholderObject.getAsJsonArray(ADDITIONAL_HINTS);
+        if (additionalHints != null) {
+          hintsArray.addAll(additionalHints);
+        }
+        subtaskInfo.add(HINTS, hintsArray);
+        subtaskInfo.addProperty(POSSIBLE_ANSWER, placeholderObject.getAsJsonPrimitive(POSSIBLE_ANSWER).getAsString());
+      }
+
+      private static void convertMultipleHints(Gson gson, JsonObject placeholderObject) {
+        final String hintString = placeholderObject.getAsJsonPrimitive(HINT).getAsString();
+        final JsonArray hintsArray = new JsonArray();
+
+        try {
+          final Type listType = new TypeToken<List<String>>() {
+          }.getType();
+          final List<String> hints = gson.fromJson(hintString, listType);
+          if (hints != null && !hints.isEmpty()) {
+            for (int i = 0; i < hints.size(); i++) {
+              if (i == 0) {
+                placeholderObject.addProperty(HINT, hints.get(0));
+                continue;
               }
-              placeholderObject.add(ADDITIONAL_HINTS, hintsArray);
-            }
-            else {
-              placeholderObject.addProperty(HINT, "");
+              hintsArray.add(hints.get(i));
             }
+            placeholderObject.add(ADDITIONAL_HINTS, hintsArray);
           }
-          catch (JsonParseException e) {
-            hintsArray.add(hintString);
+          else {
+            placeholderObject.addProperty(HINT, "");
           }
         }
-
-        return gson.fromJson(json, TaskFile.class);
+        catch (JsonParseException e) {
+          hintsArray.add(hintString);
+        }
       }
-    }
 
-    public static class StepicAnswerPlaceholderAdapter implements JsonSerializer<AnswerPlaceholder> {
-      @Override
-      public JsonElement serialize(AnswerPlaceholder src, Type typeOfSrc, JsonSerializationContext context) {
-        final List<String> hints = src.getHints();
-
-        final int length = src.getLength();
-        final int start = src.getOffset();
-        final String possibleAnswer = src.getPossibleAnswer();
-        int line = -1;
-
-        final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
-        final JsonObject answerPlaceholder = new JsonObject();
-        answerPlaceholder.addProperty(LINE, line);
-        answerPlaceholder.addProperty(START, start);
-        answerPlaceholder.addProperty(LENGTH, length);
-        answerPlaceholder.addProperty(POSSIBLE_ANSWER, possibleAnswer);
-
-        final String jsonHints = gson.toJson(hints);
-        answerPlaceholder.addProperty(HINT, jsonHints);
-
-        return answerPlaceholder;
+      private static void convertToAbsoluteOffset(JsonObject taskFileObject, JsonObject placeholderObject) {
+        int line = placeholderObject.getAsJsonPrimitive(LINE).getAsInt();
+        int start = placeholderObject.getAsJsonPrimitive(START).getAsInt();
+        if (line == -1) {
+          placeholderObject.addProperty(OFFSET, start);
+        }
+        else {
+          Document document = EditorFactory.getInstance().createDocument(taskFileObject.getAsJsonPrimitive(TEXT).getAsString());
+          placeholderObject.addProperty(OFFSET, document.getLineStartOffset(line) + start);
+        }
       }
     }
   }
index d952d934c793aa2c85d59f783f26a05d6c2c6cb3..ab23d693e6a3e6db2ed45bf19487d3600d352e08 100644 (file)
@@ -33,6 +33,7 @@ public class AnswerPlaceholder {
 
   @Transient private TaskFile myTaskFile;
 
+  @SerializedName("subtask_infos")
   @Expose private Map<Integer, AnswerPlaceholderSubtaskInfo> mySubtaskInfos = new HashMap<>();
   public AnswerPlaceholder() {
   }
index 33d686984e746ffe6b8a235488418e47b8f2d802..33b7cda8de1d4fc14e9a1d64925ed6f882ddb6c9 100644 (file)
@@ -17,12 +17,17 @@ public class AnswerPlaceholderSubtaskInfo {
   @SerializedName("possible_answer")
   @Expose private String possibleAnswer = "";
 
+  @SerializedName("placeholder_text")
   @Expose private String myPlaceholderText;
 
   private String myAnswer = "";
   private boolean mySelected = false;
   private StudyStatus myStatus = StudyStatus.Unchecked;
+
+  @SerializedName("has_frame")
   @Expose private boolean myHasFrame = true;
+
+  @SerializedName("need_insert_text")
   @Expose private boolean myNeedInsertText = false;
 
   public StudyStatus getStatus() {
index 47e4d59cad770e0c18d369907ae8a68b4355f281..c25955f2bc5d3afb65e5d5a741820a90a2595757 100644 (file)
@@ -12,10 +12,8 @@ import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.vfs.VfsUtil;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.openapi.vfs.VirtualFileFilter;
-import com.jetbrains.edu.learning.StudySerializationUtils;
 import com.jetbrains.edu.learning.core.EduNames;
 import com.jetbrains.edu.learning.core.EduUtils;
-import com.jetbrains.edu.learning.courseFormat.AnswerPlaceholder;
 import com.jetbrains.edu.learning.courseFormat.Course;
 import com.jetbrains.edu.learning.courseFormat.Lesson;
 import com.jetbrains.edu.learning.courseFormat.Task;
@@ -219,8 +217,7 @@ public class CCStepicConnector {
     final int lessonId = lesson.getId();
 
     final HttpPut request = new HttpPut(EduStepicNames.STEPIC_API_URL + "/step-sources/" + String.valueOf(task.getStepId()));
-    final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().
-      registerTypeAdapter(AnswerPlaceholder.class, new StudySerializationUtils.Json.StepicAnswerPlaceholderAdapter()).create();
+    final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().create();
     ApplicationManager.getApplication().invokeLater(() -> {
       task.addTestsTexts("tests.py", task.getTestsText(project));
       final String requestBody = gson.toJson(new StepicWrappers.StepSourceWrapper(project, task, lessonId));
@@ -331,9 +328,7 @@ public class CCStepicConnector {
 
   public static void postTask(final Project project, @NotNull final Task task, final int lessonId) {
     final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + "/step-sources");
-    //TODO: register type adapter for task files here?
-    final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().
-      registerTypeAdapter(AnswerPlaceholder.class, new StudySerializationUtils.Json.StepicAnswerPlaceholderAdapter()).create();
+    final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().create();
     ApplicationManager.getApplication().invokeLater(() -> {
       final String requestBody = gson.toJson(new StepicWrappers.StepSourceWrapper(project, task, lessonId));
       request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
index d7fb980d5a6cb85a76673fcd60577b78ad471ae0..94accb19510d7c21d240c2152fd3a997839f53e5 100644 (file)
@@ -27,7 +27,7 @@ public class CourseInfo {
 
   List<StepicUser> myAuthors = new ArrayList<>();
   @SerializedName("summary") private String myDescription;
-  @SerializedName("course_format") private String myType = "pycharm Python"; //course type in format "pycharm <language>"
+  @SerializedName("course_format") private String myType = "pycharm" + EduStepicConnector.CURRENT_VERSION + " Python"; //course type in format "pycharm <language>"
   @Nullable private String username;
 
   @SerializedName("update_date") private Date updateDate;
index 3ed86017f1ee154056b1582c755b5e19b0222361..8d7b29ac7acb6aec50d7c17ebb816ab949d38f57 100644 (file)
@@ -7,8 +7,6 @@ import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.util.net.HttpConfigurable;
 import com.intellij.util.net.ssl.CertificateManager;
 import com.jetbrains.edu.learning.StudySerializationUtils;
-import com.jetbrains.edu.learning.courseFormat.AnswerPlaceholder;
-import com.jetbrains.edu.learning.courseFormat.TaskFile;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpHost;
 import org.apache.http.HttpStatus;
@@ -69,8 +67,11 @@ public class EduStepicClient {
     if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
       throw new IOException("Stepic returned non 200 status code " + responseString);
     }
-    Gson gson = new GsonBuilder().registerTypeAdapter(TaskFile.class, new StudySerializationUtils.Json.StepicTaskFileAdapter()).
-      registerTypeAdapter(AnswerPlaceholder.class, new StudySerializationUtils.Json.StepicAnswerPlaceholderAdapter()).
+    return deserializeStepicResponse(container, responseString);
+  }
+
+  static <T> T deserializeStepicResponse(Class<T> container, String responseString) {
+    Gson gson = new GsonBuilder().registerTypeAdapter(StepicWrappers.StepOptions.class, new StudySerializationUtils.Json.StepicStepOptionsAdapter()).
       setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").
       setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
     return gson.fromJson(responseString, container);
index 243e302130000884a58a22ca09401f090b80a7ba..2fe31bbbe410e41cf2a79129b555000b67a63a8d 100644 (file)
@@ -29,6 +29,7 @@ import java.util.*;
 public class EduStepicConnector {
   private static final Logger LOG = Logger.getInstance(EduStepicConnector.class.getName());
 
+  public static final int CURRENT_VERSION = 2;
   //this prefix indicates that course can be opened by educational plugin
   public static final String PYCHARM_PREFIX = "pycharm";
   private static final String ADAPTIVE_NOTE =
@@ -125,26 +126,51 @@ public class EduStepicConnector {
       return false;
     }
     final StepicWrappers.CoursesContainer coursesContainer = EduStepicClient.getFromStepic(url.toString(), StepicWrappers.CoursesContainer.class);
+    addAvailableCourses(result, coursesContainer);
+    return coursesContainer.meta.containsKey("has_next") && coursesContainer.meta.get("has_next") == Boolean.TRUE;
+  }
+
+  static void addAvailableCourses(List<CourseInfo> result, StepicWrappers.CoursesContainer coursesContainer) throws IOException {
     final List<CourseInfo> courseInfos = coursesContainer.courses;
     for (CourseInfo info : courseInfos) {
-      final String courseType = info.getType();
-      if (!info.isAdaptive() && StringUtil.isEmptyOrSpaces(courseType)) continue;
-      final List<String> typeLanguage = StringUtil.split(courseType, " ");
-      if (info.isAdaptive() || (typeLanguage.size() == 2 && PYCHARM_PREFIX.equals(typeLanguage.get(0)))) {
+      if (!info.isAdaptive() && StringUtil.isEmptyOrSpaces(info.getType())) continue;
+      if (canBeOpened(info)) {
         for (Integer instructor : info.instructors) {
           final StepicUser author = EduStepicClient.getFromStepic(EduStepicNames.USERS + "/" + String.valueOf(instructor),
-                                                  StepicWrappers.AuthorWrapper.class).users.get(0);
+                                                                  StepicWrappers.AuthorWrapper.class).users.get(0);
           info.addAuthor(author);
         }
-        
+
         if (info.isAdaptive()) {
           info.setDescription("This is a Stepik Adaptive course.\n\n" + info.getDescription() + ADAPTIVE_NOTE);
         }
-        
+
         result.add(info);
       }
     }
-    return coursesContainer.meta.containsKey("has_next") && coursesContainer.meta.get("has_next") == Boolean.TRUE;
+  }
+
+  static boolean canBeOpened(CourseInfo courseInfo) {
+    if (courseInfo.isAdaptive) {
+      return true;
+    }
+    String courseType = courseInfo.getType();
+    final List<String> typeLanguage = StringUtil.split(courseType, " ");
+    String prefix = typeLanguage.get(0);
+    if (typeLanguage.size() != 2 || !prefix.startsWith(PYCHARM_PREFIX)) {
+      return false;
+    }
+    String versionString = prefix.substring(PYCHARM_PREFIX.length());
+    if (versionString.isEmpty()) {
+      return true;
+    }
+    try {
+      Integer version = Integer.valueOf(versionString);
+      return version <= CURRENT_VERSION;
+    } catch (NumberFormatException e) {
+      LOG.info("Wrong version format", e);
+      return false;
+    }
   }
 
   public static Course getCourse(@NotNull final Project project, @NotNull final CourseInfo info) {
@@ -158,7 +184,8 @@ public class EduStepicConnector {
     if (!course.isAdaptive()) {
       String courseType = info.getType();
       course.setName(info.getName());
-      course.setLanguage(courseType.substring(PYCHARM_PREFIX.length() + 1));
+      String language = courseType.split(" ")[1];
+      course.setLanguage(language);
       try {
         for (Integer section : info.sections) {
           course.addLessons(getLessons(section));
@@ -213,11 +240,11 @@ public class EduStepicConnector {
   private static void createTask(Lesson lesson, Integer stepicId) throws IOException {
     final StepicWrappers.StepSource step = getStep(stepicId);
     final StepicWrappers.Step block = step.block;
-    if (!block.name.equals(PYCHARM_PREFIX)) return;
+    if (!block.name.startsWith(PYCHARM_PREFIX)) return;
     final Task task = new Task();
     task.setStepId(stepicId);
     task.setUpdateDate(step.update_date);
-    task.setName(block.options != null ? block.options.title : PYCHARM_PREFIX);
+    task.setName(block.options != null ? block.options.title : (PYCHARM_PREFIX + CURRENT_VERSION));
     task.setText(block.text);
     for (StepicWrappers.TestFileWrapper wrapper : block.options.test) {
       task.addTestsTexts(wrapper.name, wrapper.text);
index edae1ae80acb450411bf6563bb2a2901b6207224..b357190931e0b66a7f399dd9191505353c8789f6 100644 (file)
@@ -1,6 +1,7 @@
 package com.jetbrains.edu.learning.stepic;
 
 import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
@@ -51,9 +52,14 @@ public class StepicWrappers {
     @Expose Integer executionMemoryLimit;
     @Expose Integer executionTimeLimit;
     @Expose CodeTemplatesWrapper codeTemplates;
+    @SerializedName("format_version")
+    @Expose public int formatVersion = 2;
+    @SerializedName("last_subtask_index")
+    @Expose int lastSubtaskIndex = 0;
 
     public static StepOptions fromTask(final Project project, @NotNull final Task task) {
       final StepOptions source = new StepOptions();
+      task.setLastSubtaskIndex(source.lastSubtaskIndex);
       setTests(task, source, project);
       source.files = new ArrayList<>();
       source.title = task.getName();
diff --git a/python/educational-core/student/testData/stepic/1.json b/python/educational-core/student/testData/stepic/1.json
new file mode 100644 (file)
index 0000000..9a40915
--- /dev/null
@@ -0,0 +1,72 @@
+{
+  "meta": {
+    "page": 1,
+    "has_next": false,
+    "has_previous": false
+  },
+  "steps": [
+    {
+      "id": 98626,
+      "lesson": 13416,
+      "position": 1,
+      "status": "ready",
+      "block": {
+        "name": "pycharm",
+        "text": "\nTraditionally the first program you write in any programming language is <code>\"Hello World!\"</code>.\n<br><br>\nIntroduce yourself to the World.\n<br>\n",
+        "video": null,
+        "animation": null,
+        "options": {
+          "test": [
+            {
+              "text": "from test_helper import run_common_tests, failed, passed, get_answer_placeholders\n\n\ndef test_ASCII():\n    windows = get_answer_placeholders()\n    for window in windows:\n        all_ascii = all(ord(c) < 128 for c in window)\n        if not all_ascii:\n            failed(\"Please use only English characters this time.\")\n            return\n    passed()\n\n\ndef test_is_alpha():\n    window = get_answer_placeholders()[0]\n    is_multiline = window.find(\"\\n\")\n    if is_multiline != -1:\n        window = window[:is_multiline-1]\n    splitted = window.split()\n    for s in splitted:\n        if not s.isalpha():\n            failed(\"Please use only English characters this time.\")\n            return\n\n    passed()\n\n\nif __name__ == '__main__':\n    test_ASCII()\n    run_common_tests(\"You should enter your name\")\n    test_is_alpha()\n\n\n",
+              "name": "tests.py"
+            }
+          ],
+          "files": [
+            {
+              "placeholders": [
+                {
+                  "hint": "[\"Type your name here.\"]",
+                  "start": 32,
+                  "length": 14,
+                  "possible_answer": "Liana",
+                  "line": -1
+                }
+              ],
+              "text": "print(\"Hello, world! My name is type your name\")\n",
+              "name": "hello_world.py"
+            }
+          ],
+          "title": "Our first program"
+        },
+        "subtitle_files": []
+      },
+      "actions": {
+        "submit": "#"
+      },
+      "progress": "77-98626",
+      "subscriptions": [
+        "31-77-98626",
+        "30-77-98626"
+      ],
+      "instruction": null,
+      "session": null,
+      "instruction_type": null,
+      "viewed_by": 46,
+      "passed_by": 2433,
+      "correct_ratio": 0.7805243445692884,
+      "worth": null,
+      "is_solutions_unlocked": false,
+      "solutions_unlocked_attempts": 3,
+      "has_submissions_restrictions": false,
+      "max_submissions_count": 3,
+      "create_date": "2016-04-11T15:25:39Z",
+      "update_date": "2016-08-03T11:05:13Z",
+      "discussions_count": 0,
+      "discussion_proxy": "77-98626-1",
+      "discussion_threads": [
+        "77-98626-1"
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/python/educational-core/student/testData/stepic/2.json b/python/educational-core/student/testData/stepic/2.json
new file mode 100644 (file)
index 0000000..facaedf
--- /dev/null
@@ -0,0 +1,78 @@
+{
+  "meta": {
+    "page": 1,
+    "has_next": false,
+    "has_previous": false
+  },
+  "steps": [
+    {
+      "id": 98626,
+      "lesson": 13416,
+      "position": 1,
+      "status": "ready",
+      "block": {
+        "name": "pycharm",
+        "text": "\nTraditionally the first program you write in any programming language is <code>\"Hello World!\"</code>.\n<br><br>\nIntroduce yourself to the World.\n<br>\n",
+        "video": null,
+        "animation": null,
+        "options": {
+          "test": [
+            {
+              "text": "from test_helper import run_common_tests, failed, passed, get_answer_placeholders\n\n\ndef test_ASCII():\n    windows = get_answer_placeholders()\n    for window in windows:\n        all_ascii = all(ord(c) < 128 for c in window)\n        if not all_ascii:\n            failed(\"Please use only English characters this time.\")\n            return\n    passed()\n\n\ndef test_is_alpha():\n    window = get_answer_placeholders()[0]\n    is_multiline = window.find(\"\\n\")\n    if is_multiline != -1:\n        window = window[:is_multiline-1]\n    splitted = window.split()\n    for s in splitted:\n        if not s.isalpha():\n            failed(\"Please use only English characters this time.\")\n            return\n\n    passed()\n\n\nif __name__ == '__main__':\n    test_ASCII()\n    run_common_tests(\"You should enter your name\")\n    test_is_alpha()\n\n\n",
+              "name": "tests.py"
+            }
+          ],
+          "files": [
+            {
+              "placeholders": [
+                {
+                  "hint": "[\"Type your name here.\"]",
+                  "offset": 32,
+                  "length": 14,
+                  "subtask_infos": {
+                    "0": {
+                      "hints": ["Type your name here."],
+                      "possible_answer": "Liana"
+                    }
+                  }
+                }
+              ],
+              "text": "print(\"Hello, world! My name is type your name\")\n",
+              "name": "hello_world.py"
+            }
+          ],
+          "title": "Our first program",
+          "format_version": 2,
+          "last_subtask_index": 0
+        },
+        "subtitle_files": []
+      },
+      "actions": {
+        "submit": "#"
+      },
+      "progress": "77-98626",
+      "subscriptions": [
+        "31-77-98626",
+        "30-77-98626"
+      ],
+      "instruction": null,
+      "session": null,
+      "instruction_type": null,
+      "viewed_by": 46,
+      "passed_by": 2433,
+      "correct_ratio": 0.7805243445692884,
+      "worth": null,
+      "is_solutions_unlocked": false,
+      "solutions_unlocked_attempts": 3,
+      "has_submissions_restrictions": false,
+      "max_submissions_count": 3,
+      "create_date": "2016-04-11T15:25:39Z",
+      "update_date": "2016-08-03T11:05:13Z",
+      "discussions_count": 0,
+      "discussion_proxy": "77-98626-1",
+      "discussion_threads": [
+        "77-98626-1"
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/python/educational-core/student/testData/stepic/3.json b/python/educational-core/student/testData/stepic/3.json
new file mode 100644 (file)
index 0000000..e8ac788
--- /dev/null
@@ -0,0 +1,82 @@
+{
+  "meta": {
+    "page": 1,
+    "has_next": false,
+    "has_previous": false
+  },
+  "steps": [
+    {
+      "id": 98626,
+      "lesson": 13416,
+      "position": 1,
+      "status": "ready",
+      "block": {
+        "name": "pycharm",
+        "text": "\nTraditionally the first program you write in any programming language is <code>\"Hello World!\"</code>.\n<br><br>\nIntroduce yourself to the World.\n<br>\n",
+        "video": null,
+        "animation": null,
+        "options": {
+          "test": [
+            {
+              "text": "from test_helper import run_common_tests, failed, passed, get_answer_placeholders\n\n\ndef test_ASCII():\n    windows = get_answer_placeholders()\n    for window in windows:\n        all_ascii = all(ord(c) < 128 for c in window)\n        if not all_ascii:\n            failed(\"Please use only English characters this time.\")\n            return\n    passed()\n\n\ndef test_is_alpha():\n    window = get_answer_placeholders()[0]\n    is_multiline = window.find(\"\\n\")\n    if is_multiline != -1:\n        window = window[:is_multiline-1]\n    splitted = window.split()\n    for s in splitted:\n        if not s.isalpha():\n            failed(\"Please use only English characters this time.\")\n            return\n\n    passed()\n\n\nif __name__ == '__main__':\n    test_ASCII()\n    run_common_tests(\"You should enter your name\")\n    test_is_alpha()\n\n\n",
+              "name": "tests.py"
+            }
+          ],
+          "files": [
+            {
+              "placeholders": [
+                {
+                  "hint": "[\"Type your name here.\"]",
+                  "offset": 32,
+                  "length": 14,
+                  "subtask_infos": {
+                    "0": {
+                      "hints": ["Type your name here."],
+                      "possible_answer": "Liana"
+                    },
+                    "1": {
+                      "hints":[],
+                      "possible_answer": "miss X"
+                    }
+                  }
+                }
+              ],
+              "text": "print(\"Hello, world! My name is type your name\")\n",
+              "name": "hello_world.py"
+            }
+          ],
+          "title": "Our first program",
+          "format_version": 2,
+          "last_subtask_index": 1
+        },
+        "subtitle_files": []
+      },
+      "actions": {
+        "submit": "#"
+      },
+      "progress": "77-98626",
+      "subscriptions": [
+        "31-77-98626",
+        "30-77-98626"
+      ],
+      "instruction": null,
+      "session": null,
+      "instruction_type": null,
+      "viewed_by": 46,
+      "passed_by": 2433,
+      "correct_ratio": 0.7805243445692884,
+      "worth": null,
+      "is_solutions_unlocked": false,
+      "solutions_unlocked_attempts": 3,
+      "has_submissions_restrictions": false,
+      "max_submissions_count": 3,
+      "create_date": "2016-04-11T15:25:39Z",
+      "update_date": "2016-08-03T11:05:13Z",
+      "discussions_count": 0,
+      "discussion_proxy": "77-98626-1",
+      "discussion_threads": [
+        "77-98626-1"
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/python/educational-core/student/testData/stepic/courses.json b/python/educational-core/student/testData/stepic/courses.json
new file mode 100644 (file)
index 0000000..e4ab452
--- /dev/null
@@ -0,0 +1,330 @@
+{
+  "meta": {
+    "page": 1,
+    "has_next": false,
+    "has_previous": false
+  },
+  "courses": [
+    {
+      "id": 568,
+      "summary": "Adaptive problem set to learn Python. \r\nJoin the course and you can try out the first prototype of the adaptive engine! ",
+      "workload": "",
+      "cover": "/media/covers/py-en.png",
+      "intro": "",
+      "course_format": "Adaptive course with only programming assignments for Python",
+      "target_audience": "",
+      "certificate_footer": null,
+      "certificate_cover_org": null,
+      "is_certificate_auto_issued": false,
+      "certificate_regular_threshold": null,
+      "certificate_distinction_threshold": null,
+      "instructors": [],
+      "certificate": "",
+      "requirements": "<p>\r\n\r\nThe desire to test your knowledge of the Python language.&nbsp;<br></p>",
+      "description": "<p>The course consists of few hundreds of programming assignments for Python, ranging from basics up to complex topics.</p>\n<p>\n\nRight now Stepik.org is developing an adaptive learning engine which chooses content for each learner individually \u2013 by his/her level and knowledge gaps. In this course you can try out the first prototype of this engine\n\n</p><p>Learn more \u2014 in the&nbsp;<a href=\"https://stepic.zendesk.com/hc/en-us/articles/210007905\">https://stepic.zendesk.com/hc/en-us/articles/210007905</a>\ufeff.</p>",
+      "sections": [
+        1785,
+        1786
+      ],
+      "total_units": 347,
+      "enrollment": null,
+      "is_favorite": false,
+      "actions": {},
+      "progress": null,
+      "certificate_link": null,
+      "certificate_regular_link": null,
+      "certificate_distinction_link": null,
+      "schedule_link": null,
+      "schedule_long_link": null,
+      "first_deadline": null,
+      "last_deadline": null,
+      "subscriptions": [
+        "31-78-568",
+        "30-78-568"
+      ],
+      "announcements": [],
+      "is_contest": false,
+      "is_adaptive": true,
+      "is_idea_compatible": true,
+      "last_step": "78-568",
+      "intro_video": null,
+      "social_providers": [],
+      "authors": [
+        17813950
+      ],
+      "tags": [
+        147
+      ],
+      "has_tutors": false,
+      "is_enabled": true,
+      "review_summary": 295,
+      "owner": 17813950,
+      "language": "en",
+      "is_featured": true,
+      "is_public": true,
+      "title": "Adaptive Python",
+      "slug": "Adaptive-Python-568",
+      "begin_date": null,
+      "end_date": null,
+      "soft_deadline": null,
+      "hard_deadline": null,
+      "grading_policy": "halved",
+      "begin_date_source": null,
+      "end_date_source": null,
+      "soft_deadline_source": null,
+      "hard_deadline_source": null,
+      "grading_policy_source": "halved",
+      "is_active": true,
+      "create_date": "2016-03-17T13:57:04Z",
+      "update_date": "2016-10-20T12:25:01Z",
+      "learners_group": null,
+      "testers_group": null,
+      "moderators_group": null,
+      "teachers_group": null,
+      "admins_group": null,
+      "discussions_count": 0,
+      "discussion_proxy": null,
+      "discussion_threads": []
+    },
+    {
+      "id": 238,
+      "summary": "Introduction course to Python",
+      "workload": "",
+      "cover": null,
+      "intro": "",
+      "course_format": "pycharm Python",
+      "target_audience": "",
+      "certificate_footer": null,
+      "certificate_cover_org": null,
+      "is_certificate_auto_issued": false,
+      "certificate_regular_threshold": 0,
+      "certificate_distinction_threshold": 0,
+      "instructors": [
+        1794841
+      ],
+      "certificate": "",
+      "requirements": "",
+      "description": "",
+      "sections": [
+        662
+      ],
+      "total_units": 10,
+      "enrollment": null,
+      "is_favorite": false,
+      "actions": {},
+      "progress": null,
+      "certificate_link": null,
+      "certificate_regular_link": null,
+      "certificate_distinction_link": null,
+      "schedule_link": null,
+      "schedule_long_link": null,
+      "first_deadline": null,
+      "last_deadline": null,
+      "subscriptions": [
+        "31-78-238",
+        "30-78-238"
+      ],
+      "announcements": [],
+      "is_contest": false,
+      "is_adaptive": false,
+      "is_idea_compatible": true,
+      "last_step": "78-238",
+      "intro_video": null,
+      "social_providers": [],
+      "authors": [
+        625832
+      ],
+      "tags": [],
+      "has_tutors": false,
+      "is_enabled": true,
+      "review_summary": 135,
+      "owner": 625832,
+      "language": "en",
+      "is_featured": false,
+      "is_public": true,
+      "title": "Introduction to Python",
+      "slug": "Introduction-to-Python-238",
+      "begin_date": null,
+      "end_date": null,
+      "soft_deadline": null,
+      "hard_deadline": null,
+      "grading_policy": "halved",
+      "begin_date_source": null,
+      "end_date_source": null,
+      "soft_deadline_source": null,
+      "hard_deadline_source": null,
+      "grading_policy_source": "halved",
+      "is_active": true,
+      "create_date": "2015-08-25T08:57:04Z",
+      "update_date": "2016-08-10T13:14:02Z",
+      "learners_group": null,
+      "testers_group": null,
+      "moderators_group": null,
+      "teachers_group": null,
+      "admins_group": null,
+      "discussions_count": 0,
+      "discussion_proxy": null,
+      "discussion_threads": []
+    },
+    {
+      "id": 163,
+      "summary": "Test PyCharm course in 2 format",
+      "workload": "",
+      "cover": null,
+      "intro": "",
+      "course_format": "pycharm2 Python",
+      "target_audience": "",
+      "certificate_footer": null,
+      "certificate_cover_org": null,
+      "is_certificate_auto_issued": false,
+      "certificate_regular_threshold": 0,
+      "certificate_distinction_threshold": 0,
+      "instructors": [
+        1777801
+      ],
+      "certificate": "",
+      "requirements": "",
+      "description": "",
+      "sections": [
+        460
+      ],
+      "total_units": 6,
+      "enrollment": null,
+      "is_favorite": false,
+      "actions": {},
+      "progress": null,
+      "certificate_link": null,
+      "certificate_regular_link": null,
+      "certificate_distinction_link": null,
+      "schedule_link": null,
+      "schedule_long_link": null,
+      "first_deadline": null,
+      "last_deadline": null,
+      "subscriptions": [
+        "31-78-163",
+        "30-78-163"
+      ],
+      "announcements": [],
+      "is_contest": false,
+      "is_adaptive": false,
+      "is_idea_compatible": true,
+      "last_step": "78-163",
+      "intro_video": null,
+      "social_providers": [],
+      "authors": [
+        625832
+      ],
+      "tags": [],
+      "has_tutors": false,
+      "is_enabled": true,
+      "review_summary": 113,
+      "owner": 625832,
+      "language": "en",
+      "is_featured": false,
+      "is_public": true,
+      "title": "format2",
+      "slug": "Introduction-to-Classic-Ciphers-163",
+      "begin_date": null,
+      "end_date": null,
+      "soft_deadline": null,
+      "hard_deadline": null,
+      "grading_policy": "halved",
+      "begin_date_source": null,
+      "end_date_source": null,
+      "soft_deadline_source": null,
+      "hard_deadline_source": null,
+      "grading_policy_source": "halved",
+      "is_active": true,
+      "create_date": "2015-07-01T17:07:48Z",
+      "update_date": "2016-08-05T09:26:50Z",
+      "learners_group": null,
+      "testers_group": null,
+      "moderators_group": null,
+      "teachers_group": null,
+      "admins_group": null,
+      "discussions_count": 0,
+      "discussion_proxy": null,
+      "discussion_threads": []
+    },
+    {
+      "id": 162,
+      "summary": "Test Pycharm future format",
+      "workload": "",
+      "cover": null,
+      "intro": "",
+      "course_format": "pycharm3 Python",
+      "target_audience": "",
+      "certificate_footer": null,
+      "certificate_cover_org": null,
+      "is_certificate_auto_issued": false,
+      "certificate_regular_threshold": 0,
+      "certificate_distinction_threshold": 0,
+      "instructors": [],
+      "certificate": "",
+      "requirements": "",
+      "description": "",
+      "sections": [
+        459,
+        2510
+      ],
+      "total_units": 9,
+      "enrollment": null,
+      "is_favorite": false,
+      "actions": {},
+      "progress": null,
+      "certificate_link": null,
+      "certificate_regular_link": null,
+      "certificate_distinction_link": null,
+      "schedule_link": null,
+      "schedule_long_link": null,
+      "first_deadline": null,
+      "last_deadline": null,
+      "subscriptions": [
+        "31-78-162",
+        "30-78-162"
+      ],
+      "announcements": [],
+      "is_contest": false,
+      "is_adaptive": false,
+      "is_idea_compatible": true,
+      "last_step": "78-162",
+      "intro_video": null,
+      "social_providers": [],
+      "authors": [
+        625832
+      ],
+      "tags": [],
+      "has_tutors": false,
+      "is_enabled": true,
+      "review_summary": 112,
+      "owner": 625832,
+      "language": "en",
+      "is_featured": false,
+      "is_public": true,
+      "title": "format3",
+      "slug": "Logging-in-Python-162",
+      "begin_date": null,
+      "end_date": null,
+      "soft_deadline": null,
+      "hard_deadline": null,
+      "grading_policy": "halved",
+      "begin_date_source": null,
+      "end_date_source": null,
+      "soft_deadline_source": null,
+      "hard_deadline_source": null,
+      "grading_policy_source": "halved",
+      "is_active": true,
+      "create_date": "2015-07-01T16:21:09Z",
+      "update_date": "2016-09-07T15:22:29Z",
+      "learners_group": null,
+      "testers_group": null,
+      "moderators_group": null,
+      "teachers_group": null,
+      "admins_group": null,
+      "discussions_count": 0,
+      "discussion_proxy": null,
+      "discussion_threads": []
+    }
+  ],
+  "enrollments": []
+}
\ No newline at end of file
diff --git a/python/educational-core/student/testSrc/com/jetbrains/edu/learning/stepic/StudyStepicFormatTest.java b/python/educational-core/student/testSrc/com/jetbrains/edu/learning/stepic/StudyStepicFormatTest.java
new file mode 100644 (file)
index 0000000..3133039
--- /dev/null
@@ -0,0 +1,72 @@
+package com.jetbrains.edu.learning.stepic;
+
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.testFramework.PlatformTestUtil;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.edu.learning.courseFormat.AnswerPlaceholder;
+import com.jetbrains.edu.learning.courseFormat.AnswerPlaceholderSubtaskInfo;
+import com.jetbrains.edu.learning.courseFormat.TaskFile;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+
+public class StudyStepicFormatTest {
+
+  @Test
+  public void fromFirstVersion() throws IOException {
+    doStepOptionsCreationTest("1.json");
+  }
+
+  @Test
+  public void fromSecondVersion() throws IOException {
+    doStepOptionsCreationTest("2.json");
+  }
+
+  @Test
+  public void testWithSubtasks() throws IOException {
+    StepicWrappers.StepOptions stepOptions = doStepOptionsCreationTest("3.json");
+    assertEquals(1, stepOptions.lastSubtaskIndex);
+  }
+
+
+  private static StepicWrappers.StepOptions doStepOptionsCreationTest(String fileName) throws IOException {
+    String responseString =
+      FileUtil.loadFile(new File(getTestDataPath(), fileName));
+    StepicWrappers.StepSource stepSource =
+      EduStepicClient.deserializeStepicResponse(StepicWrappers.StepContainer.class, responseString).steps.get(0);
+    StepicWrappers.StepOptions options = stepSource.block.options;
+    List<TaskFile> files = options.files;
+    assertTrue("Wrong number of task files", files.size() == 1);
+    List<AnswerPlaceholder> placeholders = files.get(0).getAnswerPlaceholders();
+    assertTrue("Wrong number of placeholders", placeholders.size() == 1);
+    Map<Integer, AnswerPlaceholderSubtaskInfo> infos = placeholders.get(0).getSubtaskInfos();
+    assertNotNull(infos);
+    assertEquals(Collections.singletonList("Type your name here."), infos.get(0).getHints());
+    assertEquals("Liana", infos.get(0).getPossibleAnswer());
+    return options;
+  }
+
+  @Test
+  public void testAvailableCourses() throws IOException {
+    String responseString = FileUtil.loadFile(new File(getTestDataPath(), "courses.json"));
+    StepicWrappers.CoursesContainer container =
+      EduStepicClient.deserializeStepicResponse(StepicWrappers.CoursesContainer.class, responseString);
+    assertNotNull(container.courses);
+    assertTrue("Incorrect number of courses", container.courses.size() == 4);
+    List<CourseInfo> filtered = ContainerUtil.filter(container.courses, info -> EduStepicConnector.canBeOpened(info));
+    assertEquals(ContainerUtil.newArrayList("Adaptive Python", "Introduction to Python", "format2"), ContainerUtil.map(filtered, CourseInfo::getName));
+  }
+
+  @NotNull
+  private static String getTestDataPath() {
+    return FileUtil.join(PlatformTestUtil.getCommunityPath(), "python/educational-core/student/testData/stepic");
+  }
+}