diff --git a/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/CCRefactoringElementListenerProvider.java b/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/CCRefactoringElementListenerProvider.java index d828102bfb22..86121ab4340f 100644 --- a/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/CCRefactoringElementListenerProvider.java +++ b/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/CCRefactoringElementListenerProvider.java @@ -17,6 +17,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; @@ -68,7 +69,8 @@ public class CCRefactoringElementListenerProvider implements RefactoringElementL private static void tryToRenameTaskFile(PsiFile file, String oldName) { final PsiDirectory taskDir = file.getContainingDirectory(); - Course course = StudyTaskManager.getInstance(file.getProject()).getCourse(); + final Project project = file.getProject(); + Course course = StudyTaskManager.getInstance(project).getCourse(); if (course == null) { return; } diff --git a/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/CCUtils.java b/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/CCUtils.java index 6ac5ab418049..e1504ce29ede 100644 --- a/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/CCUtils.java +++ b/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/CCUtils.java @@ -221,7 +221,7 @@ public class CCUtils { if (child == null) { continue; } - Document patternDocument = StudyUtils.getPatternDocument(entry.getValue(), name); + Document patternDocument = StudyUtils.getPatternDocument(project, entry.getValue(), name); Document document = FileDocumentManager.getInstance().getDocument(child); if (document == null || patternDocument == null) { return; diff --git a/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/actions/CCAddAnswerPlaceholder.java b/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/actions/CCAddAnswerPlaceholder.java index 5eae370909ee..254d64dd534c 100644 --- a/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/actions/CCAddAnswerPlaceholder.java +++ b/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/actions/CCAddAnswerPlaceholder.java @@ -85,7 +85,7 @@ public class CCAddAnswerPlaceholder extends CCAnswerPlaceholderAction { } private static void computeInitialState(Project project, PsiFile file, TaskFile taskFile, Document document) { - Document patternDocument = StudyUtils.getPatternDocument(taskFile, file.getName()); + Document patternDocument = StudyUtils.getPatternDocument(project, taskFile, file.getName()); if (patternDocument == null) { return; } diff --git a/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/actions/CCHideFromStudent.java b/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/actions/CCHideFromStudent.java index 07cd46cfdf20..9ac18ba159a3 100644 --- a/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/actions/CCHideFromStudent.java +++ b/python/educational-core/course-creator/src/com/jetbrains/edu/coursecreator/actions/CCHideFromStudent.java @@ -24,7 +24,7 @@ public class CCHideFromStudent extends CCTaskFileActionBase { return; } String name = file.getName(); - VirtualFile patternFile = StudyUtils.getPatternFile(taskFile, name); + VirtualFile patternFile = StudyUtils.getPatternFile(project, taskFile, name); ApplicationManager.getApplication().runWriteAction(() -> { if (patternFile != null) { try { diff --git a/python/educational-core/student/resources/META-INF/plugin.xml b/python/educational-core/student/resources/META-INF/plugin.xml index 70219b1442b9..d950bc814f9b 100644 --- a/python/educational-core/student/resources/META-INF/plugin.xml +++ b/python/educational-core/student/resources/META-INF/plugin.xml @@ -87,11 +87,10 @@ - + diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/StudyBasePluginConfigurator.java b/python/educational-core/student/src/com/jetbrains/edu/learning/StudyBasePluginConfigurator.java index 4fde0a889748..9fbfb736a5e4 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/StudyBasePluginConfigurator.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/StudyBasePluginConfigurator.java @@ -66,6 +66,7 @@ public abstract class StudyBasePluginConfigurator implements StudyPluginConfigur Task task = getTask(file); setTaskText(task, file.getParent()); } + toolWindow.setBottomComponent(null); } @Nullable diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/StudyTaskManager.java b/python/educational-core/student/src/com/jetbrains/edu/learning/StudyTaskManager.java index 0ee3584adfd6..e017f69e13c0 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/StudyTaskManager.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/StudyTaskManager.java @@ -14,6 +14,7 @@ import com.intellij.util.xmlb.XmlSerializer; import com.jetbrains.edu.learning.core.EduUtils; import com.jetbrains.edu.learning.courseFormat.*; import com.jetbrains.edu.learning.oldCourseFormat.OldCourse; +import com.jetbrains.edu.learning.stepic.StepicUser; import com.jetbrains.edu.learning.ui.StudyToolWindow; import org.jdom.Element; import org.jetbrains.annotations.NotNull; @@ -34,6 +35,7 @@ public class StudyTaskManager implements PersistentStateComponent, Dumb private Course myCourse; private OldCourse myOldCourse; public int VERSION = 2; + public StepicUser myUser; public Map myStudyStatusMap = new HashMap<>(); public Map myTaskStatusMap = new HashMap<>(); public Map> myUserTests = new HashMap<>(); @@ -74,7 +76,7 @@ public class StudyTaskManager implements PersistentStateComponent, Dumb @NotNull public List getUserTests(@NotNull final Task task) { final List userTests = myUserTests.get(task); - return userTests != null ? userTests : Collections.emptyList(); + return userTests != null ? userTests : Collections.emptyList(); } public void removeUserTest(@NotNull final Task task, @NotNull final UserTest userTest) { @@ -212,6 +214,7 @@ public class StudyTaskManager implements PersistentStateComponent, Dumb myTaskStatusMap = taskManager.myTaskStatusMap; myStudyStatusMap = taskManager.myStudyStatusMap; myShouldUseJavaFx = taskManager.myShouldUseJavaFx; + myUser = taskManager.getUser(); } } final Element oldCourseElement = state.getChild(COURSE_ELEMENT); @@ -276,4 +279,38 @@ public class StudyTaskManager implements PersistentStateComponent, Dumb public void setTurnEditingMode(boolean turnEditingMode) { myTurnEditingMode = turnEditingMode; } + + public String getLogin() { + if (myUser != null) { + return myUser.getEmail(); + } + return ""; + } + + public String getPassword() { + if (myUser != null) { + return myUser.getPassword(); + } + return ""; + } + + public void setLogin(String login) { + if (myUser != null) { + myUser.setEmail(login); + } + } + + public void setPassword(String password) { + if (myUser != null) { + myUser.setPassword(password); + } + } + + public StepicUser getUser() { + return myUser; + } + + public void setUser(StepicUser user) { + myUser = user; + } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/StudyUtils.java b/python/educational-core/student/src/com/jetbrains/edu/learning/StudyUtils.java index 78f8a7133005..175007d52afc 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/StudyUtils.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/StudyUtils.java @@ -20,7 +20,9 @@ import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; +import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.util.Disposer; @@ -36,6 +38,7 @@ import com.intellij.psi.PsiFile; import com.intellij.ui.JBColor; import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.content.Content; +import com.intellij.util.TimeoutUtil; import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.text.MarkdownUtil; @@ -47,6 +50,7 @@ import com.jetbrains.edu.learning.core.EduAnswerPlaceholderPainter; 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.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.editor.StudyEditor; import com.jetbrains.edu.learning.ui.StudyProgressToolWindowFactory; import com.jetbrains.edu.learning.ui.StudyToolWindow; @@ -61,6 +65,9 @@ import java.io.*; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; public class StudyUtils { private StudyUtils() { @@ -358,12 +365,12 @@ public class StudyUtils { @Nullable - public static VirtualFile getPatternFile(@NotNull TaskFile taskFile, String name) { + public static VirtualFile getPatternFile(@NotNull Project project, @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()); + File resourceFile = getCourseDirectory(project, course); if (!resourceFile.exists()) { return null; } @@ -376,8 +383,8 @@ public class StudyUtils { } @Nullable - public static Document getPatternDocument(@NotNull final TaskFile taskFile, String name) { - VirtualFile patternFile = getPatternFile(taskFile, name); + public static Document getPatternDocument(@NotNull Project project, @NotNull final TaskFile taskFile, String name) { + VirtualFile patternFile = getPatternFile(project, taskFile, name); if (patternFile == null) { return null; } @@ -495,7 +502,7 @@ public class StudyUtils { return null; } - + @Nullable public static String getTaskText(@NotNull final Project project) { TaskFile taskFile = getSelectedTaskFile(project); if (taskFile == null) { @@ -519,12 +526,33 @@ public class StudyUtils { } return taskFile; } + + @Nullable + public static Task getCurrentTask(@NotNull final Project project) { + VirtualFile[] files = FileEditorManager.getInstance(project).getSelectedFiles(); + TaskFile taskFile = null; + for (VirtualFile file : files) { + taskFile = getTaskFile(project, file); + if (taskFile != null) { + break; + } + } + if (taskFile != null) { + return taskFile.getTask(); + } + return null; + } public static void updateStudyToolWindow(Project project) { final StudyToolWindow studyToolWindow = getStudyToolWindow(project); if (studyToolWindow != null) { String taskText = getTaskText(project); - studyToolWindow.setTaskText(taskText, null, project); + if (taskText != null) { + studyToolWindow.setTaskText(taskText, null, project); + } + else { + LOG.warn("Task text is null"); + } } } @@ -532,6 +560,32 @@ public class StudyUtils { return StudyTaskManager.getInstance(project).getCourse() != null; } + @Nullable + public static Project getStudyProject() { + Project studyProject = null; + Project[] openProjects = ProjectManager.getInstance().getOpenProjects(); + for (Project project : openProjects) { + if (StudyTaskManager.getInstance(project).getCourse() != null) { + studyProject = project; + } + } + return studyProject; + } + + @NotNull + public static File getCourseDirectory(@NotNull Project project, Course course) { + final File courseDirectory; + if (course.isAdaptive()) { + courseDirectory = new File(StudyProjectGenerator.OUR_COURSES_DIR, + StudyProjectGenerator.ADAPTIVE_COURSE_PREFIX + course.getName() + + "_" + StudyTaskManager.getInstance(project).getUser().getEmail()); + } + else { + courseDirectory = new File(StudyProjectGenerator.OUR_COURSES_DIR, course.getName()); + } + return courseDirectory; + } + public static boolean hasJavaFx() { try { Class.forName("javafx.application.Platform"); @@ -575,6 +629,36 @@ public class StudyUtils { return null; } + // supposd to be called under progress + @Nullable + public static T execCancelable(@NotNull final Callable callable) { + final Future future = ApplicationManager.getApplication().executeOnPooledThread(callable); + + while (!future.isCancelled() && !future.isDone()) { + ProgressManager.checkCanceled(); + TimeoutUtil.sleep(500); + } + T result = null; + try { + result = future.get(); + } + catch (InterruptedException | ExecutionException e) { + LOG.warn(e.getMessage()); + } + return result; + } + + @Nullable + public static Task getTaskFromSelectedEditor(Project project) { + final StudyEditor editor = getSelectedStudyEditor(project); + Task task = null; + if (editor != null) { + final TaskFile file = editor.getTaskFile(); + task = file.getTask(); + } + return task; + } + private static String convertToHtml(@NotNull final String content) { ArrayList lines = ContainerUtil.newArrayList(content.split("\n|\r|\r\n")); MarkdownUtil.replaceHeaders(lines); diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/actions/StudyRefreshAnswerPlaceholder.java b/python/educational-core/student/src/com/jetbrains/edu/learning/actions/StudyRefreshAnswerPlaceholder.java index fc3acfd7870a..b05088812f2c 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/actions/StudyRefreshAnswerPlaceholder.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/actions/StudyRefreshAnswerPlaceholder.java @@ -45,7 +45,7 @@ public class StudyRefreshAnswerPlaceholder extends DumbAwareAction { } StudyEditor studyEditor = StudyUtils.getSelectedStudyEditor(project); final StudyState studyState = new StudyState(studyEditor); - Document patternDocument = StudyUtils.getPatternDocument(answerPlaceholder.getTaskFile(), studyState.getVirtualFile().getName()); + Document patternDocument = StudyUtils.getPatternDocument(project, answerPlaceholder.getTaskFile(), studyState.getVirtualFile().getName()); if (patternDocument == null) { return; } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/actions/StudyRefreshTaskFileAction.java b/python/educational-core/student/src/com/jetbrains/edu/learning/actions/StudyRefreshTaskFileAction.java index b19b56a3578d..cb47386d2a4b 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/actions/StudyRefreshTaskFileAction.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/actions/StudyRefreshTaskFileAction.java @@ -81,7 +81,7 @@ public class StudyRefreshTaskFileAction extends StudyActionWithShortcut { @NotNull final Project project, TaskFile taskFile, String name) { - if (!resetDocument(document, taskFile, name)) { + if (!resetDocument(project, document, taskFile, name)) { return false; } taskFile.getTask().setStatus(StudyStatus.Unchecked); @@ -110,10 +110,11 @@ public class StudyRefreshTaskFileAction extends StudyActionWithShortcut { } - private static boolean resetDocument(@NotNull final Document document, + private static boolean resetDocument(@NotNull final Project project, + @NotNull final Document document, @NotNull final TaskFile taskFile, String fileName) { - final Document patternDocument = StudyUtils.getPatternDocument(taskFile, fileName); + final Document patternDocument = StudyUtils.getPatternDocument(project, taskFile, fileName); if (patternDocument == null) { return false; } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckTask.java b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckTask.java index f8302304b9b0..965347321167 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckTask.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckTask.java @@ -8,6 +8,7 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.MessageType; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; @@ -17,11 +18,13 @@ import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.actions.StudyAfterCheckAction; import com.jetbrains.edu.learning.core.EduUtils; +import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.courseFormat.StudyStatus; import com.jetbrains.edu.learning.courseFormat.Task; +import com.jetbrains.edu.learning.stepic.EduAdaptiveStepicConnector; import com.jetbrains.edu.learning.stepic.EduStepicConnector; -import com.jetbrains.edu.learning.stepic.StudySettings; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class StudyCheckTask extends com.intellij.openapi.progress.Task.Backgroundable { @@ -70,57 +73,132 @@ public class StudyCheckTask extends com.intellij.openapi.progress.Task.Backgroun @Override public void run(@NotNull ProgressIndicator indicator) { + final Course course = StudyTaskManager.getInstance(myProject).getCourse(); + if (course != null && course.isAdaptive()) { + checkForAdaptiveCourse(indicator); + } + else { + checkForEduCourse(indicator); + } + } + + private void checkForEduCourse(@NotNull ProgressIndicator indicator) { + final StudyTestsOutputParser.TestsOutput testsOutput = getTestOutput(indicator); + + if (testsOutput != null) { + if (testsOutput.isSuccess()) { + onTaskSolved(testsOutput.getMessage()); + } + else { + onTaskFailed(testsOutput.getMessage()); + } + runAfterTaskCheckedActions(); + postAttemptToStepic(testsOutput); + } + } + + @Nullable + private StudyTestsOutputParser.TestsOutput getTestOutput(@NotNull ProgressIndicator indicator) { final CapturingProcessHandler handler = new CapturingProcessHandler(myTestProcess, null, myCommandLine); final ProcessOutput output = handler.runProcessWithProgressIndicator(indicator); if (indicator.isCanceled()) { ApplicationManager.getApplication().invokeLater( () -> StudyCheckUtils.showTestResultPopUp("Check cancelled", MessageType.WARNING.getPopupBackground(), myProject)); - return; } - - final StudyTestsOutputParser.TestsOutput testsOutput = StudyTestsOutputParser.getTestsOutput(output); - String stderr = output.getStderr(); - if (!stderr.isEmpty()) { - ApplicationManager.getApplication().invokeLater(() -> - StudyCheckUtils.showTestResultPopUp("Failed to launch checking", - MessageType.WARNING.getPopupBackground(), - myProject)); - //log error output of tests - LOG.info("#educational " + stderr); - return; + final Course course = StudyTaskManager.getInstance(myProject).getCourse(); + if (course != null) { + final StudyTestsOutputParser.TestsOutput testsOutput = StudyTestsOutputParser.getTestsOutput(output, course.isAdaptive()); + String stderr = output.getStderr(); + if (!stderr.isEmpty()) { + ApplicationManager.getApplication().invokeLater(() -> + StudyCheckUtils.showTestResultPopUp("Failed to launch checking", + MessageType.WARNING.getPopupBackground(), + myProject)); + //log error output of tests + LOG.info("#educational " + stderr); + return null; + } + return testsOutput; } - - postAttemptToStepic(testsOutput); - - - if (testsOutput.isSuccess()) { - onTaskSolved(testsOutput); - } - else { - onTaskFailed(testsOutput); - } - runAfterTaskSolvedActions(); + return null; } - protected void onTaskFailed(StudyTestsOutputParser.TestsOutput testsOutput) { + private void checkForAdaptiveCourse(ProgressIndicator indicator) { + final StudyTestsOutputParser.TestsOutput testOutput = getTestOutput(indicator); + if (testOutput != null) { + if (testOutput.isSuccess()) { + final Pair pair = EduAdaptiveStepicConnector.checkTask(myProject, myTask); + if (pair != null && !(!pair.getFirst() && pair.getSecond().isEmpty())) { + if (pair.getFirst()) { + onTaskSolved("Congratulations! Remote tests passed."); + if (myStatusBeforeCheck != StudyStatus.Solved) { + EduAdaptiveStepicConnector.addNextRecommendedTask(myProject, 2); + } + } + else { + final String checkMessage = pair.getSecond(); + onTaskFailed(checkMessage); + } + runAfterTaskCheckedActions(); + } + else { + ApplicationManager.getApplication().invokeLater(() -> StudyCheckUtils.showTestResultPopUp("Failed to launch checking", + MessageType.WARNING + .getPopupBackground(), + myProject)); + } + } + else { + onTaskFailed(testOutput.getMessage()); + } + } + } + + protected void onTaskFailed(String message) { myTaskManger.setStatus(myTask, StudyStatus.Failed); - ApplicationManager.getApplication().invokeLater( - () -> StudyCheckUtils.showTestResultPopUp(testsOutput.getMessage(), MessageType.ERROR.getPopupBackground(), myProject)); + final Course course = StudyTaskManager.getInstance(myProject).getCourse(); + + if (course != null) { + if (course.isAdaptive()) { + ApplicationManager.getApplication().invokeLater( + () -> { + StudyCheckUtils.showTestResultPopUp("Failed", MessageType.ERROR.getPopupBackground(), myProject); + StudyCheckUtils.showTestResultsToolWindow(myProject, message, false); + }); + } + else { + ApplicationManager.getApplication() + .invokeLater(() -> StudyCheckUtils.showTestResultPopUp(message, MessageType.ERROR.getPopupBackground(), myProject)); + } + } } - protected void onTaskSolved(StudyTestsOutputParser.TestsOutput testsOutput) { + protected void onTaskSolved(String message) { myTaskManger.setStatus(myTask, StudyStatus.Solved); - ApplicationManager.getApplication().invokeLater( - () -> StudyCheckUtils.showTestResultPopUp(testsOutput.getMessage(), MessageType.INFO.getPopupBackground(), myProject)); + final Course course = StudyTaskManager.getInstance(myProject).getCourse(); + + if (course != null) { + if (course.isAdaptive()) { + ApplicationManager.getApplication().invokeLater( + () -> { + StudyCheckUtils.showTestResultPopUp("Congratulations!", MessageType.INFO.getPopupBackground(), myProject); + StudyCheckUtils.showTestResultsToolWindow(myProject, message, true); + }); + } + else { + ApplicationManager.getApplication() + .invokeLater(() -> StudyCheckUtils.showTestResultPopUp(message, MessageType.INFO.getPopupBackground(), myProject)); + } + } } - private void runAfterTaskSolvedActions() { + private void runAfterTaskCheckedActions() { StudyPluginConfigurator configurator = StudyUtils.getConfigurator(myProject); if (configurator != null) { StudyAfterCheckAction[] checkActions = configurator.getAfterCheckActions(); if (checkActions != null) { - for (StudyAfterCheckAction action: checkActions) { + for (StudyAfterCheckAction action : checkActions) { action.run(myProject, myTask, myStatusBeforeCheck); } } @@ -130,8 +208,8 @@ public class StudyCheckTask extends com.intellij.openapi.progress.Task.Backgroun } } - protected void postAttemptToStepic(StudyTestsOutputParser.TestsOutput testsOutput) { - final StudySettings studySettings = StudySettings.getInstance(); + protected void postAttemptToStepic(@NotNull StudyTestsOutputParser.TestsOutput testsOutput) { + final StudyTaskManager studySettings = StudyTaskManager.getInstance(myProject); final String login = studySettings.getLogin(); final String password = StringUtil.isEmptyOrSpaces(login) ? "" : studySettings.getPassword(); EduStepicConnector.postAttempt(myTask, testsOutput.isSuccess(), login, password); diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckUtils.java b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckUtils.java index b1626645bf7e..b91bc4773ba4 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckUtils.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyCheckUtils.java @@ -1,5 +1,6 @@ package com.jetbrains.edu.learning.checker; +import com.intellij.execution.ui.ConsoleViewContentType; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; @@ -15,24 +16,26 @@ import com.intellij.openapi.ui.popup.BalloonBuilder; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.wm.IdeFocusManager; -import com.intellij.openapi.wm.IdeFrame; -import com.intellij.openapi.wm.WindowManager; +import com.intellij.openapi.wm.*; import com.intellij.openapi.wm.ex.StatusBarEx; import com.intellij.openapi.wm.ex.WindowManagerEx; +import com.intellij.ui.content.Content; +import com.jetbrains.edu.learning.StudyState; +import com.jetbrains.edu.learning.StudyTaskManager; +import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduDocumentListener; 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.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; -import com.jetbrains.edu.learning.StudyState; -import com.jetbrains.edu.learning.StudyTaskManager; -import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.editor.StudyEditor; import com.jetbrains.edu.learning.navigation.StudyNavigator; +import com.jetbrains.edu.learning.ui.StudyTestResultsToolWindowFactory; +import com.jetbrains.python.console.PythonConsoleView; import org.jetbrains.annotations.NotNull; +import javax.swing.*; import java.awt.*; import java.io.IOException; import java.util.List; @@ -61,9 +64,9 @@ public class StudyCheckUtils { } public static void navigateToFailedPlaceholder(@NotNull final StudyState studyState, - @NotNull final Task task, - @NotNull final VirtualFile taskDir, - @NotNull final Project project) { + @NotNull final Task task, + @NotNull final VirtualFile taskDir, + @NotNull final Project project) { TaskFile selectedTaskFile = studyState.getTaskFile(); Editor editor = studyState.getEditor(); TaskFile taskFileToNavigate = selectedTaskFile; @@ -109,10 +112,10 @@ public class StudyCheckUtils { public static void runSmartTestProcess(@NotNull final VirtualFile taskDir, - @NotNull final StudyTestRunner testRunner, - final String taskFileName, - @NotNull final TaskFile taskFile, - @NotNull final Project project) { + @NotNull final StudyTestRunner testRunner, + final String taskFileName, + @NotNull final TaskFile taskFile, + @NotNull final Project project) { final TaskFile answerTaskFile = new TaskFile(); answerTaskFile.name = taskFileName; final VirtualFile virtualFile = taskDir.findChild(taskFileName); @@ -135,11 +138,10 @@ public class StudyCheckUtils { } - private static VirtualFile getCopyWithAnswers(@NotNull final VirtualFile taskDir, - @NotNull final VirtualFile file, - @NotNull final TaskFile source, - @NotNull final TaskFile target) { + @NotNull final VirtualFile file, + @NotNull final TaskFile source, + @NotNull final TaskFile target) { VirtualFile copy = null; try { @@ -191,4 +193,31 @@ public class StudyCheckUtils { EduUtils.flushWindows(taskFile, virtualFile, true); } } + + public static void showTestResultsToolWindow(@NotNull final Project project, @NotNull final String message, boolean solved) { + final ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project); + ToolWindow window = toolWindowManager.getToolWindow("Test Results"); + if (window == null) { + toolWindowManager.registerToolWindow("Test Results", true, ToolWindowAnchor.BOTTOM); + window = toolWindowManager.getToolWindow("Test Results"); + new StudyTestResultsToolWindowFactory().createToolWindowContent(project, window); + } + + final Content[] contents = window.getContentManager().getContents(); + for (Content content : contents) { + final JComponent component = content.getComponent(); + if (component instanceof PythonConsoleView) { + ((PythonConsoleView)component).clear(); + if (!solved) { + ((PythonConsoleView)component).print(message, ConsoleViewContentType.ERROR_OUTPUT); + } + else { + ((PythonConsoleView)component).print(message, ConsoleViewContentType.NORMAL_OUTPUT); + } + window.setAvailable(true, () -> {}); + window.show(() -> {}); + return; + } + } + } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudySmartChecker.java b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudySmartChecker.java index 7bea87d39e51..c75904d0e3a5 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudySmartChecker.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudySmartChecker.java @@ -14,6 +14,7 @@ import com.jetbrains.edu.learning.core.EduDocumentListener; 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.StudyStatus; import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.StudyTaskManager; @@ -65,14 +66,17 @@ public class StudySmartChecker { Process smartTestProcess = testRunner.createCheckProcess(project, windowCopy.getPath()); final CapturingProcessHandler handler = new CapturingProcessHandler(smartTestProcess, null, windowCopy.getPath()); final ProcessOutput output = handler.runProcess(); - boolean res = StudyTestsOutputParser.getTestsOutput(output).isSuccess(); - StudyTaskManager.getInstance(project).setStatus(userAnswerPlaceholder, res ? StudyStatus.Solved : StudyStatus.Failed); - StudyUtils.deleteFile(windowCopy); - if (fileWindows != null) { - StudyUtils.deleteFile(fileWindows); - } - if (!resourceFile.delete()) { - LOG.error("failed to delete", resourceFile.getPath()); + final Course course = StudyTaskManager.getInstance(project).getCourse(); + if (course != null) { + boolean res = StudyTestsOutputParser.getTestsOutput(output, course.isAdaptive()).isSuccess(); + StudyTaskManager.getInstance(project).setStatus(userAnswerPlaceholder, res ? StudyStatus.Solved : StudyStatus.Failed); + StudyUtils.deleteFile(windowCopy); + if (fileWindows != null) { + StudyUtils.deleteFile(fileWindows); + } + if (!resourceFile.delete()) { + LOG.error("failed to delete", resourceFile.getPath()); + } } } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyTestsOutputParser.java b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyTestsOutputParser.java index 55bfe7273bac..90cf27d4c958 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyTestsOutputParser.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/checker/StudyTestsOutputParser.java @@ -3,8 +3,10 @@ package com.jetbrains.edu.learning.checker; import com.intellij.execution.process.ProcessOutput; import org.jetbrains.annotations.NotNull; +import java.util.List; + public class StudyTestsOutputParser { - private static final String ourStudyPrefix = "#educational_plugin"; + private static final String STUDY_PREFIX = "#educational_plugin"; public static final String TEST_OK = "test OK"; private static final String TEST_FAILED = "FAILED + "; private static final String CONGRATS_MESSAGE = "CONGRATS_MESSAGE "; @@ -29,10 +31,12 @@ public class StudyTestsOutputParser { } @NotNull - public static TestsOutput getTestsOutput(@NotNull final ProcessOutput processOutput) { + public static TestsOutput getTestsOutput(@NotNull final ProcessOutput processOutput, final boolean isAdaptive) { String congratulations = CONGRATULATIONS; - for (String line : processOutput.getStdoutLines()) { - if (line.startsWith(ourStudyPrefix)) { + final List lines = processOutput.getStdoutLines(); + for (int i = 0; i < lines.size(); i++) { + final String line = lines.get(i); + if (line.startsWith(STUDY_PREFIX)) { if (line.contains(TEST_OK)) { continue; } @@ -42,7 +46,23 @@ public class StudyTestsOutputParser { } if (line.contains(TEST_FAILED)) { - return new TestsOutput(false, line.substring(line.indexOf(TEST_FAILED) + TEST_FAILED.length())); + if (!isAdaptive) { + return new TestsOutput(false, line.substring(line.indexOf(TEST_FAILED) + TEST_FAILED.length())); + } + else { + final StringBuilder builder = new StringBuilder(line.substring(line.indexOf(TEST_FAILED) + TEST_FAILED.length()) + "\n"); + for (int j = i + 1; j < lines.size(); j++) { + final String failedTextLine = lines.get(j); + if (!failedTextLine.contains(STUDY_PREFIX) || !failedTextLine.contains(CONGRATS_MESSAGE)) { + builder.append(failedTextLine); + builder.append("\n"); + } + else { + break; + } + } + return new TestsOutput(false, builder.toString()); + } } } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Course.java b/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Course.java index 162011126916..03dbc110882c 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Course.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Course.java @@ -7,31 +7,27 @@ import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.Function; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.core.EduUtils; -import com.jetbrains.edu.learning.stepic.CourseInfo; +import com.jetbrains.edu.learning.stepic.StepicUser; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; public class Course { - @Expose - private List lessons = new ArrayList(); - + @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 List authors = new ArrayList(); - private boolean myUpToDate; + @Expose private String myCourseDirectory = ""; + @Expose private int id; + @Expose private boolean myUpToDate; + @Expose private boolean isAdaptive; + @Expose @SerializedName("language") private String myLanguage = "Python"; - @Expose @SerializedName("language") - private String myLanguage = "Python"; - - //this field is used to distinguish ordinary and CheckIO projects + //this field is used to distinguish ordinary and CheckIO projects, //"PyCharm" is used here for historical reasons - private String courseType = EduNames.PYCHARM; - - //this field is used to distinguish study and course creator modes - private String courseMode = EduNames.STUDY; + @Expose private String courseType = EduNames.PYCHARM; + @Expose private String courseMode = EduNames.STUDY; //this field is used to distinguish study and course creator modes /** * Initializes state of course @@ -72,20 +68,20 @@ public class Course { } @NotNull - public List getAuthors() { + public List getAuthors() { return authors; } - public static String getAuthorsString(@NotNull List authors) { + public static String getAuthorsString(@NotNull List authors) { return StringUtil.join(authors, author -> author.getName(), ", "); } public void setAuthors(String[] authors) { - this.authors = new ArrayList(); + this.authors = new ArrayList(); for (String name : authors) { final List pair = StringUtil.split(name, " "); if (!pair.isEmpty()) - this.authors.add(new CourseInfo.Author(pair.get(0), pair.size() > 1 ? pair.get(1) : "")); + this.authors.add(new StepicUser(pair.get(0), pair.size() > 1 ? pair.get(1) : "")); } } @@ -133,7 +129,7 @@ public class Course { myLanguage = language; } - public void setAuthors(List authors) { + public void setAuthors(List authors) { this.authors = authors; } @@ -146,6 +142,22 @@ public class Course { this.courseType = courseType; } + public boolean isAdaptive() { + return isAdaptive; + } + + public void setAdaptive(boolean adaptive) { + isAdaptive = adaptive; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + public String getCourseMode() { return courseMode; } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Lesson.java b/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Lesson.java index 5b2f6acf9f22..4110dd621c28 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Lesson.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Lesson.java @@ -11,25 +11,13 @@ import java.util.ArrayList; import java.util.List; public class Lesson implements StudyItem { - public int id; - @Transient - public List steps; - @Transient - public List tags; - @Transient - Boolean is_public; - @Expose - @SerializedName("title") - private String name; - @Expose - @SerializedName("task_list") - public List taskList = new ArrayList(); - - @Transient - private Course myCourse = null; - - // index is visible to user number of lesson from 1 to lesson number - private int myIndex = -1; + @Expose public int id; + @Expose private int myIndex = -1; // index is visible to user number of lesson from 1 to lesson number + @Expose @Transient public List steps; + @Expose @SerializedName("title") private String name; + @Expose @SerializedName("task_list") public List taskList = new ArrayList(); + @Transient public List tags; + @Transient private Course myCourse = null; public void initLesson(final Course course, boolean isRestarted) { setCourse(course); diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Task.java b/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Task.java index 71ca327a45be..8cb75e13ee3c 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Task.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/Task.java @@ -20,22 +20,13 @@ import java.util.Map; * Implementation of task which contains task files, tests, input file for tests */ public class Task implements StudyItem { - @Expose - private String name; - - // index is visible to user number of task from 1 to task number - private int myIndex; - private StudyStatus myStatus = StudyStatus.Uninitialized; - - private int myStepicId; - - @Expose - @SerializedName("task_files") - public Map taskFiles = new HashMap(); - - private String text; - private Map testsText = new HashMap(); - + @Expose private String name; + @Expose private String text; + @Expose private int myStepicId; + @Expose private int myIndex; // index is visible to user number of task from 1 to task number + @Expose @SerializedName("task_files") public Map taskFiles = new HashMap(); + @Expose private Map testsText = new HashMap(); + @Expose private StudyStatus myStatus = StudyStatus.Uninitialized; @Transient private Lesson myLesson; public Task() {} diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/TaskFile.java b/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/TaskFile.java index 33e00d574325..65ef81d5e6b5 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/TaskFile.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/courseFormat/TaskFile.java @@ -18,19 +18,14 @@ import java.util.List; */ public class TaskFile { - @SerializedName("placeholders") - @Expose - private List myAnswerPlaceholders = new ArrayList(); - private int myIndex = -1; - - @Expose - public String name; - @Expose - public String text; + @Expose public String name; + @Expose public String text; + @Expose private int myIndex = -1; + @Expose private boolean myUserCreated = false; + @Expose private boolean myTrackChanges = true; + @Expose private boolean myHighlightErrors = false; + @Expose @SerializedName("placeholders") private List myAnswerPlaceholders = new ArrayList(); @Transient private Task myTask; - private boolean myUserCreated = false; - private boolean myTrackChanges = true; - private boolean myHighlightErrors = false; public void initTaskFile(final Task task, boolean isRestarted) { setTask(task); diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java b/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java index 57b84b9f6df1..6e031380977b 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyGenerator.java @@ -4,13 +4,13 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.io.FileUtil; 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.courseFormat.Course; import com.jetbrains.edu.learning.courseFormat.Lesson; import com.jetbrains.edu.learning.courseFormat.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; -import com.jetbrains.edu.learning.StudyTaskManager; -import com.jetbrains.edu.learning.StudyUtils; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -106,30 +106,30 @@ public class StudyGenerator { public static void createCourse(@NotNull final Course course, @NotNull final VirtualFile baseDir, @NotNull final File resourceRoot, @NotNull final Project project) { - try { - final List lessons = course.getLessons(); - for (int i = 1; i <= lessons.size(); i++) { - Lesson lesson = lessons.get(i - 1); - lesson.setIndex(i); - createLesson(lesson, baseDir, resourceRoot, project); - } - baseDir.createChildDirectory(project, EduNames.SANDBOX_DIR); - File[] files = resourceRoot.listFiles( - (dir, name) -> !name.contains(EduNames.LESSON) && !name.equals(EduNames.COURSE_META_FILE) && !name.equals(EduNames.HINTS)); - for (File file : files) { - File dir = new File(baseDir.getPath(), file.getName()); - if (file.isDirectory()) { - FileUtil.copyDir(file, dir); - continue; - } - - FileUtil.copy(file, dir); - - } - } - catch (IOException e) { - LOG.error(e); - } + try { + final List lessons = course.getLessons(); + for (int i = 1; i <= lessons.size(); i++) { + Lesson lesson = lessons.get(i - 1); + lesson.setIndex(i); + createLesson(lesson, baseDir, resourceRoot, project); + } + baseDir.createChildDirectory(project, EduNames.SANDBOX_DIR); + File[] files = resourceRoot.listFiles( + (dir, name) -> !name.contains(EduNames.LESSON) && !name.equals(EduNames.COURSE_META_FILE) && !name.equals(EduNames.HINTS)); + for (File file : files) { + File dir = new File(baseDir.getPath(), file.getName()); + if (file.isDirectory()) { + FileUtil.copyDir(file, dir); + continue; + } + + FileUtil.copy(file, dir); + + } + } + catch (IOException e) { + LOG.error(e); + } } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java b/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java index d87b5be1b4fa..2200c7b791cb 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java @@ -8,10 +8,12 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.DumbModePermission; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.ThrowableComputable; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; @@ -22,38 +24,40 @@ 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.jetbrains.edu.learning.StudyProjectComponent; +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.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; -import com.jetbrains.edu.learning.StudyProjectComponent; -import com.jetbrains.edu.learning.StudyTaskManager; -import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.stepic.CourseInfo; 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.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; + +import static com.jetbrains.edu.learning.StudyUtils.execCancelable; public class StudyProjectGenerator { - private static final Logger LOG = Logger.getInstance(StudyProjectGenerator.class.getName()); - private final List myListeners = ContainerUtil.newArrayList(); - protected static final File ourCoursesDir = new File(PathManager.getConfigPath(), "courses"); - private static final String CACHE_NAME = "courseNames.txt"; - private List myCourses = new ArrayList<>(); - protected CourseInfo mySelectedCourseInfo; - private static final String COURSE_NAME_ATTRIBUTE = "name"; - private static final String COURSE_DESCRIPTION = "description"; 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(); + public StepicUser myUser; + private List myCourses = new ArrayList<>(); + protected CourseInfo mySelectedCourseInfo; public void setCourses(List courses) { myCourses = courses; @@ -64,48 +68,67 @@ public class StudyProjectGenerator { } public void generateProject(@NotNull final Project project, @NotNull final VirtualFile baseDir) { - final Course course = getCourse(); + StudyTaskManager.getInstance(project).setUser(myUser); + final Course course = getCourse(project); if (course == null) { LOG.warn("Course is null"); return; } + final File courseDirectory = StudyUtils.getCourseDirectory(project, course); StudyTaskManager.getInstance(project).setCourse(course); ApplicationManager.getApplication().invokeLater( () -> DumbService.allowStartingDumbModeInside(DumbModePermission.MAY_START_BACKGROUND, () -> ApplicationManager.getApplication().runWriteAction(() -> { - course.initCourse(false); - final File courseDirectory = new File(ourCoursesDir, course.getName()); StudyGenerator.createCourse(course, baseDir, courseDirectory, project); - course.setCourseDirectory(new File(ourCoursesDir, mySelectedCourseInfo.getName()).getAbsolutePath()); + course.setCourseDirectory(courseDirectory.getAbsolutePath()); VirtualFileManager.getInstance().refreshWithoutFileWatcher(true); StudyProjectComponent.getInstance(project).registerStudyToolWindow(course); openFirstTask(course, project); }))); } - protected Course getCourse() { - Reader reader = null; - try { - final File courseFile = new File(new File(ourCoursesDir, mySelectedCourseInfo.getName()), EduNames.COURSE_META_FILE); - if (courseFile.exists()) { - reader = new InputStreamReader(new FileInputStream(courseFile), "UTF-8"); - Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); - final Course course = gson.fromJson(reader, Course.class); - course.initCourse(false); - return course; + @Nullable + protected 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()) { + return readCourseFromCache(courseFile, false); + } + else if (myUser != null) { + final File adaptiveCourseFile = new File(new File(OUR_COURSES_DIR, ADAPTIVE_COURSE_PREFIX + + mySelectedCourseInfo.getName() + "_" + + myUser.getEmail()), EduNames.COURSE_META_FILE); + if (adaptiveCourseFile.exists()) { + return readCourseFromCache(adaptiveCourseFile, true); } } - catch (FileNotFoundException | UnsupportedEncodingException e) { - LOG.error(e); + final Course course = EduStepicConnector.getCourse(project, mySelectedCourseInfo); + if (course != null) { + flushCourse(project, course); + course.initCourse(false); + } + return course; + } + + @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().create(); + final Course course = gson.fromJson(reader, Course.class); + course.initCourse(isAdaptive); + return course; + } + catch (UnsupportedEncodingException e) { + LOG.warn(e.getMessage()); + } + catch (FileNotFoundException e) { + LOG.warn(e.getMessage()); } finally { StudyUtils.closeSilently(reader); } - final Course course = EduStepicConnector.getCourse(mySelectedCourseInfo); - if (course != null) { - flushCourse(course); - } - return course; + return null; } public static void openFirstTask(@NotNull final Course course, @NotNull final Project project) { @@ -131,7 +154,8 @@ public class StudyProjectGenerator { final PsiFile file = PsiManager.getInstance(project).findFile(activeVirtualFile); ProjectView.getInstance(project).select(file, activeVirtualFile, true); FileEditorManager.getInstance(project).openFile(activeVirtualFile, true); - } else { + } + else { String first = StudyUtils.getFirst(taskFiles.keySet()); if (first != null) { NewVirtualFile firstFile = ((VirtualDirectoryImpl)taskDir).refreshAndFindChild(first); @@ -142,8 +166,8 @@ public class StudyProjectGenerator { } } - public void flushCourse(@NotNull final Course course) { - final File courseDirectory = new File(ourCoursesDir, course.getName()); + public static void flushCourse(@NotNull final Project project, @NotNull final Course course) { + final File courseDirectory = StudyUtils.getCourseDirectory(project, course); FileUtil.createDirectory(courseDirectory); flushCourseJson(course, courseDirectory); @@ -208,7 +232,6 @@ public class StudyProjectGenerator { else { FileUtil.writeToFile(file, taskFile.text); } - } catch (IOException e) { LOG.error("ERROR copying file " + name); @@ -217,9 +240,12 @@ public class StudyProjectGenerator { final Map testsText = task.getTestsText(); for (Map.Entry entry : testsText.entrySet()) { final File testsFile = new File(taskDirectory, entry.getKey()); + if (testsFile.exists()) { + FileUtil.delete(testsFile); + } FileUtil.createIfDoesntExist(testsFile); try { - FileUtil.writeToFile(testsFile, entry.getValue()); + FileUtil.writeToFile(testsFile, entry.getValue()); } catch (IOException e) { LOG.error("ERROR copying tests file"); @@ -235,8 +261,8 @@ public class StudyProjectGenerator { } } - private static void flushCourseJson(@NotNull final Course course, @NotNull final File courseDirectory) { - final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + public static void flushCourseJson(@NotNull final Course course, @NotNull final File courseDirectory) { + final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().create(); final String json = gson.toJson(course); final File courseJson = new File(courseDirectory, EduNames.COURSE_META_FILE); final FileOutputStream fileOutputStream; @@ -269,7 +295,7 @@ public class StudyProjectGenerator { */ @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") public static void flushCache(List courses) { - File cacheFile = new File(ourCoursesDir, CACHE_NAME); + File cacheFile = new File(OUR_COURSES_DIR, CACHE_NAME); PrintWriter writer = null; try { if (!createCacheFile(cacheFile)) return; @@ -290,8 +316,8 @@ public class StudyProjectGenerator { } private static boolean createCacheFile(File cacheFile) throws IOException { - if (!ourCoursesDir.exists()) { - final boolean created = ourCoursesDir.mkdirs(); + 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; @@ -307,12 +333,13 @@ public class StudyProjectGenerator { return true; } - public List getCourses(boolean force) { - if (ourCoursesDir.exists()) { + // Supposed to be called under progress + public List getCoursesAsynchronouslyIfNeeded(boolean force) { + if (OUR_COURSES_DIR.exists()) { myCourses = getCoursesFromCache(); } if (force || myCourses.isEmpty()) { - myCourses = EduStepicConnector.getCourses(); + myCourses = execCancelable(EduStepicConnector::getCourses); flushCache(myCourses); } if (myCourses.isEmpty()) { @@ -321,6 +348,22 @@ public class StudyProjectGenerator { return myCourses; } + public List getCoursesUnderProgress(boolean force, @NotNull final String progressTitle, @NotNull final Project project) { + try { + return ProgressManager.getInstance() + .runProcessWithProgressSynchronously(new ThrowableComputable, RuntimeException>() { + @Override + public List compute() throws RuntimeException { + ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); + return getCoursesAsynchronouslyIfNeeded(force); + } + }, progressTitle, true, project); + } + catch (RuntimeException e) { + return Collections.singletonList(CourseInfo.INVALID_COURSE); + } + } + public void addSettingsStateListener(@NotNull SettingsListener listener) { myListeners.add(listener); } @@ -336,7 +379,7 @@ public class StudyProjectGenerator { } public static List getBundledIntro() { - final File introCourse = new File(ourCoursesDir, "Introduction to Python"); + final File introCourse = new File(OUR_COURSES_DIR, "Introduction to Python"); if (introCourse.exists()) { final CourseInfo courseInfo = getCourseInfo(introCourse); @@ -347,7 +390,7 @@ public class StudyProjectGenerator { public static List getCoursesFromCache() { List courses = new ArrayList<>(); - final File cacheFile = new File(ourCoursesDir, CACHE_NAME); + final File cacheFile = new File(OUR_COURSES_DIR, CACHE_NAME); if (!cacheFile.exists()) { return courses; } @@ -360,7 +403,9 @@ public class StudyProjectGenerator { 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); + if (!courseInfo.isAdaptive()) { + courses.add(courseInfo); + } } } catch (IOException | JsonSyntaxException e) { @@ -369,7 +414,8 @@ public class StudyProjectGenerator { finally { StudyUtils.closeSilently(reader); } - } finally { + } + finally { StudyUtils.closeSilently(inputStream); } } @@ -378,6 +424,7 @@ public class StudyProjectGenerator { } return courses; } + /** * Adds course from zip archive to courses * @@ -389,12 +436,14 @@ public class StudyProjectGenerator { try { String fileName = file.getName(); String unzippedName = fileName.substring(0, fileName.indexOf(".")); - File courseDir = new File(ourCoursesDir, unzippedName); + 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)) { - courseDir.renameTo(new File(ourCoursesDir, courseName.getName())); + //noinspection ResultOfMethodCallIgnored + courseDir.renameTo(new File(OUR_COURSES_DIR, courseName.getName())); + //noinspection ResultOfMethodCallIgnored courseDir.delete(); } return courseName; @@ -409,7 +458,6 @@ public class StudyProjectGenerator { /** * 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 @@ -431,6 +479,7 @@ public class StudyProjectGenerator { } return null; } + /** * Parses course json meta file and finds course name * @@ -462,10 +511,13 @@ public class StudyProjectGenerator { courseInfo.setName(courseName); courseInfo.setDescription(courseDescription); courseInfo.setType("pycharm " + language); - final ArrayList authors = new ArrayList<>(); + final ArrayList authors = new ArrayList<>(); for (JsonElement author : courseAuthors) { final JsonObject authorAsJsonObject = author.getAsJsonObject(); - authors.add(new CourseInfo.Author(authorAsJsonObject.get("first_name").getAsString(), authorAsJsonObject.get("last_name").getAsString())); + final StepicUser stepicUser = new StepicUser(); + stepicUser.setFirstName(authorAsJsonObject.get("first_name").getAsString()); + stepicUser.setLastName(authorAsJsonObject.get("last_name").getAsString()); + authors.add(stepicUser); } courseInfo.setAuthors(authors); } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/editor/StudyEditor.java b/python/educational-core/student/src/com/jetbrains/edu/learning/editor/StudyEditor.java index 50f8ad8cf759..2985e125d38a 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/editor/StudyEditor.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/editor/StudyEditor.java @@ -5,9 +5,9 @@ import com.intellij.openapi.fileEditor.impl.text.PsiAwareTextEditorImpl; import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; +import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduDocumentListener; import com.jetbrains.edu.learning.courseFormat.TaskFile; -import com.jetbrains.edu.learning.StudyUtils; import org.jetbrains.annotations.NotNull; import java.util.HashMap; @@ -21,6 +21,11 @@ public class StudyEditor extends PsiAwareTextEditorImpl { private final TaskFile myTaskFile; private static final Map myDocumentListeners = new HashMap(); + public StudyEditor(@NotNull final Project project, @NotNull final VirtualFile file) { + super(project, file, TextEditorProvider.getInstance()); + myTaskFile = StudyUtils.getTaskFile(project, file); + } + public TaskFile getTaskFile() { return myTaskFile; } @@ -30,11 +35,6 @@ public class StudyEditor extends PsiAwareTextEditorImpl { myDocumentListeners.put(document, listener); } - public StudyEditor(@NotNull final Project project, @NotNull final VirtualFile file) { - super(project, file, TextEditorProvider.getInstance()); - myTaskFile = StudyUtils.getTaskFile(project, file); - } - public static void removeListener(Document document) { final EduDocumentListener listener = myDocumentListeners.get(document); if (listener != null) { diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/editor/StudyFileEditorProvider.java b/python/educational-core/student/src/com/jetbrains/edu/learning/editor/StudyFileEditorProvider.java index cdfd8d4535db..34ee809bb0cf 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/editor/StudyFileEditorProvider.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/editor/StudyFileEditorProvider.java @@ -8,14 +8,14 @@ import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; -import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.StudyUtils; +import com.jetbrains.edu.learning.courseFormat.TaskFile; import org.jdom.Element; import org.jetbrains.annotations.NotNull; -class StudyFileEditorProvider implements FileEditorProvider, DumbAware { - static final private String EDITOR_TYPE_ID = "StudyEditor"; - final private FileEditorProvider defaultTextEditorProvider = TextEditorProvider.getInstance(); +public class StudyFileEditorProvider implements FileEditorProvider, DumbAware { + public static final String EDITOR_TYPE_ID = "StudyEditor"; + private final FileEditorProvider defaultTextEditorProvider = TextEditorProvider.getInstance(); @Override public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/CourseInfo.java b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/CourseInfo.java index 2bc2099924d9..c8c0c90aaf04 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/CourseInfo.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/CourseInfo.java @@ -3,6 +3,7 @@ package com.jetbrains.edu.learning.stepic; import com.google.gson.annotations.SerializedName; import com.intellij.openapi.util.text.StringUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -12,29 +13,26 @@ import java.util.List; * and when project is being created */ public class CourseInfo { - boolean is_public; - public List sections; - @SerializedName("title") - private String myName; - @SerializedName("summary") - private String myDescription; - @SerializedName("course_format") - //course type in format "pycharm " - private String myType = "pycharm Python"; + public static CourseInfo INVALID_COURSE = new CourseInfo(); + @SerializedName("title") private String myName; + int id; + boolean isAdaptive; + boolean isPublic; + List sections; List instructors = new ArrayList(); - List myAuthors = new ArrayList(); - int id; - - public static CourseInfo INVALID_COURSE = new CourseInfo(); + List myAuthors = new ArrayList<>(); + @SerializedName("summary") private String myDescription; + @SerializedName("course_format") private String myType = "pycharm Python"; //course type in format "pycharm " + @Nullable private String username; public String getName() { return myName; } @NotNull - public List getAuthors() { + public List getAuthors() { return myAuthors; } @@ -48,7 +46,7 @@ public class CourseInfo { @Override public String toString() { - return myName; + return getName(); } @Override @@ -57,17 +55,26 @@ public class CourseInfo { if (o == null || getClass() != o.getClass()) return false; CourseInfo that = (CourseInfo)o; if (that.getName() == null || that.getDescription() == null) return false; - return that.getName().equals(myName) + return that.getName().equals(getName()) && that.getDescription().equals(myDescription); } @Override public int hashCode() { - int result = myName != null ? myName.hashCode() : 0; + int result = getName() != null ? getName().hashCode() : 0; result = 31 * result + (myDescription != null ? myDescription.hashCode() : 0); return result; } + @Nullable + public String getUsername() { + return username; + } + + public void setUsername(@Nullable String username) { + this.username = username; + } + public static class Author { int id; String first_name = ""; @@ -93,18 +100,18 @@ public class CourseInfo { myName = name; } - public void setAuthors(List authors) { + public void setAuthors(List authors) { myAuthors = authors; - for (Author author : authors) { + for (StepicUser author : authors) { if (author.id > 0) { instructors.add(author.id); } } } - public void addAuthor(Author author) { + public void addAuthor(StepicUser author) { if (myAuthors == null) { - myAuthors = new ArrayList(); + myAuthors = new ArrayList<>(); } myAuthors.add(author); } @@ -116,4 +123,20 @@ public class CourseInfo { public void setType(String type) { myType = type; } + + public boolean isAdaptive() { + return isAdaptive; + } + + public void setAdaptive(boolean adaptive) { + isAdaptive = adaptive; + } + + public boolean isPublic() { + return isPublic; + } + + public void setPublic(boolean aPublic) { + isPublic = aPublic; + } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java new file mode 100644 index 000000000000..3da3bfc16b78 --- /dev/null +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java @@ -0,0 +1,511 @@ +package com.jetbrains.edu.learning.stepic; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +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.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.jetbrains.edu.learning.StudyTaskManager; +import com.jetbrains.edu.learning.StudyUtils; +import com.jetbrains.edu.learning.checker.StudyExecutor; +import com.jetbrains.edu.learning.core.EduNames; +import com.jetbrains.edu.learning.courseFormat.*; +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.ui.StudyToolWindow; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.jetbrains.edu.learning.stepic.EduStepicConnector.*; + +public class EduAdaptiveStepicConnector { + public static final String PYTHON27 = "python27"; + public static final String PYTHON3 = "python3"; + + private static final Logger LOG = Logger.getInstance(EduAdaptiveStepicConnector.class); + private static final String STEPIC_URL = "https://stepic.org/"; + private static final String STEPIC_API_URL = STEPIC_URL + "api/"; + private static final String RECOMMENDATIONS_URL = "recommendations"; + private static final String CONTENT_TYPE_APPL_JSON = "application/json"; + private static final String LESSON_URL = "lessons/"; + private static final String RECOMMENDATION_REACTIONS_URL = "recommendation-reactions"; + private static final String ATTEMPTS_URL = "attempts"; + private static final String SUBMISSION_URL = "submissions"; + private static final String ASSIGNMENT_URL = "/assignments"; + private static final String VIEWS_URL = "/views"; + private static final String UNITS_URL = "/units"; + private static final String DEFAULT_TASKFILE_NAME = "code.py"; + + @Nullable + public static Task getNextRecommendation(@NotNull final Project project, @NotNull Course course) { + try { + final CloseableHttpClient client = getHttpClient(project); + final URI uri = new URIBuilder(STEPIC_API_URL + RECOMMENDATIONS_URL) + .addParameter("course", String.valueOf(course.getId())) + .build(); + final HttpGet request = new HttpGet(uri); + setHeaders(request, CONTENT_TYPE_APPL_JSON); + + final CloseableHttpResponse response = client.execute(request); + final StatusLine statusLine = response.getStatusLine(); + final HttpEntity responseEntity = response.getEntity(); + final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : ""; + + if (statusLine.getStatusCode() == HttpStatus.SC_OK) { + final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + final StepicWrappers.RecommendationWrapper recomWrapper = gson.fromJson(responseString, StepicWrappers.RecommendationWrapper.class); + + if (recomWrapper.recommendations.length != 0) { + final StepicWrappers.Recommendation recommendation = recomWrapper.recommendations[0]; + final String lessonId = recommendation.lesson; + final StepicWrappers.LessonContainer + lessonContainer = getFromStepic(LESSON_URL + lessonId, StepicWrappers.LessonContainer.class); + if (lessonContainer.lessons.size() == 1) { + final Lesson realLesson = lessonContainer.lessons.get(0); + course.getLessons().get(0).id = Integer.parseInt(lessonId); + + viewAllSteps(client, realLesson.id); + + for (int stepId : realLesson.steps) { + final StepicWrappers.Step step = getStep(stepId); + if (step.name.equals("code")) { + return getTaskFromStep(project, stepId, step, realLesson.getName()); + } + } + + LOG.warn("Got a lesson without code part as a recommendation"); + } + else { + LOG.warn("Got unexpected number of lessons: " + lessonContainer.lessons.size()); + } + } + } + else { + throw new IOException("Stepic returned non 200 status code: " + responseString); + } + } + catch (IOException e) { + LOG.warn(e.getMessage()); + } + catch (URISyntaxException e) { + LOG.warn(e.getMessage()); + } + return null; + } + + private static void viewAllSteps(CloseableHttpClient client, int lessonId) throws URISyntaxException, IOException { + final URI unitsUrl = new URIBuilder(UNITS_URL).addParameter("lesson", String.valueOf(lessonId)).build(); + final StepicWrappers.UnitContainer unitContainer = getFromStepic(unitsUrl.toString(), StepicWrappers.UnitContainer.class); + if (unitContainer.units.size() != 1) { + LOG.warn("Got unexpected numbers of units: " + unitContainer.units.size()); + return; + } + + final URIBuilder builder = new URIBuilder(ASSIGNMENT_URL); + for (Integer step : unitContainer.units.get(0).assignments) { + builder.addParameter("ids[]", String.valueOf(step)); + } + final URI assignmentUrl = builder.build(); + final StepicWrappers.AssignmentsWrapper assignments = getFromStepic(assignmentUrl.toString(), StepicWrappers.AssignmentsWrapper.class); + if (assignments.assignments.size() > 0) { + for (StepicWrappers.Assignment assignment : assignments.assignments) { + final HttpPost post = new HttpPost(STEPIC_API_URL + VIEWS_URL); + final StepicWrappers.ViewsWrapper viewsWrapper = new StepicWrappers.ViewsWrapper(assignment.id, assignment.step); + post.setEntity(new StringEntity(new Gson().toJson(viewsWrapper))); + setHeaders(post, CONTENT_TYPE_APPL_JSON); + final CloseableHttpResponse viewPostResult = client.execute(post); + if (viewPostResult.getStatusLine().getStatusCode() != HttpStatus.SC_CREATED) { + LOG.warn("Error while Views post, code: " + viewPostResult.getStatusLine().getStatusCode()); + } + } + } + else { + LOG.warn("Got assignments of incorrect length: " + assignments.assignments.size()); + } + } + + public static boolean postRecommendationReaction(@NotNull final Project project, @NotNull final String lessonId, + @NotNull final String user, int reaction) { + final HttpPost post = new HttpPost(STEPIC_API_URL + RECOMMENDATION_REACTIONS_URL); + final String json = new Gson() + .toJson(new StepicWrappers.RecommendationReactionWrapper(new StepicWrappers.RecommendationReaction(reaction, user, lessonId))); + post.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON)); + final CloseableHttpClient client = getHttpClient(project); + setHeaders(post, CONTENT_TYPE_APPL_JSON); + try { + final CloseableHttpResponse execute = client.execute(post); + return execute.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED; + } + catch (IOException e) { + LOG.warn(e.getMessage()); + } + return false; + } + + public static void addNextRecommendedTask(@NotNull final Project project, int reaction) { + final StudyEditor editor = StudyUtils.getSelectedStudyEditor(project); + final Course course = StudyTaskManager.getInstance(project).getCourse(); + if (course != null && editor != null && editor.getTaskFile() != null) { + final StepicUser user = StudyTaskManager.getInstance(project).getUser(); + + final boolean recommendationReaction = + user != null && postRecommendationReaction(project, String.valueOf(editor.getTaskFile().getTask().getLesson().id), + String.valueOf(user.id), reaction); + if (recommendationReaction) { + final Task task = getNextRecommendation(project, course); + + if (task != null) { + final Lesson adaptive = course.getLessons().get(0); + final Task unsolvedTask = adaptive.getTaskList().get(adaptive.getTaskList().size() - 1); + if (reaction == 0 || reaction == -1) { + unsolvedTask.setName(task.getName()); + unsolvedTask.setStepicId(task.getStepicId()); + unsolvedTask.setText(task.getText()); + unsolvedTask.getTestsText().clear(); + final Map testsText = task.getTestsText(); + for (String testName : testsText.keySet()) { + unsolvedTask.addTestsTexts(testName, testsText.get(testName)); + } + final Map taskFiles = task.getTaskFiles(); + if (taskFiles.size() == 1) { + final TaskFile taskFile = editor.getTaskFile(); + taskFile.text = ((TaskFile)taskFiles.values().toArray()[0]).text; + ApplicationManager.getApplication().invokeLater(() -> + ApplicationManager.getApplication().runWriteAction(() -> + editor.getEditor() + .getDocument() + .setText( + taskFiles.get( + DEFAULT_TASKFILE_NAME).text))); + } + else { + LOG.warn("Got task without unexpected number of task files: " + taskFiles.size()); + } + + final File lessonDirectory = new File(course.getCourseDirectory(), EduNames.LESSON + String.valueOf(adaptive.getIndex())); + final File taskDirectory = new File(lessonDirectory, EduNames.TASK + String.valueOf(adaptive.getTaskList().size())); + StudyProjectGenerator.flushTask(task, taskDirectory); + StudyProjectGenerator.flushCourseJson(course, new File(course.getCourseDirectory())); + final VirtualFile lessonDir = project.getBaseDir().findChild(EduNames.LESSON + String.valueOf(adaptive.getIndex())); + + if (lessonDir != null) { + createTestFiles(course, task, unsolvedTask, lessonDir); + } + final StudyToolWindow window = StudyUtils.getStudyToolWindow(project); + if (window != null) { + window.setTaskText(unsolvedTask.getText(), unsolvedTask.getTaskDir(project), project); + } + } + else { + adaptive.addTask(task); + task.setIndex(adaptive.getTaskList().size()); + final VirtualFile lessonDir = project.getBaseDir().findChild(EduNames.LESSON + String.valueOf(adaptive.getIndex())); + + if (lessonDir != null) { + ApplicationManager.getApplication().invokeLater(() -> ApplicationManager.getApplication().runWriteAction(() -> { + try { + StudyGenerator.createTask(task, lessonDir, new File(course.getCourseDirectory(), lessonDir.getName()), project); + } + catch (IOException e) { + LOG.warn(e.getMessage()); + } + })); + } + + final File lessonDirectory = new File(course.getCourseDirectory(), EduNames.LESSON + String.valueOf(adaptive.getIndex())); + StudyProjectGenerator.flushLesson(lessonDirectory, adaptive); + StudyProjectGenerator.flushCourseJson(course, new File(course.getCourseDirectory())); + adaptive.initLesson(course, true); + } + } + ApplicationManager.getApplication().invokeLater(() -> { + VirtualFileManager.getInstance().refreshWithoutFileWatcher(false); + ProjectView.getInstance(project).refresh(); + }); + } + else { + LOG.warn("Recommendation reactions weren't posted"); + } + } + } + + private static void createTestFiles(Course course, Task task, Task unsolvedTask, VirtualFile lessonDir) { + ApplicationManager.getApplication().invokeLater(() -> ApplicationManager.getApplication().runWriteAction(() -> { + try { + final VirtualFile taskDir = VfsUtil + .findFileByIoFile(new File(lessonDir.getCanonicalPath(), EduNames.TASK + unsolvedTask.getIndex()), true); + final File resourceRoot = new File(course.getCourseDirectory(), lessonDir.getName()); + File newResourceRoot = null; + if (taskDir != null) { + newResourceRoot = new File(resourceRoot, taskDir.getName()); + 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.copy(resourceFile, fileInProject); + } + } + } + } + else { + LOG.warn("Task directory is null"); + } + } + catch (IOException e) { + LOG.warn(e.getMessage()); + } + })); + } + + @NotNull + private static Task getTaskFromStep(Project project, + int lessonID, + @NotNull final StepicWrappers.Step step, @NotNull String name) { + final Task task = new Task(); + task.setName(name); + task.setStepicId(lessonID); + task.setText(step.text); + task.setStatus(StudyStatus.Unchecked); + if (step.options.samples != null) { + final StringBuilder builder = new StringBuilder(); + for (List sample : step.options.samples) { + if (sample.size() == 2) { + builder.append("Sample Input:
"); + builder.append(StringUtil.replace(sample.get(0), "\n", "
")); + builder.append("
"); + builder.append("Sample Output:
"); + builder.append(StringUtil.replace(sample.get(1), "\n", "
")); + builder.append("

"); + } + } + task.setText(task.getText() + "
" + builder.toString()); + } + + if (step.options.executionMemoryLimit != null && step.options.executionTimeLimit != null) { + String builder = "Memory limit: " + + step.options.executionMemoryLimit + " Mb" + + "
" + + "Time limit: " + + step.options.executionTimeLimit + "s" + + "

"; + task.setText(task.getText() + builder); + } + + if (step.options.test != null) { + for (StepicWrappers.TestFileWrapper wrapper : step.options.test) { + task.addTestsTexts(wrapper.name, wrapper.text); + } + } + else { + if (step.options.samples != null) { + createTestFileFromSamples(task, step.options.samples); + } + } + + task.taskFiles = new HashMap(); // TODO: it looks like we don't need taskFiles as map anymore + if (step.options.files != null) { + for (TaskFile taskFile : step.options.files) { + task.taskFiles.put(taskFile.name, taskFile); + } + } + else { + final TaskFile taskFile = new TaskFile(); + taskFile.name = "code"; + final String templateForTask = getCodeTemplateForTask(step.options.codeTemplates, task, project); + taskFile.text = templateForTask == null ? "# write your answer here \n" : templateForTask; + task.taskFiles.put("code.py", taskFile); + } + return task; + } + + private static String getCodeTemplateForTask(@Nullable StepicWrappers.CodeTemplatesWrapper codeTemplates, + @NotNull final Task task, @NotNull final Project project) { + + if (codeTemplates != null) { + final String languageString = getLanguageString(task, project); + if (languageString != null) { + return codeTemplates.getTemplateForLanguage(languageString); + } + } + + return null; + } + + @Nullable + public static Pair checkTask(@NotNull final Project project, @NotNull final Task task) { + int attemptId = -1; + try { + attemptId = getAttemptId(project, task, ATTEMPTS_URL); + } + catch (IOException e) { + LOG.warn(e.getMessage()); + } + if (attemptId != -1) { + final Editor editor = StudyUtils.getSelectedEditor(project); + String language = getLanguageString(task, project); + if (editor != null && language != null) { + final CloseableHttpClient client = getHttpClient(project); + StepicWrappers.ResultSubmissionWrapper wrapper = postResultsForCheck(client, attemptId, language, editor.getDocument().getText()); + + final StepicUser user = StudyTaskManager.getInstance(project).getUser(); + if (user != null) { + final int id = user.getId(); + wrapper = getCheckResults(attemptId, id, client, wrapper); + if (wrapper.submissions.length == 1) { + final boolean isSolved = !wrapper.submissions[0].status.equals("wrong"); + return Pair.create(isSolved, wrapper.submissions[0].hint); + } + else { + LOG.warn("Got a submission wrapper with incorrect submissions number: " + wrapper.submissions.length); + } + } + else { + LOG.warn("User is null"); + } + } + } + else { + LOG.warn("Got an incorrect attempt id: " + attemptId); + } + return Pair.create(false, ""); + } + + @Nullable + private static StepicWrappers.ResultSubmissionWrapper postResultsForCheck(@NotNull final CloseableHttpClient client, + final int attemptId, + @NotNull final String language, + @NotNull final String text) { + final CloseableHttpResponse response; + try { + final StepicWrappers.SubmissionToPostWrapper submissionToPostWrapper = + new StepicWrappers.SubmissionToPostWrapper(String.valueOf(attemptId), language, text); + final HttpPost httpPost = new HttpPost(STEPIC_API_URL + SUBMISSION_URL); + setHeaders(httpPost, CONTENT_TYPE_APPL_JSON); + try { + httpPost.setEntity(new StringEntity(new Gson().toJson(submissionToPostWrapper))); + } + catch (UnsupportedEncodingException e) { + LOG.warn(e.getMessage()); + } + response = client.execute(httpPost); + return new Gson().fromJson(EntityUtils.toString(response.getEntity()), StepicWrappers.ResultSubmissionWrapper.class); + } + catch (IOException e) { + LOG.warn(e.getMessage()); + } + return null; + } + + @NotNull + private static StepicWrappers.ResultSubmissionWrapper getCheckResults(int attemptId, + int id, + CloseableHttpClient client, + StepicWrappers.ResultSubmissionWrapper wrapper) { + try { + while (wrapper.submissions.length == 1 && wrapper.submissions[0].status.equals("evaluation")) { + TimeUnit.MILLISECONDS.sleep(500); + final URI submissionURI = new URIBuilder(STEPIC_API_URL + SUBMISSION_URL) + .addParameter("attempt", String.valueOf(attemptId)) + .addParameter("order", "desc") + .addParameter("user", String.valueOf(id)) + .build(); + final HttpGet httpGet = new HttpGet(submissionURI); + setHeaders(httpGet, CONTENT_TYPE_APPL_JSON); + final CloseableHttpResponse httpResponse = client.execute(httpGet); + final String entity = EntityUtils.toString(httpResponse.getEntity()); + wrapper = new Gson().fromJson(entity, StepicWrappers.ResultSubmissionWrapper.class); + } + } + catch (InterruptedException e) { + LOG.warn(e.getMessage()); + } + catch (IOException e) { + LOG.warn(e.getMessage()); + } + catch (URISyntaxException e) { + LOG.warn(e.getMessage()); + } + return wrapper; + } + + @Nullable + private static String getLanguageString(@NotNull Task task, @NotNull Project project) { + final Language pythonLanguage = Language.findLanguageByID("Python"); + if (pythonLanguage != null) { + final Sdk language = StudyExecutor.INSTANCE.forLanguage(pythonLanguage).findSdk(project); + if (language != null) { + final String versionString = language.getVersionString(); + if (versionString != null) { + final List versionStringParts = StringUtil.split(versionString, " "); + if (versionStringParts.size() == 2) { + return versionStringParts.get(1).startsWith("2") ? PYTHON27 : PYTHON3; + } + } + } + else { + StudyUtils.showNoSdkNotification(task, project); + } + } + return null; + } + + private static int getAttemptId(@NotNull final Project project, @NotNull Task task, @NotNull final String attempts) throws IOException { + final StepicWrappers.AttemptToPostWrapper attemptWrapper = new StepicWrappers.AttemptToPostWrapper(task.getStepicId()); + + final HttpPost post = new HttpPost(STEPIC_API_URL + attempts); + post.setEntity(new StringEntity(new Gson().toJson(attemptWrapper))); + + final CloseableHttpClient client = getHttpClient(project); + setHeaders(post, CONTENT_TYPE_APPL_JSON); + final CloseableHttpResponse httpResponse = client.execute(post); + final String entity = EntityUtils.toString(httpResponse.getEntity()); + final StepicWrappers.AttemptContainer container = + new Gson().fromJson(entity, StepicWrappers.AttemptContainer.class); + return (container.attempts != null && !container.attempts.isEmpty()) ? container.attempts.get(0).id : -1; + } + + private static void createTestFileFromSamples(@NotNull final Task task, + @NotNull final List> samples) { + + String testText = "from test_helper import check_samples\n\n" + + "if __name__ == '__main__':\n" + + " check_samples(samples=" + new GsonBuilder().create().toJson(samples) + ")"; + task.addTestsTexts("tests.py", testText); + } +} diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java index 8a20545cfc2e..f796dddbaad9 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java @@ -3,7 +3,6 @@ package com.jetbrains.edu.learning.stepic; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.annotations.Expose; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.diagnostic.Logger; @@ -17,6 +16,7 @@ import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileFilter; import com.intellij.util.net.ssl.CertificateManager; +import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.core.EduUtils; import com.jetbrains.edu.learning.courseFormat.Course; @@ -24,10 +24,7 @@ import com.jetbrains.edu.learning.courseFormat.Lesson; import com.jetbrains.edu.learning.courseFormat.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; import org.apache.commons.codec.binary.Base64; -import org.apache.http.HttpEntity; -import org.apache.http.HttpStatus; -import org.apache.http.NameValuePair; -import org.apache.http.StatusLine; +import org.apache.http.*; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.*; import org.apache.http.cookie.Cookie; @@ -40,7 +37,6 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicNameValuePair; -import org.apache.http.protocol.HTTP; import org.apache.http.util.EntityUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -49,7 +45,6 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -57,9 +52,9 @@ import java.security.cert.X509Certificate; import java.util.*; public class EduStepicConnector { + private static final Logger LOG = Logger.getInstance(EduStepicConnector.class.getName()); private static final String stepicUrl = "https://stepic.org/"; private static final String stepicApiUrl = stepicUrl + "api/"; - private static final Logger LOG = Logger.getInstance(EduStepicConnector.class.getName()); private static String ourCSRFToken = ""; private static CloseableHttpClient ourClient; @@ -70,15 +65,21 @@ public class EduStepicConnector { private EduStepicConnector() { } - public static boolean login(@NotNull final String user, @NotNull final String password) { + public static StepicUser login(@NotNull final String username, @NotNull final String password) { initializeClient(); - return postCredentials(user, password); + if (postCredentials(username, password)) { + final StepicWrappers.AuthorWrapper stepicUserWrapper = getCurrentUser(); + if (stepicUserWrapper != null && stepicUserWrapper.users.size() == 1) { + return stepicUserWrapper.users.get(0); + } + } + return null; } @Nullable - public static AuthorWrapper getCurrentUser() { + public static StepicWrappers.AuthorWrapper getCurrentUser() { try { - return getFromStepic("stepics/1", AuthorWrapper.class); + return getFromStepic("stepics/1", StepicWrappers.AuthorWrapper.class); } catch (IOException e) { LOG.warn("Couldn't get author info"); @@ -90,7 +91,7 @@ public class EduStepicConnector { final HttpPost userRequest = new HttpPost(stepicApiUrl + "users"); initializeClient(); setHeaders(userRequest, "application/json"); - String requestBody = new Gson().toJson(new UserWrapper(user, password)); + String requestBody = new Gson().toJson(new StepicWrappers.UserWrapper(user, password)); userRequest.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); try { @@ -109,38 +110,47 @@ public class EduStepicConnector { return true; } - private static void initializeClient() { - final HttpGet request = new HttpGet(stepicUrl); - request.addHeader(new BasicHeader("referer", "https://stepic.org")); - request.addHeader(new BasicHeader("content-type", "application/json")); + public static void initializeClient() { + if (ourClient == null) { + final HttpGet request = new HttpGet(stepicUrl); + request.addHeader(new BasicHeader("referer", "https://stepic.org")); + request.addHeader(new BasicHeader("content-type", "application/json")); - HttpClientBuilder builder = HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext()).setMaxConnPerRoute(100000). - setConnectionReuseStrategy(DefaultConnectionReuseStrategy.INSTANCE); - ourCookieStore = new BasicCookieStore(); + HttpClientBuilder builder = + HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext()).setMaxConnPerRoute(100000). + setConnectionReuseStrategy(DefaultConnectionReuseStrategy.INSTANCE); + ourCookieStore = new BasicCookieStore(); - try { - // Create a trust manager that does not validate certificate for this connection - TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { return null; } - public void checkClientTrusted(X509Certificate[] certs, String authType) {} - public void checkServerTrusted(X509Certificate[] certs, String authType) {} - }}; - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustAllCerts, new SecureRandom()); - ourClient = builder.setDefaultCookieStore(ourCookieStore).setSslcontext(sslContext).build(); + try { + // Create a trust manager that does not validate certificate for this connection + TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return null; + } - ourClient.execute(request); - saveCSRFToken(); - } - catch (IOException e) { - LOG.error(e.getMessage()); - } - catch (NoSuchAlgorithmException e) { - LOG.error(e.getMessage()); - } - catch (KeyManagementException e) { - LOG.error(e.getMessage()); + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + }}; + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, new SecureRandom()); + ourClient = builder.setDefaultCookieStore(ourCookieStore).setSslcontext(sslContext).build(); + + ourClient.execute(request); + saveCSRFToken(); + } + catch (IOException e) { + LOG.error(e.getMessage()); + } + catch (NoSuchAlgorithmException e) { + LOG.error(e.getMessage()); + } + catch (KeyManagementException e) { + LOG.error(e.getMessage()); + } } } @@ -164,14 +174,7 @@ public class EduStepicConnector { nvps.add(new BasicNameValuePair("password", password)); nvps.add(new BasicNameValuePair("remember", "on")); - try { - request.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); - } - catch (UnsupportedEncodingException e) { - LOG.error(e.getMessage()); - ourClient = null; - return false; - } + request.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); setHeaders(request, "application/x-www-form-urlencoded"); @@ -182,7 +185,8 @@ public class EduStepicConnector { if (line.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) { final HttpEntity responseEntity = response.getEntity(); final String responseString = responseEntity != null ? EntityUtils.toString(responseEntity) : ""; - LOG.error("Failed to login " + responseString); + LOG.warn("Failed to login: " + line.getStatusCode() + line.getReasonPhrase()); + LOG.debug("Failed to login " + responseString); ourClient = null; return false; } @@ -195,7 +199,7 @@ public class EduStepicConnector { return true; } - private static T getFromStepic(String link, final Class container) throws IOException { + static T getFromStepic(String link, final Class container) throws IOException { final HttpGet request = new HttpGet(stepicApiUrl + link); if (ourClient == null) { initializeClient(); @@ -212,6 +216,15 @@ public class EduStepicConnector { Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); return gson.fromJson(responseString, container); } + + @NotNull + public static CloseableHttpClient getHttpClient(@NotNull final Project project) { + if (ourClient == null) { + login(project); + initializeClient(); + } + return ourClient; + } @NotNull public static List getCourses() { @@ -226,21 +239,21 @@ public class EduStepicConnector { catch (IOException e) { LOG.error("Cannot load course list " + e.getMessage()); } - return Collections.emptyList(); + return Collections.singletonList(CourseInfo.INVALID_COURSE); } private static boolean addCoursesFromStepic(List result, int pageNumber) throws IOException { final String url = pageNumber == 0 ? "courses" : "courses?page=" + String.valueOf(pageNumber); - final CoursesContainer coursesContainer = getFromStepic(url, CoursesContainer.class); + final StepicWrappers.CoursesContainer coursesContainer = getFromStepic(url, StepicWrappers.CoursesContainer.class); final List courseInfos = coursesContainer.courses; for (CourseInfo info : courseInfos) { final String courseType = info.getType(); - if (StringUtil.isEmptyOrSpaces(courseType)) continue; + if (!info.isAdaptive() && StringUtil.isEmptyOrSpaces(courseType)) continue; final List typeLanguage = StringUtil.split(courseType, " "); - if (typeLanguage.size() == 2 && PYCHARM_PREFIX.equals(typeLanguage.get(0))) { - + // TODO: should adaptive course be of PyCharmType ? + if (info.isAdaptive() || (typeLanguage.size() == 2 && PYCHARM_PREFIX.equals(typeLanguage.get(0)))) { for (Integer instructor : info.instructors) { - final CourseInfo.Author author = getFromStepic("users/" + String.valueOf(instructor), AuthorWrapper.class).users.get(0); + final StepicUser author = getFromStepic("users/" + String.valueOf(instructor), StepicWrappers.AuthorWrapper.class).users.get(0); info.addAuthor(author); } @@ -250,34 +263,55 @@ public class EduStepicConnector { return coursesContainer.meta.containsKey("has_next") && coursesContainer.meta.get("has_next") == Boolean.TRUE; } - public static Course getCourse(@NotNull final CourseInfo info) { + public static Course getCourse(@NotNull final Project project, @NotNull final CourseInfo info) { final Course course = new Course(); course.setAuthors(info.getAuthors()); course.setDescription(info.getDescription()); - course.setName(info.getName()); - String courseType = info.getType(); - course.setLanguage(courseType.substring(PYCHARM_PREFIX.length() + 1)); + course.setAdaptive(info.isAdaptive()); + course.setId(info.id); course.setUpToDate(true); // TODO: get from stepic - try { - for (Integer section : info.sections) { - course.addLessons(getLessons(section)); + + if (!course.isAdaptive()) { + String courseType = info.getType(); + course.setName(info.getName()); + course.setLanguage(courseType.substring(PYCHARM_PREFIX.length() + 1)); + try { + for (Integer section : info.sections) { + course.addLessons(getLessons(section)); + } + return course; + } + catch (IOException e) { + LOG.error("IOException " + e.getMessage()); } - return course; } - catch (IOException e) { - LOG.error("IOException " + e.getMessage()); + else { + final Lesson lesson = new Lesson(); + course.setName(info.getName()); + //TODO: more specific name? + lesson.setName("Adaptive"); + course.addLesson(lesson); + final Task recommendation = EduAdaptiveStepicConnector.getNextRecommendation(project, course); + if (recommendation != null) { + lesson.addTask(recommendation); + } + + return course; } return null; } public static List getLessons(int sectionId) throws IOException { - final SectionContainer sectionContainer = getFromStepic("sections/" + String.valueOf(sectionId), SectionContainer.class); + final StepicWrappers.SectionContainer + sectionContainer = getFromStepic("sections/" + String.valueOf(sectionId), StepicWrappers.SectionContainer.class); List unitIds = sectionContainer.sections.get(0).units; final List lessons = new ArrayList(); for (Integer unitId : unitIds) { - UnitContainer unit = getFromStepic("units/" + String.valueOf(unitId), UnitContainer.class); + StepicWrappers.UnitContainer + unit = getFromStepic("units/" + String.valueOf(unitId), StepicWrappers.UnitContainer.class); int lessonID = unit.units.get(0).lesson; - LessonContainer lesson = getFromStepic("lessons/" + String.valueOf(lessonID), LessonContainer.class); + StepicWrappers.LessonContainer + lesson = getFromStepic("lessons/" + String.valueOf(lessonID), StepicWrappers.LessonContainer.class); Lesson realLesson = lesson.lessons.get(0); realLesson.taskList = new ArrayList(); for (Integer s : realLesson.steps) { @@ -291,13 +325,13 @@ public class EduStepicConnector { } private static void createTask(Lesson lesson, Integer stepicId) throws IOException { - final Step step = getStep(stepicId); + final StepicWrappers.Step step = getStep(stepicId); if (!step.name.equals(PYCHARM_PREFIX)) return; final Task task = new Task(); task.setStepicId(stepicId); task.setName(step.options != null ? step.options.title : PYCHARM_PREFIX); task.setText(step.text); - for (TestFileWrapper wrapper : step.options.test) { + for (StepicWrappers.TestFileWrapper wrapper : step.options.test) { task.addTestsTexts(wrapper.name, wrapper.text); } @@ -310,8 +344,8 @@ public class EduStepicConnector { lesson.taskList.add(task); } - public static Step getStep(Integer step) throws IOException { - return getFromStepic("steps/" + String.valueOf(step), StepContainer.class).steps.get(0).block; + public static StepicWrappers.Step getStep(Integer step) throws IOException { + return getFromStepic("steps/" + String.valueOf(step), StepicWrappers.StepContainer.class).steps.get(0).block; } @@ -334,14 +368,13 @@ public class EduStepicConnector { return; } else { - final boolean success = login(login, password); - if (!success) return; + if (login(login, password) == null) return; } } final HttpPost attemptRequest = new HttpPost(stepicApiUrl + "attempts"); setHeaders(attemptRequest, "application/json"); - String attemptRequestBody = new Gson().toJson(new AttemptWrapper(task.getStepicId())); + String attemptRequestBody = new Gson().toJson(new StepicWrappers.AttemptWrapper(task.getStepicId())); attemptRequest.setEntity(new StringEntity(attemptRequestBody, ContentType.APPLICATION_JSON)); try { @@ -352,12 +385,12 @@ public class EduStepicConnector { if (statusLine.getStatusCode() != HttpStatus.SC_CREATED) { LOG.error("Failed to make attempt " + attemptResponseString); } - final AttemptWrapper.Attempt attempt = new Gson().fromJson(attemptResponseString, AttemptContainer.class).attempts.get(0); + final StepicWrappers.AttemptWrapper.Attempt attempt = new Gson().fromJson(attemptResponseString, StepicWrappers.AttemptContainer.class).attempts.get(0); final Map taskFiles = task.getTaskFiles(); - final ArrayList files = new ArrayList(); + final ArrayList files = new ArrayList(); for (TaskFile fileEntry : taskFiles.values()) { - files.add(new SolutionFile(fileEntry.name, fileEntry.text)); + files.add(new StepicWrappers.SolutionFile(fileEntry.name, fileEntry.text)); } postSubmission(passed, attempt, files); } @@ -366,11 +399,11 @@ public class EduStepicConnector { } } - private static void postSubmission(boolean passed, AttemptWrapper.Attempt attempt, ArrayList files) throws IOException { + private static void postSubmission(boolean passed, StepicWrappers.AttemptWrapper.Attempt attempt, ArrayList files) throws IOException { final HttpPost request = new HttpPost(stepicApiUrl + "submissions"); setHeaders(request, "application/json"); - String requestBody = new Gson().toJson(new SubmissionWrapper(attempt.id, passed ? "1" : "0", files)); + String requestBody = new Gson().toJson(new StepicWrappers.SubmissionWrapper(attempt.id, passed ? "1" : "0", files)); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); final CloseableHttpResponse response = ourClient.execute(request); final HttpEntity responseEntity = response.getEntity(); @@ -398,15 +431,15 @@ public class EduStepicConnector { indicator.setText("Uploading course to " + stepicUrl); final HttpPost request = new HttpPost(stepicApiUrl + "courses"); if (ourClient == null || !relogin) { - if (!login()) return; + if (!login(project)) return; } - final AuthorWrapper user = getCurrentUser(); + final StepicWrappers.AuthorWrapper user = getCurrentUser(); if (user != null) { course.setAuthors(user.users); } setHeaders(request, "application/json"); - String requestBody = new Gson().toJson(new CourseWrapper(course)); + String requestBody = new Gson().toJson(new StepicWrappers.CourseWrapper(course)); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); try { @@ -416,13 +449,13 @@ public class EduStepicConnector { final StatusLine line = response.getStatusLine(); if (line.getStatusCode() != HttpStatus.SC_CREATED) { if (!relogin) { - login(); + login(project); postCourse(project, course, true, indicator); } LOG.error("Failed to push " + responseString); return; } - final CourseInfo postedCourse = new Gson().fromJson(responseString, CoursesContainer.class).courses.get(0); + final CourseInfo postedCourse = new Gson().fromJson(responseString, StepicWrappers.CoursesContainer.class).courses.get(0); final int sectionId = postModule(postedCourse.id, 1, String.valueOf(postedCourse.getName())); int position = 1; @@ -439,14 +472,13 @@ public class EduStepicConnector { } } - private static boolean login() { - final String login = StudySettings.getInstance().getLogin(); + private static boolean login(@NotNull final Project project) { + final String login = StudyTaskManager.getInstance(project).getLogin(); if (StringUtil.isEmptyOrSpaces(login)) { return showLoginDialog(); } else { - boolean success = login(login, StudySettings.getInstance().getPassword()); - if (!success) { + if (login(login, StudyTaskManager.getInstance(project).getPassword()) == null) { return showLoginDialog(); } } @@ -498,8 +530,8 @@ public class EduStepicConnector { private static void postUnit(int lessonId, int position, int sectionId) { final HttpPost request = new HttpPost(stepicApiUrl + "units"); setHeaders(request, "application/json"); - final UnitWrapper unitWrapper = new UnitWrapper(); - unitWrapper.unit = new Unit(); + final StepicWrappers.UnitWrapper unitWrapper = new StepicWrappers.UnitWrapper(); + unitWrapper.unit = new StepicWrappers.Unit(); unitWrapper.unit.lesson = lessonId; unitWrapper.unit.position = position; unitWrapper.unit.section = sectionId; @@ -524,11 +556,11 @@ public class EduStepicConnector { private static int postModule(int courseId, int position, @NotNull final String title) { final HttpPost request = new HttpPost(stepicApiUrl + "sections"); setHeaders(request, "application/json"); - final Section section = new Section(); + final StepicWrappers.Section section = new StepicWrappers.Section(); section.course = courseId; section.title = title; section.position = position; - final SectionWrapper sectionContainer = new SectionWrapper(); + final StepicWrappers.SectionWrapper sectionContainer = new StepicWrappers.SectionWrapper(); sectionContainer.section = section; String requestBody = new Gson().toJson(sectionContainer); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); @@ -541,7 +573,8 @@ public class EduStepicConnector { if (line.getStatusCode() != HttpStatus.SC_CREATED) { LOG.error("Failed to push " + responseString); } - final Section postedSection = new Gson().fromJson(responseString, SectionContainer.class).sections.get(0); + final StepicWrappers.Section + postedSection = new Gson().fromJson(responseString, StepicWrappers.SectionContainer.class).sections.get(0); return postedSection.id; } catch (IOException e) { @@ -550,17 +583,17 @@ public class EduStepicConnector { return -1; } - public static int updateLesson(Project project, @NotNull final Lesson lesson, ProgressIndicator indicator) { + public static int updateLesson(@NotNull final Project project, @NotNull final Lesson lesson, ProgressIndicator indicator) { final HttpPut request = new HttpPut(stepicApiUrl + "lessons/" + String.valueOf(lesson.id)); if (ourClient == null) { - if (!login()) { + if (!login(project)) { LOG.error("Failed to push lesson"); return 0; } } setHeaders(request, "application/json"); - String requestBody = new Gson().toJson(new LessonWrapper(lesson)); + String requestBody = new Gson().toJson(new StepicWrappers.LessonWrapper(lesson)); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); try { @@ -589,14 +622,14 @@ public class EduStepicConnector { return -1; } - public static int postLesson(Project project, @NotNull final Lesson lesson, ProgressIndicator indicator) { + public static int postLesson(@NotNull final Project project, @NotNull final Lesson lesson, ProgressIndicator indicator) { final HttpPost request = new HttpPost(stepicApiUrl + "lessons"); if (ourClient == null) { - login(); + login(project); } setHeaders(request, "application/json"); - String requestBody = new Gson().toJson(new LessonWrapper(lesson)); + String requestBody = new Gson().toJson(new StepicWrappers.LessonWrapper(lesson)); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); try { @@ -646,7 +679,7 @@ public class EduStepicConnector { setHeaders(request, "application/json"); final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().create(); ApplicationManager.getApplication().invokeLater(() -> { - final String requestBody = gson.toJson(new StepSourceWrapper(project, task, lessonId)); + final String requestBody = gson.toJson(new StepicWrappers.StepSourceWrapper(project, task, lessonId)); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); try { @@ -664,274 +697,9 @@ public class EduStepicConnector { }); } - private static void setHeaders(@NotNull final HttpRequestBase request, String contentType) { + static void setHeaders(@NotNull final HttpRequestBase request, String contentType) { request.addHeader(new BasicHeader("referer", stepicUrl)); request.addHeader(new BasicHeader("X-CSRFToken", ourCSRFToken)); request.addHeader(new BasicHeader("content-type", contentType)); } - - private static class StepContainer { - List steps; - } - - private static class Step { - @Expose StepOptions options; - @Expose String text; - @Expose String name = "pycharm"; - @Expose StepOptions source; - - public static Step fromTask(Project project, @NotNull final Task task) { - final Step step = new Step(); - step.text = task.getTaskText(project); - step.source = StepOptions.fromTask(project, task); - return step; - } - } - - private static class StepOptions { - @Expose List test; - @Expose String title; - @Expose List files; - @Expose String text; - - public static StepOptions fromTask(final Project project, @NotNull final Task task) { - final StepOptions source = new StepOptions(); - setTests(task, source, project); - source.files = new ArrayList(); - source.title = task.getName(); - for (final Map.Entry entry : task.getTaskFiles().entrySet()) { - final TaskFile taskFile = new TaskFile(); - TaskFile.copy(entry.getValue(), taskFile); - ApplicationManager.getApplication().runWriteAction(() -> { - final VirtualFile taskDir = task.getTaskDir(project); - assert taskDir != null; - VirtualFile ideaDir = project.getBaseDir().findChild(".idea"); - assert ideaDir != null; - EduUtils.createStudentFileFromAnswer(project, ideaDir, taskDir, entry.getKey(), taskFile); - }); - taskFile.name = entry.getKey(); - - VirtualFile ideaDir = project.getBaseDir().findChild(".idea"); - if (ideaDir == null) return null; - final VirtualFile file = ideaDir.findChild(taskFile.name); - try { - if (file != null) { - if (EduUtils.isImage(taskFile.name)) { - taskFile.text = Base64.encodeBase64URLSafeString(FileUtil.loadBytes(file.getInputStream())); - } - else { - taskFile.text = FileUtil.loadTextAndClose(file.getInputStream()); - } - } - } - catch (IOException e) { - LOG.error("Can't find file " + file.getPath()); - } - - source.files.add(taskFile); - } - return source; - } - - private static void setTests(@NotNull final Task task, @NotNull final StepOptions source, @NotNull final Project project) { - final Map testsText = task.getTestsText(); - if (testsText.isEmpty()) { - ApplicationManager.getApplication().runReadAction(() -> { - source.test = Collections.singletonList(new TestFileWrapper(EduNames.TESTS_FILE, task.getTestsText(project))); - }); - } - else { - source.test = new ArrayList(); - for (Map.Entry entry : testsText.entrySet()) { - source.test.add(new TestFileWrapper(entry.getKey(), entry.getValue())); - } - } - } - } - - private static class CoursesContainer { - public List courses; - public Map meta; - } - - static class StepSourceWrapper { - @Expose - StepSource stepSource; - - public StepSourceWrapper(Project project, Task task, int lessonId) { - stepSource = new StepSource(project, task, lessonId); - } - } - - static class CourseWrapper { - CourseInfo course; - - public CourseWrapper(Course course) { - this.course = new CourseInfo(); - this.course.setName(course.getName()); - this.course.setDescription(course.getDescription()); - this.course.setAuthors(course.getAuthors()); - } - } - - static class LessonWrapper { - Lesson lesson; - - public LessonWrapper(Lesson lesson) { - this.lesson = new Lesson(); - this.lesson.setName(lesson.getName()); - this.lesson.id = lesson.id; - this.lesson.steps = new ArrayList(); - } - } - - static class LessonContainer { - List lessons; - } - - static class StepSource { - @Expose Step block; - @Expose int position = 0; - @Expose int lesson = 0; - - public StepSource(Project project, Task task, int lesson) { - this.lesson = lesson; - position = task.getIndex(); - block = Step.fromTask(project, task); - } - } - - static class TestFileWrapper { - @Expose private final String name; - @Expose private final String text; - - public TestFileWrapper(String name, String text) { - this.name = name; - this.text = text; - } - } - - static class Section { - List units; - int course; - String title; - int position; - int id; - } - - static class SectionWrapper { - Section section; - } - - static class SectionContainer { - List
sections; - List lessons; - - List units; - } - - static class Unit { - int id; - int section; - int lesson; - int position; - } - - static class UnitContainer { - - List units; - } - - static class UnitWrapper{ - Unit unit; - } - - - static class AttemptWrapper { - static class Attempt { - public Attempt(int step) { - this.step = step; - } - - int step; - int id; - } - public AttemptWrapper(int step) { - attempt = new Attempt(step); - } - - Attempt attempt; - } - - static class AttemptContainer { - List attempts; - } - - static class SolutionFile { - String name; - String text; - - public SolutionFile(String name, String text) { - this.name = name; - this.text = text; - } - } - - static class AuthorWrapper { - List users; - } - - static class SubmissionWrapper { - Submission submission; - - - public SubmissionWrapper(int attempt, String score, ArrayList files) { - submission = new Submission(score, attempt, files); - } - - static class Submission { - int attempt; - - private final Reply reply; - - public Submission(String score, int attempt, ArrayList files) { - reply = new Reply(files, score); - this.attempt = attempt; - } - - static class Reply { - String score; - List solution; - - public Reply(ArrayList files, String score) { - this.score = score; - solution = files; - } - } - } - - } - - static class User { - String first_name; - String last_name; - String email; - String password; - - public User(String user, String password) { - email = user; - this.password = password; - this.first_name = user; - this.last_name = user; - } - } - - static class UserWrapper { - User user; - - public UserWrapper(String user, String password) { - this.user = new User(user, password); - } - } - } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/LoginDialog.java b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/LoginDialog.java index bb95494d1540..09a132765873 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/LoginDialog.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/LoginDialog.java @@ -14,6 +14,7 @@ public class LoginDialog extends DialogWrapper { myLoginPanel = new LoginPanel(this); setTitle("Login to Stepic"); setOKButtonText("Login"); + setTitle("Login to Stepic"); init(); } @@ -40,14 +41,12 @@ public class LoginDialog extends DialogWrapper { @Override protected void doOKAction() { AuthDataHolder authData = myLoginPanel.getAuthData(); - final boolean success = EduStepicConnector.login(authData.email, authData.password); - if (!success) { - setErrorText("Login failed"); + final StepicUser user = EduStepicConnector.login(authData.email, authData.password); + if (user != null) { + super.doOKAction(); } else { - StudySettings.getInstance().setLogin(authData.email); - StudySettings.getInstance().setPassword(authData.password); - super.doOKAction(); + setErrorText("Login failed"); } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicAdaptiveReactionsPanel.java b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicAdaptiveReactionsPanel.java new file mode 100644 index 000000000000..fb24f7f51330 --- /dev/null +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicAdaptiveReactionsPanel.java @@ -0,0 +1,196 @@ +package com.jetbrains.edu.learning.stepic; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.FileEditorManagerEvent; +import com.intellij.openapi.fileEditor.FileEditorManagerListener; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.progress.util.ProgressIndicatorBase; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ui.UIUtil; +import com.jetbrains.edu.learning.StudyUtils; +import com.jetbrains.edu.learning.courseFormat.StudyStatus; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + + +public class StepicAdaptiveReactionsPanel extends JPanel { + private final ReactionButtonPanel myHardPanel; + private final ReactionButtonPanel myBoringPanel; + private final Project myProject; + private static final int TOO_HARD_REACTION = 0; + private static final int TOO_BORING_REACTION = -1; + private static final String HARD_REACTION = "Too Hard"; + private static final String BORING_REACTION = "Too Boring"; + private static final String SOLVED_TASK_TOOLTIP = "Task Is Solved"; + private static final String HARD_LABEL_TOOLTIP = "Click To Get An Easier Task"; + private static final String BORING_LABEL_TOOLTIP = "Click To Get A More Challenging Task"; + + public StepicAdaptiveReactionsPanel(@NotNull final Project project) { + myProject = project; + setLayout(new GridBagLayout()); + setBackground(UIUtil.getTextFieldBackground()); + + myHardPanel = new ReactionButtonPanel(HARD_REACTION, HARD_LABEL_TOOLTIP, SOLVED_TASK_TOOLTIP, TOO_HARD_REACTION); + myBoringPanel = new ReactionButtonPanel(BORING_REACTION, BORING_LABEL_TOOLTIP, SOLVED_TASK_TOOLTIP, TOO_BORING_REACTION); + addFileListener(); + + final GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + c.gridx = 0; + c.gridy = 0; + add(Box.createVerticalStrut(3), c); + c.gridx = 1; + c.gridy = 1; + add(Box.createHorizontalStrut(3), c); + c.weightx = 1; + c.gridx = 2; + add(myHardPanel, c); + c.gridx = 3; + c.weightx = 0; + add(Box.createHorizontalStrut(3), c); + c.weightx = 1; + c.gridx = 4; + add(myBoringPanel, c); + c.gridx = 5; + c.weightx = 0; + add(Box.createHorizontalStrut(3), c); + } + + public void setEnabledRecursive(final boolean isEnabled) { + ApplicationManager.getApplication().invokeLater(() -> { + super.setEnabled(isEnabled); + myHardPanel.setEnabledRecursive(isEnabled); + myBoringPanel.setEnabledRecursive(isEnabled); + }); + } + + private void addFileListener() { + final FileEditorManagerListener editorManagerListener = new FileEditorManagerListener() { + @Override + public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) { + final com.jetbrains.edu.learning.courseFormat.Task task = StudyUtils.getTaskFromSelectedEditor(myProject); + final boolean isEnabled = task != null && task.getStatus() != StudyStatus.Solved; + StepicAdaptiveReactionsPanel.this.setEnabledRecursive(isEnabled); + } + + @Override + public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) { + + } + + @Override + public void selectionChanged(@NotNull FileEditorManagerEvent event) { + final com.jetbrains.edu.learning.courseFormat.Task task = StudyUtils.getTaskFromSelectedEditor(myProject); + final boolean isEnabled = task != null && task.getStatus() != StudyStatus.Solved; + StepicAdaptiveReactionsPanel.this.setEnabledRecursive(isEnabled); + } + }; + myProject.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, editorManagerListener); + } + + private class ReactionButtonPanel extends JPanel { + private final JPanel myButtonPanel; + private final JLabel myLabel; + + public ReactionButtonPanel(@NotNull final String text, + @NotNull final String enabledTooltip, + @NotNull final String disabledTooltip, + int reaction) { + com.jetbrains.edu.learning.courseFormat.Task task = StudyUtils.getTaskFromSelectedEditor(myProject); + final boolean isEnabled = task != null && task.getStatus() != StudyStatus.Solved; + + myLabel = new JLabel(text); + + myButtonPanel = new JPanel(); + myButtonPanel.setLayout(new BoxLayout(myButtonPanel, BoxLayout.PAGE_AXIS)); + myButtonPanel.setToolTipText(isEnabled ? enabledTooltip : disabledTooltip); + myButtonPanel.add(Box.createVerticalStrut(5)); + myButtonPanel.add(myLabel); + myButtonPanel.add(Box.createVerticalStrut(5)); + + setEnabledRecursive(isEnabled); + + setLayout(new GridBagLayout()); + setBorder(BorderFactory.createEtchedBorder()); + add(myButtonPanel); + addMouseListener(() -> EduAdaptiveStepicConnector.addNextRecommendedTask(myProject, reaction)); + } + + private void addMouseListener(@NotNull Runnable onClickAction) { + final ReactionMouseAdapter mouseAdapter = new ReactionMouseAdapter(onClickAction, this); + this.addMouseListener(mouseAdapter); + myButtonPanel.addMouseListener(mouseAdapter); + myLabel.addMouseListener(mouseAdapter); + } + + + public void setEnabledRecursive(final boolean isEnabled) { + ApplicationManager.getApplication().invokeLater(() -> { + super.setEnabled(isEnabled); + myButtonPanel.setEnabled(isEnabled); + myLabel.setEnabled(isEnabled); + }); + } + + private class ReactionMouseAdapter extends MouseAdapter { + private final Runnable myClickAction; + private final ReactionButtonPanel myPanel; + + public ReactionMouseAdapter(@NotNull final Runnable onClickAction, @NotNull final ReactionButtonPanel mainPanel) { + myClickAction = onClickAction; + myPanel = mainPanel; + } + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 1) { + final com.jetbrains.edu.learning.courseFormat.Task task = StudyUtils.getCurrentTask(myProject); + if (task != null && task.getStatus() != StudyStatus.Solved) { + final ProgressIndicatorBase progress = new ProgressIndicatorBase(); + progress.setText("Loading Next Recommendation"); + ProgressManager.getInstance().run(new Task.Backgroundable(myProject, + "Loading Next Recommendation") { + @Override + public void run(@NotNull ProgressIndicator indicator) { + StepicAdaptiveReactionsPanel.this.setEnabledRecursive(false); + myClickAction.run(); + } + + @Override + protected void onFinished() { + StepicAdaptiveReactionsPanel.this.setEnabledRecursive(true); + } + }); + } + } + } + + @Override + public void mouseEntered(MouseEvent e) { + final com.jetbrains.edu.learning.courseFormat.Task task = StudyUtils.getCurrentTask(myProject); + if (task != null && task.getStatus() != StudyStatus.Solved && myPanel.isEnabled()) { + setBackground(UIUtil.getButtonSelectColor()); + } + } + + @Override + public void mouseExited(MouseEvent e) { + setBackground(UIUtil.getLabelBackground()); + } + + private void setBackground(Color color) { + myPanel.setBackground(color); + myButtonPanel.setBackground(color); + } + } + } +} + diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicStudyOptions.java b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicStudyOptions.java index c3994496fef7..097c4a64ef9c 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicStudyOptions.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicStudyOptions.java @@ -15,8 +15,12 @@ */ package com.jetbrains.edu.learning.stepic; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.DocumentAdapter; +import com.jetbrains.edu.learning.StudyTaskManager; +import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.settings.StudyOptionsProvider; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -31,6 +35,7 @@ import java.awt.event.FocusListener; public class StepicStudyOptions implements StudyOptionsProvider { private static final String DEFAULT_PASSWORD_TEXT = "************"; + private static final Logger LOG = Logger.getInstance(StepicStudyOptions.class); private JTextField myLoginTextField; private JPasswordField myPasswordField; private JPanel myPane; @@ -98,13 +103,19 @@ public class StepicStudyOptions implements StudyOptionsProvider { myPasswordField.setText(StringUtil.isEmpty(password) ? null : password); } - + @Override public void reset() { - final StudySettings studySettings = StudySettings.getInstance(); - setLogin(studySettings.getLogin()); - setPassword(DEFAULT_PASSWORD_TEXT); + Project project = StudyUtils.getStudyProject(); + if (project != null) { + final StudyTaskManager studySettings = StudyTaskManager.getInstance(project); + setLogin(studySettings.getLogin()); + setPassword(DEFAULT_PASSWORD_TEXT); - resetCredentialsModification(); + resetCredentialsModification(); + } + else { + LOG.warn("No study object is opened"); + } } @Override @@ -112,13 +123,20 @@ public class StepicStudyOptions implements StudyOptionsProvider { } + @Override public void apply() { if (myCredentialsModified) { - final StudySettings studySettings = StudySettings.getInstance(); - studySettings.setLogin(getLogin()); - studySettings.setPassword(getPassword()); - if (!StringUtil.isEmptyOrSpaces(getLogin()) && !StringUtil.isEmptyOrSpaces(getPassword())) { - EduStepicConnector.login(getLogin(), getPassword()); + final Project project = StudyUtils.getStudyProject(); + if (project != null) { + final StudyTaskManager studyTaskManager = StudyTaskManager.getInstance(project); + studyTaskManager.setLogin(getLogin()); + studyTaskManager.setPassword(getPassword()); + if (!StringUtil.isEmptyOrSpaces(getLogin()) && !StringUtil.isEmptyOrSpaces(getPassword())) { + EduStepicConnector.login(getLogin(), getPassword()); + } + } + else { + LOG.warn("No study object is opened"); } } resetCredentialsModification(); diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicUser.java b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicUser.java new file mode 100644 index 000000000000..ff40e3428d37 --- /dev/null +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicUser.java @@ -0,0 +1,85 @@ +package com.jetbrains.edu.learning.stepic; + +import com.intellij.ide.passwordSafe.PasswordSafe; +import com.intellij.ide.passwordSafe.PasswordSafeException; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.text.StringUtil; +import com.jetbrains.edu.learning.StudyTaskManager; + +public class StepicUser { + private static final String STEPIC_SETTINGS_PASSWORD_KEY = "STEPIC_SETTINGS_PASSWORD_KEY"; + private static final Logger LOG = Logger.getInstance(StepicUser.class); + int id; + String firstName; + String last_name; + String email; + + public StepicUser() { + } + + public StepicUser(String email, String password) { + this.email = email; + setPassword(password); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return last_name; + } + + public void setLastName(String last_name) { + this.last_name = last_name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + final String login = getEmail(); + if (StringUtil.isEmptyOrSpaces(login)) return ""; + + String password; + try { + password = PasswordSafe.getInstance().getPassword(null, StudyTaskManager.class, STEPIC_SETTINGS_PASSWORD_KEY + login); + } + catch (PasswordSafeException e) { + LOG.info("Couldn't get password for key [" + STEPIC_SETTINGS_PASSWORD_KEY + "]", e); + password = ""; + } + + return StringUtil.notNullize(password); + } + + public void setPassword(String password) { + try { + PasswordSafe.getInstance().storePassword(null, StudyTaskManager.class, STEPIC_SETTINGS_PASSWORD_KEY + getEmail(), password); + } + catch (PasswordSafeException e) { + LOG.info("Couldn't set password for key [" + STEPIC_SETTINGS_PASSWORD_KEY + getEmail() + "]", e); + } + } + + public String getName() { + return StringUtil.join(new String[]{firstName, last_name}, " "); + } +} diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java new file mode 100644 index 000000000000..c8e970fdb07f --- /dev/null +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java @@ -0,0 +1,421 @@ +package com.jetbrains.edu.learning.stepic; + +import com.google.gson.annotations.Expose; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VirtualFile; +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.Task; +import com.jetbrains.edu.learning.courseFormat.TaskFile; +import org.apache.commons.codec.binary.Base64; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class StepicWrappers { + private static final Logger LOG = Logger.getInstance(StepOptions.class); + + static class StepContainer { + List steps; + } + + public static class Step { + @Expose StepOptions options; + @Expose String text; + @Expose String name = "pycharm"; + @Expose StepOptions source; + + public static Step fromTask(Project project, @NotNull final Task task) { + final Step step = new Step(); + step.text = task.getTaskText(project); + step.source = StepOptions.fromTask(project, task); + return step; + } + } + + public static class StepOptions { + @Expose List test; + @Expose String title; + @Expose List files; + @Expose String text; + @Expose List> samples; + @Expose Integer executionMemoryLimit; + @Expose Integer executionTimeLimit; + @Expose CodeTemplatesWrapper codeTemplates; + + public static StepOptions fromTask(final Project project, @NotNull final Task task) { + final StepOptions source = new StepOptions(); + setTests(task, source, project); + source.files = new ArrayList(); + source.title = task.getName(); + for (final Map.Entry entry : task.getTaskFiles().entrySet()) { + final TaskFile taskFile = new TaskFile(); + TaskFile.copy(entry.getValue(), taskFile); + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + final VirtualFile taskDir = task.getTaskDir(project); + assert taskDir != null; + VirtualFile ideaDir = project.getBaseDir().findChild(".idea"); + assert ideaDir != null; + EduUtils.createStudentFileFromAnswer(project, ideaDir, taskDir, entry.getKey(), taskFile); + } + }); + taskFile.name = entry.getKey(); + + VirtualFile ideaDir = project.getBaseDir().findChild(".idea"); + if (ideaDir == null) return null; + final VirtualFile file = ideaDir.findChild(taskFile.name); + try { + if (file != null) { + if (EduUtils.isImage(taskFile.name)) { + taskFile.text = Base64.encodeBase64URLSafeString(FileUtil.loadBytes(file.getInputStream())); + } + else { + taskFile.text = FileUtil.loadTextAndClose(file.getInputStream()); + } + } + } + catch (IOException e) { + LOG.error("Can't find file " + file.getPath()); + } + + source.files.add(taskFile); + } + return source; + } + + private static void setTests(@NotNull final Task task, @NotNull final StepOptions source, @NotNull final Project project) { + final Map testsText = task.getTestsText(); + if (testsText.isEmpty()) { + ApplicationManager.getApplication().runReadAction(new Runnable() { + @Override + public void run() { + source.test = Collections.singletonList(new TestFileWrapper(EduNames.TESTS_FILE, task.getTestsText(project))); + } + }); + } + else { + source.test = new ArrayList(); + for (Map.Entry entry : testsText.entrySet()) { + source.test.add(new TestFileWrapper(entry.getKey(), entry.getValue())); + } + } + } + } + + static class CodeTemplatesWrapper { + String python3; + String python27; + + @Nullable + public String getTemplateForLanguage(@NotNull final String langauge) { + if (langauge.equals(EduAdaptiveStepicConnector.PYTHON27)) { + return python27; + } + + if (langauge.equals(EduAdaptiveStepicConnector.PYTHON3)) { + return python3; + } + + return null; + } + } + + static class CoursesContainer { + public List courses; + public Map meta; + } + + static class StepSourceWrapper { + @Expose + StepSource stepSource; + + public StepSourceWrapper(Project project, Task task, int lessonId) { + stepSource = new StepSource(project, task, lessonId); + } + } + + static class CourseWrapper { + CourseInfo course; + + public CourseWrapper(Course course) { + this.course = new CourseInfo(); + this.course.setName(course.getName()); + this.course.setDescription(course.getDescription()); + this.course.setAuthors(course.getAuthors()); + } + } + + static class LessonWrapper { + Lesson lesson; + + public LessonWrapper(Lesson lesson) { + this.lesson = new Lesson(); + this.lesson.setName(lesson.getName()); + this.lesson.id = lesson.id; + this.lesson.steps = new ArrayList(); + } + } + + static class LessonContainer { + List lessons; + } + + static class StepSource { + @Expose Step block; + @Expose int position = 0; + @Expose int lesson = 0; + + public StepSource(Project project, Task task, int lesson) { + this.lesson = lesson; + position = task.getIndex(); + block = Step.fromTask(project, task); + } + } + + static class TestFileWrapper { + @Expose public final String name; + @Expose public final String text; + + public TestFileWrapper(String name, String text) { + this.name = name; + this.text = text; + } + } + + static class Section { + List units; + int course; + String title; + int position; + int id; + } + + static class SectionWrapper { + Section section; + } + + static class SectionContainer { + List
sections; + List lessons; + + List units; + } + + static class Unit { + int id; + int section; + int lesson; + int position; + List assignments; + } + + static class UnitContainer { + + List units; + } + + static class UnitWrapper { + Unit unit; + } + + static class AttemptWrapper { + static class Attempt { + public Attempt(int step) { + this.step = step; + } + + int step; + int id; + } + + public AttemptWrapper(int step) { + attempt = new Attempt(step); + } + + Attempt attempt; + } + + static class AttemptToPostWrapper { + static class Attempt { + int step; + String dataset_url; + String status; + String time; + String time_left; + String user; + String user_id; + + public Attempt(int step) { + this.step = step; + } + } + + public AttemptToPostWrapper(int step) { + attempt = new Attempt(step); + } + + Attempt attempt; + } + + static class AttemptContainer { + List attempts; + } + + static class SolutionFile { + String name; + String text; + + public SolutionFile(String name, String text) { + this.name = name; + this.text = text; + } + } + + static class AuthorWrapper { + List users; + } + + static class SubmissionWrapper { + Submission submission; + + + public SubmissionWrapper(int attempt, String score, ArrayList files) { + submission = new Submission(score, attempt, files); + } + + static class Submission { + int attempt; + private final Reply reply; + + public Submission(String score, int attempt, ArrayList files) { + reply = new Reply(files, score); + this.attempt = attempt; + } + + static class Reply { + String score; + List solution; + + public Reply(ArrayList files, String score) { + this.score = score; + solution = files; + } + } + } + } + + static class UserWrapper { + StepicUser user; + + public UserWrapper(String user, String password) { + this.user = new StepicUser(user, password); + } + } + + static class RecommendationReaction { + int reaction; + String user; + String lesson; + + public RecommendationReaction(int reaction, String user, String lesson) { + this.reaction = reaction; + this.user = user; + this.lesson = lesson; + } + } + + static class RecommendationReactionWrapper { + RecommendationReaction recommendationReaction; + + public RecommendationReactionWrapper(RecommendationReaction recommendationReaction) { + this.recommendationReaction = recommendationReaction; + } + } + + static class RecommendationWrapper { + Recommendation[] recommendations; + } + + static class Recommendation { + String id; + String lesson; + } + + + static class SubmissionToPostWrapper { + Submission submission; + + public SubmissionToPostWrapper(@NotNull String attemptId, @NotNull String language, @NotNull String code) { + submission = new Submission(attemptId, new Submission.Reply(language, code)); + } + + static class Submission { + String attempt; + Reply reply; + + public Submission(String attempt, Reply reply) { + this.attempt = attempt; + this.reply = reply; + } + + static class Reply { + String language; + String code; + + public Reply(String language, String code) { + this.language = language; + this.code = code; + } + } + } + } + + static class ResultSubmissionWrapper { + ResultSubmission[] submissions; + + static class ResultSubmission { + int id; + String status; + String hint; + } + } + + static class AssignmentsWrapper { + List assignments; + } + + static class Assignment { + int id; + int step; + } + + static class ViewsWrapper { + View view; + + public ViewsWrapper(final int assignment, final int step) { + this.view = new View(assignment, step); + } + } + + static class View { + int assignment; + int step; + + public View(int assignment, int step) { + this.assignment = assignment; + this.step = step; + } + } +} diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StudySettings.java b/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StudySettings.java deleted file mode 100644 index 8941d4343064..000000000000 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/stepic/StudySettings.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2000-2015 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.jetbrains.edu.learning.stepic; - -import com.intellij.ide.passwordSafe.PasswordSafe; -import com.intellij.ide.passwordSafe.PasswordSafeException; -import com.intellij.openapi.components.PersistentStateComponent; -import com.intellij.openapi.components.ServiceManager; -import com.intellij.openapi.components.State; -import com.intellij.openapi.components.Storage; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.util.text.StringUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@SuppressWarnings("MethodMayBeStatic") -@State(name = "StudySettings", storages = @Storage("stepic_settings.xml")) -public class StudySettings implements PersistentStateComponent { - - private State myState = new State(); - - public static class State { - @Nullable public String LOGIN = null; - } - - private static final String STEPIC_SETTINGS_PASSWORD_KEY = "STEPIC_SETTINGS_PASSWORD_KEY"; - private static final Logger LOG = Logger.getInstance(StudySettings.class.getName()); - - public static StudySettings getInstance() { - return ServiceManager.getService(StudySettings.class); - } - - @NotNull - public String getPassword() { - final String login = getLogin(); - if (StringUtil.isEmptyOrSpaces(login)) return ""; - - String password; - try { - password = PasswordSafe.getInstance().getPassword(null, StudySettings.class, STEPIC_SETTINGS_PASSWORD_KEY); - } - catch (PasswordSafeException e) { - LOG.info("Couldn't get password for key [" + STEPIC_SETTINGS_PASSWORD_KEY + "]", e); - password = ""; - } - - return StringUtil.notNullize(password); - } - - public void setPassword(@NotNull String password) { - try { - PasswordSafe.getInstance().storePassword(null, StudySettings.class, STEPIC_SETTINGS_PASSWORD_KEY, password); - } - catch (PasswordSafeException e) { - LOG.info("Couldn't set password for key [" + STEPIC_SETTINGS_PASSWORD_KEY + "]", e); - } - } - - @Nullable - public String getLogin() { - return myState.LOGIN; - } - - public void setLogin(@Nullable String login) { - myState.LOGIN = login; - } - @Nullable - @Override - public StudySettings.State getState() { - return myState; - } - - @Override - public void loadState(StudySettings.State state) { - myState = state; - } -} \ No newline at end of file diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyAddRemoteCourse.java b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyAddRemoteCourse.java index adce70291557..f61e12f93599 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyAddRemoteCourse.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyAddRemoteCourse.java @@ -40,6 +40,10 @@ public class StudyAddRemoteCourse { return myContentPanel; } + public JTextField getLoginField() { + return myLoginField; + } + public String getPassword() { return String.valueOf(myPasswordField.getPassword()); } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyBrowserWindow.java b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyBrowserWindow.java index fd7a12a5e01e..75d6146c48c9 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyBrowserWindow.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyBrowserWindow.java @@ -35,7 +35,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; -class StudyBrowserWindow extends JFrame { +public class StudyBrowserWindow extends JFrame { private static final Logger LOG = Logger.getInstance(StudyToolWindow.class); private static final String EVENT_TYPE_CLICK = "click"; private JFXPanel myPanel; diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyJavaFxToolWindow.java b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyJavaFxToolWindow.java index 058bd633a29b..d9507f11164f 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyJavaFxToolWindow.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyJavaFxToolWindow.java @@ -18,6 +18,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectUtil; import com.jetbrains.edu.learning.StudyPluginConfigurator; import com.jetbrains.edu.learning.StudyUtils; +import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -39,8 +40,8 @@ public class StudyJavaFxToolWindow extends StudyToolWindow { } @Override - public void setText(String text) { + public void setText(@NotNull String text) { StudyPluginConfigurator configurator = StudyUtils.getConfigurator(ProjectUtil.guessCurrentProject(this)); - myBrowserWindow.loadContent(text, configurator); + myBrowserWindow.loadContent(text, configurator); } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java index 065ada094360..8f4475372b85 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java @@ -3,8 +3,13 @@ package com.jetbrains.edu.learning.ui; import com.intellij.facet.ui.FacetValidatorsManager; import com.intellij.facet.ui.ValidationResult; import com.intellij.icons.AllIcons; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileChooser.FileChooser; import com.intellij.openapi.fileChooser.FileChooserDescriptor; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.project.DefaultProjectFactory; +import com.intellij.openapi.project.DefaultProjectFactoryImpl; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.ui.DialogWrapper; @@ -14,18 +19,20 @@ import com.intellij.openapi.ui.popup.PopupStep; import com.intellij.openapi.ui.popup.util.BaseListPopupStep; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.AncestorListenerAdapter; import com.intellij.util.Consumer; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.learning.stepic.CourseInfo; import com.jetbrains.edu.learning.stepic.EduStepicConnector; -import com.jetbrains.edu.learning.stepic.StudySettings; +import com.jetbrains.edu.learning.stepic.StepicUser; import icons.InteractiveLearningIcons; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; +import javax.swing.event.AncestorEvent; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -36,10 +43,11 @@ import java.util.List; * author: liana * data: 7/31/14. */ -public class StudyNewProjectPanel{ +public class StudyNewProjectPanel { + private static final Logger LOG = Logger.getInstance(StudyNewProjectPanel.class); private List myAvailableCourses = new ArrayList(); private JButton myBrowseButton; - private JComboBox myCoursesComboBox; + private JComboBox myCoursesComboBox; private JButton myRefreshButton; private JPanel myContentPanel; private JLabel myAuthorLabel; @@ -50,19 +58,37 @@ public class StudyNewProjectPanel{ private static final String CONNECTION_ERROR = "Failed to download courses.
Check your Internet connection."; private static final String INVALID_COURSE = "Selected course is invalid"; private FacetValidatorsManager myValidationManager; + private boolean isComboboxInitialized; - public StudyNewProjectPanel(StudyProjectGenerator generator) { + public StudyNewProjectPanel(@NotNull final StudyProjectGenerator generator) { myGenerator = generator; - myAvailableCourses = myGenerator.getCourses(false); myBrowseButton.setPreferredSize(new Dimension(28, 28)); myRefreshButton.setPreferredSize(new Dimension(28, 28)); - if (myAvailableCourses.isEmpty()) { + initListeners(); + myRefreshButton.setVisible(true); + myRefreshButton.putClientProperty("JButton.buttonType", "null"); + myRefreshButton.setIcon(AllIcons.Actions.Refresh); + + myLabel.setPreferredSize(new JLabel("Project name").getPreferredSize()); + myContentPanel.addAncestorListener(new AncestorListenerAdapter() { + @Override + public void ancestorMoved(AncestorEvent event) { + if (!isComboboxInitialized && myContentPanel.isVisible()) { + isComboboxInitialized = true; + initCoursesCombobox(); + } + } + }); + } + + private void initCoursesCombobox() { + myAvailableCourses = + myGenerator.getCoursesUnderProgress(false, "Getting Available Courses", ProjectManager.getInstance().getDefaultProject()); + if (myAvailableCourses == null || myAvailableCourses.isEmpty()) { setError(CONNECTION_ERROR); } else { - for (CourseInfo courseInfo : myAvailableCourses) { - myCoursesComboBox.addItem(courseInfo); - } + addCoursesToCombobox(myAvailableCourses); final CourseInfo selectedCourse = StudyUtils.getFirst(myAvailableCourses); final String authorsString = Course.getAuthorsString(selectedCourse.getAuthors()); myAuthorLabel.setText(!StringUtil.isEmptyOrSpaces(authorsString) ? "Author: " + authorsString : ""); @@ -72,13 +98,6 @@ public class StudyNewProjectPanel{ myGenerator.setSelectedCourse(selectedCourse); setOK(); } - initListeners(); - myRefreshButton.setVisible(true); - myRefreshButton.putClientProperty("JButton.buttonType", "null"); - myRefreshButton.setIcon(AllIcons.Actions.Refresh); - - myLabel.setPreferredSize(new JLabel("Project name").getPreferredSize()); - } private void setupBrowseButton() { @@ -177,21 +196,22 @@ public class StudyNewProjectPanel{ private class RefreshActionListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { - refreshCoursesList(); + final List courses = + myGenerator.getCoursesUnderProgress(true, "Refreshing Course List", DefaultProjectFactory.getInstance().getDefaultProject()); + if (courses != null) { + refreshCoursesList(courses); + } } } - private void refreshCoursesList() { - final List courses = myGenerator.getCourses(true); + private void refreshCoursesList(@NotNull final List courses) { if (courses.isEmpty()) { setError(CONNECTION_ERROR); return; } myCoursesComboBox.removeAllItems(); - for (CourseInfo courseInfo : courses) { - myCoursesComboBox.addItem(courseInfo); - } + addCoursesToCombobox(courses); myGenerator.setSelectedCourse(StudyUtils.getFirst(courses)); myGenerator.setCourses(courses); @@ -199,6 +219,20 @@ public class StudyNewProjectPanel{ StudyProjectGenerator.flushCache(myAvailableCourses); } + private void addCoursesToCombobox(@NotNull List courses) { + for (CourseInfo courseInfo : courses) { + if (!courseInfo.isAdaptive()) { + myCoursesComboBox.addItem(courseInfo); + } + else { + final boolean isLoggedIn = myGenerator.myUser != null; + if (isLoggedIn) { + myCoursesComboBox.addItem(courseInfo); + } + } + } + } + /** * Handles selecting course in combo box @@ -213,10 +247,11 @@ public class StudyNewProjectPanel{ if (selectedCourse == null || selectedCourse.equals(CourseInfo.INVALID_COURSE)) { myAuthorLabel.setText(""); myDescriptionLabel.setText(""); + setError(INVALID_COURSE); return; } final String authorsString = Course.getAuthorsString(selectedCourse.getAuthors()); - myAuthorLabel.setText(!StringUtil.isEmptyOrSpaces(authorsString) ?"Author: " + authorsString : ""); + myAuthorLabel.setText(!StringUtil.isEmptyOrSpaces(authorsString) ? "Author: " + authorsString : ""); myCoursesComboBox.removeItem(CourseInfo.INVALID_COURSE); myDescriptionLabel.setText(selectedCourse.getDescription()); myGenerator.setSelectedCourse(selectedCourse); @@ -238,6 +273,7 @@ public class StudyNewProjectPanel{ protected AddRemoteDialog() { super(null); + setTitle("Login To Stepic"); myRemoteCourse = new StudyAddRemoteCourse(); init(); } @@ -248,8 +284,15 @@ public class StudyNewProjectPanel{ return myRemoteCourse.getContentPanel(); } + @Nullable + @Override + public JComponent getPreferredFocusedComponent() { + return myRemoteCourse.getLoginField(); + } + @Override protected void doOKAction() { + super.doOKAction(); if (StringUtil.isEmptyOrSpaces(myRemoteCourse.getLogin())) { myRemoteCourse.setError("Please, enter your login"); return; @@ -259,15 +302,26 @@ public class StudyNewProjectPanel{ return; } - super.doOKAction(); - final boolean isSuccess = EduStepicConnector.login(myRemoteCourse.getLogin(), myRemoteCourse.getPassword()); - if (!isSuccess) { - setError("Failed to log in"); - } - StudySettings.getInstance().setLogin(myRemoteCourse.getLogin()); - StudySettings.getInstance().setPassword(myRemoteCourse.getPassword()); - refreshCoursesList(); + ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { + ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); + + final StepicUser stepicUser = StudyUtils.execCancelable(() -> EduStepicConnector.login(myRemoteCourse.getLogin(), + myRemoteCourse.getPassword())); + if (stepicUser != null) { + stepicUser.setEmail(myRemoteCourse.getLogin()); + stepicUser.setPassword(myRemoteCourse.getPassword()); + myGenerator.myUser = stepicUser; + + + final List courses = myGenerator.getCoursesAsynchronouslyIfNeeded(true); + if (courses != null) { + ApplicationManager.getApplication().invokeLater(() -> refreshCoursesList(courses)); + } + } + else { + setError("Failed to login"); + } + }, "Signing In And Getting Stepic Course List", true, new DefaultProjectFactoryImpl().getDefaultProject()); } } - } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudySwingToolWindow.java b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudySwingToolWindow.java index ebaff662ecb8..be13a1b0f46d 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudySwingToolWindow.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudySwingToolWindow.java @@ -22,6 +22,7 @@ import com.intellij.ui.BrowserHyperlinkListener; import com.intellij.ui.ColorUtil; import com.intellij.ui.components.JBScrollPane; import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; import javax.swing.*; import javax.swing.border.EmptyBorder; @@ -61,7 +62,7 @@ public class StudySwingToolWindow extends StudyToolWindow { return scrollPane; } - public void setText(String text) { + public void setText(@NotNull String text) { myTaskTextPane.setText(text); } } diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyTestResultsToolWindowFactory.kt b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyTestResultsToolWindowFactory.kt new file mode 100644 index 000000000000..652d758ebfc6 --- /dev/null +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyTestResultsToolWindowFactory.kt @@ -0,0 +1,55 @@ +package com.jetbrains.edu.learning.ui + +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerEvent +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.wm.ToolWindow +import com.jetbrains.edu.learning.StudyUtils +import com.jetbrains.python.console.PythonConsoleView + + +class StudyTestResultsToolWindowFactory: StudyToolWindowFactory() { + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val currentTask = StudyUtils.getCurrentTask(project) + if (currentTask != null) { + val sdk = StudyUtils.findSdk(currentTask, project) + if (sdk != null) { + val testResultsToolWindow = PythonConsoleView(project, "Local test results", sdk); + testResultsToolWindow.isEditable = false + testResultsToolWindow.isConsoleEditorEnabled = false + testResultsToolWindow.prompt = null + toolWindow.isToHideOnEmptyContent = true + + val contentManager = toolWindow.contentManager + val content = contentManager.factory.createContent(testResultsToolWindow.component, null, false) + contentManager.addContent(content) + + project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, getFileEditorManagerListener(toolWindow)) + } + else { + StudyUtils.showNoSdkNotification(currentTask, project) + } + } + } + + fun getFileEditorManagerListener(toolWindow: ToolWindow): FileEditorManagerListener { + + return object : FileEditorManagerListener { + + override fun fileOpened(source: FileEditorManager, file: VirtualFile) { + } + + override fun fileClosed(source: FileEditorManager, file: VirtualFile) { + toolWindow.setAvailable(false, {}) + } + + override fun selectionChanged(event: FileEditorManagerEvent) { + toolWindow.setAvailable(false, {}) + } + } + } +} + + diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java index 846b93633f87..d47ef138d308 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindow.java @@ -35,6 +35,7 @@ import com.intellij.util.ui.JBUI; import com.jetbrains.edu.learning.*; import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.courseFormat.TaskFile; +import com.jetbrains.edu.learning.stepic.StepicAdaptiveReactionsPanel; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -45,6 +46,7 @@ public abstract class StudyToolWindow extends SimpleToolWindowPanel implements D private static final Logger LOG = Logger.getInstance(StudyToolWindow.class); private static final String TASK_INFO_ID = "taskInfo"; private static final String EMPTY_TASK_TEXT = "Please, open any task to see task description"; + private final JBCardLayout myCardLayout; private final JPanel myContentPanel; private final OnePixelSplitter mySplitPane; @@ -63,7 +65,14 @@ public abstract class StudyToolWindow extends SimpleToolWindowPanel implements D JPanel toolbarPanel = createToolbarPanel(getActionGroup(project)); setToolbar(toolbarPanel); - myContentPanel.add(TASK_INFO_ID, createTaskInfoPanel(project)); + final JPanel panel = new JPanel(new BorderLayout()); + final Course course = StudyTaskManager.getInstance(project).getCourse(); + if (course != null && course.isAdaptive()) { + panel.add(new StepicAdaptiveReactionsPanel(project), BorderLayout.NORTH); + } + JComponent taskInfoPanel = createTaskInfoPanel(project); + panel.add(taskInfoPanel, BorderLayout.CENTER); + myContentPanel.add(TASK_INFO_ID, panel); mySplitPane.setFirstComponent(myContentPanel); addAdditionalPanels(project); myCardLayout.show(myContentPanel, TASK_INFO_ID); @@ -76,14 +85,15 @@ public abstract class StudyToolWindow extends SimpleToolWindowPanel implements D project.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, listener); } - if (StudyTaskManager.getInstance(project).isTurnEditingMode() || StudyTaskManager.getInstance(project).getToolWindowMode() == StudyToolWindowMode.EDITING) { + if (StudyTaskManager.getInstance(project).isTurnEditingMode() || + StudyTaskManager.getInstance(project).getToolWindowMode() == StudyToolWindowMode.EDITING) { TaskFile file = StudyUtils.getSelectedTaskFile(project); if (file != null) { VirtualFile taskDir = file.getTask().getTaskDir(project); setTaskText(taskText, taskDir, project); - } - } else { + } + else { setTaskText(taskText, null, project); } } @@ -98,6 +108,7 @@ public abstract class StudyToolWindow extends SimpleToolWindowPanel implements D } } + public void dispose() { } @@ -180,7 +191,7 @@ public abstract class StudyToolWindow extends SimpleToolWindowPanel implements D } } - protected abstract void setText(String text); + protected abstract void setText(@NotNull String text); public void setEmptyText(@NotNull Project project) { if (StudyTaskManager.getInstance(project).getToolWindowMode() == StudyToolWindowMode.EDITING) { diff --git a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindowFactory.java b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindowFactory.java index 886936b22112..4530e004504d 100644 --- a/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindowFactory.java +++ b/python/educational-core/student/src/com/jetbrains/edu/learning/ui/StudyToolWindowFactory.java @@ -7,6 +7,7 @@ import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowFactory; import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentManager; +import com.jetbrains.edu.learning.StudyProjectComponent; import com.jetbrains.edu.learning.StudyTaskManager; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.courseFormat.Course; @@ -23,7 +24,6 @@ public class StudyToolWindowFactory implements ToolWindowFactory, DumbAware { StudyTaskManager taskManager = StudyTaskManager.getInstance(project); final Course course = taskManager.getCourse(); if (course != null) { - final StudyToolWindow studyToolWindow; if (StudyUtils.hasJavaFx() && StudyTaskManager.getInstance(project).shouldUseJavaFx()) { studyToolWindow = new StudyJavaFxToolWindow(); diff --git a/python/educational-core/student/student.iml b/python/educational-core/student/student.iml index bbd07b593eae..5fff6115855f 100644 --- a/python/educational-core/student/student.iml +++ b/python/educational-core/student/student.iml @@ -21,6 +21,8 @@ + + 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 687bdd580a95..c858ac375be0 100644 --- a/python/educational-python/resources/fileTemplates/internal/test_helper.py.ft +++ b/python/educational-python/resources/fileTemplates/internal/test_helper.py.ft @@ -9,7 +9,7 @@ def get_file_text(path): return text -def get_file_output(encoding="utf-8", path=sys.argv[-1]): +def get_file_output(encoding="utf-8", path=sys.argv[-1], arg_string=""): """ Returns answer file output :param encoding: to decode output in python3 @@ -18,7 +18,13 @@ def get_file_output(encoding="utf-8", path=sys.argv[-1]): """ import subprocess - proc = subprocess.Popen([sys.executable, path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + proc = subprocess.Popen([sys.executable, path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + if arg_string: + for arg in arg_string.split("\n"): + proc.stdin.write(bytearray(str(arg) + "\n", encoding)) + proc.stdin.flush() + return list(map(lambda x: str(x.decode(encoding)), proc.communicate()[0].splitlines())) @@ -31,7 +37,8 @@ def test_file_importable(): parent = os.path.abspath(os.path.join(path, os.pardir)) python_files = [f for f in os.listdir(parent) if os.path.isfile(os.path.join(parent, f)) and f.endswith(".py")] for python_file in python_files: - if python_file == "tests.py": continue + if python_file == "tests.py": + continue check_importable_path(os.path.join(parent, python_file)) return check_importable_path(path) @@ -187,6 +194,28 @@ def get_answer_placeholders(): return windows +def check_samples(samples=()): + """ + Check script output for all samples. Sample is a two element list, where the first is input and + the second is output. + """ + for sample in samples: + if len(sample) == 2: + output = get_file_output(arg_string=str(sample[0])) + if "\n".join(output) != sample[1]: + failed( + "Test from samples failed: \n \n" + "Input:\n{}" + "\n \n" + "Expected:\n{}" + "\n \n" + "Your result:\n{}".format(str.strip(sample[0]), str.strip(sample[1]), "\n".join(output))) + return + set_congratulation_message("All test from samples passed. Now we are checking your solution on Stepic server.") + + passed() + + def run_common_tests(error_text="Please, reload file and try again"): test_is_initial_text() test_is_not_empty() diff --git a/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java b/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java index 7fb403665248..60e91394b8ab 100644 --- a/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java +++ b/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyCheckAction.java @@ -15,12 +15,13 @@ import com.jetbrains.edu.learning.actions.StudyRunAction; import com.jetbrains.edu.learning.checker.StudyCheckTask; import com.jetbrains.edu.learning.checker.StudyCheckUtils; import com.jetbrains.edu.learning.checker.StudyTestRunner; -import com.jetbrains.edu.learning.checker.StudyTestsOutputParser; import com.jetbrains.edu.learning.core.EduNames; +import com.jetbrains.edu.learning.courseFormat.Course; import com.jetbrains.edu.learning.courseFormat.StudyStatus; import com.jetbrains.edu.learning.courseFormat.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.editor.StudyEditor; +import com.jetbrains.edu.learning.ui.StudyToolWindow; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -41,8 +42,8 @@ public class PyStudyCheckAction extends StudyCheckAction { } if (StudyCheckUtils.hasBackgroundProcesses(project)) return; - - if (!runTask(project)) return; + final Course course = StudyTaskManager.getInstance(project).getCourse(); + if (course != null && !course.isAdaptive() && !runTask(project)) return; final Task task = studyState.getTask(); final VirtualFile taskDir = studyState.getTaskDir(); @@ -90,34 +91,48 @@ public class PyStudyCheckAction extends StudyCheckAction { final Process testProcess, final String commandLine) { return new StudyCheckTask(project, studyState, myCheckInProgress, testProcess, commandLine) { - @Override - protected void onTaskFailed(StudyTestsOutputParser.TestsOutput testsOutput) { - ApplicationManager.getApplication().invokeLater(() -> { - if (myTaskDir == null) return; - myTaskManger.setStatus(myTask, StudyStatus.Failed); - for (Map.Entry entry : myTask.getTaskFiles().entrySet()) { - final String name = entry.getKey(); - final TaskFile taskFile = entry.getValue(); - if (taskFile.getAnswerPlaceholders().size() < 2) { - myTaskManger.setStatus(taskFile, StudyStatus.Failed); - continue; - } - if (EduNames.STUDY.equals(myTaskManger.getCourse().getCourseMode())) { - CommandProcessor.getInstance().runUndoTransparentAction(() -> ApplicationManager.getApplication().runWriteAction(() -> StudyCheckUtils.runSmartTestProcess(myTaskDir, testRunner, name, taskFile, project))); - } - } - StudyCheckUtils.showTestResultPopUp(testsOutput.getMessage(), MessageType.ERROR.getPopupBackground(), project); - StudyCheckUtils.navigateToFailedPlaceholder(myStudyState, myTask, myTaskDir, project); - }); + @Override + protected void onTaskFailed(String message) { + ApplicationManager.getApplication().invokeLater(() -> { + if (myTaskDir == null) return; + myTaskManger.setStatus(myTask, StudyStatus.Failed); + for (Map.Entry entry : myTask.getTaskFiles().entrySet()) { + final String name = entry.getKey(); + final TaskFile taskFile = entry.getValue(); + if (taskFile.getAnswerPlaceholders().size() < 2) { + myTaskManger.setStatus(taskFile, StudyStatus.Failed); + continue; } - }; + if (EduNames.STUDY.equals(myTaskManger.getCourse().getCourseMode())) { + CommandProcessor.getInstance().runUndoTransparentAction(() -> ApplicationManager.getApplication().runWriteAction(() -> { + StudyCheckUtils.runSmartTestProcess(myTaskDir, testRunner, name, taskFile, project); + })); + } + } + final StudyToolWindow toolWindow = StudyUtils.getStudyToolWindow(project); + if (toolWindow != null) { + final Course course = StudyTaskManager.getInstance(project).getCourse(); + if (course != null) { + if (course.isAdaptive()) { + StudyCheckUtils.showTestResultPopUp("Failed", MessageType.ERROR.getPopupBackground(), project); + StudyCheckUtils.showTestResultsToolWindow(project, message, false); + } + else { + StudyCheckUtils.showTestResultPopUp(message, MessageType.ERROR.getPopupBackground(), project); + } + } + StudyCheckUtils.navigateToFailedPlaceholder(myStudyState, myTask, myTaskDir, project); + } + }); + } + }; } @Nullable private static VirtualFile getTaskVirtualFile(@NotNull final StudyState studyState, - @NotNull final Task task, - @NotNull final VirtualFile taskDir) { + @NotNull final Task task, + @NotNull final VirtualFile taskDir) { VirtualFile taskVirtualFile = studyState.getVirtualFile(); for (Map.Entry entry : task.getTaskFiles().entrySet()) { String name = entry.getKey(); @@ -131,7 +146,7 @@ public class PyStudyCheckAction extends StudyCheckAction { } return taskVirtualFile; } - + @NotNull @Override public String getActionId() { diff --git a/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyDirectoryProjectGenerator.java b/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyDirectoryProjectGenerator.java index df9db88d18ae..a3492b7247b2 100644 --- a/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyDirectoryProjectGenerator.java +++ b/python/educational-python/student-python/src/com/jetbrains/edu/learning/PyStudyDirectoryProjectGenerator.java @@ -6,17 +6,19 @@ import com.intellij.facet.ui.ValidationResult; import com.intellij.ide.fileTemplates.FileTemplate; import com.intellij.ide.fileTemplates.FileTemplateManager; import com.intellij.ide.fileTemplates.FileTemplateUtil; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.platform.DirectoryProjectGenerator; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiManager; import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; -import com.jetbrains.edu.learning.ui.StudyNewProjectPanel; import com.jetbrains.edu.learning.stepic.CourseInfo; +import com.jetbrains.edu.learning.ui.StudyNewProjectPanel; import com.jetbrains.python.newProject.PythonProjectGenerator; import icons.InteractiveLearningPythonIcons; import org.jetbrains.annotations.Nls; @@ -89,14 +91,14 @@ public class PyStudyDirectoryProjectGenerator extends PythonProjectGenerator imp throw new UnsupportedOperationException(); } public void validate() { - fireStateChanged(); + ApplicationManager.getApplication().invokeLater(()->fireStateChanged()); } }); return settingsPanel.getContentPanel(); } public List getCourses() { - return myGenerator.getCourses(false); + return myGenerator.getCoursesUnderProgress(false, "Getting Courses", ProjectManager.getInstance().getDefaultProject()); } public void setSelectedCourse(CourseInfo course) {