From 8eadfc678baaee6cb332df0c05dcd0d646bfc1cd Mon Sep 17 00:00:00 2001 From: Ekaterina Tuzova Date: Thu, 30 Mar 2017 16:40:46 +0300 Subject: [PATCH] get rid of courseInfo extracted remoteCourse for stepik specific fields --- .../actions/CCCreateCourseArchive.java | 2 +- .../actions/CCFromCourseArchive.java | 2 +- .../actions/CCGetCourseFromStepic.java | 4 +- .../coursecreator/actions/CCPushCourse.java | 14 +- .../coursecreator/actions/CCPushLesson.java | 11 +- .../edu/coursecreator/actions/CCPushTask.java | 7 +- .../stepik/CCStepicConnector.java | 16 +- .../edu/learning/StudyProjectComponent.java | 6 +- .../edu/learning/StudySerializationUtils.java | 25 ++- .../edu/learning/StudyTaskManager.java | 44 ++++- .../edu/learning/courseFormat/Course.java | 75 ++------ .../edu/learning/courseFormat/CourseInfo.java | 175 ------------------ .../learning/courseFormat/RemoteCourse.java | 98 ++++++++++ .../StudyProjectGenerator.java | 68 +++---- .../EduCustomCourseModuleBuilder.java | 11 +- .../localCourses/EduLocalCoursePanel.java | 4 +- .../stepic/EduAdaptiveStepicConnector.java | 21 +-- .../edu/learning/stepic/EduStepicClient.java | 2 + .../learning/stepic/EduStepicConnector.java | 55 +++--- .../edu/learning/stepic/EduStepicUpdater.java | 16 +- .../edu/learning/stepic/StepicWrappers.java | 8 +- .../edu/learning/ui/StudyNewProjectPanel.java | 55 +++--- .../stepic/StudyStepicFormatTest.java | 6 +- .../courses/Introduction to Python.zip | Bin 18083 -> 19681 bytes .../CreateFromArchiveProjectStep.java | 13 +- .../PyStudyDirectoryProjectGenerator.java | 20 +- .../PyStudyIntroductionCourseAction.java | 4 +- 27 files changed, 340 insertions(+), 422 deletions(-) delete mode 100644 python/educational-core/src/com/jetbrains/edu/learning/courseFormat/CourseInfo.java create mode 100644 python/educational-core/src/com/jetbrains/edu/learning/courseFormat/RemoteCourse.java diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCCreateCourseArchive.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCCreateCourseArchive.java index 8703e3612117..cd09f521b621 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCCreateCourseArchive.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCCreateCourseArchive.java @@ -220,7 +220,7 @@ public class CCCreateCourseArchive extends DumbAwareAction { @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") private static void generateJson(VirtualFile parentDir, Course course) { final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation(). - registerTypeAdapter(Task.class, new StudySerializationUtils.Json.TaskSerializer()).create(); + registerTypeAdapter(Task.class, new StudySerializationUtils.Json.TaskAdapter()).create(); final String json = gson.toJson(course); final File courseJson = new File(parentDir.getPath(), EduNames.COURSE_META_FILE); OutputStreamWriter outputStreamWriter = null; diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCFromCourseArchive.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCFromCourseArchive.java index 11334cf12bdf..aa26e49c5bcc 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCFromCourseArchive.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCFromCourseArchive.java @@ -70,7 +70,7 @@ public class CCFromCourseArchive extends DumbAwareAction { reader = new InputStreamReader(new FileInputStream(courseMetaFile)); Gson gson = new GsonBuilder() .registerTypeAdapter(Course.class, new StudySerializationUtils.Json.CourseTypeAdapter(courseMetaFile)) - .registerTypeAdapter(Task.class, new StudySerializationUtils.Json.TaskDeserializer()) + .registerTypeAdapter(Task.class, new StudySerializationUtils.Json.TaskAdapter()) .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create(); Course course = gson.fromJson(reader, Course.class); diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCGetCourseFromStepic.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCGetCourseFromStepic.java index 957e47954511..33a0417fcc42 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCGetCourseFromStepic.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCGetCourseFromStepic.java @@ -20,8 +20,8 @@ import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; import com.jetbrains.edu.learning.courseFormat.Lesson; +import com.jetbrains.edu.learning.courseFormat.RemoteCourse; import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseGeneration.StudyGenerator; @@ -58,7 +58,7 @@ public class CCGetCourseFromStepic extends DumbAwareAction { private static void createCourse(Project project, String courseId) { final VirtualFile baseDir = project.getBaseDir(); - final CourseInfo info = CCStepicConnector.getCourseInfo(courseId); + final RemoteCourse info = CCStepicConnector.getCourseInfo(courseId); if (info == null) return; final Course course = EduStepicConnector.getCourse(project, info); diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushCourse.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushCourse.java index 156d2f477c4a..24807506c8a6 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushCourse.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushCourse.java @@ -14,8 +14,8 @@ import com.jetbrains.edu.coursecreator.CCUtils; import com.jetbrains.edu.coursecreator.stepik.CCStepicConnector; import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; import com.jetbrains.edu.learning.courseFormat.Lesson; +import com.jetbrains.edu.learning.courseFormat.RemoteCourse; import com.jetbrains.edu.learning.statistics.EduUsagesCollector; import org.jetbrains.annotations.NotNull; @@ -33,11 +33,8 @@ public class CCPushCourse extends DumbAwareAction { presentation.setEnabledAndVisible(project != null && CCUtils.isCourseCreator(project)); if (project != null) { final Course course = StudyTaskManager.getInstance(project).getCourse(); - if (course != null) { - final int id = course.getId(); - if (id > 0) { - presentation.setText("Update Course on Stepik"); - } + if (course instanceof RemoteCourse) { + presentation.setText("Update Course on Stepik"); } } } @@ -53,7 +50,7 @@ public class CCPushCourse extends DumbAwareAction { if (course == null) { return; } - if (course.getId() > 0) { + if (course instanceof RemoteCourse) { ProgressManager.getInstance().run(new Task.Modal(project, "Updating Course", true) { @Override public void run(@NotNull ProgressIndicator indicator) { @@ -62,10 +59,9 @@ public class CCPushCourse extends DumbAwareAction { CCStepicConnector.updateLesson(project, lesson); } else { - final CourseInfo info = CourseInfo.fromCourse(course); final int lessonId = CCStepicConnector.postLesson(project, lesson); if (lessonId != -1) { - final List sections = info.getSections(); + final List sections = ((RemoteCourse)course).getSections(); final Integer sectionId = sections.get(sections.size() - 1); CCStepicConnector.postUnit(lessonId, lesson.getIndex(), sectionId); } diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushLesson.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushLesson.java index 1c203cf6e85c..234a136b4091 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushLesson.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushLesson.java @@ -15,8 +15,8 @@ import com.jetbrains.edu.coursecreator.CCUtils; import com.jetbrains.edu.coursecreator.stepik.CCStepicConnector; import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; import com.jetbrains.edu.learning.courseFormat.Lesson; +import com.jetbrains.edu.learning.courseFormat.RemoteCourse; import com.jetbrains.edu.learning.stepic.EduStepicNames; import org.jetbrains.annotations.NotNull; @@ -36,7 +36,7 @@ public class CCPushLesson extends DumbAwareAction { return; } final Course course = StudyTaskManager.getInstance(project).getCourse(); - if (course == null) { + if (course == null || !(course instanceof RemoteCourse)) { return; } if (!course.getCourseMode().equals(CCUtils.COURSE_MODE)) return; @@ -45,7 +45,7 @@ public class CCPushLesson extends DumbAwareAction { return; } final Lesson lesson = course.getLesson(lessonDir.getName()); - if (lesson != null && course.getId() > 0) { + if (lesson != null && ((RemoteCourse)course).getId() > 0) { e.getPresentation().setEnabledAndVisible(true); if (lesson.getId() <= 0) { e.getPresentation().setText("Upload Lesson to Stepik"); @@ -61,7 +61,7 @@ public class CCPushLesson extends DumbAwareAction { return; } final Course course = StudyTaskManager.getInstance(project).getCourse(); - if (course == null) { + if (course == null || !(course instanceof RemoteCourse)) { return; } PsiDirectory lessonDir = DirectoryChooserUtil.getOrChooseDirectory(view); @@ -80,9 +80,8 @@ public class CCPushLesson extends DumbAwareAction { CCStepicConnector.updateLesson(project, lesson); } else { - final CourseInfo info = CourseInfo.fromCourse(course); final int lessonId = CCStepicConnector.postLesson(project, lesson); - final List sections = info.getSections(); + final List sections = ((RemoteCourse)course).getSections(); final Integer sectionId = sections.get(sections.size()-1); CCStepicConnector.postUnit(lessonId, lesson.getIndex(), sectionId); } diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushTask.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushTask.java index 7346b1286cf6..725c55e4ba05 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushTask.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCPushTask.java @@ -12,11 +12,12 @@ import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiDirectory; import com.jetbrains.edu.coursecreator.CCUtils; +import com.jetbrains.edu.coursecreator.stepik.CCStepicConnector; import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.courseFormat.Lesson; -import com.jetbrains.edu.coursecreator.stepik.CCStepicConnector; +import com.jetbrains.edu.learning.courseFormat.RemoteCourse; import com.jetbrains.edu.learning.stepic.EduStepicNames; import org.jetbrains.annotations.NotNull; @@ -34,7 +35,7 @@ public class CCPushTask extends DumbAwareAction { return; } final Course course = StudyTaskManager.getInstance(project).getCourse(); - if (course == null) { + if (course == null || !(course instanceof RemoteCourse)) { return; } if (!course.getCourseMode().equals(CCUtils.COURSE_MODE)) return; @@ -45,7 +46,7 @@ public class CCPushTask extends DumbAwareAction { final PsiDirectory lessonDir = taskDir.getParentDirectory(); if (lessonDir == null) return; final Lesson lesson = course.getLesson(lessonDir.getName()); - if (lesson != null && lesson.getId() > 0 && course.getId() > 0) { + if (lesson != null && lesson.getId() > 0 && ((RemoteCourse)course).getId() > 0) { e.getPresentation().setEnabledAndVisible(true); final com.jetbrains.edu.learning.courseFormat.tasks.Task task = lesson.getTask(taskDir.getName()); if (task.getStepId() <= 0) { diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/stepik/CCStepicConnector.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/stepik/CCStepicConnector.java index d052e1bbbe51..c5ac73922cb9 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/stepik/CCStepicConnector.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/stepik/CCStepicConnector.java @@ -10,11 +10,12 @@ import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.jetbrains.edu.coursecreator.CCUtils; import com.jetbrains.edu.learning.StudySerializationUtils; +import com.jetbrains.edu.learning.StudyTaskManager; 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.CourseInfo; import com.jetbrains.edu.learning.courseFormat.Lesson; +import com.jetbrains.edu.learning.courseFormat.RemoteCourse; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.stepic.EduStepicAuthorizedClient; import com.jetbrains.edu.learning.stepic.EduStepicNames; @@ -43,7 +44,7 @@ public class CCStepicConnector { private CCStepicConnector() { } - public static CourseInfo getCourseInfo(String courseId) { + public static RemoteCourse getCourseInfo(String courseId) { final String url = EduStepicNames.COURSES + "/" + courseId; try { final StepicWrappers.CoursesContainer coursesContainer = @@ -103,8 +104,10 @@ public class CCStepicConnector { LOG.error("Failed to push " + responseString); return; } - final CourseInfo postedCourse = new Gson().fromJson(responseString, StepicWrappers.CoursesContainer.class).courses.get(0); - course.setId(postedCourse.getId()); + final RemoteCourse postedCourse = new Gson().fromJson(responseString, StepicWrappers.CoursesContainer.class).courses.get(0); + postedCourse.setLessons(course.getLessons(true)); + postedCourse.setAuthors(course.getAuthors()); + postedCourse.setCourseMode(CCUtils.COURSE_MODE); final int sectionId = postModule(postedCourse.getId(), 1, String.valueOf(postedCourse.getName())); int position = 1; for (Lesson lesson : course.getLessons()) { @@ -116,6 +119,7 @@ public class CCStepicConnector { position += 1; } ApplicationManager.getApplication().runReadAction(() -> postAdditionalFiles(course, project, postedCourse.getId())); + StudyTaskManager.getInstance(project).setCourse(postedCourse); } catch (IOException e) { LOG.error(e.getMessage()); @@ -241,7 +245,7 @@ public class CCStepicConnector { LOG.error("Failed to push " + responseString); return -1; } - final Lesson postedLesson = new Gson().fromJson(responseString, Course.class).getLessons().get(0); + final Lesson postedLesson = new Gson().fromJson(responseString, RemoteCourse.class).getLessons().get(0); for (Integer step : postedLesson.steps) { deleteTask(step); } @@ -279,7 +283,7 @@ public class CCStepicConnector { LOG.error("Failed to push " + responseString); return 0; } - final Lesson postedLesson = new Gson().fromJson(responseString, Course.class).getLessons().get(0); + final Lesson postedLesson = new Gson().fromJson(responseString, RemoteCourse.class).getLessons(true).get(0); lesson.setId(postedLesson.getId()); for (Task task : lesson.getTaskList()) { final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); diff --git a/python/educational-core/src/com/jetbrains/edu/learning/StudyProjectComponent.java b/python/educational-core/src/com/jetbrains/edu/learning/StudyProjectComponent.java index 335857dfc4a0..9a7b90f2c48b 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/StudyProjectComponent.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/StudyProjectComponent.java @@ -146,10 +146,8 @@ public class StudyProjectComponent implements ProjectComponent { private void updateCourse() { final Course currentCourse = StudyTaskManager.getInstance(myProject).getCourse(); - final CourseInfo info = CourseInfo.fromCourse(currentCourse); - if (info == null) return; - - final Course course = EduStepicConnector.getCourse(myProject, info); + if (currentCourse == null || !(currentCourse instanceof RemoteCourse)) return; + final Course course = EduStepicConnector.getCourse(myProject, (RemoteCourse)currentCourse); if (course == null) return; course.initCourse(false); diff --git a/python/educational-core/src/com/jetbrains/edu/learning/StudySerializationUtils.java b/python/educational-core/src/com/jetbrains/edu/learning/StudySerializationUtils.java index 1a8165ebcc16..dda547cebe20 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/StudySerializationUtils.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/StudySerializationUtils.java @@ -16,6 +16,7 @@ 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.RemoteCourse; import com.jetbrains.edu.learning.courseFormat.StudyStatus; import com.jetbrains.edu.learning.courseFormat.tasks.*; import com.jetbrains.edu.learning.stepic.EduStepicConnector; @@ -62,6 +63,7 @@ public class StudySerializationUtils { public static class Xml { public final static String COURSE_ELEMENT = "courseElement"; public final static String MAIN_ELEMENT = "StudyTaskManager"; + public final static String REMOTE_COURSE = "RemoteCourse"; public static final String MAP = "map"; public static final String KEY = "key"; public static final String VALUE = "value"; @@ -498,7 +500,7 @@ public class StudySerializationUtils { } } } - return new GsonBuilder().registerTypeAdapter(Task.class, new TaskDeserializer()).create().fromJson(json, Course.class); + return new GsonBuilder().registerTypeAdapter(Task.class, new TaskAdapter()).create().fromJson(json, Course.class); } private static void convertToAbsoluteOffset(Document document, JsonElement placeholder) { @@ -676,7 +678,23 @@ public class StudySerializationUtils { subtaskInfo.addProperty(POSSIBLE_ANSWER, placeholderObject.getAsJsonPrimitive(POSSIBLE_ANSWER).getAsString()); } - public static class TaskSerializer implements JsonSerializer { + public static class StepikCourseSerializer implements JsonSerializer, JsonDeserializer { + + @Override + public Course deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return null; + } + + @Override + public JsonElement serialize(Course src, Type typeOfSrc, JsonSerializationContext context) { + if (src instanceof RemoteCourse) { + context.serialize(src, RemoteCourse.class); + } + return context.serialize(src); + } + } + + public static class TaskAdapter implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(Task src, Type typeOfSrc, JsonSerializationContext context) { @@ -686,9 +704,6 @@ public class StudySerializationUtils { task.add(TASK_TYPE, new JsonPrimitive(src.getTaskType())); return task; } - } - - public static class TaskDeserializer implements JsonDeserializer { @Override public Task deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { diff --git a/python/educational-core/src/com/jetbrains/edu/learning/StudyTaskManager.java b/python/educational-core/src/com/jetbrains/edu/learning/StudyTaskManager.java index 8b981f97cc74..a6518bad3f00 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/StudyTaskManager.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/StudyTaskManager.java @@ -24,6 +24,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import static com.jetbrains.edu.learning.StudySerializationUtils.Xml.REMOTE_COURSE; + /** * Implementation of class which contains all the information * about study in context of current project @@ -115,10 +117,32 @@ public class StudyTaskManager implements PersistentStateComponent, Dumb if (myCourse == null) { return null; } + + return serialize(); + } + + @NotNull + private Element serialize() { Element el = new Element("taskManager"); - Element courseElement = new Element(StudySerializationUtils.Xml.MAIN_ELEMENT); - XmlSerializer.serializeInto(this, courseElement); - el.addContent(courseElement); + Element taskManagerElement = new Element(StudySerializationUtils.Xml.MAIN_ELEMENT); + XmlSerializer.serializeInto(this, taskManagerElement); + + if (myCourse instanceof RemoteCourse) { + try { + Element course = new Element(REMOTE_COURSE); + //noinspection RedundantCast + XmlSerializer.serializeInto((RemoteCourse)myCourse, course); + + final Element xmlCourse = StudySerializationUtils.Xml.getChildWithName(taskManagerElement, StudySerializationUtils.COURSE); + xmlCourse.removeContent(); + xmlCourse.addContent(course); + } + catch (StudySerializationUtils.StudyUnrecognizedFormatException e) { + LOG.error("Failed to serialize remote course"); + } + } + + el.addContent(taskManagerElement); return el; } @@ -143,7 +167,7 @@ public class StudyTaskManager implements PersistentStateComponent, Dumb //case 5: // state = StudySerializationUtils.Xml.convertToSixthVersion(state, myProject); } - XmlSerializer.deserializeInto(this, state.getChild(StudySerializationUtils.Xml.MAIN_ELEMENT)); + deserialize(state); VERSION = CURRENT_VERSION; if (myCourse != null) { myCourse.initCourse(true); @@ -154,6 +178,18 @@ public class StudyTaskManager implements PersistentStateComponent, Dumb } } + private void deserialize(Element state) throws StudySerializationUtils.StudyUnrecognizedFormatException { + final Element taskManagerElement = state.getChild(StudySerializationUtils.Xml.MAIN_ELEMENT); + XmlSerializer.deserializeInto(this, taskManagerElement); + final Element xmlCourse = StudySerializationUtils.Xml.getChildWithName(taskManagerElement, StudySerializationUtils.COURSE); + final Element remoteCourseElement = xmlCourse.getChild(REMOTE_COURSE); + if (remoteCourseElement != null) { + final RemoteCourse remoteCourse = new RemoteCourse(); + XmlSerializer.deserializeInto(remoteCourse, remoteCourseElement); + myCourse = remoteCourse; + } + } + public static StudyTaskManager getInstance(@NotNull final Project project) { return ServiceManager.getService(project, StudyTaskManager.class); } diff --git a/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/Course.java b/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/Course.java index 44976a6078dc..de5a18fd26f2 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/Course.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/Course.java @@ -8,32 +8,29 @@ import com.intellij.util.xmlb.XmlSerializer; import com.intellij.util.xmlb.annotations.Transient; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.core.EduUtils; -import com.jetbrains.edu.learning.courseFormat.tasks.Task; -import com.jetbrains.edu.learning.stepic.EduStepicConnector; import com.jetbrains.edu.learning.stepic.StepicUser; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.stream.Collectors; public class Course { - @Expose private List lessons = new ArrayList<>(); - @Expose private List authors = new ArrayList<>(); - @Expose private String description; - @Expose private String name; - @Expose private int id; - @Expose @SerializedName("update_date") private Date myUpdateDate; - @Expose private boolean isAdaptive = false; - @Expose @SerializedName("language") private String myLanguage = "Python"; + public static final Course INVALID_COURSE = new Course(); + + @Expose protected List lessons = new ArrayList<>(); + transient private List authors = new ArrayList<>(); + @Expose @SerializedName("summary") private String description; + @Expose @SerializedName("title") private String name; + + transient private String myLanguage = "Python"; //this field is used to distinguish ordinary and CheckIO projects, //"PyCharm" is used here for historical reasons private String courseType = EduNames.PYCHARM; - private String courseMode = EduNames.STUDY; //this field is used to distinguish study and course creator modes + protected String courseMode = EduNames.STUDY; //this field is used to distinguish study and course creator modes public Course() {} @@ -136,35 +133,10 @@ public class Course { } public boolean isUpToDate() { - if (id == 0) return true; - if (!EduNames.STUDY.equals(courseMode)) return true; - final Date date = EduStepicConnector.getCourseUpdateDate(id); - if (date == null) return true; - if (myUpdateDate == null) return true; - if (date.after(myUpdateDate)) return false; - for (Lesson lesson : lessons) { - if (!lesson.isUpToDate()) return false; - } return true; } - public void setUpdated() { - setUpdateDate(EduStepicConnector.getCourseUpdateDate(id)); - for (Lesson lesson : lessons) { - lesson.setUpdateDate(EduStepicConnector.getLessonUpdateDate(lesson.getId())); - for (Task task : lesson.getTaskList()) { - task.setUpdateDate(EduStepicConnector.getTaskUpdateDate(task.getStepId())); - } - } - } - - public void setUpdateDate(Date date) { - myUpdateDate = date; - } - - public Date getUpdateDate() { - return myUpdateDate; - } + public void setUpdated() {} public Language getLanguageById() { return Language.findLanguageByID(getLanguageID()); @@ -209,22 +181,6 @@ public class Course { this.courseType = courseType; } - public boolean isAdaptive() { - return isAdaptive; - } - - public void setAdaptive(boolean adaptive) { - isAdaptive = adaptive; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - public String getCourseMode() { return courseMode; } @@ -235,8 +191,17 @@ public class Course { public Course copy() { Element element = XmlSerializer.serialize(this); - Course copy = XmlSerializer.deserialize(element, Course.class); + Course copy = XmlSerializer.deserialize(element, getClass()); copy.initCourse(true); return copy; } + + public boolean isAdaptive() { + return false; + } + + @Override + public String toString() { + return getName(); + } } diff --git a/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/CourseInfo.java b/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/CourseInfo.java deleted file mode 100644 index 89fe65b8941e..000000000000 --- a/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/CourseInfo.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.jetbrains.edu.learning.courseFormat; - -import com.google.gson.annotations.SerializedName; -import com.jetbrains.edu.learning.stepic.EduStepicConnector; -import com.jetbrains.edu.learning.stepic.StepicUser; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -/** - * Implementation of class which contains information to be shawn in course description in tool window - * and when project is being created - */ -public class CourseInfo { - public static final CourseInfo INVALID_COURSE = new CourseInfo(); - - @SerializedName("title") private String myName; - int id; - boolean isAdaptive; - @SerializedName("is_public") boolean isPublic; - List sections; - List instructors = new ArrayList<>(); - - List myAuthors = new ArrayList<>(); - @SerializedName("summary") private String myDescription; - @SerializedName("course_format") private String myType = "pycharm" + EduStepicConnector.CURRENT_VERSION + " Python"; //course type in format "pycharm " - @Nullable private String username; - - @SerializedName("update_date") private Date updateDate; - @SerializedName("is_idea_compatible") private boolean isCompatible = true; - - public String getName() { - return myName; - } - - @NotNull - public List getAuthors() { - return myAuthors; - } - - public String getDescription() { - return myDescription; - } - - public String getType() { - return myType; - } - - @Override - public String toString() { - return getName(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CourseInfo that = (CourseInfo)o; - final String thatName = that.getName(); - final String thatDescription = that.getDescription(); - if (thatName == null && getName() == null && thatDescription == null && - getDescription() == null) { - return true; - } - if (thatName == null || thatDescription == null) { - return false; - } - return thatName.equals(getName()) - && thatDescription.equals(myDescription); - } - - @Override - public int hashCode() { - int result = getName() != null ? getName().hashCode() : 0; - result = 31 * result + (myDescription != null ? myDescription.hashCode() : 0); - return result; - } - - @Nullable - public String getUsername() { - return username; - } - - public void setUsername(@Nullable String username) { - this.username = username; - } - - public void setName(String name) { - myName = name; - } - - public void setAuthors(List authors) { - myAuthors = authors; - for (StepicUser author : authors) { - if (author.getId() > 0) { - instructors.add(author.getId()); - } - } - } - - public void addAuthor(StepicUser author) { - if (myAuthors == null) { - myAuthors = new ArrayList<>(); - } - myAuthors.add(author); - } - - public void setDescription(String description) { - myDescription = description; - } - - public void setType(String type) { - myType = type; - } - - public boolean isAdaptive() { - return isAdaptive; - } - - public void setAdaptive(boolean adaptive) { - isAdaptive = adaptive; - } - - public boolean isPublic() { - return isPublic; - } - - public void setPublic(boolean aPublic) { - isPublic = aPublic; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public Date getUpdateDate() { - return updateDate; - } - - public void setUpdateDate(Date updateDate) { - this.updateDate = updateDate; - } - - public static CourseInfo fromCourse(@Nullable final Course course) { - if (course == null) return null; - CourseInfo courseInfo = new CourseInfo(); - courseInfo.setName(course.getName()); - courseInfo.setDescription(course.getDescription()); - courseInfo.setAuthors(course.getAuthors()); - return courseInfo; - } - - public List getSections() { - return sections; - } - - public boolean isCompatible() { - return isCompatible; - } - - public void setCompatible(boolean compatible) { - isCompatible = compatible; - } - - public List getInstructors() { - return instructors; - } -} diff --git a/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/RemoteCourse.java b/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/RemoteCourse.java new file mode 100644 index 000000000000..01e764610e62 --- /dev/null +++ b/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/RemoteCourse.java @@ -0,0 +1,98 @@ +package com.jetbrains.edu.learning.courseFormat; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import com.jetbrains.edu.learning.core.EduNames; +import com.jetbrains.edu.learning.courseFormat.tasks.Task; +import com.jetbrains.edu.learning.stepic.EduStepicConnector; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class RemoteCourse extends Course { + //course type in format "pycharm " + @SerializedName("course_format") private String myType = "pycharm" + EduStepicConnector.CURRENT_VERSION + " Python"; + @SerializedName("is_idea_compatible") private boolean isCompatible = true; + List sections; + List instructors = new ArrayList<>(); + @Expose private int id; + @Expose @SerializedName("update_date") private Date myUpdateDate; + @Expose private boolean isAdaptive = false; + @SerializedName("is_public") boolean isPublic; + + public String getType() { + return myType; + } + + public List getSections() { + return sections; + } + + public void setSections(List sections) { + this.sections = sections; + } + + public void setInstructors(List instructors) { + this.instructors = instructors; + } + + public List getInstructors() { + return instructors; + } + + + public boolean isUpToDate() { + if (id == 0) return true; + if (!EduNames.STUDY.equals(courseMode)) return true; + final Date date = EduStepicConnector.getCourseUpdateDate(id); + if (date == null) return true; + if (myUpdateDate == null) return true; + if (date.after(myUpdateDate)) return false; + for (Lesson lesson : lessons) { + if (!lesson.isUpToDate()) return false; + } + return true; + } + + public void setUpdated() { + setUpdateDate(EduStepicConnector.getCourseUpdateDate(id)); + for (Lesson lesson : lessons) { + lesson.setUpdateDate(EduStepicConnector.getLessonUpdateDate(lesson.getId())); + for (Task task : lesson.getTaskList()) { + task.setUpdateDate(EduStepicConnector.getTaskUpdateDate(task.getStepId())); + } + } + } + + public void setUpdateDate(Date date) { + myUpdateDate = date; + } + + public Date getUpdateDate() { + return myUpdateDate; + } + + public boolean isAdaptive() { + return isAdaptive; + } + + public void setAdaptive(boolean adaptive) { + isAdaptive = adaptive; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public void copyCourseParameters(RemoteCourse course) { + setName(course.getName()); + + setUpdateDate(course.getUpdateDate()); + + } +} diff --git a/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java b/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java index 2eab0dba91b1..f57c22a5914a 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java @@ -23,7 +23,7 @@ import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.edu.learning.courseFormat.RemoteCourse; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.statistics.EduUsagesCollector; import com.jetbrains.edu.learning.stepic.EduStepicConnector; @@ -41,12 +41,11 @@ import static com.jetbrains.edu.learning.StudyUtils.execCancelable; public class StudyProjectGenerator { private static final Logger LOG = Logger.getInstance(StudyProjectGenerator.class.getName()); private final List myListeners = ContainerUtil.newArrayList(); - private List myCourses = new ArrayList<>(); - private List myLocalCourses = new ArrayList<>(); + private List myCourses = new ArrayList<>(); private List myEnrolledCoursesIds = new ArrayList<>(); - protected CourseInfo mySelectedCourseInfo; + protected Course mySelectedCourse; - public void setCourses(List courses) { + public void setCourses(List courses) { myCourses = courses; } @@ -64,12 +63,12 @@ public class StudyProjectGenerator { return myEnrolledCoursesIds; } - public void setSelectedCourse(@NotNull final CourseInfo courseName) { - mySelectedCourseInfo = courseName; + public void setSelectedCourse(@NotNull final Course course) { + mySelectedCourse = course; } - public CourseInfo getSelectedCourseInfo() { - return mySelectedCourseInfo; + public Course getSelectedCourse() { + return mySelectedCourse; } public void generateProject(@NotNull final Project project, @NotNull final VirtualFile baseDir) { @@ -94,22 +93,18 @@ public class StudyProjectGenerator { @Nullable public Course getCourse(@NotNull final Project project) { - final CourseInfo info = mySelectedCourseInfo; - for (Course course : myLocalCourses) { - if (course.getName().equals(info.getName())){ - course.initCourse(false); - return course; - } - + if (mySelectedCourse instanceof RemoteCourse) { + return getCourseFromStepic(project, (RemoteCourse)mySelectedCourse); } - return getCourseFromStepic(project); + mySelectedCourse.initCourse(false); + return mySelectedCourse; } - private Course getCourseFromStepic(@NotNull Project project) { + private static RemoteCourse getCourseFromStepic(@NotNull Project project, RemoteCourse selectedCourse) { return ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); return execCancelable(() -> { - final Course course = EduStepicConnector.getCourse(project, mySelectedCourseInfo); + final RemoteCourse course = EduStepicConnector.getCourse(project, selectedCourse); if (StudyUtils.isCourseValid(course)) { course.initCourse(false); } @@ -119,25 +114,25 @@ public class StudyProjectGenerator { } // Supposed to be called under progress - public List getCourses(boolean force) { + public List getCourses(boolean force) { if (force) { myCourses = execCancelable(() -> EduStepicConnector.getCourses(StepicUpdateSettings.getInstance().getUser())); } - if (myCourses == null || myCourses.isEmpty() || (myCourses.size() == 1 && myCourses.contains(CourseInfo.INVALID_COURSE))) { + if (myCourses == null || myCourses.isEmpty() || (myCourses.size() == 1 && myCourses.contains(Course.INVALID_COURSE))) { myCourses = Collections.singletonList(getBundledIntro()); } sortCourses(myCourses); return myCourses; } - public void sortCourses(List result) { + public void sortCourses(List result) { // sort courses so as to have non-adaptive courses in the beginning of the list Collections.sort(result, (c1, c2) -> { - if (mySelectedCourseInfo != null) { - if (mySelectedCourseInfo.equals(c1)) { + if (mySelectedCourse != null) { + if (mySelectedCourse.equals(c1)) { return -1; } - if (mySelectedCourseInfo.equals(c2)) { + if (mySelectedCourse.equals(c2)) { return 1; } } @@ -149,7 +144,7 @@ public class StudyProjectGenerator { } @NotNull - public List getCoursesUnderProgress(boolean force, @NotNull final String progressTitle, @NotNull final Project project) { + public List getCoursesUnderProgress(boolean force, @NotNull final String progressTitle, @NotNull final Project project) { try { return ProgressManager.getInstance() .runProcessWithProgressSynchronously(() -> { @@ -158,7 +153,7 @@ public class StudyProjectGenerator { }, progressTitle, true, project); } catch (RuntimeException e) { - return Collections.singletonList(CourseInfo.INVALID_COURSE); + return Collections.singletonList(Course.INVALID_COURSE); } } @@ -177,21 +172,21 @@ public class StudyProjectGenerator { } @Nullable - public CourseInfo getBundledIntro() { + public Course getBundledIntro() { final LanguageExtensionPoint[] extensions = Extensions.getExtensions(EduPluginConfigurator.EP_NAME, null); for (LanguageExtensionPoint extension : extensions) { final EduPluginConfigurator configurator = extension.getInstance(); final String path = configurator.getBundledCoursePath(); if (path != null) { - return getCourseInfo(path); + return getCourse(path); } } return null; } @Nullable - public CourseInfo addLocalCourse(String zipFilePath) { - final CourseInfo courseInfo = getCourseInfo(zipFilePath); + public Course addLocalCourse(String zipFilePath) { + final Course courseInfo = getCourse(zipFilePath); if (courseInfo != null) { myCourses.add(0, courseInfo); } @@ -199,22 +194,17 @@ public class StudyProjectGenerator { } @Nullable - public CourseInfo getCourseInfo(String zipFilePath) { + public Course getCourse(String zipFilePath) { try { final JBZipFile zipFile = new JBZipFile(zipFilePath); final JBZipEntry entry = zipFile.getEntry(EduNames.COURSE_META_FILE); byte[] bytes = entry.getData(); final String jsonText = new String(bytes, CharsetToolkit.UTF8_CHARSET); Gson gson = new GsonBuilder() - .registerTypeAdapter(Task.class, new StudySerializationUtils.Json.TaskDeserializer()) + .registerTypeAdapter(Task.class, new StudySerializationUtils.Json.TaskAdapter()) .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create(); - Course course = gson.fromJson(jsonText, Course.class); - final CourseInfo courseInfo = CourseInfo.fromCourse(course); - if (courseInfo != null) { - myLocalCourses.add(course); - } - return courseInfo; + return gson.fromJson(jsonText, Course.class); } catch (IOException e) { LOG.error("Failed to unzip course archive"); diff --git a/python/educational-core/src/com/jetbrains/edu/learning/intellij/localCourses/EduCustomCourseModuleBuilder.java b/python/educational-core/src/com/jetbrains/edu/learning/intellij/localCourses/EduCustomCourseModuleBuilder.java index c630d13e910b..c6020344e10e 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/intellij/localCourses/EduCustomCourseModuleBuilder.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/intellij/localCourses/EduCustomCourseModuleBuilder.java @@ -14,7 +14,6 @@ import com.intellij.openapi.util.InvalidDataException; import com.jetbrains.edu.learning.EduPluginConfigurator; import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.intellij.generation.EduProjectGenerator; import org.jdom.JDOMException; @@ -27,7 +26,7 @@ import java.io.IOException; class EduCustomCourseModuleBuilder extends JavaModuleBuilder { private static final Logger LOG = Logger.getInstance(EduCustomCourseModuleBuilder.class); private EduProjectGenerator myGenerator = new EduProjectGenerator(); - private CourseInfo mySelectedCourse; + private Course mySelectedCourse; @Nullable @Override @@ -36,8 +35,7 @@ class EduCustomCourseModuleBuilder extends JavaModuleBuilder { if (module == null) { return null; } - String type = mySelectedCourse.getType(); - String languageName = type.substring("pycharm ".length()); + String languageName = mySelectedCourse.getLanguageID(); Language language = Language.findLanguageByID(languageName); if (language == null) { return module; @@ -87,7 +85,7 @@ class EduCustomCourseModuleBuilder extends JavaModuleBuilder { return ProjectWizardStepFactory.getInstance().createJavaSettingsStep(settingsStep, this, Conditions.alwaysTrue()); } - void setSelectedCourse(CourseInfo selectedCourse) { + void setSelectedCourse(Course selectedCourse) { mySelectedCourse = selectedCourse; } @@ -97,8 +95,7 @@ class EduCustomCourseModuleBuilder extends JavaModuleBuilder { throws InvalidDataException, IOException, ModuleWithNameAlreadyExists, JDOMException, ConfigurationException { Module baseModule = super.createModule(moduleModel); if (mySelectedCourse != null) { - String type = mySelectedCourse.getType(); - String languageName = type.substring("pycharm ".length()); + String languageName = mySelectedCourse.getLanguageID(); Language language = Language.findLanguageByID(languageName); if (language != null) { EduPluginConfigurator configurator = EduPluginConfigurator.INSTANCE.forLanguage(language); diff --git a/python/educational-core/src/com/jetbrains/edu/learning/intellij/localCourses/EduLocalCoursePanel.java b/python/educational-core/src/com/jetbrains/edu/learning/intellij/localCourses/EduLocalCoursePanel.java index 35ac626e3230..790b2829865a 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/intellij/localCourses/EduLocalCoursePanel.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/intellij/localCourses/EduLocalCoursePanel.java @@ -2,7 +2,7 @@ package com.jetbrains.edu.learning.intellij.localCourses; import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; import com.intellij.openapi.ui.TextFieldWithBrowseButton; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.ui.StudyNewProjectPanel; @@ -25,7 +25,7 @@ public class EduLocalCoursePanel { @Override public void actionPerformed(ActionEvent actionEvent) { String coursePath = myCourseArchivePath.getText(); - CourseInfo courseInfo = generator.addLocalCourse(coursePath); + Course courseInfo = generator.addLocalCourse(coursePath); if (courseInfo != null) { myBuilder.setSelectedCourse(courseInfo); myGenerator.setSelectedCourse(courseInfo); diff --git a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java index 5bda69ff6384..3023bc9e6792 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java @@ -67,11 +67,12 @@ public class EduAdaptiveStepicConnector { private static final Logger LOG = Logger.getInstance(EduAdaptiveStepicConnector.class); private static final int CONNECTION_TIMEOUT = 60 * 1000; private static final String CODE_TASK_TYPE = "code"; + private static final String CODE_TASK_NAME = "code.py"; private static final String CHOICE_TYPE_TEXT = "choice"; private static final String TEXT_STEP_TYPE = "text"; @Nullable - public static Task getNextRecommendation(@NotNull Project project, @NotNull Course course) { + public static Task getNextRecommendation(@NotNull Project project, @NotNull RemoteCourse course) { try { final CloseableHttpClient client = EduStepicAuthorizedClient.getHttpClient(); if (client == null) { @@ -204,8 +205,8 @@ public class EduAdaptiveStepicConnector { private static void createMockTaskFile(@NotNull Task task, String editorText) { final TaskFile taskFile = new TaskFile(); taskFile.text = editorText; - taskFile.name = "code.py"; - task.taskFiles.put("code.py", taskFile); + taskFile.name = CODE_TASK_NAME; + task.taskFiles.put(CODE_TASK_NAME, taskFile); } @Nullable @@ -297,7 +298,7 @@ public class EduAdaptiveStepicConnector { int reaction) { final StudyEditor editor = StudyUtils.getSelectedStudyEditor(project); final Course course = StudyTaskManager.getInstance(project).getCourse(); - if (course == null || editor == null || editor.getTaskFile() == null) { + if (course == null || editor == null || editor.getTaskFile() == null || !(course instanceof RemoteCourse)) { return; } indicator.checkCanceled(); @@ -314,7 +315,7 @@ public class EduAdaptiveStepicConnector { String.valueOf(user.getId()), reaction); if (recommendationReaction) { indicator.checkCanceled(); - final Task task = getNextRecommendation(project, course); + final Task task = getNextRecommendation(project, (RemoteCourse)course); if (task != null) { task.initTask(lesson, false); @@ -438,11 +439,9 @@ public class EduAdaptiveStepicConnector { } } else { - final TaskFile taskFile = new TaskFile(); - taskFile.name = CODE_TASK_TYPE; final String templateForTask = getCodeTemplateForTask(project, task, step.options.codeTemplates); - taskFile.text = templateForTask == null ? "# write your answer here \n" : templateForTask; - task.taskFiles.put("code.py", taskFile); + String text = templateForTask == null ? "# write your answer here \n" : templateForTask; + createMockTaskFile(task, text); } return task; } @@ -662,11 +661,11 @@ public class EduAdaptiveStepicConnector { public static List getEnrolledCoursesIds(@NotNull StepicUser stepicUser) { try { final URI enrolledCoursesUri = new URIBuilder(EduStepicNames.COURSES).addParameter("enrolled", "true").build(); - final List courses = EduStepicAuthorizedClient.getFromStepic(enrolledCoursesUri.toString(), + final List courses = EduStepicAuthorizedClient.getFromStepic(enrolledCoursesUri.toString(), StepicWrappers.CoursesContainer.class, stepicUser).courses; final ArrayList ids = new ArrayList<>(); - for (CourseInfo course : courses) { + for (RemoteCourse course : courses) { ids.add(course.getId()); } return ids; diff --git a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicClient.java b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicClient.java index d81b6f8bd53c..d4ed1b144f15 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicClient.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicClient.java @@ -25,6 +25,7 @@ import org.jetbrains.annotations.NotNull; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import java.io.IOException; +import java.lang.reflect.Modifier; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.URI; @@ -70,6 +71,7 @@ public class EduStepicClient { static T deserializeStepicResponse(Class container, String responseString) { Gson gson = new GsonBuilder().registerTypeAdapter(StepicWrappers.StepOptions.class, new StudySerializationUtils.Json.StepicStepOptionsAdapter()). + excludeFieldsWithModifiers(Modifier.TRANSIENT). setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"). setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); return gson.fromJson(responseString, container); diff --git a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java index 1931f7a2e768..1372bf1f9693 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java @@ -12,8 +12,8 @@ import com.intellij.openapi.vfs.VirtualFile; import com.jetbrains.edu.learning.StudySettings; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; import com.jetbrains.edu.learning.courseFormat.Lesson; +import com.jetbrains.edu.learning.courseFormat.RemoteCourse; import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; @@ -65,9 +65,9 @@ public class EduStepicConnector { } @NotNull - public static List getCourses(@Nullable StepicUser user) { + public static List getCourses(@Nullable StepicUser user) { try { - List result = new ArrayList<>(); + List result = new ArrayList<>(); int pageNumber = 1; while (addCoursesFromStepic(user, result, pageNumber)) { pageNumber += 1; @@ -77,13 +77,13 @@ public class EduStepicConnector { catch (IOException e) { LOG.warn("Cannot load course list " + e.getMessage()); } - return Collections.singletonList(CourseInfo.INVALID_COURSE); + return Collections.emptyList(); } public static Date getCourseUpdateDate(final int courseId) { final String url = EduStepicNames.COURSES + "/" + courseId; try { - final List courses = EduStepicClient.getFromStepic(url, StepicWrappers.CoursesContainer.class).courses; + final List courses = EduStepicClient.getFromStepic(url, StepicWrappers.CoursesContainer.class).courses; if (!courses.isEmpty()) { return courses.get(0).getUpdateDate(); } @@ -125,7 +125,7 @@ public class EduStepicConnector { return null; } - private static boolean addCoursesFromStepic(@Nullable StepicUser user, List result, int pageNumber) throws IOException { + private static boolean addCoursesFromStepic(@Nullable StepicUser user, List result, int pageNumber) throws IOException { final URI url; try { url = new URIBuilder(EduStepicNames.COURSES).addParameter("is_idea_compatible", "true"). @@ -146,27 +146,28 @@ public class EduStepicConnector { return coursesContainer.meta.containsKey("has_next") && coursesContainer.meta.get("has_next") == Boolean.TRUE; } - static void addAvailableCourses(List result, StepicWrappers.CoursesContainer coursesContainer) throws IOException { - final List courseInfos = coursesContainer.courses; - for (CourseInfo info : courseInfos) { + static void addAvailableCourses(List result, StepicWrappers.CoursesContainer coursesContainer) throws IOException { + final List courses = coursesContainer.courses; + for (RemoteCourse info : courses) { if (!info.isAdaptive() && StringUtil.isEmptyOrSpaces(info.getType())) continue; if (canBeOpened(info)) { + final ArrayList authors = new ArrayList<>(); for (Integer instructor : info.getInstructors()) { final StepicUser author = EduStepicClient.getFromStepic(EduStepicNames.USERS + String.valueOf(instructor), StepicWrappers.AuthorWrapper.class).users.get(0); - info.addAuthor(author); + authors.add(author); } + info.setAuthors(authors); if (info.isAdaptive()) { info.setDescription("This is a Stepik Adaptive course.\n\n" + info.getDescription() + ADAPTIVE_NOTE); } - result.add(info); } } } - static boolean canBeOpened(CourseInfo courseInfo) { + static boolean canBeOpened(RemoteCourse courseInfo) { if (courseInfo.isAdaptive()) { return true; } @@ -190,26 +191,19 @@ public class EduStepicConnector { } } - public static Course getCourse(@NotNull final Project project, @NotNull final CourseInfo info) { - final Course course = new Course(); - course.setAuthors(info.getAuthors()); - course.setDescription(info.getDescription()); - course.setAdaptive(info.isAdaptive()); - course.setId(info.getId()); - course.setUpdateDate(getCourseUpdateDate(info.getId())); - - if (!course.isAdaptive()) { - String courseType = info.getType(); - course.setName(info.getName()); + public static RemoteCourse getCourse(@NotNull final Project project, @NotNull final RemoteCourse course) { + final RemoteCourse remoteCourse = (RemoteCourse)course.copy(); + if (!remoteCourse.isAdaptive()) { + String courseType = remoteCourse.getType(); final int separator = courseType.indexOf(" "); assert separator != -1; final String language = courseType.substring(separator + 1); - course.setLanguage(language); + remoteCourse.setLanguage(language); try { - for (Integer section : info.getSections()) { - course.addLessons(getLessons(section)); + for (Integer section : remoteCourse.getSections()) { + remoteCourse.addLessons(getLessons(section)); } - return course; + return remoteCourse; } catch (IOException e) { LOG.error("IOException " + e.getMessage()); @@ -218,14 +212,13 @@ public class EduStepicConnector { else { final Lesson lesson = new Lesson(); lesson.setName("Adaptive"); - course.addLesson(lesson); - course.setName(info.getName()); + remoteCourse.addLesson(lesson); //TODO: more specific name? - final Task recommendation = EduAdaptiveStepicConnector.getNextRecommendation(project, course); + final Task recommendation = EduAdaptiveStepicConnector.getNextRecommendation(project, remoteCourse); if (recommendation != null) { lesson.addTask(recommendation); } - return course; + return remoteCourse; } return null; } diff --git a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicUpdater.java b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicUpdater.java index f511b8547d25..d77b13c38bdd 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicUpdater.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicUpdater.java @@ -11,7 +11,8 @@ import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.Alarm; import com.intellij.util.text.DateFormatUtil; import com.jetbrains.edu.learning.StudySettings; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.edu.learning.courseFormat.Course; +import com.jetbrains.edu.learning.courseFormat.RemoteCourse; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -50,13 +51,14 @@ public class EduStepicUpdater { private static ActionCallback updateCourseList() { ActionCallback callback = new ActionCallback(); ApplicationManager.getApplication().executeOnPooledThread(() -> { - final List courses = EduStepicConnector.getCourses(null); - StudySettings.getInstance().setLastTimeChecked(System.currentTimeMillis()); + final List courses = EduStepicConnector.getCourses(null); + StepicSettings.getInstance().setLastTimeChecked(System.currentTimeMillis()); if (!courses.isEmpty()) { - List updated = new ArrayList<>(); - for (CourseInfo course : courses) { - if (course.getUpdateDate().after(new Date(StepicUpdateSettings.getInstance().getLastTimeChecked()))) { + List updated = new ArrayList<>(); + for (Course course : courses) { + if (course instanceof RemoteCourse && ((RemoteCourse)course).getUpdateDate(). + after(new Date(StepicUpdateSettings.getInstance().getLastTimeChecked()))) { updated.add(course); } } @@ -69,7 +71,7 @@ public class EduStepicUpdater { } else { title = "New courses available"; - message = StringUtil.join(updated, CourseInfo::getName, ", "); + message = StringUtil.join(updated, Course::getName, ", "); } final Notification notification = new Notification("New.course", title, message, NotificationType.INFORMATION); notification.notify(null); diff --git a/python/educational-core/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java b/python/educational-core/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java index e3c20424e020..1690b383f063 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java @@ -11,8 +11,8 @@ import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.core.EduUtils; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; import com.jetbrains.edu.learning.courseFormat.Lesson; +import com.jetbrains.edu.learning.courseFormat.RemoteCourse; import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; @@ -171,7 +171,7 @@ public class StepicWrappers { } public static class CoursesContainer { - public List courses; + public List courses; public Map meta; } @@ -185,10 +185,10 @@ public class StepicWrappers { } public static class CourseWrapper { - CourseInfo course; + RemoteCourse course; public CourseWrapper(Course course) { - this.course = new CourseInfo(); + this.course = new RemoteCourse(); this.course.setName(course.getName()); this.course.setDescription(course.getDescription()); this.course.setAuthors(course.getAuthors()); diff --git a/python/educational-core/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java b/python/educational-core/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java index 501beee24444..f7eb72f4b607 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java @@ -29,7 +29,6 @@ import com.intellij.util.ui.UIUtil; import com.jetbrains.edu.learning.StudySettings; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.stepic.EduAdaptiveStepicConnector; import com.jetbrains.edu.learning.stepic.EduStepicAuthorizedClient; @@ -52,9 +51,9 @@ import java.util.List; * data: 7/31/14. */ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { - private List myAvailableCourses = new ArrayList<>(); + private List myAvailableCourses = new ArrayList<>(); private JButton myBrowseButton; - private ComboBox myCoursesComboBox; + private ComboBox myCoursesComboBox; private JButton myRefreshButton; private JLabel myAuthorLabel; private JPanel myInfoPanel; @@ -122,20 +121,20 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { private void initCoursesCombobox() { myAvailableCourses = myGenerator.getCoursesUnderProgress(!isLocal, "Getting Available Courses", ProjectManager.getInstance().getDefaultProject()); - if (myAvailableCourses.contains(CourseInfo.INVALID_COURSE)) { + if (myAvailableCourses.contains(Course.INVALID_COURSE)) { setError(CONNECTION_ERROR); } else { addCoursesToCombobox(myAvailableCourses); - final CourseInfo selectedCourse = StudyUtils.getFirst(myAvailableCourses); + final Course selectedCourse = StudyUtils.getFirst(myAvailableCourses); if (selectedCourse == null) return; setAuthors(selectedCourse); myDescriptionPane.setText(selectedCourse.getDescription()); myDescriptionPane.setEditable(false); //setting the first course in list as selected myGenerator.setSelectedCourse(selectedCourse); - if (myGenerator.getSelectedCourseInfo() != null) { - myCoursesComboBox.setSelectedItem(myGenerator.getSelectedCourseInfo()); + if (myGenerator.getSelectedCourse() != null) { + myCoursesComboBox.setSelectedItem(myGenerator.getSelectedCourse()); } if (selectedCourse.isAdaptive() && !myGenerator.isLoggedIn()) { setError(LOGIN_TO_STEPIC_MESSAGE); @@ -174,7 +173,7 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { file -> { String fileName = file.getPath(); int oldSize = myAvailableCourses.size(); - CourseInfo courseInfo = myGenerator.addLocalCourse(fileName); + Course courseInfo = myGenerator.addLocalCourse(fileName); if (courseInfo != null) { if (oldSize != myAvailableCourses.size()) { myCoursesComboBox.addItem(courseInfo); @@ -185,11 +184,11 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { else { setError(INVALID_COURSE); myCoursesComboBox.removeAllItems(); - myCoursesComboBox.addItem(CourseInfo.INVALID_COURSE); - for (CourseInfo course : myAvailableCourses) { + myCoursesComboBox.addItem(Course.INVALID_COURSE); + for (Course course : myAvailableCourses) { myCoursesComboBox.addItem(course); } - myCoursesComboBox.setSelectedItem(CourseInfo.INVALID_COURSE); + myCoursesComboBox.setSelectedItem(Course.INVALID_COURSE); } }); } @@ -221,8 +220,8 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { isComboboxInitialized = true; initCoursesCombobox(); } - CourseInfo selectedCourse = (CourseInfo)myCoursesComboBox.getSelectedItem(); - if (selectedCourse == null || selectedCourse.equals(CourseInfo.INVALID_COURSE)) { + Course selectedCourse = (Course)myCoursesComboBox.getSelectedItem(); + if (selectedCourse == null || selectedCourse.equals(Course.INVALID_COURSE)) { setError(CONNECTION_ERROR); } } @@ -256,9 +255,9 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { myAnchor = anchor; } - public void updateInfoPanel(@NotNull CourseInfo selectedCourseInfo) { - setAuthors(selectedCourseInfo); - myDescriptionPane.setText(selectedCourseInfo.getDescription()); + public void updateInfoPanel(@NotNull Course selectedCourse) { + setAuthors(selectedCourse); + myDescriptionPane.setText(selectedCourse.getDescription()); } /** @@ -269,15 +268,15 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { private class RefreshActionListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { - final List courses = + final List courses = myGenerator.getCoursesUnderProgress(true, "Refreshing Course List", DefaultProjectFactory.getInstance().getDefaultProject()); - if (!courses.contains(CourseInfo.INVALID_COURSE)) { + if (!courses.contains(Course.INVALID_COURSE)) { refreshCoursesList(courses); } } } - private void refreshCoursesList(@NotNull final List courses) { + private void refreshCoursesList(@NotNull final List courses) { if (courses.isEmpty()) { setError(CONNECTION_ERROR); return; @@ -285,7 +284,7 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { myCoursesComboBox.removeAllItems(); addCoursesToCombobox(courses); - final CourseInfo selectedCourse = StudyUtils.getFirst(courses); + final Course selectedCourse = StudyUtils.getFirst(courses); if (selectedCourse == null) return; myGenerator.setSelectedCourse(selectedCourse); @@ -293,8 +292,8 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { myAvailableCourses = courses; } - private void addCoursesToCombobox(@NotNull List courses) { - for (CourseInfo courseInfo : courses) { + private void addCoursesToCombobox(@NotNull List courses) { + for (Course courseInfo : courses) { myCoursesComboBox.addItem(courseInfo); } } @@ -309,15 +308,15 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { @Override public void actionPerformed(ActionEvent e) { JComboBox cb = (JComboBox)e.getSource(); - CourseInfo selectedCourse = (CourseInfo)cb.getSelectedItem(); - if (selectedCourse == null || selectedCourse.equals(CourseInfo.INVALID_COURSE)) { + Course selectedCourse = (Course)cb.getSelectedItem(); + if (selectedCourse == null || selectedCourse.equals(Course.INVALID_COURSE)) { myAuthorLabel.setText(""); myDescriptionPane.setText(""); setError(INVALID_COURSE); return; } updateInfoPanel(selectedCourse); - myCoursesComboBox.removeItem(CourseInfo.INVALID_COURSE); + myCoursesComboBox.removeItem(Course.INVALID_COURSE); myGenerator.setSelectedCourse(selectedCourse); setOK(); @@ -329,12 +328,12 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { } } - private void setAuthors(CourseInfo selectedCourse) { + private void setAuthors(Course selectedCourse) { final String authorsString = Course.getAuthorsString(selectedCourse.getAuthors()); myAuthorLabel.setText(!StringUtil.isEmptyOrSpaces(authorsString) ? "Author: " + authorsString : ""); } - public JComboBox getCoursesComboBox() { + public JComboBox getCoursesComboBox() { return myCoursesComboBox; } @@ -367,7 +366,7 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { stepicUser.setPassword(myLoginPanel.getPassword()); StudySettings.getInstance().setUser(stepicUser); myGenerator.setEnrolledCoursesIds(EduAdaptiveStepicConnector.getEnrolledCoursesIds(stepicUser)); - final List courses = myGenerator.getCourses(true); + final List courses = myGenerator.getCourses(true); if (courses != null && myRefreshCourseList) { ApplicationManager.getApplication().invokeLater(() -> refreshCoursesList(courses)); } diff --git a/python/educational-core/testSrc/com/jetbrains/edu/learning/stepic/StudyStepicFormatTest.java b/python/educational-core/testSrc/com/jetbrains/edu/learning/stepic/StudyStepicFormatTest.java index f524d01fdfe6..e7d6a4b112a3 100644 --- a/python/educational-core/testSrc/com/jetbrains/edu/learning/stepic/StudyStepicFormatTest.java +++ b/python/educational-core/testSrc/com/jetbrains/edu/learning/stepic/StudyStepicFormatTest.java @@ -8,7 +8,7 @@ import com.intellij.util.containers.ContainerUtil; import com.jetbrains.edu.learning.StudySerializationUtils; import com.jetbrains.edu.learning.courseFormat.AnswerPlaceholder; import com.jetbrains.edu.learning.courseFormat.AnswerPlaceholderSubtaskInfo; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.edu.learning.courseFormat.RemoteCourse; import com.jetbrains.edu.learning.courseFormat.TaskFile; import org.jetbrains.annotations.NotNull; import org.junit.Test; @@ -65,9 +65,9 @@ public class StudyStepicFormatTest { EduStepicClient.deserializeStepicResponse(StepicWrappers.CoursesContainer.class, responseString); assertNotNull(container.courses); assertTrue("Incorrect number of courses", container.courses.size() == 4); - List filtered = ContainerUtil.filter(container.courses, info -> EduStepicConnector.canBeOpened(info)); + List filtered = ContainerUtil.filter(container.courses, info -> EduStepicConnector.canBeOpened(info)); assertEquals(ContainerUtil.newArrayList("Adaptive Python", "Introduction to Python", "format2"), - ContainerUtil.map(filtered, CourseInfo::getName)); + ContainerUtil.map(filtered, RemoteCourse::getName)); } @Test diff --git a/python/educational-python/Edu-Python/resources/courses/Introduction to Python.zip b/python/educational-python/Edu-Python/resources/courses/Introduction to Python.zip index a6293bc906a3452862966f4865f7d8a1ad8e3bd8..1b47952d554fefb6508a7c6897a85754257d470f 100644 GIT binary patch delta 19474 zcmV(&K;gfmjRE1A0S!<~0|XQR2nYxOr-6Qv4IY2}lG{d-=l}B*a4Qr+S`^jnmMpvP zj|;7A?`X~*J1osjY^W6s5G*z^0s#&Hs+%)05&IZ-ueVQf`B9%hfgsr+o9k=MuDctk zFH~h^{wgyo^PhhRg0n;vMV1z4SHVa4GYI}k|AiOv6rPPPUyi;Rs7Ga7CIX(Gy-UkH zo34MNGS1R7^Exa(kCV74bz^peR3r@-)W9#RJeb9KQ3lIAyUD|aZiR{!rC7$%n1*of zyrDj3F^n2s*-gW{xk!?1e4FLTbhupC-do*QN)D>!N^yrDakk9kwCr7<{SnI#0;=#; z@bfyrhJv^V%JotN>kPgIpIo2yuhV*K%Orn{#5_x;A}^e&@lBDgmCa^_z)@UYR9mF? ziAZnCIj+%5@B3mkp~;HVSyrDcUF^K^70xJpzJJKN$OmTzg1N}W@T{TuC*MG+m1R~G z@gx!BFfDFHj&uK09HwDyGEEILsbjTV?rJpGorlGEmNPAs`ARevOof=j1QsGMRVRP5 zFe!w${J%U;>(8oBKK-HT?Y~^Y7e(1b`1r11l0Tc}*&?9AK;%o22k~N=N>@)Z{u{D-4?<1 z>iqn2+SEj^&!uNsE^7pFs_M#?@LzwJB;&A%;#jo@4|-WX?L~cl5(T4+Z>gEcw9q#V z#IqpHO4E+jHuXnyiB#(i`Ag^B|gsc^H*279)6%ROK~v{ljS_DOjPSUeLVlf<)MMaDkFx3FLWC07TaB9d*jAPT!!BbIiANk0gl@ODsj;a63?5K)#+1Kh_*ZK|%U0RKmvQPm_;eG-Jz zX^@Ir-5!eX13;M3<+qzL%*FYMV3^8WbOU9qR(At5zU6s@HJf0~zMOwEpC@2xEd&Go zF8H}(RmiS?U6=C=#=UY`*W zUY|{%l95KbcX`QyYqbR++6oMhnKHfln$2Tz}f8CR;+v0i_j&4|%op%E~;b2<%13Y{nh%C8TB zLPp=6yyXL_jtn2!EWk5WZHXD61b@y-EF6~R1y(D9#i}UnBOmN|Q6j>{9lr?I2x3b_ z!~7UzsbrvXQ$w*A2!2jf<387_i4!ESBYZpdZoE zElhvt3*}P}ilNtV(gRws(Ud}0Rgh3OO2kuO*YPYCxkedfzP2^@9-M*C#a$$pW$^*-ozG{=!;&u#b6te<;L}r>AQ<}y%F#>(b zj*ugrQjNpYN_zw(qNQ`;QoKQCNfwslN}I!T)vEIvn>qxx_|C7uFxcYtLt=}cvg~s( zhf^(_7iJldCxd=Hi*klj*2-ScuA1^8tw1ya1a8k_H?V+K+iteM8#Jh`>mxeYtTk!S zWmE-!RYw9dJ6@4UzK@0kn*l=8ZWfc82opXBvsFs&WKwi@V#BfJqctm?NXB={UNgY2 z02zm;{_-{p7NojHc8(lS;_@q)s8ZaZI|EK5O=1CZ<^xQ?B9rR~9DnaHP@9PbB0ial zC|-m~u*{%G7Plba;?${doubqw;pbLp6~NyE1j!dl+oJgV#Yt_ zj(p0zn}IZ%hGobc5f#;J++q0&Gh*tq4ng`QWRt&%2*^XAi5ZX1>5VihQ@eDyPgX`m z8L5TSd)KcCGgY(hU3XV6OXE}<*3$>kL?^v4mw>N(PBLC%0Dn)HXjk6Yp`5X8`(^XG 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 delta 17888 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 P0h6svE(TOa00000;OocH diff --git a/python/educational-python/Edu-Python/src/com/jetbrains/edu/coursecreator/CreateFromArchiveProjectStep.java b/python/educational-python/Edu-Python/src/com/jetbrains/edu/coursecreator/CreateFromArchiveProjectStep.java index de1889a84fce..ea4a9dafdab3 100644 --- a/python/educational-python/Edu-Python/src/com/jetbrains/edu/coursecreator/CreateFromArchiveProjectStep.java +++ b/python/educational-python/Edu-Python/src/com/jetbrains/edu/coursecreator/CreateFromArchiveProjectStep.java @@ -10,7 +10,6 @@ import com.jetbrains.edu.learning.PyStudyDirectoryProjectGenerator; import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.python.newProject.steps.PyCharmNewProjectStep; import org.jetbrains.annotations.NotNull; @@ -44,10 +43,10 @@ class CreateFromArchiveProjectStep extends PyCharmNewProjectStep { @NotNull @Override protected DirectoryProjectGenerator createEmptyProjectGenerator() { - Course course = StudyTaskManager.getInstance(myProject).getCourse(); - if (course != null) { + Course currentCourse = StudyTaskManager.getInstance(myProject).getCourse(); + if (currentCourse != null) { VirtualFile folder = CCUtils.getGeneratedFilesFolder(myProject, myModule); - String zipName = FileUtil.sanitizeFileName(course.getName()); + String zipName = FileUtil.sanitizeFileName(currentCourse.getName()); if (zipName.isEmpty()) { zipName = EduNames.COURSE; } @@ -55,9 +54,9 @@ class CreateFromArchiveProjectStep extends PyCharmNewProjectStep { CCCreateCourseArchive.createCourseArchive(myProject, myModule, zipName, locationDir, false); String path = FileUtil.join(FileUtil.toSystemDependentName(folder.getPath()), zipName + ".zip"); StudyProjectGenerator generator = myGenerator.getGenerator(); - CourseInfo info = generator.addLocalCourse(path); - assert info != null; - generator.setSelectedCourse(info); + Course course = generator.addLocalCourse(path); + assert course != null; + generator.setSelectedCourse(course); } return myGenerator; } diff --git a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyDirectoryProjectGenerator.java b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyDirectoryProjectGenerator.java index d26b7f640b04..c28aa9b9f3e5 100644 --- a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyDirectoryProjectGenerator.java +++ b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyDirectoryProjectGenerator.java @@ -25,7 +25,7 @@ import com.intellij.psi.PsiManager; import com.intellij.util.BooleanFunction; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.edu.learning.courseFormat.RemoteCourse; import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.stepic.EduStepicConnector; import com.jetbrains.edu.learning.ui.StudyNewProjectPanel; @@ -87,7 +87,7 @@ public class PyStudyDirectoryProjectGenerator extends PythonProjectGenerator beforeProjectGenerated(@Nullable Sdk sdk) { return generator -> { final List enrolledCoursesIds = myGenerator.getEnrolledCoursesIds(); - final CourseInfo course = (CourseInfo)mySettingsPanel.getCoursesComboBox().getSelectedItem(); - if (course == null) return true; - if (course.getId() > 0 && !enrolledCoursesIds.contains(course.getId())) { + final Course course = (Course)mySettingsPanel.getCoursesComboBox().getSelectedItem(); + if (course == null || !(course instanceof RemoteCourse)) return true; + if (((RemoteCourse)course).getId() > 0 && !enrolledCoursesIds.contains(((RemoteCourse)course).getId())) { ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); - return StudyUtils.execCancelable(() -> EduStepicConnector.enrollToCourse(course.getId(), - StudySettings.getInstance().getUser())); + return StudyUtils.execCancelable(() -> EduStepicConnector.enrollToCourse(((RemoteCourse)course).getId(), + StepicSettings.getInstance().getUser())); }, "Creating Course", true, ProjectManager.getInstance().getDefaultProject()); } diff --git a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/actions/PyStudyIntroductionCourseAction.java b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/actions/PyStudyIntroductionCourseAction.java index 413df2d68c63..e2f33e58221a 100644 --- a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/actions/PyStudyIntroductionCourseAction.java +++ b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/actions/PyStudyIntroductionCourseAction.java @@ -21,7 +21,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.Presentation; import com.jetbrains.edu.learning.EduPluginConfigurator; import com.jetbrains.edu.learning.PyStudyDirectoryProjectGenerator; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.python.PythonLanguage; import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep; import com.jetbrains.python.newProject.steps.PythonGenerateProjectCallback; @@ -64,7 +64,7 @@ public class PyStudyIntroductionCourseAction extends AnAction { final PyStudyDirectoryProjectGenerator generator = new PyStudyDirectoryProjectGenerator(true); final EduPluginConfigurator configurator = EduPluginConfigurator.INSTANCE.forLanguage(PythonLanguage.getInstance()); final String bundledCoursePath = configurator.getBundledCoursePath(); - CourseInfo introCourse = generator.getGenerator().getCourseInfo(bundledCoursePath); + Course introCourse = generator.getGenerator().getCourse(bundledCoursePath); if (introCourse == null) { return; }