From 2168b64066b4ec8f3a902208424b51606da03bbb Mon Sep 17 00:00:00 2001 From: Ekaterina Tuzova Date: Sat, 25 Mar 2017 19:08:22 +0300 Subject: [PATCH] EDU-894 Save all course content in course.json EDU-891 Drop courses cache First iteration --- .../resources/META-INF/plugin.xml | 1 - .../edu/coursecreator/CCCheckListener.java | 22 - .../edu/coursecreator/CCProjectComponent.java | 7 +- .../CCRefactoringElementListenerProvider.java | 20 +- .../jetbrains/edu/coursecreator/CCUtils.java | 157 +++--- .../coursecreator/CCVirtualFileListener.java | 8 +- .../actions/CCAddAsTaskFile.java | 10 +- .../actions/CCCreateCourseArchive.java | 136 ++--- .../actions/CCFromCourseArchive.java | 9 +- .../actions/CCGetCourseFromStepic.java | 19 +- .../actions/CCHideFromStudent.java | 29 +- .../coursecreator/actions/CCPushCourse.java | 8 +- .../coursecreator/actions/CCPushLesson.java | 8 +- .../coursecreator/actions/CCShowPreview.java | 25 +- .../intellij/EduCCModuleBuilder.java | 4 - .../edu/coursecreator/settings/CCOptions.java | 10 +- .../stepik/CCStepicConnector.java | 78 +-- .../edu/learning/EduPluginConfigurator.java | 7 +- .../edu/learning/StudyProjectComponent.java | 46 +- .../edu/learning/StudySerializationUtils.java | 6 +- .../edu/learning/StudyTaskManager.java | 17 - .../jetbrains/edu/learning/StudyUtils.java | 114 ++-- .../StudyRefreshAnswerPlaceholder.java | 28 +- .../actions/StudyRefreshTaskFileAction.java | 28 +- .../learning/checker/StudySmartChecker.java | 6 - .../jetbrains/edu/learning/core/EduUtils.java | 100 ++-- .../edu/learning/courseFormat/Course.java | 25 +- .../edu/learning/courseFormat/CourseInfo.java | 11 +- .../edu/learning/courseFormat/tasks/Task.java | 11 +- .../courseGeneration/StudyGenerator.java | 192 ++++--- .../StudyProjectGenerator.java | 506 ++---------------- .../generation/EduProjectGenerator.java | 3 - .../statistics/EduUsagesCollector.java | 2 - .../stepic/EduAdaptiveStepicConnector.java | 221 ++++---- .../edu/learning/stepic/EduStepicUpdater.java | 29 +- .../edu/learning/stepic/StepicWrappers.java | 20 +- .../edu/learning/ui/StudyNewProjectPanel.java | 1 - .../edu/learning/ui/StudyToolWindow.java | 1 - .../edu/coursecreator/CCTestCase.java | 2 - .../courses/Introduction to Python.zip | Bin 88701 -> 18083 bytes .../coursecreator/PyCCProjectGenerator.java | 13 +- .../run/PyCCCommandLineState.java | 6 +- .../edu/learning/PyEduPluginConfigurator.java | 20 +- .../edu/learning/PyStudyCheckAction.java | 6 +- .../PyStudyDirectoryProjectGenerator.java | 8 +- .../learning/PyStudyInitialConfigurator.java | 49 +- .../edu/learning/PyStudyShowTutorial.java | 43 +- .../edu/learning/PyStudyTestRunner.java | 4 - .../PyStudyIntroductionCourseAction.java | 10 +- .../fileTemplates/internal/test_helper.py.ft | 34 -- .../edu/PyExecuteFileLineMarkerProvider.java | 2 +- .../PyEduConsoleInputFilterProvider.java | 2 - 52 files changed, 685 insertions(+), 1439 deletions(-) delete mode 100644 python/educational-core/src/com/jetbrains/edu/coursecreator/CCCheckListener.java diff --git a/python/educational-core/resources/META-INF/plugin.xml b/python/educational-core/resources/META-INF/plugin.xml index d1523e23db1d..941567b8c6ee 100644 --- a/python/educational-core/resources/META-INF/plugin.xml +++ b/python/educational-core/resources/META-INF/plugin.xml @@ -198,7 +198,6 @@ - \ No newline at end of file diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/CCCheckListener.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/CCCheckListener.java deleted file mode 100644 index 12aa92541496..000000000000 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/CCCheckListener.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.jetbrains.edu.coursecreator; - -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; -import com.jetbrains.edu.learning.StudyCheckListener; -import com.jetbrains.edu.learning.courseFormat.tasks.Task; -import org.jetbrains.annotations.NotNull; - -public class CCCheckListener implements StudyCheckListener { - - @Override - public void beforeCheck(@NotNull Project project, @NotNull Task task) { - if (!CCUtils.isCourseCreator(project)) { - return; - } - VirtualFile taskDir = task.getTaskDir(project); - if (taskDir == null) { - return; - } - CCUtils.updateResources(project, task, taskDir); - } -} diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/CCProjectComponent.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/CCProjectComponent.java index f5c081335c40..61d0323f968c 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/CCProjectComponent.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/CCProjectComponent.java @@ -1,7 +1,6 @@ package com.jetbrains.edu.coursecreator; import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.PathManager; import com.intellij.openapi.components.AbstractProjectComponent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; @@ -15,12 +14,11 @@ 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.Lesson; -import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; +import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.statistics.EduUsagesCollector; import org.jetbrains.annotations.NotNull; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -47,9 +45,6 @@ public class CCProjectComponent extends AbstractProjectComponent { CCProjectService.getInstance(myProject).setCourse(null); oldCourse.initCourse(true); oldCourse.setCourseMode(CCUtils.COURSE_MODE); - File coursesDir = new File(PathManager.getConfigPath(), "courses"); - File courseDir = new File(coursesDir, oldCourse.getName() + "-" + myProject.getName()); - oldCourse.setCourseDirectory(courseDir.getPath()); StudyUtils.registerStudyToolWindow(oldCourse, myProject); transformFiles(oldCourse, myProject); } diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/CCRefactoringElementListenerProvider.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/CCRefactoringElementListenerProvider.java index b9d46aa47d06..a917b185b31c 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/CCRefactoringElementListenerProvider.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/CCRefactoringElementListenerProvider.java @@ -15,10 +15,7 @@ */ package com.jetbrains.edu.coursecreator; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -30,16 +27,14 @@ 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.Lesson; -import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; +import com.jetbrains.edu.learning.courseFormat.tasks.Task; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.IOException; import java.util.Map; public class CCRefactoringElementListenerProvider implements RefactoringElementListenerProvider { - private static final Logger LOG = Logger.getInstance(CCRefactoringElementListenerProvider.class); @Nullable @Override @@ -94,21 +89,8 @@ public class CCRefactoringElementListenerProvider implements RefactoringElementL if (taskFile == null) { return; } - ApplicationManager.getApplication().runWriteAction(() -> { - VirtualFile patternFile = StudyUtils.getPatternFile(taskFile, oldName); - if (patternFile != null) { - try { - patternFile.delete(CCRefactoringElementListenerProvider.class); - } - catch (IOException e) { - LOG.info(e); - } - } - }); - taskFiles.remove(oldName); taskFiles.put(StudyUtils.pathRelativeToTask(file.getVirtualFile()), taskFile); - CCUtils.createResourceFile(file.getVirtualFile(), course, taskDir.getVirtualFile()); } @Override diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/CCUtils.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/CCUtils.java index c12b680a292b..7ec7d188972b 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/CCUtils.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/CCUtils.java @@ -8,34 +8,37 @@ import com.intellij.openapi.actionSystem.Presentation; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; -import com.intellij.openapi.project.DumbModePermission; -import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ModuleRootModificationUtil; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtilRt; -import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileVisitor; import com.intellij.psi.PsiDirectory; import com.intellij.util.Function; +import com.intellij.util.containers.hash.HashMap; import com.jetbrains.edu.learning.EduPluginConfigurator; import com.jetbrains.edu.learning.StudyTaskManager; 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.Lesson; import com.jetbrains.edu.learning.courseFormat.StudyItem; import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.courseFormat.tasks.Task; -import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; +import org.apache.commons.codec.binary.Base64; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.File; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; public class CCUtils { public static final String ANSWER_EXTENSION_DOTTED = ".answer."; @@ -137,27 +140,22 @@ public class CCUtils { return folder; } final Ref generatedRoot = new Ref<>(); - DumbService.allowStartingDumbModeInside(DumbModePermission.MAY_START_BACKGROUND, new Runnable() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { - ApplicationManager.getApplication().runWriteAction(new Runnable() { - @Override - public void run() { - try { - generatedRoot.set(baseDir.createChildDirectory(this, GENERATED_FILES_FOLDER)); - VirtualFile contentRootForFile = - ProjectRootManager.getInstance(module.getProject()).getFileIndex().getContentRootForFile(generatedRoot.get()); - if (contentRootForFile == null) { - return; - } - ModuleRootModificationUtil.updateExcludedFolders(module, contentRootForFile, Collections.emptyList(), - Collections.singletonList(generatedRoot.get().getUrl())); - } - catch (IOException e) { - LOG.info("Failed to create folder for generated files", e); - } + try { + generatedRoot.set(baseDir.createChildDirectory(this, GENERATED_FILES_FOLDER)); + VirtualFile contentRootForFile = + ProjectRootManager.getInstance(module.getProject()).getFileIndex().getContentRootForFile(generatedRoot.get()); + if (contentRootForFile == null) { + return; } - }); + ModuleRootModificationUtil.updateExcludedFolders(module, contentRootForFile, Collections.emptyList(), + Collections.singletonList(generatedRoot.get().getUrl())); + } + catch (IOException e) { + LOG.info("Failed to create folder for generated files", e); + } } }); return generatedRoot.get(); @@ -211,63 +209,6 @@ public class CCUtils { return configurator.isTestFile(file); } - public static void createResourceFile(VirtualFile createdFile, Course course, VirtualFile taskVF) { - VirtualFile lessonVF = taskVF.getParent(); - if (lessonVF == null) { - return; - } - - String taskResourcesPath = FileUtil.join(course.getCourseDirectory(), lessonVF.getName(), taskVF.getName()); - File taskResourceFile = new File(taskResourcesPath); - if (!taskResourceFile.exists()) { - if (!taskResourceFile.mkdirs()) { - LOG.info("Failed to create resources for task " + taskResourcesPath); - } - } - try { - File toFile = new File(taskResourceFile, createdFile.getName()); - FileUtil.copy(new File(createdFile.getPath()), toFile); - } - catch (IOException e) { - LOG.info("Failed to copy created task file to resources " + createdFile.getPath()); - } - } - - - public static void updateResources(Project project, Task task, VirtualFile taskDir) { - Course course = StudyTaskManager.getInstance(project).getCourse(); - if (course == null) { - return; - } - VirtualFile lessonVF = taskDir.getParent(); - if (lessonVF == null) { - return; - } - - String taskResourcesPath = FileUtil.join(course.getCourseDirectory(), lessonVF.getName(), taskDir.getName()); - File taskResourceFile = new File(taskResourcesPath); - if (!taskResourceFile.exists()) { - if (!taskResourceFile.mkdirs()) { - LOG.info("Failed to create resources for task " + taskResourcesPath); - } - } - VirtualFile studentDir = LocalFileSystem.getInstance().findFileByIoFile(taskResourceFile); - if (studentDir == null) { - return; - } - for (Map.Entry entry : task.getTaskFiles().entrySet()) { - String name = entry.getKey(); - VirtualFile answerFile = taskDir.findFileByRelativePath(name); - if (answerFile == null) { - continue; - } - ApplicationManager.getApplication().runWriteAction(() -> { - EduUtils.createStudentFile(CCUtils.class, project, answerFile, studentDir, null, - task instanceof TaskWithSubtasks ? ((TaskWithSubtasks)task).getActiveSubtaskIndex() : 0); - }); - } - } - public static void updateActionGroup(AnActionEvent e) { Presentation presentation = e.getPresentation(); Project project = e.getProject(); @@ -315,4 +256,58 @@ public class CCUtils { } }); } + + @Nullable + public static Lesson createAdditionalLesson(Course course, Project project) { + final VirtualFile baseDir = project.getBaseDir(); + EduPluginConfigurator configurator = EduPluginConfigurator.INSTANCE.forLanguage(course.getLanguageById()); + + final Lesson lesson = new Lesson(); + lesson.setName(EduNames.PYCHARM_ADDITIONAL); + final Task task = new Task(); + task.setLesson(lesson); + task.setName(EduNames.PYCHARM_ADDITIONAL); + task.setIndex(1); + + VfsUtilCore.visitChildrenRecursively(baseDir, new VirtualFileVisitor(VirtualFileVisitor.NO_FOLLOW_SYMLINKS) { + @Override + public boolean visitFile(@NotNull VirtualFile file) { + final String name = file.getName(); + if (name.equals(EduNames.COURSE_META_FILE) || name.equals(EduNames.HINTS) || name.startsWith(".")) return false; + String sanitizedName = FileUtil.sanitizeFileName(course.getName()); + final String archiveName = sanitizedName.startsWith("_") ? EduNames.COURSE : sanitizedName; + if (name.equals(archiveName + ".zip")) return false; + if (GENERATED_FILES_FOLDER.equals(name) || Project.DIRECTORY_STORE_FOLDER.equals(name)) { + return false; + } + if (file.isDirectory()) return true; + + if (StudyUtils.isTaskDescriptionFile(name) || StudyUtils.isTestsFile(project, name)) return true; + + if (name.contains(".iml") || (configurator != null && configurator.excludeFromArchive(name))) { + return false; + } + final TaskFile taskFile = StudyUtils.getTaskFile(project, file); + if (taskFile == null) { + final String path = VfsUtilCore.getRelativePath(file, baseDir); + try { + if (EduUtils.isImage(file.getName())) { + task.addTestsTexts(path, Base64.encodeBase64URLSafeString(FileUtil.loadBytes(file.getInputStream()))); + } + else { + task.addTestsTexts(path, FileUtil.loadTextAndClose(file.getInputStream())); + } + } + catch (IOException e) { + LOG.error("Can't find file " + path); + } + } + return true; + } + }); + if (task.getTestsText().isEmpty()) return null; + lesson.addTask(task); + lesson.setIndex(1); + return lesson; + } } diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/CCVirtualFileListener.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/CCVirtualFileListener.java index a45193c712ca..52b81d8f222c 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/CCVirtualFileListener.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/CCVirtualFileListener.java @@ -48,7 +48,7 @@ public class CCVirtualFileListener implements VirtualFileListener { String taskRelativePath = StudyUtils.pathRelativeToTask(createdFile); EduPluginConfigurator configurator = EduPluginConfigurator.INSTANCE.forLanguage(course.getLanguageById()); - if (configurator != null && configurator.excludeFromArchive(new File(createdFile.getPath()))) { + if (configurator != null && configurator.excludeFromArchive(createdFile.getName())) { return; } @@ -68,8 +68,6 @@ public class CCVirtualFileListener implements VirtualFileListener { return; } - CCUtils.createResourceFile(createdFile, course, taskVF); - task.addTaskFile(taskRelativePath, 1); } @@ -89,7 +87,7 @@ public class CCVirtualFileListener implements VirtualFileListener { return; } Course course = StudyTaskManager.getInstance(project).getCourse(); - if (course == null || path.contains(FileUtil.toSystemIndependentName(course.getCourseDirectory()))) { + if (course == null) { return; } final TaskFile taskFile = StudyUtils.getTaskFile(project, removedFile); @@ -112,7 +110,7 @@ public class CCVirtualFileListener implements VirtualFileListener { } VirtualFile courseDir = project.getBaseDir(); CCUtils.updateHigherElements(courseDir.getChildren(), file -> course.getLesson(file.getName()), removedLesson.getIndex(), EduNames.LESSON, -1); - course.getLessons().remove(removedLesson); + course.removeLesson(removedLesson); } private static void deleteTask(@NotNull final Course course, @NotNull final VirtualFile removedTask) { diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCAddAsTaskFile.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCAddAsTaskFile.java index a1bf760c5a6a..9b9904b0f883 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCAddAsTaskFile.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCAddAsTaskFile.java @@ -9,8 +9,8 @@ import com.jetbrains.edu.coursecreator.CCUtils; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduUtils; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; +import com.jetbrains.edu.learning.courseFormat.tasks.Task; public class CCAddAsTaskFile extends CCTaskFileActionBase { public static final String ACTION_NAME = "Make Visible to Student"; @@ -21,7 +21,7 @@ public class CCAddAsTaskFile extends CCTaskFileActionBase { protected void performAction(VirtualFile file, Task task, Course course, Project project) { - EduUtils.runUndoableAction(project, ACTION_NAME, new AddTaskFile(file, null, course, project, task)); + EduUtils.runUndoableAction(project, ACTION_NAME, new AddTaskFile(file, null, project, task)); } protected boolean isAvailable(Project project, VirtualFile file) { @@ -31,21 +31,20 @@ public class CCAddAsTaskFile extends CCTaskFileActionBase { private static class AddTaskFile extends BasicUndoableAction { private final VirtualFile myFile; private TaskFile myTaskFile; - private final Course myCourse; private final Project myProject; private final Task myTask; - public AddTaskFile(VirtualFile file, TaskFile taskFile, Course course, Project project, Task task) { + public AddTaskFile(VirtualFile file, TaskFile taskFile, Project project, Task task) { super(file); myFile = file; myTaskFile = taskFile; - myCourse = course; myProject = project; myTask = task; } @Override public void undo() throws UnexpectedUndoException { + if (myTaskFile == null) return; CCHideFromStudent.hideFromStudent(myFile, myProject, myTask.getTaskFiles(), myTaskFile); ProjectView.getInstance(myProject).refresh(); } @@ -59,7 +58,6 @@ public class CCAddAsTaskFile extends CCTaskFileActionBase { myTask.addTaskFile(taskRelativePath, myTask.getTaskFiles().size()); myTaskFile = myTask.getTaskFile(taskRelativePath); } - CCUtils.createResourceFile(myFile, myCourse, StudyUtils.getTaskDir(myFile)); ProjectView.getInstance(myProject).refresh(); } 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 a199f2d0ebc1..8703e3612117 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 @@ -14,30 +14,31 @@ import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.util.Condition; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; -import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.hash.HashMap; import com.intellij.util.io.ZipUtil; import com.jetbrains.edu.coursecreator.CCUtils; import com.jetbrains.edu.coursecreator.ui.CreateCourseArchiveDialog; import com.jetbrains.edu.learning.StudySerializationUtils; -import com.jetbrains.edu.learning.EduPluginConfigurator; import com.jetbrains.edu.learning.StudyTaskManager; +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.Lesson; +import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.courseFormat.tasks.Task; -import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; import com.jetbrains.edu.learning.statistics.EduUsagesCollector; import org.jetbrains.annotations.NotNull; import java.io.*; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import java.util.zip.ZipOutputStream; public class CCCreateCourseArchive extends DumbAwareAction { @@ -90,34 +91,26 @@ public class CCCreateCourseArchive extends DumbAwareAction { return; } - EduPluginConfigurator configurator = EduPluginConfigurator.INSTANCE.forLanguage(course.getLanguageById()); - if (configurator == null) { - return; - } - FileFilter filter = pathname -> !configurator.excludeFromArchive(pathname); - - for (VirtualFile child : baseDir.getChildren()) { - String name = child.getName(); - File fromFile = new File(child.getPath()); - if (CCUtils.GENERATED_FILES_FOLDER.equals(name) || Project.DIRECTORY_STORE_FOLDER.equals(name) - || name.contains("iml") || configurator.excludeFromArchive(fromFile)) { - continue; - } - copyChild(archiveFolder, filter, child, fromFile); - } - ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { archiveFolder.refresh(false, true); Course courseCopy = course.copy(); replaceAnswerFilesWithTaskFiles(courseCopy); + additionalFiles(courseCopy); generateJson(archiveFolder, courseCopy); VirtualFileManager.getInstance().refreshWithoutFileWatcher(false); packCourse(archiveFolder, locationDir, zipName, showMessage); synchronize(project); } + private void additionalFiles(Course course) { + final Lesson lesson = CCUtils.createAdditionalLesson(course, project); + if (lesson != null) { + course.addLesson(lesson); + } + } + private void replaceAnswerFilesWithTaskFiles(Course courseCopy) { for (Lesson lesson : courseCopy.getLessons()) { String lessonDirName = EduNames.LESSON + String.valueOf(lesson.getIndex()); @@ -126,61 +119,76 @@ public class CCCreateCourseArchive extends DumbAwareAction { for (Task task : lesson.getTaskList()) { final VirtualFile taskDir = task.getTaskDir(project); if (taskDir == null) continue; - String taskDirName = EduNames.TASK + String.valueOf(task.getIndex()); - VirtualFile studentFileDir = VfsUtil.findRelativeFile(archiveFolder, lessonDirName, taskDirName); - if (studentFileDir == null) { - continue; - } - VirtualFile srcDir = studentFileDir.findChild(EduNames.SRC); - if (srcDir != null) { - studentFileDir = srcDir; - } - if (task instanceof TaskWithSubtasks) { - transformSubtaskTestsToTextFiles(studentFileDir); - } - for (String taskFile : task.getTaskFiles().keySet()) { - VirtualFile answerFile = taskDir.findFileByRelativePath(taskFile); - if (answerFile == null) { - continue; - } - EduUtils.createStudentFile(this, project, answerFile, studentFileDir, task, 0); - } + convertToStudentTaskFiles(task, taskDir); + addTestsToTask(task); + addDescriptions(task); } } } - private void transformSubtaskTestsToTextFiles(VirtualFile studentFileDir) { - Condition isSubtaskTestFile = - file -> CCUtils.isTestsFile(project, file) && file.getName().contains(EduNames.SUBTASK_MARKER); - List subtaskTests = ContainerUtil.filter(Arrays.asList(studentFileDir.getChildren()), isSubtaskTestFile); - for (VirtualFile subtaskTest : subtaskTests) { + private void convertToStudentTaskFiles(Task task, VirtualFile taskDir) { + final HashMap studentTaskFiles = new HashMap<>(); + for (Map.Entry entry : task.getTaskFiles().entrySet()) { + VirtualFile answerFile = taskDir.findFileByRelativePath(entry.getKey()); + if (answerFile == null) { + continue; + } + final TaskFile studentFile = EduUtils.createStudentFile(project, answerFile, task, 0); + if (studentFile != null) { + studentTaskFiles.put(entry.getKey(), studentFile); + } + } + task.taskFiles = studentTaskFiles; + } + + private void addDescriptions(@NotNull final Task task) { + final List descriptions = getDescriptionFiles(task, project); + for (VirtualFile file : descriptions) { try { - subtaskTest.rename(this, subtaskTest.getNameWithoutExtension() + ".txt"); + task.addTaskText(file.getName(), VfsUtilCore.loadText(file)); } catch (IOException e) { - LOG.error(e); + LOG.warn("Failed to load text " + file.getName()); } } } - }); - } - private static void copyChild(VirtualFile archiveFolder, FileFilter filter, VirtualFile child, File fromFile) { - File toFile = new File(archiveFolder.getPath(), child.getName()); - - try { - if (child.isDirectory()) { - FileUtil.copyDir(fromFile, toFile, filter); - } - else { - if (filter.accept(fromFile)) { - FileUtil.copy(fromFile, toFile); + private void addTestsToTask(Task task) { + final List testFiles = getTestFiles(task, project); + for (VirtualFile file : testFiles) { + try { + task.addTestsTexts(file.getName(), VfsUtilCore.loadText(file)); + } + catch (IOException e) { + LOG.warn("Failed to load text " + file.getName()); + } } } - } - catch (IOException e) { - LOG.info("Failed to copy" + fromFile.getPath(), e); - } + + private List getTestFiles(@NotNull Task task, @NotNull Project project) { + List testFiles = new ArrayList<>(); + VirtualFile taskDir = task.getTaskDir(project); + if (taskDir == null) { + return testFiles; + } + testFiles.addAll(Arrays.stream(taskDir.getChildren()) + .filter(file -> StudyUtils.isTestsFile(project, file.getName())) + .collect(Collectors.toList())); + return testFiles; + } + + private List getDescriptionFiles(@NotNull Task task, @NotNull Project project) { + List testFiles = new ArrayList<>(); + VirtualFile taskDir = task.getTaskDir(project); + if (taskDir == null) { + return testFiles; + } + testFiles.addAll(Arrays.stream(taskDir.getChildren()) + .filter(file -> StudyUtils.isTaskDescriptionFile(file.getName())) + .collect(Collectors.toList())); + return testFiles; + } + }); } private static void synchronize(@NotNull final Project project) { 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 6949ff558c5f..11334cf12bdf 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 @@ -27,7 +27,10 @@ import com.jetbrains.edu.learning.StudySerializationUtils; import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.core.EduDocumentListener; import com.jetbrains.edu.learning.core.EduNames; -import com.jetbrains.edu.learning.courseFormat.*; +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.TaskFile; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import org.jetbrains.annotations.NotNull; @@ -35,8 +38,6 @@ import java.io.*; import java.util.ArrayList; import java.util.Map; -import static com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator.OUR_COURSES_DIR; - public class CCFromCourseArchive extends DumbAwareAction { private static final Logger LOG = Logger.getInstance(CCFromCourseArchive.class.getName()); @@ -80,8 +81,6 @@ public class CCFromCourseArchive extends DumbAwareAction { } StudyTaskManager.getInstance(project).setCourse(course); - File courseDir = new File(OUR_COURSES_DIR, course.getName() + "-" + project.getName()); - course.setCourseDirectory(courseDir.getPath()); course.setCourseMode(CCUtils.COURSE_MODE); project.getBaseDir().refresh(false, true); int index = 1; 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 5c16882a183a..957e47954511 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 @@ -15,25 +15,22 @@ import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.jetbrains.edu.coursecreator.CCUtils; +import com.jetbrains.edu.coursecreator.stepik.CCStepicConnector; 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.Lesson; -import com.jetbrains.edu.learning.courseFormat.tasks.Task; -import com.jetbrains.edu.learning.courseFormat.TaskFile; -import com.jetbrains.edu.learning.courseGeneration.StudyGenerator; -import com.jetbrains.edu.coursecreator.stepik.CCStepicConnector; import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.edu.learning.courseFormat.Lesson; +import com.jetbrains.edu.learning.courseFormat.TaskFile; +import com.jetbrains.edu.learning.courseFormat.tasks.Task; +import com.jetbrains.edu.learning.courseGeneration.StudyGenerator; import com.jetbrains.edu.learning.stepic.EduStepicConnector; import org.jetbrains.annotations.NotNull; -import java.io.File; import java.util.Map; import static com.jetbrains.edu.coursecreator.actions.CCFromCourseArchive.createAnswerFile; -import static com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator.OUR_COURSES_DIR; -import static com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator.flushCourse; public class CCGetCourseFromStepic extends DumbAwareAction { @@ -66,21 +63,17 @@ public class CCGetCourseFromStepic extends DumbAwareAction { final Course course = EduStepicConnector.getCourse(project, info); if (course != null) { - flushCourse(course); - final File courseDirectory = StudyUtils.getCourseDirectory(course); ApplicationManager.getApplication().invokeAndWait(() -> ApplicationManager.getApplication().runWriteAction(() -> { final VirtualFile[] children = baseDir.getChildren(); for (VirtualFile child : children) { StudyUtils.deleteFile(child); } - StudyGenerator.createCourse(course, baseDir, courseDirectory, project); + StudyGenerator.createCourse(course, baseDir); })); StudyTaskManager.getInstance(project).setCourse(course); - File courseDir = new File(OUR_COURSES_DIR, course.getName() + "-" + project.getName()); - course.setCourseDirectory(courseDir.getPath()); course.setCourseMode(CCUtils.COURSE_MODE); project.getBaseDir().refresh(false, true); int index = 1; diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCHideFromStudent.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCHideFromStudent.java index 7e13155c0425..accb05906ebf 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCHideFromStudent.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCHideFromStudent.java @@ -1,29 +1,24 @@ package com.jetbrains.edu.coursecreator.actions; import com.intellij.ide.projectView.ProjectView; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.undo.BasicUndoableAction; import com.intellij.openapi.command.undo.UnexpectedUndoException; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; -import com.jetbrains.edu.coursecreator.CCUtils; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduUtils; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; +import com.jetbrains.edu.learning.courseFormat.tasks.Task; +import org.jetbrains.annotations.NotNull; -import java.io.IOException; import java.util.Map; public class CCHideFromStudent extends CCTaskFileActionBase { - - private static final Logger LOG = Logger.getInstance(CCHideFromStudent.class); private static final String ACTION_NAME = "Hide from Student"; public CCHideFromStudent() { @@ -36,21 +31,19 @@ public class CCHideFromStudent extends CCTaskFileActionBase { if (taskFile == null) { return; } - EduUtils.runUndoableAction(project, ACTION_NAME, new HideTaskFile(project, course, file, task, taskFile)); + EduUtils.runUndoableAction(project, ACTION_NAME, new HideTaskFile(project, file, task, taskFile)); } private static class HideTaskFile extends BasicUndoableAction { private final Project myProject; - private final Course myCourse; private final VirtualFile myFile; private final Task myTask; private final TaskFile myTaskFile; - public HideTaskFile(Project project, Course course, VirtualFile file, Task task, TaskFile taskFile) { + public HideTaskFile(Project project, VirtualFile file, Task task, TaskFile taskFile) { super(file); myProject = project; - myCourse = course; myFile = file; myTask = task; myTaskFile = taskFile; @@ -59,7 +52,6 @@ public class CCHideFromStudent extends CCTaskFileActionBase { @Override public void undo() throws UnexpectedUndoException { myTask.getTaskFiles().put(StudyUtils.pathRelativeToTask(myFile), myTaskFile); - CCUtils.createResourceFile(myFile, myCourse, StudyUtils.getTaskDir(myFile)); if (!myTaskFile.getAnswerPlaceholders().isEmpty() && FileEditorManager.getInstance(myProject).isFileOpen(myFile)) { for (FileEditor fileEditor : FileEditorManager.getInstance(myProject).getEditors(myFile)) { if (fileEditor instanceof TextEditor) { @@ -83,7 +75,7 @@ public class CCHideFromStudent extends CCTaskFileActionBase { } } - public static void hideFromStudent(VirtualFile file, Project project, Map taskFiles, TaskFile taskFile) { + public static void hideFromStudent(VirtualFile file, Project project, Map taskFiles, @NotNull final TaskFile taskFile) { if (!taskFile.getAnswerPlaceholders().isEmpty() && FileEditorManager.getInstance(project).isFileOpen(file)) { for (FileEditor fileEditor : FileEditorManager.getInstance(project).getEditors(file)) { if (fileEditor instanceof TextEditor) { @@ -93,17 +85,6 @@ public class CCHideFromStudent extends CCTaskFileActionBase { } } String taskRelativePath = StudyUtils.pathRelativeToTask(file); - VirtualFile patternFile = StudyUtils.getPatternFile(taskFile, taskRelativePath); - ApplicationManager.getApplication().runWriteAction(() -> { - if (patternFile != null) { - try { - patternFile.delete(CCHideFromStudent.class); - } - catch (IOException e) { - LOG.info(e); - } - } - }); taskFiles.remove(taskRelativePath); } 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 2ac39ca62623..156d2f477c4a 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 @@ -11,12 +11,12 @@ import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; 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.statistics.EduUsagesCollector; -import com.jetbrains.edu.coursecreator.stepik.CCStepicConnector; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -59,11 +59,11 @@ public class CCPushCourse extends DumbAwareAction { public void run(@NotNull ProgressIndicator indicator) { for (Lesson lesson : course.getLessons()) { if (lesson.getId() > 0) { - CCStepicConnector.updateLesson(project, lesson, indicator); + CCStepicConnector.updateLesson(project, lesson); } else { final CourseInfo info = CourseInfo.fromCourse(course); - final int lessonId = CCStepicConnector.postLesson(project, lesson, indicator); + final int lessonId = CCStepicConnector.postLesson(project, lesson); if (lessonId != -1) { final List sections = info.getSections(); final Integer sectionId = sections.get(sections.size() - 1); 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 3eda00f494fa..1c203cf6e85c 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 @@ -12,11 +12,11 @@ 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.courseFormat.Course; -import com.jetbrains.edu.learning.courseFormat.Lesson; -import com.jetbrains.edu.coursecreator.stepik.CCStepicConnector; import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.edu.learning.courseFormat.Lesson; import com.jetbrains.edu.learning.stepic.EduStepicNames; import org.jetbrains.annotations.NotNull; @@ -77,11 +77,11 @@ public class CCPushLesson extends DumbAwareAction { public void run(@NotNull ProgressIndicator indicator) { indicator.setText("Uploading lesson to " + EduStepicNames.STEPIC_URL); if (lesson.getId() > 0) { - CCStepicConnector.updateLesson(project, lesson, indicator); + CCStepicConnector.updateLesson(project, lesson); } else { final CourseInfo info = CourseInfo.fromCourse(course); - final int lessonId = CCStepicConnector.postLesson(project, lesson, indicator); + final int lessonId = CCStepicConnector.postLesson(project, lesson); final List sections = info.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/CCShowPreview.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCShowPreview.java index 896aa1e5d2fb..067a0b47d443 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCShowPreview.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/actions/CCShowPreview.java @@ -31,10 +31,10 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.FrameWrapper; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiFile; +import com.intellij.testFramework.LightVirtualFile; import com.intellij.ui.JBColor; import com.jetbrains.edu.coursecreator.CCUtils; import com.jetbrains.edu.learning.StudyTaskManager; @@ -109,33 +109,24 @@ public class CCShowPreview extends DumbAwareAction { return; } - if (taskFile.getActivePlaceholders().isEmpty()) { Messages.showInfoMessage("Preview is available for task files with answer placeholders only", "No Preview for This File"); return; } - VirtualFile generatedFilesFolder = CCUtils.getGeneratedFilesFolder(project, module); - - if (generatedFilesFolder == null) { - return; - } final Task task = taskFile.getTask(); - ApplicationManager.getApplication().runWriteAction(new Runnable() { - @Override - public void run() { - Pair pair = - EduUtils.createStudentFile(this, project, virtualFile, generatedFilesFolder, null, - task instanceof TaskWithSubtasks ? ((TaskWithSubtasks)task).getActiveSubtaskIndex() : 0); - if (pair != null) { - showPreviewDialog(project, pair.getFirst(), pair.getSecond()); - } + ApplicationManager.getApplication().runWriteAction(() -> { + TaskFile studentTaskFile = EduUtils.createStudentFile(project, virtualFile, task.copy(), + task instanceof TaskWithSubtasks ? ((TaskWithSubtasks)task).getActiveSubtaskIndex() : 0); + if (studentTaskFile != null) { + showPreviewDialog(project, studentTaskFile); } }); } - private static void showPreviewDialog(@NotNull Project project, @NotNull VirtualFile userFile, @NotNull TaskFile taskFile) { + private static void showPreviewDialog(@NotNull Project project, @NotNull TaskFile taskFile) { final FrameWrapper showPreviewFrame = new FrameWrapper(project); + final LightVirtualFile userFile = new LightVirtualFile(taskFile.name, taskFile.text); showPreviewFrame.setTitle(userFile.getName()); LabeledEditor labeledEditor = new LabeledEditor(null); final EditorFactory factory = EditorFactory.getInstance(); diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/intellij/EduCCModuleBuilder.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/intellij/EduCCModuleBuilder.java index 927cb26a4548..b8fc3e83c486 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/intellij/EduCCModuleBuilder.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/intellij/EduCCModuleBuilder.java @@ -28,7 +28,6 @@ import com.jetbrains.edu.coursecreator.ui.CCNewProjectPanel; import com.jetbrains.edu.learning.EduPluginConfigurator; import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.courseFormat.Course; -import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.intellij.generation.EduCourseModuleBuilder; import org.jdom.JDOMException; import org.jetbrains.annotations.NotNull; @@ -36,7 +35,6 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.text.JTextComponent; -import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.function.Function; @@ -93,8 +91,6 @@ class EduCCModuleBuilder extends EduCourseModuleBuilder { Language language = wrapper.getLanguage(); course.setLanguage(language.getID()); course.setCourseMode(CCUtils.COURSE_MODE); - File courseDir = new File(StudyProjectGenerator.OUR_COURSES_DIR, myPanel.getName() + "-" + project.getName()); - course.setCourseDirectory(courseDir.getPath()); StudyTaskManager.getInstance(project).setCourse(course); EduPluginConfigurator configurator = EduPluginConfigurator.INSTANCE.forLanguage(language); String languageName = language.getDisplayName(); diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/settings/CCOptions.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/settings/CCOptions.java index 6f5e3cd7768b..85b018c7df81 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/settings/CCOptions.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/settings/CCOptions.java @@ -19,15 +19,13 @@ public class CCOptions implements StudyOptionsProvider { if (!StudySettings.getInstance().isCourseCreatorEnabled()) return null; if (CCSettings.getInstance().useHtmlAsDefaultTaskFormat()) { myHtmlRadioButton.setSelected(true); - IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> { - IdeFocusManager.getGlobalInstance().requestFocus(myHtmlRadioButton, true); - }); + IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown( + () -> IdeFocusManager.getGlobalInstance().requestFocus(myHtmlRadioButton, true)); } else { myMarkdownRadioButton.setSelected(true); - IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> { - IdeFocusManager.getGlobalInstance().requestFocus(myMarkdownRadioButton, true); - }); + IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown( + () -> IdeFocusManager.getGlobalInstance().requestFocus(myMarkdownRadioButton, true)); } return myPanel; } 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 1b45fb108668..d052e1bbbe51 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 @@ -8,13 +8,9 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; -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.coursecreator.CCUtils; 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.CourseInfo; @@ -24,7 +20,6 @@ import com.jetbrains.edu.learning.stepic.EduStepicAuthorizedClient; import com.jetbrains.edu.learning.stepic.EduStepicNames; import com.jetbrains.edu.learning.stepic.StepicUser; import com.jetbrains.edu.learning.stepic.StepicWrappers; -import org.apache.commons.codec.binary.Base64; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; import org.apache.http.StatusLine; @@ -65,13 +60,16 @@ public class CCStepicConnector { ProgressManager.getInstance().run(new com.intellij.openapi.progress.Task.Modal(project, "Uploading Course", true) { @Override public void run(@NotNull final ProgressIndicator indicator) { - postCourse(project, course, indicator); + postCourse(project, course); } }); } - private static void postCourse(final Project project, @NotNull Course course, @NotNull final ProgressIndicator indicator) { - indicator.setText("Uploading course to " + EduStepicNames.STEPIC_URL); + private static void postCourse(final Project project, @NotNull Course course) { + final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); + if (indicator != null) { + indicator.setText("Uploading course to " + EduStepicNames.STEPIC_URL); + } final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + "/courses"); final StepicUser currentUser = EduStepicAuthorizedClient.getCurrentUser(); @@ -110,55 +108,25 @@ public class CCStepicConnector { final int sectionId = postModule(postedCourse.getId(), 1, String.valueOf(postedCourse.getName())); int position = 1; for (Lesson lesson : course.getLessons()) { - indicator.checkCanceled(); - final int lessonId = postLesson(project, lesson, indicator); + if (indicator != null) { + indicator.checkCanceled(); + } + final int lessonId = postLesson(project, lesson); postUnit(lessonId, position, sectionId); position += 1; } - ApplicationManager.getApplication().runReadAction(() -> postAdditionalFiles(project, postedCourse.getId(), indicator)); + ApplicationManager.getApplication().runReadAction(() -> postAdditionalFiles(course, project, postedCourse.getId())); } catch (IOException e) { LOG.error(e.getMessage()); } } - private static void postAdditionalFiles(@NotNull final Project project, int id, ProgressIndicator indicator) { - final VirtualFile baseDir = project.getBaseDir(); - final List files = VfsUtil.getChildren(baseDir, new VirtualFileFilter() { - @Override - public boolean accept(VirtualFile file) { - final String name = file.getName(); - return !name.contains(EduNames.LESSON) && !name.equals(EduNames.COURSE_META_FILE) && !name.equals(EduNames.HINTS) && - !"pyc".equals(file.getExtension()) && !file.isDirectory() && !name.equals(EduNames.TEST_HELPER) && !name.startsWith("."); - } - }); - - if (!files.isEmpty()) { + private static void postAdditionalFiles(Course course, @NotNull final Project project, int id) { + final Lesson lesson = CCUtils.createAdditionalLesson(course, project); + if (lesson != null) { final int sectionId = postModule(id, 2, EduNames.PYCHARM_ADDITIONAL); - final Lesson lesson = new Lesson(); - lesson.setName(EduNames.PYCHARM_ADDITIONAL); - final Task task = new Task(); - task.setLesson(lesson); - task.setName(EduNames.PYCHARM_ADDITIONAL); - task.setIndex(1); - for (VirtualFile file : files) { - try { - if (file != null) { - if (EduUtils.isImage(file.getName())) { - task.addTestsTexts(file.getName(), Base64.encodeBase64URLSafeString(FileUtil.loadBytes(file.getInputStream()))); - } - else { - task.addTestsTexts(file.getName(), FileUtil.loadTextAndClose(file.getInputStream())); - } - } - } - catch (IOException e) { - LOG.error("Can't find file " + file.getPath()); - } - } - lesson.addTask(task); - lesson.setIndex(1); - final int lessonId = postLesson(project, lesson, indicator); + final int lessonId = postLesson(project, lesson); postUnit(lessonId, 1, sectionId); } } @@ -255,7 +223,7 @@ public class CCStepicConnector { return -1; } - public static int updateLesson(@NotNull final Project project, @NotNull final Lesson lesson, ProgressIndicator indicator) { + public static int updateLesson(@NotNull final Project project, @NotNull final Lesson lesson) { final HttpPut request = new HttpPut(EduStepicNames.STEPIC_API_URL + EduStepicNames.LESSONS + String.valueOf(lesson.getId())); String requestBody = new Gson().toJson(new StepicWrappers.LessonWrapper(lesson)); @@ -279,7 +247,10 @@ public class CCStepicConnector { } for (Task task : lesson.getTaskList()) { - indicator.checkCanceled(); + final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); + if (indicator != null) { + indicator.checkCanceled(); + } postTask(project, task, lesson.getId()); } return lesson.getId(); @@ -290,7 +261,7 @@ public class CCStepicConnector { return -1; } - public static int postLesson(@NotNull final Project project, @NotNull final Lesson lesson, ProgressIndicator indicator) { + public static int postLesson(@NotNull final Project project, @NotNull final Lesson lesson) { final HttpPost request = new HttpPost(EduStepicNames.STEPIC_API_URL + "/lessons"); String requestBody = new Gson().toJson(new StepicWrappers.LessonWrapper(lesson)); @@ -311,7 +282,10 @@ public class CCStepicConnector { final Lesson postedLesson = new Gson().fromJson(responseString, Course.class).getLessons().get(0); lesson.setId(postedLesson.getId()); for (Task task : lesson.getTaskList()) { - indicator.checkCanceled(); + final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); + if (indicator != null) { + indicator.checkCanceled(); + } postTask(project, task, postedLesson.getId()); } return postedLesson.getId(); diff --git a/python/educational-core/src/com/jetbrains/edu/learning/EduPluginConfigurator.java b/python/educational-core/src/com/jetbrains/edu/learning/EduPluginConfigurator.java index da549bc885e8..74dd51a872c8 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/EduPluginConfigurator.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/EduPluginConfigurator.java @@ -21,8 +21,6 @@ import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.File; - public interface EduPluginConfigurator { String EP_NAME = "Edu.pluginConfigurator"; LanguageExtension INSTANCE = new LanguageExtension<>(EP_NAME); @@ -64,7 +62,7 @@ public interface EduPluginConfigurator { /** * Used in educator plugin to filter files to be packed into course archive */ - boolean excludeFromArchive(@NotNull File pathname); + boolean excludeFromArchive(@NotNull String name); /** * @return true for all the test files including tests for subtasks @@ -128,4 +126,7 @@ public interface EduPluginConfigurator { @Nullable String moduleDir) { } + default String getBundledCoursePath() { + return null; + } } 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 8173ff07d68c..335857dfc4a0 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/StudyProjectComponent.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/StudyProjectComponent.java @@ -39,6 +39,7 @@ import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.core.EduUtils; import com.jetbrains.edu.learning.courseFormat.*; import com.jetbrains.edu.learning.courseFormat.tasks.Task; +import com.jetbrains.edu.learning.courseGeneration.StudyGenerator; import com.jetbrains.edu.learning.editor.StudyEditorFactoryListener; import com.jetbrains.edu.learning.statistics.EduUsagesCollector; import com.jetbrains.edu.learning.stepic.EduStepicConnector; @@ -49,14 +50,12 @@ import org.jetbrains.annotations.NotNull; import javax.swing.*; import javax.swing.event.HyperlinkEvent; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import static com.jetbrains.edu.learning.StudyUtils.execCancelable; -import static com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator.flushCourse; public class StudyProjectComponent implements ProjectComponent { @@ -150,15 +149,8 @@ public class StudyProjectComponent implements ProjectComponent { final CourseInfo info = CourseInfo.fromCourse(currentCourse); if (info == null) return; - final File resourceDirectory = new File(currentCourse.getCourseDirectory()); - if (resourceDirectory.exists()) { - FileUtil.delete(resourceDirectory); - } - final Course course = EduStepicConnector.getCourse(myProject, info); - if (course == null) return; - flushCourse(course); course.initCourse(false); EduPluginConfigurator configurator = EduPluginConfigurator.INSTANCE.forLanguage(course.getLanguageById()); @@ -175,14 +167,14 @@ public class StudyProjectComponent implements ProjectComponent { Lesson studentLesson = currentCourse.getLesson(lesson.getId()); final String lessonDirName = EduNames.LESSON + String.valueOf(lessonIndex); - final File lessonDir = new File(myProject.getBasePath(), lessonDirName); - if (!lessonDir.exists()){ - final File fromLesson = new File(resourceDirectory, lessonDirName); + final VirtualFile baseDir = myProject.getBaseDir(); + final VirtualFile lessonDir = baseDir.findChild(lessonDirName); + if (lessonDir == null) { try { - FileUtil.copyDir(fromLesson, lessonDir); + StudyGenerator.createLesson(lesson, baseDir); } catch (IOException e) { - LOG.warn("Failed to copy lesson " + fromLesson.getPath()); + LOG.error("Failed to create lesson"); } lesson.setIndex(lessonIndex); lesson.initLesson(currentCourse, false); @@ -210,15 +202,14 @@ public class StudyProjectComponent implements ProjectComponent { task.setIndex(index); final String taskDirName = EduNames.TASK + String.valueOf(index); - final File toTask = new File(lessonDir, taskDirName); + final VirtualFile taskDir = lessonDir.findChild(taskDirName); - final String taskPath = FileUtil.join(resourceDirectory.getPath(), lessonDirName, taskDirName); - final File taskDir = new File(taskPath); - if (!taskDir.exists()) return; - final File[] taskFiles = taskDir.listFiles(); - if (taskFiles == null) continue; - for (File fromFile : taskFiles) { - copyFile(fromFile, new File(toTask, fromFile.getName())); + if (taskDir != null) return; + try { + StudyGenerator.createTask(task, lessonDir); + } + catch (IOException e) { + LOG.error("Failed to create task"); } tasks.add(task); } @@ -231,17 +222,6 @@ public class StudyProjectComponent implements ProjectComponent { notification.notify(myProject); } - private static void copyFile(@NotNull final File from, @NotNull final File to) { - if (from.exists()) { - try { - FileUtil.copyFileOrDir(from, to); - } - catch (IOException e) { - LOG.warn("Failed to copy " + from.getName()); - } - } - } - private void addShortcut(@NotNull final String actionIdString, @NotNull final String[] shortcuts) { KeymapManagerEx keymapManager = KeymapManagerEx.getInstanceEx(); for (Keymap keymap : keymapManager.getAllKeymaps()) { 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 e6b1c052afb2..1a8165ebcc16 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/StudySerializationUtils.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/StudySerializationUtils.java @@ -528,9 +528,9 @@ public class StudySerializationUtils { stepOptionsJson = convertToSecondVersion(stepOptionsJson); case 2: stepOptionsJson = convertToThirdVersion(stepOptionsJson); - // uncomment for future versions - //case 3: - // stepOptionsJson = convertToFourthVersion(stepOptionsJson); + // uncomment for future versions + //case 3: + // stepOptionsJson = convertToFourthVersion(stepOptionsJson); } convertSubtaskInfosToMap(stepOptionsJson); StepicWrappers.StepOptions stepOptions = 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 9d74279928a7..8b981f97cc74 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/StudyTaskManager.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/StudyTaskManager.java @@ -13,14 +13,12 @@ import com.intellij.util.xmlb.XmlSerializer; import com.intellij.util.xmlb.annotations.Transient; import com.jetbrains.edu.learning.courseFormat.*; import com.jetbrains.edu.learning.courseFormat.tasks.Task; -import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.ui.StudyToolWindow; import org.jdom.Element; import org.jdom.output.XMLOutputter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -39,7 +37,6 @@ public class StudyTaskManager implements PersistentStateComponent, Dumb public int VERSION = 5; public Map> myUserTests = new HashMap<>(); - public List myInvisibleFiles = new ArrayList<>(); public boolean myShouldUseJavaFx = StudyUtils.hasJavaFx(); private StudyToolWindow.StudyToolWindowMode myToolWindowMode = StudyToolWindow.StudyToolWindowMode.TEXT; @@ -150,12 +147,6 @@ public class StudyTaskManager implements PersistentStateComponent, Dumb VERSION = CURRENT_VERSION; if (myCourse != null) { myCourse.initCourse(true); - if (version != VERSION) { - final File updatedCourse = new File(StudyProjectGenerator.OUR_COURSES_DIR, myCourse.getName()); - if (updatedCourse.exists()) { - myCourse.setCourseDirectory(updatedCourse.getAbsolutePath()); - } - } } } catch (StudySerializationUtils.StudyUnrecognizedFormatException e) { @@ -167,14 +158,6 @@ public class StudyTaskManager implements PersistentStateComponent, Dumb return ServiceManager.getService(project, StudyTaskManager.class); } - public void addInvisibleFiles(String filePath) { - myInvisibleFiles.add(filePath); - } - - public boolean isInvisibleFile(String path) { - return myInvisibleFiles.contains(path); - } - public boolean shouldUseJavaFx() { return myShouldUseJavaFx; } diff --git a/python/educational-core/src/com/jetbrains/edu/learning/StudyUtils.java b/python/educational-core/src/com/jetbrains/edu/learning/StudyUtils.java index 94651eecc62a..8909c6936fd7 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/StudyUtils.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/StudyUtils.java @@ -7,6 +7,7 @@ import com.intellij.ide.IdeView; import com.intellij.ide.fileTemplates.FileTemplate; import com.intellij.ide.fileTemplates.FileTemplateManager; import com.intellij.ide.fileTemplates.FileTemplateUtil; +import com.intellij.ide.projectView.ProjectView; import com.intellij.ide.util.EditorHelper; import com.intellij.lang.Language; import com.intellij.openapi.actionSystem.AnActionEvent; @@ -40,8 +41,9 @@ import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.newvfs.NewVirtualFile; +import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowAnchor; @@ -49,6 +51,7 @@ import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; import com.intellij.ui.JBColor; import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.content.Content; @@ -72,9 +75,7 @@ import com.jetbrains.edu.learning.courseFormat.tasks.ChoiceTask; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; import com.jetbrains.edu.learning.courseFormat.tasks.TheoryTask; -import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.editor.StudyEditor; -import com.jetbrains.edu.learning.stepic.StepicUser; import com.jetbrains.edu.learning.ui.StudyToolWindow; import com.jetbrains.edu.learning.ui.StudyToolWindowFactory; import com.petebevin.markdown.MarkdownProcessor; @@ -84,9 +85,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.io.*; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; +import java.util.*; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -238,20 +237,6 @@ public class StudyUtils { } } - public static File copyResourceFile(@NotNull final String sourceName, @NotNull final String copyName, @NotNull final Project project, - @NotNull final Task task) - throws IOException { - final StudyTaskManager taskManager = StudyTaskManager.getInstance(project); - final Course course = taskManager.getCourse(); - int taskNum = task.getIndex(); - int lessonNum = task.getLesson().getIndex(); - assert course != null; - final String pathToResource = FileUtil.join(course.getCourseDirectory(), EduNames.LESSON + lessonNum, EduNames.TASK + taskNum); - final File resourceFile = new File(pathToResource, copyName); - FileUtil.copy(new File(pathToResource, sourceName), resourceFile); - return resourceFile; - } - @Nullable public static Sdk findSdk(@NotNull final Task task, @NotNull final Project project) { final Language language = task.getLesson().getCourse().getLanguageById(); @@ -413,34 +398,6 @@ public class StudyUtils { } } - - @Nullable - public static VirtualFile getPatternFile(@NotNull TaskFile taskFile, String name) { - Task task = taskFile.getTask(); - String lessonDir = EduNames.LESSON + String.valueOf(task.getLesson().getIndex()); - String taskDir = EduNames.TASK + String.valueOf(task.getIndex()); - Course course = task.getLesson().getCourse(); - File resourceFile = new File(course.getCourseDirectory()); - if (!resourceFile.exists()) { - return null; - } - String patternPath = FileUtil.join(resourceFile.getPath(), lessonDir, taskDir, name); - VirtualFile patternFile = VfsUtil.findFileByIoFile(new File(patternPath), true); - if (patternFile == null) { - return null; - } - return patternFile; - } - - @Nullable - public static Document getPatternDocument(@NotNull final TaskFile taskFile, String name) { - VirtualFile patternFile = getPatternFile(taskFile, name); - if (patternFile == null) { - return null; - } - return FileDocumentManager.getInstance().getDocument(patternFile); - } - public static boolean isRenameableOrMoveable(@NotNull final Project project, @NotNull final Course course, @NotNull final PsiElement element) { if (element instanceof PsiFile) { VirtualFile virtualFile = ((PsiFile)element).getVirtualFile(); @@ -623,23 +580,6 @@ public class StudyUtils { return course != null && EduNames.STUDY.equals(course.getCourseMode()); } - @NotNull - public static File getCourseDirectory(Course course) { - final File courseDirectory; - if (course.isAdaptive()) { - StepicUser user = StudySettings.getInstance().getUser(); - String email = user == null ? "" : user.getEmail(); - - courseDirectory = new File(StudyProjectGenerator.OUR_COURSES_DIR, - StudyProjectGenerator.ADAPTIVE_COURSE_PREFIX + course.getName() - + "_" + email); - } - else { - courseDirectory = new File(StudyProjectGenerator.OUR_COURSES_DIR, course.getName()); - } - return courseDirectory; - } - public static boolean hasJavaFx() { try { Class.forName("javafx.application.Platform"); @@ -880,4 +820,48 @@ public class StudyUtils { LOG.error(e); } } + public static void openFirstTask(@NotNull final Course course, @NotNull final Project project) { + LocalFileSystem.getInstance().refresh(false); + final Lesson firstLesson = getFirst(course.getLessons()); + if (firstLesson == null) return; + final Task firstTask = getFirst(firstLesson.getTaskList()); + if (firstTask == null) return; + final VirtualFile taskDir = firstTask.getTaskDir(project); + if (taskDir == null) return; + final Map taskFiles = firstTask.getTaskFiles(); + VirtualFile activeVirtualFile = null; + for (Map.Entry entry : taskFiles.entrySet()) { + final String relativePath = entry.getKey(); + final TaskFile taskFile = entry.getValue(); + taskDir.refresh(false, true); + final VirtualFile virtualFile = taskDir.findFileByRelativePath(relativePath); + if (virtualFile != null) { + if (!taskFile.getActivePlaceholders().isEmpty()) { + activeVirtualFile = virtualFile; + } + } + } + if (activeVirtualFile != null) { + final PsiFile file = PsiManager.getInstance(project).findFile(activeVirtualFile); + ProjectView.getInstance(project).select(file, activeVirtualFile, false); + final FileEditor[] editors = FileEditorManager.getInstance(project).openFile(activeVirtualFile, true); + if (editors.length == 0) { + return; + } + final FileEditor studyEditor = editors[0]; + if (studyEditor instanceof StudyEditor) { + selectFirstAnswerPlaceholder((StudyEditor)studyEditor, project); + } + FileEditorManager.getInstance(project).openFile(activeVirtualFile, true); + } + else { + String first = getFirst(taskFiles.keySet()); + if (first != null) { + NewVirtualFile firstFile = ((VirtualDirectoryImpl)taskDir).refreshAndFindChild(first); + if (firstFile != null) { + FileEditorManager.getInstance(project).openFile(firstFile, true); + } + } + } + } } diff --git a/python/educational-core/src/com/jetbrains/edu/learning/actions/StudyRefreshAnswerPlaceholder.java b/python/educational-core/src/com/jetbrains/edu/learning/actions/StudyRefreshAnswerPlaceholder.java index 85f3d3869ae3..8c6ea0bd36cf 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/actions/StudyRefreshAnswerPlaceholder.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/actions/StudyRefreshAnswerPlaceholder.java @@ -3,13 +3,9 @@ package com.jetbrains.edu.learning.actions; import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.Presentation; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.command.CommandProcessor; -import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.TextRange; import com.jetbrains.edu.learning.StudyState; import com.jetbrains.edu.learning.StudySubtaskUtils; import com.jetbrains.edu.learning.StudyTaskManager; @@ -17,8 +13,8 @@ import com.jetbrains.edu.learning.StudyUtils; 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.courseFormat.tasks.TaskWithSubtasks; import com.jetbrains.edu.learning.editor.StudyEditor; import org.jetbrains.annotations.Nullable; @@ -41,24 +37,12 @@ public class StudyRefreshAnswerPlaceholder extends DumbAwareAction { return; } StudyEditor studyEditor = StudyUtils.getSelectedStudyEditor(project); - final StudyState studyState = new StudyState(studyEditor); - if (answerPlaceholder.getTaskFile().getTask() instanceof TaskWithSubtasks) { - StudySubtaskUtils.refreshPlaceholder(studyState.getEditor(), answerPlaceholder); - return; + if (studyEditor != null) { + StudySubtaskUtils.refreshPlaceholder(studyEditor.getEditor(), answerPlaceholder); + final StudyTaskManager taskManager = StudyTaskManager.getInstance(project); + answerPlaceholder.reset(); + taskManager.setStatus(answerPlaceholder, StudyStatus.Unchecked); } - Document patternDocument = StudyUtils.getPatternDocument(answerPlaceholder.getTaskFile(), studyState.getVirtualFile().getName()); - if (patternDocument == null) { - return; - } - AnswerPlaceholder.MyInitialState initialState = answerPlaceholder.getInitialState(); - int startOffset = initialState.getOffset(); - final String text = patternDocument.getText(new TextRange(startOffset, startOffset + initialState.getLength())); - CommandProcessor.getInstance().executeCommand(project, () -> ApplicationManager.getApplication().runWriteAction(() -> { - Document document = studyState.getEditor().getDocument(); - int offset = answerPlaceholder.getOffset(); - document.deleteString(offset, offset + answerPlaceholder.getRealLength()); - document.insertString(offset, text); - }), NAME, null); } @Override diff --git a/python/educational-core/src/com/jetbrains/edu/learning/actions/StudyRefreshTaskFileAction.java b/python/educational-core/src/com/jetbrains/edu/learning/actions/StudyRefreshTaskFileAction.java index 08fb50fcfb43..5cbb7c749ecd 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/actions/StudyRefreshTaskFileAction.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/actions/StudyRefreshTaskFileAction.java @@ -76,7 +76,7 @@ public class StudyRefreshTaskFileAction extends StudyActionWithShortcut { } } else { - if (!resetTaskFile(editor.getDocument(), project, taskFile, studyState.getVirtualFile().getName())) { + if (!resetTaskFile(editor.getDocument(), project, taskFile)) { Messages.showInfoMessage("The initial text of task file is unavailable", "Failed to Refresh Task File"); return; } @@ -95,16 +95,13 @@ public class StudyRefreshTaskFileAction extends StudyActionWithShortcut { () -> IdeFocusManager.getInstance(project).requestFocus(editor.getContentComponent(), true)); StudyNavigator.navigateToFirstAnswerPlaceholder(editor, taskFile); - showBalloon(project, "You can start again now", MessageType.INFO); + showBalloon(project, MessageType.INFO); } private static boolean resetTaskFile(@NotNull final Document document, @NotNull final Project project, - TaskFile taskFile, - String name) { - if (!resetDocument(document, taskFile, name)) { - return false; - } + TaskFile taskFile) { + resetDocument(document, taskFile); final Task task = taskFile.getTask(); task.setStatus(StudyStatus.Unchecked); if (task instanceof ChoiceTask) { @@ -116,9 +113,9 @@ public class StudyRefreshTaskFileAction extends StudyActionWithShortcut { return true; } - private static void showBalloon(@NotNull final Project project, String text, @NotNull final MessageType messageType) { + private static void showBalloon(@NotNull final Project project, @NotNull final MessageType messageType) { BalloonBuilder balloonBuilder = - JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(text, messageType, null); + JBPopupFactory.getInstance().createHtmlTextBalloonBuilder("You can start again now", messageType, null); final Balloon balloon = balloonBuilder.createBalloon(); StudyEditor selectedStudyEditor = StudyUtils.getSelectedStudyEditor(project); assert selectedStudyEditor != null; @@ -135,19 +132,14 @@ public class StudyRefreshTaskFileAction extends StudyActionWithShortcut { } - private static boolean resetDocument(@NotNull final Document document, - @NotNull final TaskFile taskFile, - String fileName) { - final Document patternDocument = StudyUtils.getPatternDocument(taskFile, fileName); - if (patternDocument == null) { - return false; - } + private static void resetDocument(@NotNull final Document document, + @NotNull final TaskFile taskFile) { StudyUtils.deleteGuardedBlocks(document); taskFile.setTrackChanges(false); clearDocument(document); - document.setText(patternDocument.getCharsSequence()); + + document.setText(taskFile.text); taskFile.setTrackChanges(true); - return true; } private static void clearDocument(@NotNull final Document document) { diff --git a/python/educational-core/src/com/jetbrains/edu/learning/checker/StudySmartChecker.java b/python/educational-core/src/com/jetbrains/edu/learning/checker/StudySmartChecker.java index 3604bd8665c0..daa1d3491eb5 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/checker/StudySmartChecker.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/checker/StudySmartChecker.java @@ -21,7 +21,6 @@ import com.jetbrains.edu.learning.courseFormat.StudyStatus; import com.jetbrains.edu.learning.courseFormat.TaskFile; import org.jetbrains.annotations.NotNull; -import java.io.File; import java.io.IOException; public class StudySmartChecker { @@ -40,7 +39,6 @@ public class StudySmartChecker { @NotNull final VirtualFile virtualFile, @NotNull final Document usersDocument) { VirtualFile fileWindows = null; - File resourceFile = null; VirtualFile windowCopy = null; try { final int index = placeholder.getIndex(); @@ -49,7 +47,6 @@ public class StudySmartChecker { final FileDocumentManager documentManager = FileDocumentManager.getInstance(); final Document windowDocument = documentManager.getDocument(windowCopy); if (windowDocument != null) { - resourceFile = StudyUtils.copyResourceFile(virtualFile.getName(), windowCopy.getName(), project, usersTaskFile.getTask()); TaskFile windowTaskFile = answerTaskFile.getTask().copy().getTaskFile(StudyUtils.pathRelativeToTask(virtualFile)); if (windowTaskFile == null) { return; @@ -81,9 +78,6 @@ public class StudySmartChecker { finally { StudyUtils.deleteFile(windowCopy); StudyUtils.deleteFile(fileWindows); - if (resourceFile != null && resourceFile.exists() && !resourceFile.delete()) { - LOG.error("failed to delete", resourceFile.getPath()); - } } } } diff --git a/python/educational-core/src/com/jetbrains/edu/learning/core/EduUtils.java b/python/educational-core/src/com/jetbrains/edu/learning/core/EduUtils.java index 0f6a8e5fe719..0ec946b2aefe 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/core/EduUtils.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/core/EduUtils.java @@ -13,18 +13,20 @@ import com.intellij.openapi.command.undo.UndoableAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileTypes.PlainTextFileType; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.psi.PsiDirectory; +import com.intellij.testFramework.LightVirtualFile; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.courseFormat.*; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; +import org.apache.commons.codec.binary.Base64; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -120,68 +122,52 @@ public class EduUtils { VirtualFileManager.getInstance().refreshWithoutFileWatcher(true); } - - public static VirtualFile copyFile(Object requestor, VirtualFile toDir, VirtualFile file) { - Document document = FileDocumentManager.getInstance().getDocument(file); - if (document != null) { - FileDocumentManager.getInstance().saveDocument(document); - } - String taskRelativePath = StudyUtils.pathRelativeToTask(file); - try { - VirtualFile userFile = toDir.findFileByRelativePath(taskRelativePath); - if (userFile != null) { - userFile.delete(requestor); - } - return VfsUtil.copyFileRelative(requestor, file, toDir, taskRelativePath); - } - catch (IOException e) { - LOG.info("Failed to create file " + taskRelativePath + " in folder " + toDir.getPath(), e); - } - return null; - } - @Nullable - public static Pair createStudentFile(Object requestor, - Project project, - VirtualFile answerFile, - VirtualFile parentDir, - @Nullable Task task, - int targetSubtaskIndex) { - - VirtualFile studentFile = copyFile(requestor, parentDir, answerFile); - if (studentFile == null) { - return null; - } - Document studentDocument = FileDocumentManager.getInstance().getDocument(studentFile); - if (studentDocument == null) { - return null; - } - if (task == null) { - task = StudyUtils.getTaskForFile(project, answerFile); + public static TaskFile createStudentFile(Project project, VirtualFile answerFile, @Nullable Task task, int targetSubtaskIndex) { + try { if (task == null) { + task = StudyUtils.getTaskForFile(project, answerFile); + if (task == null) { + return null; + } + task = task.copy(); + } + TaskFile taskFile = task.getTaskFile(StudyUtils.pathRelativeToTask(answerFile)); + if (taskFile == null) { return null; } - task = task.copy(); - } - TaskFile taskFile = task.getTaskFile(StudyUtils.pathRelativeToTask(answerFile)); - if (taskFile == null) { - return null; - } - EduDocumentListener listener = new EduDocumentListener(taskFile, false); - studentDocument.addDocumentListener(listener); - taskFile.setTrackLengths(false); - for (AnswerPlaceholder placeholder : taskFile.getAnswerPlaceholders()) { - if (task instanceof TaskWithSubtasks) { - int fromSubtask = ((TaskWithSubtasks)task).getActiveSubtaskIndex(); - placeholder.switchSubtask(studentDocument, fromSubtask, targetSubtaskIndex); + if (isImage(taskFile.name)) { + taskFile.text = Base64.encodeBase64String(answerFile.contentsToByteArray()); + return taskFile; } + final LightVirtualFile studentFile = new LightVirtualFile("student_task", PlainTextFileType.INSTANCE, + VfsUtilCore.loadText(answerFile)); + Document studentDocument = FileDocumentManager.getInstance().getDocument(studentFile); + if (studentDocument == null) { + return null; + } + EduDocumentListener listener = new EduDocumentListener(taskFile, false); + studentDocument.addDocumentListener(listener); + taskFile.setTrackLengths(false); + for (AnswerPlaceholder placeholder : taskFile.getAnswerPlaceholders()) { + if (task instanceof TaskWithSubtasks) { + int fromSubtask = ((TaskWithSubtasks)task).getActiveSubtaskIndex(); + placeholder.switchSubtask(studentDocument, fromSubtask, targetSubtaskIndex); + } + } + for (AnswerPlaceholder placeholder : taskFile.getAnswerPlaceholders()) { + replaceWithTaskText(studentDocument, placeholder, targetSubtaskIndex); + } + taskFile.setTrackChanges(true); + studentDocument.removeDocumentListener(listener); + taskFile.text = studentDocument.getImmutableCharSequence().toString(); + return taskFile; } - for (AnswerPlaceholder placeholder : taskFile.getAnswerPlaceholders()) { - replaceWithTaskText(studentDocument, placeholder, targetSubtaskIndex); + catch (IOException e) { + LOG.error("Failed to convert answer file to student one"); } - taskFile.setTrackChanges(true); - studentDocument.removeDocumentListener(listener); - return Pair.create(studentFile, taskFile); + + return null; } private static void replaceWithTaskText(Document studentDocument, AnswerPlaceholder placeholder, int toSubtaskIndex) { 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 392ce6b4c6b2..44976a6078dc 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 @@ -18,13 +18,13 @@ 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; - private String myCourseDirectory = ""; @Expose private int id; @Expose @SerializedName("update_date") private Date myUpdateDate; @Expose private boolean isAdaptive = false; @@ -35,8 +35,7 @@ public class Course { private String courseType = EduNames.PYCHARM; private String courseMode = EduNames.STUDY; //this field is used to distinguish study and course creator modes - public Course() { - } + public Course() {} /** * Initializes state of course @@ -48,12 +47,18 @@ public class Course { } public List getLessons() { - return lessons; + return getLessons(false); + } + + public List getLessons(boolean withAadditional) { + return withAadditional ? lessons + : lessons.stream().filter(lesson -> !EduNames.PYCHARM_ADDITIONAL.equals(lesson.getName())).collect(Collectors.toList()); } public void setLessons(List lessons) { this.lessons = lessons; } + public void addLessons(List lessons) { this.lessons.addAll(lessons); } @@ -62,6 +67,10 @@ public class Course { lessons.add(lesson); } + public void removeLesson(Lesson lesson) { + lessons.remove(lesson); + } + public Lesson getLesson(@NotNull final String name) { int lessonIndex = EduUtils.getIndex(name, EduNames.LESSON); List lessons = getLessons(); @@ -118,14 +127,6 @@ public class Course { this.name = name; } - public String getCourseDirectory() { - return myCourseDirectory; - } - - public void setCourseDirectory(@NotNull final String courseDirectory) { - myCourseDirectory = courseDirectory; - } - public String getDescription() { return description; } 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 index acac3c5f951d..89fe65b8941e 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/CourseInfo.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/CourseInfo.java @@ -1,7 +1,6 @@ package com.jetbrains.edu.learning.courseFormat; import com.google.gson.annotations.SerializedName; -import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.stepic.EduStepicConnector; import com.jetbrains.edu.learning.stepic.StepicUser; import org.jetbrains.annotations.NotNull; @@ -10,7 +9,6 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.stream.Collectors; /** * Implementation of class which contains information to be shawn in course description in tool window @@ -152,10 +150,11 @@ public class CourseInfo { public static CourseInfo fromCourse(@Nullable final Course course) { if (course == null) return null; - final List infos = StudyProjectGenerator.getCoursesFromCache().stream(). - filter(info -> info.id == course.getId()).collect(Collectors.toList()); - if (infos.isEmpty()) return null; - return infos.get(0); + CourseInfo courseInfo = new CourseInfo(); + courseInfo.setName(course.getName()); + courseInfo.setDescription(course.getDescription()); + courseInfo.setAuthors(course.getAuthors()); + return courseInfo; } public List getSections() { diff --git a/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/tasks/Task.java b/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/tasks/Task.java index d5b56e06dd01..f5467dddee04 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/tasks/Task.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/tasks/Task.java @@ -21,6 +21,11 @@ import java.util.Map; /** * Implementation of task which contains task files, tests, input file for tests + * + * To implement new task there are 3 steps to be done: + * - extend Task class + * - go to Lesson and update elementTypes in taskList AbstractCollection. Needed for proper xml serialization + * - Update TaskSerializer and TaskDeserializer in StudySerializationUtil to handle json serialization */ public class Task implements StudyItem { @Expose private String name; @@ -35,8 +40,10 @@ public class Task implements StudyItem { @SerializedName("task_files") @Expose public Map taskFiles = new HashMap<>(); - protected Map testsText = new HashMap<>(); - protected Map taskTexts = new HashMap<>(); + @SerializedName("test_files") + @Expose protected Map testsText = new HashMap<>(); + @SerializedName("task_texts") + @Expose protected Map taskTexts = new HashMap<>(); @Transient private Lesson myLesson; @Expose @SerializedName("update_date") private Date myUpdateDate; diff --git a/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java b/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java index e19a67d25430..91e5eb7523c8 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java @@ -1,125 +1,33 @@ package com.jetbrains.edu.learning.courseGeneration; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; -import com.jetbrains.edu.learning.StudyTaskManager; -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.Lesson; -import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; +import com.jetbrains.edu.learning.courseFormat.tasks.Task; +import org.apache.commons.codec.binary.Base64; import org.jetbrains.annotations.NotNull; -import java.io.File; import java.io.IOException; import java.util.List; import java.util.Map; public class StudyGenerator { - private StudyGenerator() { - - } private static final Logger LOG = Logger.getInstance(StudyGenerator.class.getName()); - /** - * Creates task files in its task folder in project user created - * - * @param taskDir project directory of task which task file belongs to - * @param resourceRoot directory where original task file stored - * @throws IOException - */ - public static void createTaskFile(@NotNull final VirtualFile taskDir, @NotNull final File resourceRoot, - @NotNull final String name) throws IOException { - String systemIndependentName = FileUtil.toSystemIndependentName(name); - File resourceFile = new File(resourceRoot, name); - File fileInProject = new File(taskDir.getPath(), systemIndependentName); - FileUtil.copyFileOrDir(resourceFile, fileInProject); - } - - /** - * Creates task directory in its lesson folder in project user created - * - * @param lessonDir project directory of lesson which task belongs to - * @param resourceRoot directory where original task file stored - * @throws IOException - */ - public static void createTask(@NotNull final Task task, @NotNull final VirtualFile lessonDir, @NotNull final File resourceRoot, - @NotNull final Project project) throws IOException { - VirtualFile taskDir = lessonDir.createChildDirectory(project, EduNames.TASK + Integer.toString(task.getIndex())); - File newResourceRoot = new File(resourceRoot, taskDir.getName()); - int i = 0; - for (Map.Entry taskFile : task.getTaskFiles().entrySet()) { - TaskFile taskFileContent = taskFile.getValue(); - taskFileContent.setIndex(i); - i++; - createTaskFile(taskDir, newResourceRoot, taskFile.getKey()); - } - File[] filesInTask = newResourceRoot.listFiles(); - if (filesInTask != null) { - for (File file : filesInTask) { - String fileName = file.getName(); - if (!task.isTaskFile(fileName)) { - File resourceFile = new File(newResourceRoot, fileName); - File fileInProject = new File(taskDir.getCanonicalPath(), fileName); - FileUtil.copyFileOrDir(resourceFile, fileInProject); - if (!StudyUtils.isTestsFile(project, fileName) && !StudyUtils.isTaskDescriptionFile(fileName)) { - StudyTaskManager.getInstance(project).addInvisibleFiles(FileUtil.toSystemIndependentName(fileInProject.getPath())); - } - } - } - } - } - - /** - * Creates lesson directory in its course folder in project user created - * - * @param courseDir project directory of course - * @param resourceRoot directory where original lesson stored - * @throws IOException - */ - public static void createLesson(@NotNull final Lesson lesson, @NotNull final VirtualFile courseDir, @NotNull final File resourceRoot, - @NotNull final Project project) throws IOException { - if (EduNames.PYCHARM_ADDITIONAL.equals(lesson.getName())) return; - String lessonDirName = EduNames.LESSON + Integer.toString(lesson.getIndex()); - VirtualFile lessonDir = courseDir.createChildDirectory(project, lessonDirName); - final List taskList = lesson.getTaskList(); - for (int i = 1; i <= taskList.size(); i++) { - Task task = taskList.get(i - 1); - task.setIndex(i); - createTask(task, lessonDir, new File(resourceRoot, lessonDir.getName()), project); - } - } - - /** - * Creates course directory in project user created - * - * @param baseDir project directory - * @param resourceRoot directory where original course is stored - */ - public static void createCourse(@NotNull final Course course, @NotNull final VirtualFile baseDir, @NotNull final File resourceRoot, - @NotNull final Project project) { + private StudyGenerator() {} + public static void createCourse(@NotNull final Course course, @NotNull final VirtualFile baseDir) { try { - final List lessons = course.getLessons(); + final List lessons = course.getLessons(true); for (int i = 1; i <= lessons.size(); i++) { Lesson lesson = lessons.get(i - 1); lesson.setIndex(i); - createLesson(lesson, baseDir, resourceRoot, project); - } - File[] files = resourceRoot.listFiles( - (dir, name) -> !name.contains(EduNames.LESSON) && !name.equals(EduNames.COURSE_META_FILE) && !name.equals(EduNames.HINTS)); - if (files != null) { - for (File file : files) { - File dir = new File(baseDir.getPath(), file.getName()); - if (file.isDirectory()) { - FileUtil.copyDir(file, dir); - continue; - } - FileUtil.copy(file, dir); - } + createLesson(lesson, baseDir); } } catch (IOException e) { @@ -127,4 +35,88 @@ public class StudyGenerator { } } + public static void createLesson(@NotNull final Lesson lesson, @NotNull final VirtualFile courseDir) throws IOException { + if (EduNames.PYCHARM_ADDITIONAL.equals(lesson.getName())) { + createAdditionalFiles(lesson, courseDir); + } + else { + String lessonDirName = EduNames.LESSON + Integer.toString(lesson.getIndex()); + VirtualFile lessonDir = courseDir.createChildDirectory(courseDir, lessonDirName); + final List taskList = lesson.getTaskList(); + for (int i = 1; i <= taskList.size(); i++) { + Task task = taskList.get(i - 1); + task.setIndex(i); + createTask(task, lessonDir); + } + } + } + + public static void createTask(@NotNull final Task task, @NotNull final VirtualFile lessonDir) throws IOException { + VirtualFile taskDir = lessonDir.createChildDirectory(lessonDir, EduNames.TASK + Integer.toString(task.getIndex())); + int i = 0; + for (Map.Entry taskFile : task.getTaskFiles().entrySet()) { + TaskFile taskFileContent = taskFile.getValue(); + taskFileContent.setIndex(i); + i++; + createTaskFile(taskDir, taskFile.getValue()); + } + createTestFiles(taskDir, task); + createDescriptions(taskDir, task); + } + + public static void createTaskFile(@NotNull final VirtualFile taskDir, @NotNull final TaskFile taskFile) throws IOException { + final String name = taskFile.name; + createChildFile(taskDir, name, taskFile.text); + } + + private static void createDescriptions(VirtualFile taskDir, Task task) throws IOException { + final Map texts = task.getTaskTexts(); + for (Map.Entry entry : texts.entrySet()) { + final String name = entry.getKey(); + final VirtualFile virtualTaskFile = taskDir.createChildData(taskDir, name); + VfsUtil.saveText(virtualTaskFile, entry.getValue()); + } + } + + private static void createTestFiles(VirtualFile taskDir, Task task) throws IOException { + final Map tests = task.getTestsText(); + for (Map.Entry entry : tests.entrySet()) { + final String name = entry.getKey(); + final VirtualFile virtualTaskFile = taskDir.createChildData(taskDir, name); + VfsUtil.saveText(virtualTaskFile, entry.getValue()); + } + } + + private static void createAdditionalFiles(Lesson lesson, VirtualFile courseDir) throws IOException { + final List taskList = lesson.getTaskList(); + if (taskList.size() != 1) return; + final Task task = taskList.get(0); + for (Map.Entry entry : task.getTestsText().entrySet()) { + createChildFile(courseDir, entry.getKey(), entry.getValue()); + } + } + + + private static void createChildFile(@NotNull VirtualFile taskDir, String name, String text) throws IOException { + String newDirectories = null; + String fileName = name; + VirtualFile dir = taskDir; + if (name.contains("/")) { + int pos = name.lastIndexOf("/"); + fileName = name.substring(pos + 1); + newDirectories = name.substring(0, pos); + } + if (newDirectories != null) { + dir = VfsUtil.createDirectoryIfMissing(taskDir, newDirectories); + } + if (dir != null) { + final VirtualFile virtualTaskFile = dir.createChildData(taskDir, fileName); + if (EduUtils.isImage(name)) { + virtualTaskFile.setBinaryContent(Base64.decodeBase64(text)); + } + else { + VfsUtil.saveText(virtualTaskFile, text); + } + } + } } 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 84f63d113e7a..3bd94efe7e30 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 @@ -1,62 +1,48 @@ package com.jetbrains.edu.learning.courseGeneration; -import com.google.gson.*; -import com.google.gson.stream.JsonReader; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.intellij.facet.ui.ValidationResult; -import com.intellij.ide.projectView.ProjectView; +import com.intellij.lang.LanguageExtensionPoint; import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.PathManager; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.fileEditor.FileEditor; -import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.vfs.VirtualFileManager; -import com.intellij.openapi.vfs.newvfs.NewVirtualFile; -import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl; -import com.intellij.platform.templates.github.ZipUtil; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiManager; import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.io.zip.JBZipEntry; +import com.intellij.util.io.zip.JBZipFile; +import com.jetbrains.edu.learning.EduPluginConfigurator; import com.jetbrains.edu.learning.StudySerializationUtils; import com.jetbrains.edu.learning.StudySettings; import com.jetbrains.edu.learning.StudyTaskManager; 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.TaskFile; import com.jetbrains.edu.learning.courseFormat.tasks.Task; -import com.jetbrains.edu.learning.editor.StudyEditor; import com.jetbrains.edu.learning.statistics.EduUsagesCollector; import com.jetbrains.edu.learning.stepic.EduStepicConnector; import com.jetbrains.edu.learning.stepic.StepicUser; -import org.apache.commons.codec.binary.Base64; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.*; -import java.util.*; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import static com.jetbrains.edu.learning.StudyUtils.execCancelable; public class StudyProjectGenerator { - public static final String AUTHOR_ATTRIBUTE = "authors"; - public static final String LANGUAGE_ATTRIBUTE = "language"; - public static final String ADAPTIVE_COURSE_PREFIX = "__AdaptivePyCharmPython__"; - public static final File OUR_COURSES_DIR = new File(PathManager.getConfigPath(), "courses"); private static final Logger LOG = Logger.getInstance(StudyProjectGenerator.class.getName()); - private static final String COURSE_NAME_ATTRIBUTE = "name"; - private static final String COURSE_DESCRIPTION = "description"; - private static final String CACHE_NAME = "courseNames.txt"; private final List myListeners = ContainerUtil.newArrayList(); private List myCourses = new ArrayList<>(); + private List myLocalCourses = new ArrayList<>(); private List myEnrolledCoursesIds = new ArrayList<>(); protected CourseInfo mySelectedCourseInfo; @@ -97,39 +83,24 @@ public class StudyProjectGenerator { Messages.showWarningDialog("There is no recommended tasks for this adaptive course", "Error in Course Creation"); return; } - final File courseDirectory = StudyUtils.getCourseDirectory(course); StudyTaskManager.getInstance(project).setCourse(course); ApplicationManager.getApplication().runWriteAction(() -> { - StudyGenerator.createCourse(course, baseDir, courseDirectory, project); - course.setCourseDirectory(courseDirectory.getAbsolutePath()); - VirtualFileManager.getInstance().refreshWithoutFileWatcher(true); + StudyGenerator.createCourse(course, baseDir); StudyUtils.registerStudyToolWindow(course, project); - openFirstTask(course, project); + StudyUtils.openFirstTask(course, project); EduUsagesCollector.projectTypeCreated(course.isAdaptive() ? EduNames.ADAPTIVE : EduNames.STUDY); }); } @Nullable public Course getCourse(@NotNull final Project project) { - - final File courseFile = new File(new File(OUR_COURSES_DIR, mySelectedCourseInfo.getName()), EduNames.COURSE_META_FILE); - if (courseFile.exists()) { - final Course course = readCourseFromCache(courseFile, false); - if (course != null && course.isUpToDate()) { + final CourseInfo info = mySelectedCourseInfo; + for (Course course : myLocalCourses) { + if (course.getName().equals(info.getName())){ + course.initCourse(false); return course; } - return getCourseFromStepic(project); - } - else { - final StepicUser user = StudySettings.getInstance().getUser(); - if (user != null) { - final File adaptiveCourseFile = new File(new File(OUR_COURSES_DIR, ADAPTIVE_COURSE_PREFIX + - mySelectedCourseInfo.getName() + "_" + - user.getEmail()), EduNames.COURSE_META_FILE); - if (adaptiveCourseFile.exists()) { - return readCourseFromCache(adaptiveCourseFile, true); - } - } + } return getCourseFromStepic(project); } @@ -140,7 +111,6 @@ public class StudyProjectGenerator { return execCancelable(() -> { final Course course = EduStepicConnector.getCourse(project, mySelectedCourseInfo); if (StudyUtils.isCourseValid(course)) { - flushCourse(course); course.initCourse(false); } return course; @@ -148,267 +118,12 @@ public class StudyProjectGenerator { }, "Creating Course", true, project); } - @Nullable - private static Course readCourseFromCache(@NotNull File courseFile, boolean isAdaptive) { - Reader reader = null; - try { - reader = new InputStreamReader(new FileInputStream(courseFile), "UTF-8"); - Gson gson = - new GsonBuilder().registerTypeAdapter(Course.class, new StudySerializationUtils.Json.CourseTypeAdapter(courseFile)) - .registerTypeAdapter(Task.class, new StudySerializationUtils.Json.TaskDeserializer()).create(); - final Course course = gson.fromJson(reader, Course.class); - course.initCourse(isAdaptive); - return course; - } - catch (UnsupportedEncodingException | FileNotFoundException e) { - LOG.warn(e.getMessage()); - } - finally { - StudyUtils.closeSilently(reader); - } - return null; - } - - public static void openFirstTask(@NotNull final Course course, @NotNull final Project project) { - LocalFileSystem.getInstance().refresh(false); - final Lesson firstLesson = StudyUtils.getFirst(course.getLessons()); - if (firstLesson == null) return; - final Task firstTask = StudyUtils.getFirst(firstLesson.getTaskList()); - if (firstTask == null) return; - final VirtualFile taskDir = firstTask.getTaskDir(project); - if (taskDir == null) return; - final Map taskFiles = firstTask.getTaskFiles(); - VirtualFile activeVirtualFile = null; - for (Map.Entry entry : taskFiles.entrySet()) { - final String relativePath = entry.getKey(); - final TaskFile taskFile = entry.getValue(); - taskDir.refresh(false, true); - final VirtualFile virtualFile = taskDir.findFileByRelativePath(relativePath); - if (virtualFile != null) { - if (!taskFile.getActivePlaceholders().isEmpty()) { - activeVirtualFile = virtualFile; - } - } - } - if (activeVirtualFile != null) { - final PsiFile file = PsiManager.getInstance(project).findFile(activeVirtualFile); - ProjectView.getInstance(project).select(file, activeVirtualFile, false); - final FileEditor[] editors = FileEditorManager.getInstance(project).openFile(activeVirtualFile, true); - if (editors.length == 0) { - return; - } - final FileEditor studyEditor = editors[0]; - if (studyEditor instanceof StudyEditor) { - StudyUtils.selectFirstAnswerPlaceholder((StudyEditor)studyEditor, project); - } - FileEditorManager.getInstance(project).openFile(activeVirtualFile, true); - } - else { - String first = StudyUtils.getFirst(taskFiles.keySet()); - if (first != null) { - NewVirtualFile firstFile = ((VirtualDirectoryImpl)taskDir).refreshAndFindChild(first); - if (firstFile != null) { - FileEditorManager.getInstance(project).openFile(firstFile, true); - } - } - } - } - - public static void flushCourse(@NotNull final Course course) { - final File courseDirectory = StudyUtils.getCourseDirectory(course); - FileUtil.createDirectory(courseDirectory); - flushCourseJson(course, courseDirectory); - - int lessonIndex = 1; - List additionalLessons = new ArrayList<>(); - for (Lesson lesson : course.getLessons()) { - if (lesson.getName().equals(EduNames.PYCHARM_ADDITIONAL)) { - flushAdditionalFiles(courseDirectory, lesson); - additionalLessons.add(lesson); - } - else { - final File lessonDirectory = new File(courseDirectory, EduNames.LESSON + String.valueOf(lessonIndex)); - flushLesson(lessonDirectory, lesson); - lessonIndex += 1; - } - } - for (Lesson lesson : additionalLessons) { - course.getLessons().remove(lesson); - } - } - - private static void flushAdditionalFiles(File courseDirectory, Lesson lesson) { - final List taskList = lesson.getTaskList(); - if (taskList.size() != 1) return; - final Task task = taskList.get(0); - for (Map.Entry entry : task.getTestsText().entrySet()) { - final String name = entry.getKey(); - final String text = entry.getValue(); - final File file = new File(courseDirectory, name); - FileUtil.createIfDoesntExist(file); - try { - if (EduUtils.isImage(name)) { - FileUtil.writeToFile(file, Base64.decodeBase64(text)); - } - else { - FileUtil.writeToFile(file, text); - } - } - catch (IOException e) { - LOG.error("ERROR copying file " + name); - } - } - } - - public static void flushLesson(@NotNull final File lessonDirectory, @NotNull final Lesson lesson) { - FileUtil.createDirectory(lessonDirectory); - int taskIndex = 1; - for (Task task : lesson.taskList) { - final File taskDirectory = new File(lessonDirectory, EduNames.TASK + String.valueOf(taskIndex)); - flushTask(task, taskDirectory); - taskIndex += 1; - } - } - - public static void flushTask(@NotNull final Task task, @NotNull final File taskDirectory) { - FileUtil.createDirectory(taskDirectory); - for (Map.Entry taskFileEntry : task.taskFiles.entrySet()) { - final String name = FileUtil.toSystemDependentName(taskFileEntry.getKey()); - final TaskFile taskFile = taskFileEntry.getValue(); - final File file = new File(taskDirectory, name); - FileUtil.createIfDoesntExist(file); - - try { - if (EduUtils.isImage(taskFile.name)) { - FileUtil.writeToFile(file, Base64.decodeBase64(taskFile.text)); - } - else { - FileUtil.writeToFile(file, taskFile.text); - } - } - catch (IOException e) { - LOG.error(e); - } - } - createFiles(taskDirectory, task.getTestsText()); - createFiles(taskDirectory, task.getTaskTexts()); - } - - public static void createFiles(@NotNull File taskDirectory, Map files) { - for (Map.Entry entry : files.entrySet()) { - final File file = new File(taskDirectory, entry.getKey()); - if (file.exists()) { - FileUtil.delete(file); - } - FileUtil.createIfDoesntExist(file); - try { - FileUtil.writeToFile(file, entry.getValue()); - } - catch (IOException e) { - LOG.error(e); - } - } - } - - public static void flushCourseJson(@NotNull final Course course, @NotNull final File courseDirectory) { - final Gson gson = new GsonBuilder().setPrettyPrinting(). - registerTypeAdapter(Task.class, new StudySerializationUtils.Json.TaskSerializer()). - excludeFieldsWithoutExposeAnnotation().create(); - final String json = gson.toJson(course); - final File courseJson = new File(courseDirectory, EduNames.COURSE_META_FILE); - final FileOutputStream fileOutputStream; - try { - fileOutputStream = new FileOutputStream(courseJson); - OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "UTF-8"); - try { - outputStreamWriter.write(json); - } - catch (IOException e) { - Messages.showErrorDialog(e.getMessage(), "Failed to Generate Json"); - LOG.info(e); - } - finally { - try { - outputStreamWriter.close(); - } - catch (IOException e) { - LOG.info(e); - } - } - } - catch (FileNotFoundException | UnsupportedEncodingException e) { - LOG.info(e); - } - } - - /** - * Writes courses to cache file {@link StudyProjectGenerator#CACHE_NAME} - */ - @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") - public static void flushCache(List courses) { - flushCache(courses, true); - } - - public static void flushCache(List courses, boolean preserveOld) { - File cacheFile = new File(OUR_COURSES_DIR, CACHE_NAME); - PrintWriter writer = null; - try { - if (!createCacheFile(cacheFile)) return; - Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); - - final Set courseInfos = new HashSet<>(); - courseInfos.addAll(courses); - if (preserveOld) { - courseInfos.addAll(getCoursesFromCache()); - } - - writer = new PrintWriter(cacheFile, "UTF-8"); - try { - for (CourseInfo courseInfo : courseInfos) { - final String json = gson.toJson(courseInfo); - writer.println(json); - } - } - finally { - StudyUtils.closeSilently(writer); - } - } - catch (IOException e) { - LOG.error(e); - } - finally { - StudyUtils.closeSilently(writer); - } - } - - private static boolean createCacheFile(File cacheFile) throws IOException { - if (!OUR_COURSES_DIR.exists()) { - final boolean created = OUR_COURSES_DIR.mkdirs(); - if (!created) { - LOG.error("Cannot flush courses cache. Can't create courses directory"); - return false; - } - } - if (!cacheFile.exists()) { - final boolean created = cacheFile.createNewFile(); - if (!created) { - LOG.error("Cannot flush courses cache. Can't create " + CACHE_NAME + " file"); - return false; - } - } - return true; - } - // Supposed to be called under progress public List getCourses(boolean force) { - if (OUR_COURSES_DIR.exists() && !force) { - myCourses = getCoursesFromCache(); + if (force) { + myCourses = execCancelable(() -> EduStepicConnector.getCourses(StepicUpdateSettings.getInstance().getUser())); } - if (force || myCourses.isEmpty()) { - myCourses = execCancelable(() -> EduStepicConnector.getCourses(StudySettings.getInstance().getUser())); - flushCache(myCourses); - } - if (myCourses.isEmpty() || (myCourses.size() == 1 && myCourses.contains(CourseInfo.INVALID_COURSE))) { + if (force || myCourses == null || myCourses.isEmpty() || (myCourses.size() == 1 && myCourses.contains(CourseInfo.INVALID_COURSE))) { myCourses = Collections.singletonList(getBundledIntro()); } sortCourses(myCourses); @@ -461,78 +176,45 @@ public class StudyProjectGenerator { } } - public static CourseInfo getBundledIntro() { - final File introCourse = new File(OUR_COURSES_DIR, "Introduction to Python"); - if (introCourse.exists()) { - return getCourseInfo(introCourse); + @Nullable + public CourseInfo 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 null; } - public static List getCoursesFromCache() { - List courses = new ArrayList<>(); - final File cacheFile = new File(OUR_COURSES_DIR, CACHE_NAME); - if (!cacheFile.exists()) { - return courses; - } - try { - final FileInputStream inputStream = new FileInputStream(cacheFile); - try { - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); - try { - String line; - while ((line = reader.readLine()) != null) { - Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); - final CourseInfo courseInfo = gson.fromJson(line, CourseInfo.class); - courses.add(courseInfo); - } - } - catch (IOException | JsonSyntaxException e) { - LOG.error(e.getMessage()); - } - finally { - StudyUtils.closeSilently(reader); - } - } - catch (UnsupportedEncodingException e) { - LOG.error(e.getMessage()); - } - finally { - StudyUtils.closeSilently(inputStream); - } - } - catch (FileNotFoundException e) { - LOG.error(e.getMessage()); - } - return courses; - } - - /** - * Adds course from zip archive to courses - * - * @return added course name or null if course is invalid - */ @Nullable public CourseInfo addLocalCourse(String zipFilePath) { - File file = new File(zipFilePath); + final CourseInfo courseInfo = getCourseInfo(zipFilePath); + if (courseInfo != null) { + myCourses.add(0, courseInfo); + } + return courseInfo; + } + + @Nullable + public CourseInfo getCourseInfo(String zipFilePath) { try { - String fileName = file.getName(); - String unzippedName = fileName.substring(0, fileName.indexOf(".")); - File courseDir = new File(OUR_COURSES_DIR, unzippedName); - ZipUtil.unzip(null, courseDir, file, null, null, true); - CourseInfo courseName = addCourse(myCourses, courseDir); - flushCache(myCourses); - if (courseName != null && !courseName.getName().equals(unzippedName)) { - //noinspection ResultOfMethodCallIgnored - File dest = new File(OUR_COURSES_DIR, courseName.getName()); - if (dest.exists()) { - FileUtil.delete(dest); - } - courseDir.renameTo(dest); - //noinspection ResultOfMethodCallIgnored - courseDir.delete(); + 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()) + .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 courseName; + return courseInfo; } catch (IOException e) { LOG.error("Failed to unzip course archive"); @@ -540,80 +222,4 @@ public class StudyProjectGenerator { } return null; } - - /** - * Adds course to courses specified in params - * - * @param courses - * @param courseDir must be directory containing course file - * @return added course name or null if course is invalid - */ - @Nullable - private static CourseInfo addCourse(List courses, File courseDir) { - if (courseDir.isDirectory()) { - File[] courseFiles = courseDir.listFiles((dir, name) -> name.equals(EduNames.COURSE_META_FILE)); - if (courseFiles == null || courseFiles.length != 1) { - LOG.info("User tried to add course with more than one or without course files"); - return null; - } - File courseFile = courseFiles[0]; - CourseInfo courseInfo = getCourseInfo(courseFile); - if (courseInfo != null) { - courses.add(0, courseInfo); - } - return courseInfo; - } - return null; - } - - /** - * Parses course json meta file and finds course name - * - * @return information about course or null if course file is invalid - */ - @Nullable - private static CourseInfo getCourseInfo(File courseFile) { - if (courseFile.isDirectory()) { - File[] courseFiles = courseFile.listFiles((dir, name) -> name.equals(EduNames.COURSE_META_FILE)); - if (courseFiles == null || courseFiles.length != 1) { - LOG.info("More than one or without course files"); - return null; - } - courseFile = courseFiles[0]; - } - CourseInfo courseInfo = null; - BufferedReader reader = null; - try { - if (courseFile.getName().equals(EduNames.COURSE_META_FILE)) { - reader = new BufferedReader(new InputStreamReader(new FileInputStream(courseFile), "UTF-8")); - JsonReader r = new JsonReader(reader); - JsonParser parser = new JsonParser(); - JsonElement el = parser.parse(r); - String courseName = el.getAsJsonObject().get(COURSE_NAME_ATTRIBUTE).getAsString(); - String courseDescription = el.getAsJsonObject().get(COURSE_DESCRIPTION).getAsString(); - JsonArray courseAuthors = el.getAsJsonObject().get(AUTHOR_ATTRIBUTE).getAsJsonArray(); - String language = el.getAsJsonObject().get(LANGUAGE_ATTRIBUTE).getAsString(); - courseInfo = new CourseInfo(); - courseInfo.setName(courseName); - courseInfo.setDescription(courseDescription); - courseInfo.setType("pycharm " + language); - final ArrayList authors = new ArrayList<>(); - for (JsonElement author : courseAuthors) { - final JsonObject authorAsJsonObject = author.getAsJsonObject(); - final StepicUser stepicUser = StepicUser.createEmptyUser(); - stepicUser.setFirstName(authorAsJsonObject.get("first_name").getAsString()); - stepicUser.setLastName(authorAsJsonObject.get("last_name").getAsString()); - authors.add(stepicUser); - } - courseInfo.setAuthors(authors); - } - } - catch (Exception e) { - //error will be shown in UI - } - finally { - StudyUtils.closeSilently(reader); - } - return courseInfo; - } -} +} \ No newline at end of file diff --git a/python/educational-core/src/com/jetbrains/edu/learning/intellij/generation/EduProjectGenerator.java b/python/educational-core/src/com/jetbrains/edu/learning/intellij/generation/EduProjectGenerator.java index fe98b766ea7a..6cac20d04ace 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/intellij/generation/EduProjectGenerator.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/intellij/generation/EduProjectGenerator.java @@ -8,8 +8,6 @@ import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import org.jetbrains.annotations.NotNull; -import java.io.File; - public class EduProjectGenerator extends StudyProjectGenerator { private static final Logger LOG = Logger.getInstance(EduProjectGenerator.class); @@ -21,6 +19,5 @@ public class EduProjectGenerator extends StudyProjectGenerator { return; } StudyTaskManager.getInstance(project).setCourse(course); - course.setCourseDirectory(new File(OUR_COURSES_DIR, mySelectedCourseInfo.getName()).getAbsolutePath()); } } diff --git a/python/educational-core/src/com/jetbrains/edu/learning/statistics/EduUsagesCollector.java b/python/educational-core/src/com/jetbrains/edu/learning/statistics/EduUsagesCollector.java index f500a1ee179e..c4db5d3caf2b 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/statistics/EduUsagesCollector.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/statistics/EduUsagesCollector.java @@ -23,7 +23,6 @@ import com.intellij.openapi.components.ServiceManager; import com.intellij.util.containers.FactoryMap; import com.intellij.util.containers.hash.HashSet; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.Set; @@ -31,7 +30,6 @@ public class EduUsagesCollector extends UsagesCollector { private static final String GROUP_ID = "educational"; private final FactoryMap myUsageDescriptors = new FactoryMap() { - @Nullable @Override protected UsageDescriptor create(String key) { return new UsageDescriptor(key, 0); 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 1e159afd7aa3..5bda69ff6384 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 @@ -7,16 +7,17 @@ import com.intellij.ide.projectView.ProjectView; import com.intellij.lang.Language; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.ui.MessageType; +import com.intellij.openapi.ui.popup.Balloon; +import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; -import com.jetbrains.edu.learning.StudySettings; import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.checker.StudyExecutor; @@ -27,7 +28,6 @@ import com.jetbrains.edu.learning.courseFormat.tasks.CodeTask; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.tasks.TheoryTask; import com.jetbrains.edu.learning.courseGeneration.StudyGenerator; -import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.editor.StudyEditor; import com.jetbrains.edu.learning.navigation.StudyNavigator; import com.jetbrains.edu.learning.ui.StudyToolWindow; @@ -45,12 +45,14 @@ import org.apache.http.util.EntityUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.concurrent.TimeUnit; import static com.jetbrains.edu.learning.stepic.EduStepicConnector.getStep; @@ -67,7 +69,6 @@ public class EduAdaptiveStepicConnector { private static final String CODE_TASK_TYPE = "code"; private static final String CHOICE_TYPE_TEXT = "choice"; private static final String TEXT_STEP_TYPE = "text"; - private static final String DEFAULT_TASK_NAME = "code.py"; @Nullable public static Task getNextRecommendation(@NotNull Project project, @NotNull Course course) { @@ -78,7 +79,7 @@ public class EduAdaptiveStepicConnector { return null; } - StepicUser user = StudySettings.getInstance().getUser(); + StepicUser user = StepicUpdateSettings.getInstance().getUser(); if (user == null) { LOG.warn("User is null"); return null; @@ -128,7 +129,16 @@ public class EduAdaptiveStepicConnector { } catch (IOException e) { LOG.warn(e.getMessage()); - ApplicationManager.getApplication().invokeLater(() -> StudyUtils.showErrorPopupOnToolbar(project, "Connection problems, Please, try again")); + + final String connectionMessages = "Connection problems, Please, try again"; + final Balloon balloon = + JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(connectionMessages, MessageType.ERROR, null) + .createBalloon(); + ApplicationManager.getApplication().invokeLater(() -> { + if (StudyUtils.getSelectedEditor(project) != null) { + StudyUtils.showCheckPopUp(project, balloon); + } + }); } catch (URISyntaxException e) { LOG.warn(e.getMessage()); @@ -194,8 +204,8 @@ public class EduAdaptiveStepicConnector { private static void createMockTaskFile(@NotNull Task task, String editorText) { final TaskFile taskFile = new TaskFile(); taskFile.text = editorText; - taskFile.name = "code"; - task.taskFiles.put(DEFAULT_TASK_NAME, taskFile); + taskFile.name = "code.py"; + task.taskFiles.put("code.py", taskFile); } @Nullable @@ -252,7 +262,9 @@ public class EduAdaptiveStepicConnector { request.setConfig(requestConfig); } - public static boolean postRecommendationReaction(@NotNull String lessonId, @NotNull String user, int reaction) { + public static boolean postRecommendationReaction(@NotNull String lessonId, + @NotNull String user, + int reaction) { final HttpPost post = new HttpPost(EduStepicNames.STEPIC_API_URL + EduStepicNames.RECOMMENDATION_REACTIONS_URL); final String json = new Gson() .toJson(new StepicWrappers.RecommendationReactionWrapper(new StepicWrappers.RecommendationReaction(reaction, user, lessonId))); @@ -276,100 +288,74 @@ public class EduAdaptiveStepicConnector { } catch (IOException e) { LOG.warn(e.getMessage()); - return false; } + return false; } - public static void addNextRecommendedTask(@NotNull Project project, @NotNull ProgressIndicator indicator, int reactionToPost) { + public static void addNextRecommendedTask(@NotNull Project project, + @NotNull ProgressIndicator indicator, + int reaction) { final StudyEditor editor = StudyUtils.getSelectedStudyEditor(project); final Course course = StudyTaskManager.getInstance(project).getCourse(); - if (course != null && editor != null && editor.getTaskFile() != null) { - indicator.checkCanceled(); - final StepicUser user = StudySettings.getInstance().getUser(); - if (user == null) { - LOG.warn("Can't get next recommendation: user is null"); - ApplicationManager.getApplication().invokeLater(() -> StudyUtils.showErrorPopupOnToolbar(project, - "Can't get next recommendation: you're not authorized")); - return; - } - - final Lesson adaptive = editor.getTaskFile().getTask().getLesson(); - final boolean reactionPosted = postRecommendationReaction(String.valueOf(adaptive.getId()), String.valueOf(user.getId()), reactionToPost); - - if (!reactionPosted) { - LOG.warn("Recommendation reaction wasn't posted"); - ApplicationManager.getApplication().invokeLater(() -> StudyUtils.showErrorPopupOnToolbar(project, "Couldn't post your reactionToPost")); - return; - } - + if (course == null || editor == null || editor.getTaskFile() == null) { + return; + } + indicator.checkCanceled(); + final StepicUser user = StepicUpdateSettings.getInstance().getUser(); + if (user == null) { + LOG.warn("Can't get next recommendation: user is null"); + ApplicationManager.getApplication().invokeLater(() -> StudyUtils.showErrorPopupOnToolbar(project, + "Can't get next recommendation: you're not authorized")); + return; + } + final Lesson lesson = course.getLessons().get(0); + if (lesson == null) return; + final boolean recommendationReaction = postRecommendationReaction(String.valueOf(lesson.getId()), + String.valueOf(user.getId()), reaction); + if (recommendationReaction) { indicator.checkCanceled(); final Task task = getNextRecommendation(project, course); - if (task == null) { - ApplicationManager - .getApplication().invokeLater(() -> StudyUtils.showErrorPopupOnToolbar(project, "Couldn't load a new recommendation")); - return; - } - - task.initTask(adaptive, false); - boolean replaceCurrentTask = reactionToPost == TOO_HARD_RECOMMENDATION_REACTION || reactionToPost == TOO_BORING_RECOMMENDATION_REACTION; - if (replaceCurrentTask) { - replaceCurrentTask(project, editor, task); + if (task != null) { + task.initTask(lesson, false); + if (reaction == TOO_HARD_RECOMMENDATION_REACTION || reaction == TOO_BORING_RECOMMENDATION_REACTION) { + replaceCurrentTask(project, task, lesson); + } + else { + addNewTask(project, course, task, lesson); + } } else { - addAsNextTask(project, editor, task); + ApplicationManager.getApplication().invokeLater(() -> { + final Balloon balloon = JBPopupFactory.getInstance(). + createHtmlTextBalloonBuilder("Couldn't load a new recommendation", MessageType.ERROR, null).createBalloon(); + StudyUtils.showCheckPopUp(project, balloon); + }); } - ApplicationManager.getApplication().invokeLater(() -> { VirtualFileManager.getInstance().refreshWithoutFileWatcher(false); ProjectView.getInstance(project).refresh(); - StudyNavigator.navigateToTask(project, task); }); } + else { + LOG.warn("Recommendation reactions weren't posted"); + ApplicationManager.getApplication().invokeLater(() -> StudyUtils.showErrorPopupOnToolbar(project, "Couldn't post your reaction")); + } } - private static void replaceCurrentTask(@NotNull Project project, @NotNull StudyEditor editor, @NotNull Task task) { - Course course = StudyTaskManager.getInstance(project).getCourse(); - assert course != null; - - final Lesson adaptive = editor.getTaskFile().getTask().getLesson(); - int taskIndex = adaptive.getTaskList().size(); - - task.setLesson(adaptive); - task.setIndex(taskIndex); - adaptive.getTaskList().set(taskIndex - 1, task); - - copyTaskFileParameters(editor, task); - - final String lessonName = EduNames.LESSON + adaptive.getIndex(); - updateCache(course, task, lessonName); - updateProjectFiles(project, task, lessonName, taskIndex); - setToolWindowText(project, task); - } - - private static void addAsNextTask(@NotNull Project project, @NotNull StudyEditor editor, @NotNull Task task) { - Course course = StudyTaskManager.getInstance(project).getCourse(); - assert course != null; - - final Lesson adaptive = editor.getTaskFile().getTask().getLesson(); - adaptive.addTask(task); - task.setIndex(adaptive.getTaskList().size()); - adaptive.initLesson(course, true); - - final String lessonName = EduNames.LESSON + adaptive.getIndex(); - updateCache(course, task, lessonName); - createFilesForNewTask(project, task, lessonName, course.getCourseDirectory()); - } - - private static void createFilesForNewTask(@NotNull Project project, - @NotNull Task task, - @NotNull String lessonName, - @NotNull String courseDirectory) { + private static void addNewTask(@NotNull Project project, Course course, Task task, Lesson lesson) { + final String lessonName = EduNames.LESSON + String.valueOf(lesson.getIndex()); + lesson.addTask(task); + task.setIndex(lesson.getTaskList().size()); final VirtualFile lessonDir = project.getBaseDir().findChild(lessonName); + if (lessonDir != null) { ApplicationManager.getApplication().invokeLater(() -> ApplicationManager.getApplication().runWriteAction(() -> { try { - StudyGenerator.createTask(task, lessonDir, new File(courseDirectory, lessonName), project); + final String taskName = EduNames.TASK + String.valueOf(task.getIndex()); + StudyGenerator.createTask(task, lessonDir); + lesson.initLesson(course, true); + StudyNavigator.navigateToTask(project, lessonName, taskName); } catch (IOException e) { LOG.warn(e.getMessage()); @@ -378,56 +364,30 @@ public class EduAdaptiveStepicConnector { } } - private static void copyTaskFileParameters(@NotNull StudyEditor editor, @NotNull Task task) { - final Map taskFiles = task.getTaskFiles(); - if (taskFiles.size() == 1) { - TaskFile newTaskFile = (TaskFile)taskFiles.values().toArray()[0]; - setTaskFileParameters(editor, task, newTaskFile); - updateEditorText(editor, newTaskFile); - } - else { - LOG.warn("Got task without unexpected number of task files: " + taskFiles.size()); - } - } + private static void replaceCurrentTask(@NotNull Project project, Task task, Lesson lesson) { + final Task unsolvedTask = lesson.getTaskList().get(lesson.getTaskList().size() - 1); + final String lessonName = EduNames.LESSON + String.valueOf(lesson.getIndex()); + task.setLesson(unsolvedTask.getLesson()); + task.setIndex(unsolvedTask.getIndex()); + lesson.getTaskList().set(lesson.getTaskList().size() - 1, task); + ApplicationManager.getApplication().invokeLater(() -> StudyNavigator.navigateToTask(project, task)); - private static void setToolWindowText(@NotNull Project project, @NotNull Task task) { + final String taskName = EduNames.TASK + String.valueOf(lesson.getTaskList().size()); + final VirtualFile lessonDir = project.getBaseDir().findChild(lessonName); + + if (lessonDir != null) { + try { + StudyGenerator.createTask(task, lessonDir); + } + catch (IOException e) { + LOG.warn(e.getMessage()); + } + } final StudyToolWindow window = StudyUtils.getStudyToolWindow(project); if (window != null) { - window.setTaskText(StudyUtils.wrapTextToDisplayLatex(task.getTaskDescription()), task.getTaskDir(project), project); + window.setTaskText(StudyUtils.wrapTextToDisplayLatex(unsolvedTask.getTaskDescription()), unsolvedTask.getTaskDir(project), project); } - } - - private static void updateProjectFiles(@NotNull Project project, @NotNull Task task, @NotNull String lessonName, int taskIndex) { - final VirtualFile lessonDir = project.getBaseDir().findChild(lessonName); - if (lessonDir != null) { - final File taskResourceRoot = new File(lessonDir.getCanonicalPath(), EduNames.TASK + taskIndex); - StudyProjectGenerator.createFiles(taskResourceRoot, task.getTaskTexts()); - StudyProjectGenerator.createFiles(taskResourceRoot, task.getTestsText()); - } - } - - private static void updateCache(@NotNull Course course, @NotNull Task task, @NotNull String lessonName) { - String taskName = EduNames.TASK + task.getIndex(); - final File lessonDirectory = new File(course.getCourseDirectory(), lessonName); - final File taskDir = new File(lessonDirectory, taskName); - StudyProjectGenerator.flushTask(task, taskDir); - StudyProjectGenerator.flushCourseJson(course, new File(course.getCourseDirectory())); - } - - private static void updateEditorText(@NotNull StudyEditor editor, @NotNull TaskFile newTaskFile) { - ApplicationManager.getApplication().invokeLater(() -> ApplicationManager.getApplication().runWriteAction(() -> { - final Document document = editor.getEditor().getDocument(); - document.setText(newTaskFile.text); - })); - } - - private static void setTaskFileParameters(@NotNull StudyEditor editor, @NotNull Task task, @NotNull TaskFile newTaskFile) { - TaskFile currentTaskFile = editor.getTaskFile(); - currentTaskFile.text = newTaskFile.text; - currentTaskFile.name = newTaskFile.name; - currentTaskFile.setTask(task); - task.getTaskFiles().clear(); - task.taskFiles.put(DEFAULT_TASK_NAME, currentTaskFile); + StudyNavigator.navigateToTask(project, lessonName, taskName); } @NotNull @@ -482,7 +442,7 @@ public class EduAdaptiveStepicConnector { 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(DEFAULT_TASK_NAME, taskFile); + task.taskFiles.put("code.py", taskFile); } return task; } @@ -543,7 +503,6 @@ public class EduAdaptiveStepicConnector { return answer; } - @NotNull public static Pair checkCodeTask(@NotNull Project project, @NotNull Task task, @NotNull StepicUser user) { int attemptId = -1; try { 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 ed8afb093f92..f511b8547d25 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 @@ -12,9 +12,10 @@ 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.courseGeneration.StudyProjectGenerator; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.Date; import java.util.List; public class EduStepicUpdater { @@ -50,24 +51,27 @@ public class EduStepicUpdater { ActionCallback callback = new ActionCallback(); ApplicationManager.getApplication().executeOnPooledThread(() -> { final List courses = EduStepicConnector.getCourses(null); - final List cachedCourses = StudyProjectGenerator.getCoursesFromCache(); - StudyProjectGenerator.flushCache(courses); StudySettings.getInstance().setLastTimeChecked(System.currentTimeMillis()); - courses.removeAll(cachedCourses); - if (!courses.isEmpty() && !cachedCourses.isEmpty()) { + if (!courses.isEmpty()) { + List updated = new ArrayList<>(); + for (CourseInfo course : courses) { + if (course.getUpdateDate().after(new Date(StepicUpdateSettings.getInstance().getLastTimeChecked()))) { + updated.add(course); + } + } + if (updated.isEmpty()) return; final String message; final String title; - if (courses.size() == 1) { - message = courses.get(0).getName(); + if (updated.size() == 1) { + message = updated.get(0).getName(); title = "New course available"; } else { title = "New courses available"; - message = StringUtil.join(courses, CourseInfo::getName, ", "); + message = StringUtil.join(updated, CourseInfo::getName, ", "); } - final Notification notification = - new Notification("New.course", title, message, NotificationType.INFORMATION); + final Notification notification = new Notification("New.course", title, message, NotificationType.INFORMATION); notification.notify(null); } }); @@ -79,8 +83,7 @@ public class EduStepicUpdater { } private static boolean checkNeeded() { - final List courses = StudyProjectGenerator.getCoursesFromCache(); - long timeToNextCheck = StudySettings.getInstance().getLastTimeChecked() + CHECK_INTERVAL - System.currentTimeMillis(); - return courses.isEmpty() || timeToNextCheck <= 0; + long timeToNextCheck = StepicUpdateSettings.getInstance().getLastTimeChecked() + CHECK_INTERVAL - System.currentTimeMillis(); + return timeToNextCheck <= 0; } } 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 b5a2065c3135..e3c20424e020 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 @@ -5,8 +5,6 @@ import com.google.gson.annotations.SerializedName; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Pair; -import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.jetbrains.edu.learning.StudyUtils; @@ -18,12 +16,10 @@ import com.jetbrains.edu.learning.courseFormat.Lesson; import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; -import org.apache.commons.codec.binary.Base64; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.io.InputStream; import java.util.*; import java.util.stream.Collectors; @@ -89,21 +85,11 @@ public class StepicWrappers { } String name = entry.getKey(); VirtualFile answerFile = taskDir.findFileByRelativePath(name); - Pair pair = EduUtils.createStudentFile(StepicWrappers.class, project, answerFile, stepicDir, null, 0); - if (pair == null) { + TaskFile studentTaskFile = EduUtils.createStudentFile(project, answerFile, null, 0); + if (studentTaskFile == null) { return; } - VirtualFile virtualFile = pair.getFirst(); - TaskFile taskFile = pair.getSecond(); - try { - InputStream stream = virtualFile.getInputStream(); - taskFile.text = - EduUtils.isImage(name) ? Base64.encodeBase64URLSafeString(FileUtil.loadBytes(stream)) : FileUtil.loadTextAndClose(stream); - } - catch (IOException e) { - LOG.error("Can't find file " + virtualFile.getPath()); - } - source.files.add(taskFile); + source.files.add(studentTaskFile); }); } return source; 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 e236b5487fa3..ea53e5725f80 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 @@ -290,7 +290,6 @@ public class StudyNewProjectPanel extends JPanel implements PanelWithAnchor { myGenerator.setCourses(courses); myAvailableCourses = courses; - StudyProjectGenerator.flushCache(myAvailableCourses, false); } private void addCoursesToCombobox(@NotNull List courses) { diff --git a/python/educational-core/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java b/python/educational-core/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java index 5a855a007786..9a2358a625a9 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java @@ -324,7 +324,6 @@ public abstract class StudyToolWindow extends SimpleToolWindowPanel implements D int taskNum = 0; int taskSolved = 0; for (Lesson lesson : lessons) { - if (lesson.getName().equals(EduNames.PYCHARM_ADDITIONAL)) continue; taskNum += lesson.getTaskList().size(); taskSolved += getSolvedTasks(lesson); } diff --git a/python/educational-core/testSrc/com/jetbrains/edu/coursecreator/CCTestCase.java b/python/educational-core/testSrc/com/jetbrains/edu/coursecreator/CCTestCase.java index 69d9b4eb311d..b327a074f6b6 100644 --- a/python/educational-core/testSrc/com/jetbrains/edu/coursecreator/CCTestCase.java +++ b/python/educational-core/testSrc/com/jetbrains/edu/coursecreator/CCTestCase.java @@ -81,7 +81,6 @@ public abstract class CCTestCase extends CodeInsightFixtureTestCase { super.setUp(); Course course = new Course(); course.setName("test course"); - course.setCourseDirectory(getProject().getBasePath()); StudyTaskManager.getInstance(getProject()).setCourse(course); Lesson lesson = new Lesson(); @@ -125,7 +124,6 @@ public abstract class CCTestCase extends CodeInsightFixtureTestCase { } taskFile.sortAnswerPlaceholders(); StudyUtils.drawAllAnswerPlaceholders(myFixture.getEditor(), taskFile); - CCUtils.createResourceFile(file, StudyTaskManager.getInstance(getProject()).getCourse(), file.getParent()); return file; } 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 5542a7925fc07f8a0782649ab5cd0e2cadf19187..a6293bc906a3452862966f4865f7d8a1ad8e3bd8 100644 GIT binary patch literal 18083 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 z3jhEB000000000000000003ieb#ilME^2dcZcs}F1^@s6009620672v08~Z*0001e Ck`GV- literal 88701 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 diff --git a/python/educational-python/Edu-Python/src/com/jetbrains/edu/coursecreator/PyCCProjectGenerator.java b/python/educational-python/Edu-Python/src/com/jetbrains/edu/coursecreator/PyCCProjectGenerator.java index 08b023c7d613..558c12db1091 100644 --- a/python/educational-python/Edu-Python/src/com/jetbrains/edu/coursecreator/PyCCProjectGenerator.java +++ b/python/educational-python/Edu-Python/src/com/jetbrains/edu/coursecreator/PyCCProjectGenerator.java @@ -33,10 +33,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; -import java.io.File; - -import static com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator.OUR_COURSES_DIR; - public class PyCCProjectGenerator extends PythonProjectGenerator { private static final Logger LOG = Logger.getInstance(PyCCProjectGenerator.class); @@ -60,11 +56,7 @@ public class PyCCProjectGenerator extends PythonProjectGenerator StudyCheckUtils.flushWindows(myTask, myTaskDir)); return super.execute(executor, processStarter, patchers); diff --git a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyEduPluginConfigurator.java b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyEduPluginConfigurator.java index eee62abd65a9..8a8d37ebc5b0 100644 --- a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyEduPluginConfigurator.java +++ b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyEduPluginConfigurator.java @@ -14,12 +14,14 @@ import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiManager; +import com.intellij.util.PathUtil; import com.jetbrains.edu.coursecreator.settings.CCSettings; import com.jetbrains.edu.learning.actions.StudyCheckAction; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; +import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -65,9 +67,8 @@ public class PyEduPluginConfigurator implements EduPluginConfigurator { } @Override - public boolean excludeFromArchive(@NotNull File pathname) { - String name = pathname.getName(); - return name.contains("__pycache__") || name.contains(".pyc"); + public boolean excludeFromArchive(@NotNull String name) { + return name.contains("__pycache__") || name.endsWith(".pyc"); } @Override @@ -126,4 +127,17 @@ public class PyEduPluginConfigurator implements EduPluginConfigurator { public StudyCheckAction getCheckAction() { return new PyStudyCheckAction(); } + + @Override + public String getBundledCoursePath() { + @NonNls String jarPath = PathUtil.getJarPathForClass(PyEduPluginConfigurator.class); + if (jarPath.endsWith(".jar")) { + final File jarFile = new File(jarPath); + + File pluginBaseDir = jarFile.getParentFile(); + return new File(new File(pluginBaseDir, "courses"), "Introduction to Python.zip").getPath(); + } + + return new File(new File(jarPath, "courses"), "Introduction to Python.zip").getPath(); + } } diff --git a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java index 443c80f20c5e..264596c26b46 100644 --- a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java +++ b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java @@ -94,9 +94,9 @@ public class PyStudyCheckAction extends StudyCheckAction { } final Course course = myTaskManger.getCourse(); if (course != null && EduNames.STUDY.equals(course.getCourseMode())) { - CommandProcessor.getInstance().runUndoTransparentAction(() -> ApplicationManager.getApplication().runWriteAction(() -> { - StudyCheckUtils.runSmartTestProcess(myTaskDir, testRunner, name, taskFile, project); - })); + CommandProcessor.getInstance().runUndoTransparentAction( + () -> ApplicationManager.getApplication().runWriteAction( + () -> StudyCheckUtils.runSmartTestProcess(myTaskDir, testRunner, name, taskFile, project))); } } final StudyToolWindow toolWindow = StudyUtils.getStudyToolWindow(project); 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 a9d7e496138f..7393a5f192ba 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 @@ -23,6 +23,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; 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.courseGeneration.StudyProjectGenerator; @@ -125,7 +126,7 @@ public class PyStudyDirectoryProjectGenerator extends PythonProjectGenerator getCourses() { - return myGenerator.getCoursesUnderProgress(false, "Getting Courses", ProjectManager.getInstance().getDefaultProject()); - } - public void setSelectedCourse(CourseInfo course) { myGenerator.setSelectedCourse(course); } diff --git a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyInitialConfigurator.java b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyInitialConfigurator.java index 922fd3bc2f1b..dc230cb66239 100644 --- a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyInitialConfigurator.java +++ b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyInitialConfigurator.java @@ -2,24 +2,18 @@ package com.jetbrains.edu.learning; import com.intellij.codeInsight.CodeInsightSettings; import com.intellij.ide.util.PropertiesComponent; -import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.application.PathManager; import com.intellij.openapi.fileTypes.FileTypeManager; import com.intellij.openapi.project.ex.ProjectManagerEx; import com.intellij.openapi.util.io.FileUtil; -import com.intellij.platform.templates.github.ZipUtil; -import com.intellij.util.PathUtil; import com.intellij.util.messages.MessageBus; -import com.jetbrains.edu.learning.actions.PyStudyIntroductionCourseAction; -import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import org.jetbrains.annotations.NonNls; import java.io.File; -import java.io.IOException; @SuppressWarnings({"UtilityClassWithoutPrivateConstructor", "UtilityClassWithPublicConstructor"}) public class PyStudyInitialConfigurator { - private static final Logger LOG = Logger.getInstance(PyStudyInitialConfigurator.class.getName()); - @NonNls private static final String CONFIGURED_V35 = "StudyPyCharm.InitialConfiguration35"; + @NonNls private static final String CONFIGURED_V40 = "StudyPyCharm.InitialConfiguration40"; /** * @noinspection UnusedParameters @@ -29,42 +23,9 @@ public class PyStudyInitialConfigurator { final PropertiesComponent propertiesComponent, FileTypeManager fileTypeManager, final ProjectManagerEx projectManager) { - final File file = new File(getCoursesRoot(), "Introduction to Python.zip"); - if (!propertiesComponent.getBoolean(CONFIGURED_V35) && file.exists()) { - try { - File[] children = StudyProjectGenerator.OUR_COURSES_DIR.listFiles( - (dir, name) -> name.equals(PyStudyIntroductionCourseAction.INTRODUCTION_TO_PYTHON)); - if (children != null) { - for (File child : children) { - FileUtil.delete(child); - } - } - copyCourse(file, StudyProjectGenerator.OUR_COURSES_DIR); - propertiesComponent.setValue(CONFIGURED_V35, "true"); - } - catch (IOException e) { - LOG.warn("Couldn't copy bundled courses " + e); - } + if (!propertiesComponent.getBoolean(CONFIGURED_V40)) { + final File courses = new File(PathManager.getConfigPath(), "courses"); + FileUtil.delete(courses); } } - - private static void copyCourse(File bundledCourse, File userCourseDir) throws IOException { - String fileName = bundledCourse.getName(); - String unzippedName = fileName.substring(0, fileName.indexOf(".")); - File courseDir = new File(userCourseDir, unzippedName); - ZipUtil.unzip(null, courseDir, bundledCourse, null, null, true); - } - - private static File getCoursesRoot() { - @NonNls String jarPath = PathUtil.getJarPathForClass(PyStudyInitialConfigurator.class); - if (jarPath.endsWith(".jar")) { - final File jarFile = new File(jarPath); - - - File pluginBaseDir = jarFile.getParentFile(); - return new File(pluginBaseDir, "courses"); - } - - return new File(jarPath, "courses"); - } } diff --git a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyShowTutorial.java b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyShowTutorial.java index 9adf0f45dfb5..0539ea134b28 100644 --- a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyShowTutorial.java +++ b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyShowTutorial.java @@ -23,32 +23,25 @@ public class PyStudyShowTutorial extends AbstractProjectComponent { @Override public void projectOpened() { - ApplicationManager.getApplication().invokeLater(new DumbAwareRunnable() { - @Override - public void run() { - ApplicationManager.getApplication().runWriteAction(new DumbAwareRunnable() { - @Override - public void run() { - if (PropertiesComponent.getInstance().getBoolean(ourShowPopup, true)) { - final String content = "If you'd like to learn more about PyCharm Edu, " + - "click here to watch a tutorial"; - final Notification notification = new Notification("Watch Tutorials!", "", content, NotificationType.INFORMATION, - new NotificationListener.UrlOpeningListener(true)); - Notifications.Bus.notify(notification); - Balloon balloon = notification.getBalloon(); - if (balloon != null) { - balloon.addListener(new JBPopupAdapter() { - @Override - public void onClosed(LightweightWindowEvent event) { - notification.expire(); - } - }); + ApplicationManager.getApplication().invokeLater((DumbAwareRunnable)() -> ApplicationManager.getApplication().runWriteAction( + (DumbAwareRunnable)() -> { + if (PropertiesComponent.getInstance().getBoolean(ourShowPopup, true)) { + final String content = "If you'd like to learn more about PyCharm Edu, " + + "click here to watch a tutorial"; + final Notification notification = new Notification("Watch Tutorials!", "", content, NotificationType.INFORMATION, + new NotificationListener.UrlOpeningListener(true)); + Notifications.Bus.notify(notification); + Balloon balloon = notification.getBalloon(); + if (balloon != null) { + balloon.addListener(new JBPopupAdapter() { + @Override + public void onClosed(LightweightWindowEvent event) { + notification.expire(); } - notification.whenExpired(() -> PropertiesComponent.getInstance().setValue(ourShowPopup, false, true)); - } + }); } - }); - } - }); + notification.whenExpired(() -> PropertiesComponent.getInstance().setValue(ourShowPopup, false, true)); + } + })); } } diff --git a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyTestRunner.java b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyTestRunner.java index a24db818717f..6720691d37f6 100644 --- a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyTestRunner.java +++ b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyTestRunner.java @@ -10,7 +10,6 @@ import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.vfs.VirtualFile; import com.jetbrains.edu.learning.checker.StudyTestRunner; import com.jetbrains.edu.learning.core.EduNames; -import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; import com.jetbrains.python.sdk.PythonSdkType; @@ -28,7 +27,6 @@ public class PyStudyTestRunner extends StudyTestRunner { public Process createCheckProcess(@NotNull final Project project, @NotNull final String executablePath) throws ExecutionException { final Sdk sdk = PythonSdkType.findPythonSdk(ModuleManager.getInstance(project).getModules()[0]); - Course course = myTask.getLesson().getCourse(); PyEduPluginConfigurator configurator = new PyEduPluginConfigurator(); String testsFileName = configurator.getTestFileName(); if (myTask instanceof TaskWithSubtasks) { @@ -50,8 +48,6 @@ public class PyStudyTestRunner extends StudyTestRunner { if (pythonPath != null) { commandLine.setExePath(pythonPath); commandLine.addParameter(testRunner.getPath()); - File resourceFile = new File(course.getCourseDirectory()); - commandLine.addParameter(resourceFile.getPath()); commandLine.addParameter(FileUtil.toSystemDependentName(executablePath)); return commandLine.createProcess(); } 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 1d44f500627d..578bb361df37 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 @@ -19,9 +19,10 @@ import com.intellij.ide.impl.ProjectUtil; import com.intellij.openapi.actionSystem.AnAction; 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.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.python.PythonLanguage; import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep; import com.jetbrains.python.newProject.steps.PythonGenerateProjectCallback; import icons.InteractiveLearningPythonIcons; @@ -44,7 +45,8 @@ public class PyStudyIntroductionCourseAction extends AnAction { if (projectDir.exists()) { return; } - if (StudyProjectGenerator.getBundledIntro() != null) { + final EduPluginConfigurator configurator = EduPluginConfigurator.INSTANCE.forLanguage(PythonLanguage.getInstance()); + if (configurator.getBundledCoursePath() != null) { return; } Presentation presentation = e.getPresentation(); @@ -60,7 +62,9 @@ public class PyStudyIntroductionCourseAction extends AnAction { } else { final PyStudyDirectoryProjectGenerator generator = new PyStudyDirectoryProjectGenerator(); - CourseInfo introCourse = StudyProjectGenerator.getBundledIntro(); + final EduPluginConfigurator configurator = EduPluginConfigurator.INSTANCE.forLanguage(PythonLanguage.getInstance()); + final String bundledCoursePath = configurator.getBundledCoursePath(); + CourseInfo introCourse = generator.getGenerator().getCourseInfo(bundledCoursePath); if (introCourse == null) { return; } diff --git a/python/educational-python/resources/fileTemplates/internal/test_helper.py.ft b/python/educational-python/resources/fileTemplates/internal/test_helper.py.ft index 6a3be496eb18..42bdaa245c70 100644 --- a/python/educational-python/resources/fileTemplates/internal/test_helper.py.ft +++ b/python/educational-python/resources/fileTemplates/internal/test_helper.py.ft @@ -120,39 +120,6 @@ def test_is_not_empty(): failed("The file is empty. Please, reload the task and try again.") -def test_is_initial_text(error_text="You should modify the file"): - """ - Checks that file was modified - """ - if not get_answer_placeholders(): - passed() - return - path = sys.argv[-1] - text = get_initial_text(path) - file_text = get_file_text(path) - - if file_text.strip() == text.strip(): - failed(error_text) - else: - passed() - - -def get_initial_text(path): - """ - Returns the initial task text - """ - course_lib = sys.argv[-2] - test_dir = sys.argv[-3] - import os - lesson_task = os.sep.join(test_dir.split(os.sep)[-3:-1]) - task_dir = os.sep.join(test_dir.split(os.sep)[:-1]) - relative_path = lesson_task + os.sep + path[len(task_dir):] - - # path format is "project_root/lessonX/taskY/.../file.py" - initial_file_path = os.path.join(course_lib, relative_path) - return get_file_text(initial_file_path) - - def test_text_equals(text, error_text): """ Checks that answer equals text. @@ -259,7 +226,6 @@ def do_not_run_on_check(): def run_common_tests(error_text="Please, reload file and try again"): - test_is_initial_text() test_is_not_empty() test_answer_placeholders_text_deleted() test_file_importable() diff --git a/python/educational-python/src/com/jetbrains/python/edu/PyExecuteFileLineMarkerProvider.java b/python/educational-python/src/com/jetbrains/python/edu/PyExecuteFileLineMarkerProvider.java index 8a5292e57cc9..ffc8c6372f42 100644 --- a/python/educational-python/src/com/jetbrains/python/edu/PyExecuteFileLineMarkerProvider.java +++ b/python/educational-python/src/com/jetbrains/python/edu/PyExecuteFileLineMarkerProvider.java @@ -71,7 +71,7 @@ public class PyExecuteFileLineMarkerProvider implements LineMarkerProvider { return PlatformUtils.isPyCharmEducational() ? text : actions.get(0).getTemplatePresentation().getText(); }, null, GutterIconRenderer.Alignment.RIGHT) { - @Nullable + @Override public GutterIconRenderer createGutterRenderer() { return new LineMarkerGutterIconRenderer(this) { diff --git a/python/educational-python/src/com/jetbrains/python/edu/debugger/PyEduConsoleInputFilterProvider.java b/python/educational-python/src/com/jetbrains/python/edu/debugger/PyEduConsoleInputFilterProvider.java index 1f191c949548..1e348cf533eb 100644 --- a/python/educational-python/src/com/jetbrains/python/edu/debugger/PyEduConsoleInputFilterProvider.java +++ b/python/educational-python/src/com/jetbrains/python/edu/debugger/PyEduConsoleInputFilterProvider.java @@ -7,7 +7,6 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.jetbrains.python.debugger.PyRunCythonExtensionsFilter; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.List; @@ -17,7 +16,6 @@ public class PyEduConsoleInputFilterProvider implements ConsoleInputFilterProvid @Override public InputFilter[] getDefaultFilters(@NotNull Project project) { return new InputFilter[]{new InputFilter() { - @Nullable @Override public List> applyFilter(String text, ConsoleViewContentType outputType) { if (outputType.equals(ConsoleViewContentType.SYSTEM_OUTPUT) && !text.contains("exit code")) {