diff --git a/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.form b/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.form
index 3c5369f403de..aeff7d7125ba 100644
--- a/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.form
+++ b/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.form
@@ -1,6 +1,6 @@
diff --git a/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java b/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java
index afa2484d96a6..b84c7f526a9f 100644
--- a/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java
+++ b/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java
@@ -3,10 +3,12 @@ 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.ui.HideableDecorator;
import com.jetbrains.edu.courseFormat.Course;
import com.jetbrains.edu.learning.StudyUtils;
import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator;
import com.jetbrains.edu.stepic.CourseInfo;
+import com.jetbrains.edu.stepic.EduStepicConnector;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
@@ -20,6 +22,7 @@ import java.util.List;
* data: 7/31/14.
*/
public class StudyNewProjectPanel{
+ private final HideableDecorator myDecorator;
private List myAvailableCourses = new ArrayList();
private JComboBox myCoursesComboBox;
private JButton myRefreshButton;
@@ -28,6 +31,11 @@ public class StudyNewProjectPanel{
private JLabel myDescriptionLabel;
private JLabel myLabel;
private JPanel myInfoPanel;
+ private JPanel myHideablePanel;
+ private JPanel myLoginPanel;
+ private JPasswordField myPasswordField;
+ private JTextField myLoginField;
+ private JButton myLogInButton;
private final StudyProjectGenerator myGenerator;
private static final String CONNECTION_ERROR = "Failed to download courses.
Check your Internet connection.";
private static final String INVALID_COURSE = "Selected course is invalid";
@@ -54,12 +62,26 @@ public class StudyNewProjectPanel{
myRefreshButton.setIcon(AllIcons.Actions.Refresh);
myLabel.setPreferredSize(new JLabel("Project name").getPreferredSize());
+ myDecorator = new HideableDecorator(myHideablePanel, "Credentials", false);
+ myDecorator.setOn(false);
+ myDecorator.setContentComponent(myLoginPanel);
+
}
private void initListeners() {
-
myRefreshButton.addActionListener(new RefreshActionListener());
myCoursesComboBox.addActionListener(new CourseSelectedListener());
+ myLogInButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ final boolean isSuccess = EduStepicConnector.login(myLoginField.getText(), String.valueOf(myPasswordField.getPassword()));
+ if (!isSuccess) {
+ setError("Failed to log in");
+ return;
+ }
+ refreshCoursesList();
+ }
+ });
}
private void setError(@NotNull final String errorMessage) {
@@ -93,24 +115,28 @@ public class StudyNewProjectPanel{
private class RefreshActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
- final List courses = myGenerator.getCourses(true);
- if (courses.isEmpty()) {
- setError(CONNECTION_ERROR);
- return;
- }
- myCoursesComboBox.removeAllItems();
-
- for (CourseInfo courseInfo : courses) {
- myCoursesComboBox.addItem(courseInfo);
- }
- myGenerator.setSelectedCourse(StudyUtils.getFirst(courses));
-
- myGenerator.setCourses(courses);
- myAvailableCourses = courses;
- myGenerator.flushCache();
+ refreshCoursesList();
}
}
+ private void refreshCoursesList() {
+ final List courses = myGenerator.getCourses(true);
+ if (courses.isEmpty()) {
+ setError(CONNECTION_ERROR);
+ return;
+ }
+ myCoursesComboBox.removeAllItems();
+
+ for (CourseInfo courseInfo : courses) {
+ myCoursesComboBox.addItem(courseInfo);
+ }
+ myGenerator.setSelectedCourse(StudyUtils.getFirst(courses));
+
+ myGenerator.setCourses(courses);
+ myAvailableCourses = courses;
+ myGenerator.flushCache();
+ }
+
/**
* Handles selecting course in combo box
diff --git a/python/educational/src/com/jetbrains/edu/stepic/EduStepicConnector.java b/python/educational/src/com/jetbrains/edu/stepic/EduStepicConnector.java
index f4314caca004..a37c4680cf34 100644
--- a/python/educational/src/com/jetbrains/edu/stepic/EduStepicConnector.java
+++ b/python/educational/src/com/jetbrains/edu/stepic/EduStepicConnector.java
@@ -10,69 +10,139 @@ import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.util.io.HttpRequests;
import com.intellij.util.net.ssl.CertificateManager;
import com.jetbrains.edu.EduUtils;
import com.jetbrains.edu.courseFormat.Course;
import com.jetbrains.edu.courseFormat.Lesson;
import com.jetbrains.edu.courseFormat.Task;
import com.jetbrains.edu.courseFormat.TaskFile;
-import org.apache.http.Header;
+import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
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.methods.HttpRequestBase;
+import org.apache.http.cookie.Cookie;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
-import org.apache.http.impl.cookie.BasicClientCookie;
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 java.io.BufferedReader;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
import java.util.*;
public class EduStepicConnector {
- private static final String stepicApiUrl = "https://stepic.org/api/";
+ 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 final String ourDomain = "stepic.org";
- private static String ourSessionId = "524iethiwju2tjywaqmf7tbwx0p0jk1b";
- private static String ourCSRFToken = "LJ9n6OyLVA7hxU94dlYWUu65MF51Nx37";
+ private static String ourCSRFToken = "";
+ private static CloseableHttpClient ourClient;
+
//this prefix indicates that course can be opened by educational plugin
public static final String PYCHARM_PREFIX = "pycharm";
+ private static BasicCookieStore ourCookieStore;
private EduStepicConnector() {
}
- private static T getFromStepic(String link, final Class container) throws IOException {
- return HttpRequests.request(stepicApiUrl + link).connect(new HttpRequests.RequestProcessor() {
- @Override
- public T process(@NotNull HttpRequests.Request request) throws IOException {
- final BufferedReader reader = request.getReader();
- Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
- return gson.fromJson(reader, container);
+ public static boolean login(@NotNull final String user, @NotNull final String password) {
+ if (ourClient == null || ourCookieStore == null)
+ initializeClient();
+ return postCredentials(user, password);
+ }
+
+ 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"));
+
+ HttpClientBuilder builder = HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext()).setMaxConnPerRoute(100);
+ ourCookieStore = new BasicCookieStore();
+ ourClient = builder.setDefaultCookieStore(ourCookieStore).build();
+
+ try {
+ ourClient.execute(request);
+ saveCSRFToken();
+ }
+ catch (IOException e) {
+ LOG.error(e.getMessage());
+ }
+ }
+
+ private static void saveCSRFToken() {
+ if (ourCookieStore == null) return;
+ final List cookies = ourCookieStore.getCookies();
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals("csrftoken")) {
+ ourCSRFToken = cookie.getValue();
}
- });
+ }
+ }
+
+ private static boolean postCredentials(String user, String password) {
+ String url = stepicUrl + "accounts/login/";
+ final HttpPost request = new HttpPost(url);
+ List nvps = new ArrayList ();
+ nvps.add(new BasicNameValuePair("csrfmiddlewaretoken", ourCSRFToken));
+ nvps.add(new BasicNameValuePair("login", user));
+ nvps.add(new BasicNameValuePair("next", "/"));
+ 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());
+ return false;
+ }
+
+ setHeaders(request, "application/x-www-form-urlencoded");
+
+ try {
+ final CloseableHttpResponse response = ourClient.execute(request);
+ saveCSRFToken();
+ final StatusLine line = response.getStatusLine();
+ if (line.getStatusCode() != 302) {
+ LOG.error("Failed to login " + EntityUtils.toString(response.getEntity()));
+ return false;
+ }
+ }
+ catch (IOException e) {
+ LOG.error(e.getMessage());
+ return false;
+ }
+ return true;
+ }
+
+ private static T getFromStepic(String link, final Class container) throws IOException {
+ final HttpGet request = new HttpGet(stepicApiUrl + link);
+ if (ourClient == null) {
+ initializeClient();
+ }
+ setHeaders(request, "application/json");
+
+ final CloseableHttpResponse response = ourClient.execute(request);
+ final String responseString = EntityUtils.toString(response.getEntity());
+ Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+ return gson.fromJson(responseString, container);
}
@NotNull
public static List getCourses() {
try {
List result = new ArrayList();
- final List courseInfos =
- HttpRequests.request(stepicApiUrl + "courses").connect(new HttpRequests.RequestProcessor>() {
-
- @Override
- public List process(@NotNull HttpRequests.Request request) throws IOException {
- final BufferedReader reader = request.getReader();
- Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
- return gson.fromJson(reader, CoursesContainer.class).courses;
- }
- });
+ final List courseInfos = getFromStepic("courses", CoursesContainer.class).courses;
for (CourseInfo info : courseInfos) {
final String courseType = info.getType();
if (StringUtil.isEmptyOrSpaces(courseType)) continue;
@@ -153,23 +223,29 @@ public class EduStepicConnector {
}
- public static boolean postLesson(Project project, @NotNull final Lesson lesson) {
- final HttpPost request = new HttpPost(stepicApiUrl + "lessons");
- final ArrayList headers = getHeaders(request);
- HttpClientBuilder builder = HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext());
- final BasicCookieStore cookieStore = getCookies();
- final CloseableHttpClient client = builder.setDefaultHeaders(headers).setDefaultCookieStore(cookieStore).build();
+ public static void showLoginDialog() {
+ final LoginDialog dialog = new LoginDialog();
+ dialog.show();
+ }
+
+ public static void postLesson(Project project, @NotNull final Lesson lesson) {
+ final HttpPost request = new HttpPost(stepicApiUrl + "lessons");
+ if (ourClient == null) {
+ showLoginDialog();
+ }
+
+ setHeaders(request, "application/json");
String requestBody = new Gson().toJson(new LessonWrapper(lesson));
request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
try {
- final CloseableHttpResponse response = client.execute(request);
+ final CloseableHttpResponse response = ourClient.execute(request);
final String responseString = EntityUtils.toString(response.getEntity());
final StatusLine line = response.getStatusLine();
if (line.getStatusCode() != 201) {
- LOG.error("Failed to push " + EntityUtils.toString(response.getEntity()));
- return false;
+ LOG.error("Failed to push " + responseString);
+ return;
}
final Lesson postedLesson = new Gson().fromJson(responseString, Course.class).getLessons().get(0);
for (Task task : lesson.getTaskList()) {
@@ -179,56 +255,31 @@ public class EduStepicConnector {
catch (IOException e) {
LOG.error(e.getMessage());
}
- return false;
}
- public static boolean postTask(Project project, @NotNull final Task task, int id) {
+ public static void postTask(Project project, @NotNull final Task task, int id) {
final HttpPost request = new HttpPost(stepicApiUrl + "step-sources");
- final ArrayList headers = getHeaders(request);
- HttpClientBuilder builder = HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext());
- final BasicCookieStore cookieStore = getCookies();
- final CloseableHttpClient client = builder.setDefaultHeaders(headers).setDefaultCookieStore(cookieStore).build();
+ setHeaders(request, "application/json");
final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().create();
String requestBody = gson.toJson(new StepSourceWrapper(project, task, id));
request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
try {
- final CloseableHttpResponse response = client.execute(request);
+ final CloseableHttpResponse response = ourClient.execute(request);
final StatusLine line = response.getStatusLine();
if (line.getStatusCode() != 201) {
LOG.error("Failed to push " + EntityUtils.toString(response.getEntity()));
}
- return line.getStatusCode() == 201;
}
catch (IOException e) {
LOG.error(e.getMessage());
}
-
- return false;
}
- private static BasicCookieStore getCookies() {
- final BasicCookieStore cookieStore = new BasicCookieStore();
- final BasicClientCookie sessionid = new BasicClientCookie("sessionid", ourSessionId);
- sessionid.setDomain(ourDomain);
- sessionid.setPath("/");
-
- cookieStore.addCookie(sessionid);
- final BasicClientCookie csrfToken = new BasicClientCookie("csrftoken", ourCSRFToken);
- csrfToken.setDomain(ourDomain);
- csrfToken.setPath("/");
-
- cookieStore.addCookie(csrfToken);
- return cookieStore;
- }
-
- private static ArrayList getHeaders(HttpPost request) {
- final ArrayList headers = new ArrayList();
- headers.add(new BasicHeader("referer", "https://stepic.org"));
- headers.add(new BasicHeader("X-CSRFToken", ourCSRFToken));
- headers.add(new BasicHeader("content-type", "application/json"));
- request.setHeaders(headers.toArray(new Header[headers.size()]));
- return headers;
+ private 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 {
@@ -267,6 +318,7 @@ public class EduStepicConnector {
@Override
public void run() {
final VirtualFile taskDir = task.getTaskDir(project);
+ assert taskDir != null;
EduUtils.createStudentFileFromAnswer(project, taskDir, taskDir, entry);
}
});
diff --git a/python/educational/src/com/jetbrains/edu/stepic/LoginDialog.java b/python/educational/src/com/jetbrains/edu/stepic/LoginDialog.java
new file mode 100644
index 000000000000..a670e83bc1b0
--- /dev/null
+++ b/python/educational/src/com/jetbrains/edu/stepic/LoginDialog.java
@@ -0,0 +1,62 @@
+package com.jetbrains.edu.stepic;
+
+import com.intellij.openapi.ui.DialogWrapper;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+
+public class LoginDialog extends DialogWrapper {
+ private final LoginPanel myLoginPanel;
+
+ public LoginDialog() {
+ super(false);
+ myLoginPanel = new LoginPanel(this);
+ setTitle("Login to Stepic");
+ setOKButtonText("Login");
+ init();
+ }
+
+ @NotNull
+ protected Action[] createActions() {
+ return new Action[]{getOKAction(), getCancelAction()};
+ }
+
+ @Override
+ protected JComponent createCenterPanel() {
+ return myLoginPanel.getPanel();
+ }
+
+ @Override
+ protected String getHelpId() {
+ return "login_to_stepic";
+ }
+
+ @Override
+ public JComponent getPreferredFocusedComponent() {
+ return myLoginPanel.getPreferableFocusComponent();
+ }
+
+ @Override
+ protected void doOKAction() {
+ AuthDataHolder authData = myLoginPanel.getAuthData();
+ final boolean success = EduStepicConnector.login(authData.email, authData.password);
+ if (!success) {
+ setErrorText("Log in failed");
+ }
+ super.doOKAction();
+ }
+
+ public void clearErrors() {
+ setErrorText(null);
+ }
+
+ public static class AuthDataHolder {
+ String email;
+ String password;
+
+ public AuthDataHolder(String login, String password) {
+ email = login;
+ this.password = password;
+ }
+ }
+}
\ No newline at end of file
diff --git a/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.form b/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.form
new file mode 100644
index 000000000000..57570c149e93
--- /dev/null
+++ b/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.form
@@ -0,0 +1,52 @@
+
+
diff --git a/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.java b/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.java
new file mode 100644
index 000000000000..3d883a774b24
--- /dev/null
+++ b/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.java
@@ -0,0 +1,69 @@
+/*
+ * 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.stepic;
+
+import com.intellij.ui.DocumentAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+public class LoginPanel {
+ private JPanel myPane;
+ private JTextField myLoginTextField;
+ private JPasswordField myPasswordField;
+
+ public LoginPanel(final LoginDialog dialog) {
+ DocumentListener listener = new DocumentAdapter() {
+ @Override
+ protected void textChanged(DocumentEvent e) {
+ dialog.clearErrors();
+ }
+ };
+ myLoginTextField.getDocument().addDocumentListener(listener);
+ myPasswordField.getDocument().addDocumentListener(listener);
+ }
+
+ public JComponent getPanel() {
+ return myPane;
+ }
+
+ public void setLogin(@Nullable String login) {
+ myLoginTextField.setText(login);
+ }
+
+ @NotNull
+ public String getLogin() {
+ return myLoginTextField.getText().trim();
+ }
+
+ @NotNull
+ private String getPassword() {
+ return String.valueOf(myPasswordField.getPassword());
+ }
+
+ public JComponent getPreferableFocusComponent() {
+ return myLoginTextField.isVisible() ? myLoginTextField : myPasswordField;
+ }
+
+ @NotNull
+ public LoginDialog.AuthDataHolder getAuthData() {
+ return new LoginDialog.AuthDataHolder(getLogin(), getPassword());
+ }
+}
+