Renamed and comment a lot of stuff, fixed a bunch of bugs

This commit is contained in:
Thastertyn 2023-05-23 21:14:48 +02:00
parent 009bd97d4f
commit 5f817f3dfc
41 changed files with 935 additions and 782 deletions

View File

@ -18,8 +18,11 @@ import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
import com.googlecode.lanterna.gui2.dialogs.MessageDialog; import com.googlecode.lanterna.gui2.dialogs.MessageDialog;
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton; import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton;
import xyz.thastertyn.Tuples.Triplet; import xyz.thastertyn.Types.InputtedCredentials;
/**
* A Dialog window for inputing username and password.
*/
public class CredentialsInput extends DialogWindow { public class CredentialsInput extends DialogWindow {
private TextBox username; private TextBox username;
@ -129,6 +132,9 @@ public class CredentialsInput extends DialogWindow {
setComponent(mainPanel); setComponent(mainPanel);
} }
/**
* User pressed ok, check if anything is actually typed and if so, pack and return it
*/
public void onOK() public void onOK()
{ {
this.user = username.getText(); this.user = username.getText();
@ -148,9 +154,13 @@ public class CredentialsInput extends DialogWindow {
close(); close();
} }
/**
* returns the inputted username, password and if user chose to keep credentials stored
* @return {@link InputtedCredentials} with username first, password second and whether to store the credentials third
*/
@Override @Override
public Triplet<String, String, Boolean> showDialog(WindowBasedTextGUI textGUI) { public InputtedCredentials showDialog(WindowBasedTextGUI textGUI) {
super.showDialog(textGUI); super.showDialog(textGUI);
return new Triplet<String,String,Boolean>(user, pass, remember.isChecked()); return new InputtedCredentials(user, pass, remember.isChecked());
} }
} }

View File

@ -7,15 +7,9 @@ import java.io.FileReader;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
public class LocalCredentials { import xyz.thastertyn.Types.Credentials;
/** public class LocalCredentials {
* Checks for already saved credentials, at least if its file exists
* @return <ul>
* <li> {@code true} a file exists </li>
* <li> {@code false} doesn't exist </li>
* </ul>
*/
private static LocalCredentials localCredentials = new LocalCredentials(); private static LocalCredentials localCredentials = new LocalCredentials();
@ -24,6 +18,16 @@ public class LocalCredentials {
private File credentialsFile; private File credentialsFile;
private File credentialsPath; private File credentialsPath;
/**
* Checks for already saved credentials, at least if its file exists <br>
* for Windows it checks {@code AppData\Roaming\jecnak\credemtials.txt} <br>
* for Linux it checks {@code ~/.local/share/jecnak/credentials.txt} <br>
* @return
* <ul>
* <li> {@code true} a file exists </li>
* <li> {@code false} doesn't exist </li>
* </ul>
*/
public boolean checkForExistingCredentials() public boolean checkForExistingCredentials()
{ {
if(System.getProperty("os.name").equals("Linux")) if(System.getProperty("os.name").equals("Linux"))
@ -49,10 +53,17 @@ public class LocalCredentials {
return true; return true;
} }
public String[] getCredentialsFile() /**
* Try reading the credentails from a local file
* @return String[] with username on index 0, and password on index 1, <br>
* can also return null if credentials are corrupted, inaccessible, etc.
*/
public Credentials getCredentialsFile()
{ {
// TODO Use hashmap instead of array // TODO Use hashmap instead of array
String[] result = new String[2]; String user = null;
String pass = null;
if(credentialsFile == null) if(credentialsFile == null)
{ {
return null; return null;
@ -68,27 +79,33 @@ public class LocalCredentials {
if(currentValue[0].equals("user")) if(currentValue[0].equals("user"))
{ {
result[0] = currentValue[1]; user = currentValue[1];
}else if(currentValue[0].equals("pass")) }else if(currentValue[0].equals("pass"))
{ {
result[1] = currentValue[1]; pass = currentValue[1];
} }
} }
reader.close(); reader.close();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
return null;
} }
if(result[0] == null || result[1] == null) if(user == null || pass == null)
{ {
return null; return null;
} }
return result; return new Credentials(user, pass);
} }
public boolean saveCredentials(String[] credentials) /**
* appends the current credentials to the file
* @param credentials the array containing username and password. Username is on index 0, password on index 1
* @return
*/
public boolean saveCredentials(Credentials credentials)
{ {
if(credentialsFile == null) if(credentialsFile == null)
{ {
@ -108,10 +125,10 @@ public class LocalCredentials {
try { try {
BufferedWriter writer = new BufferedWriter(new FileWriter(credentialsFile)); BufferedWriter writer = new BufferedWriter(new FileWriter(credentialsFile));
writer.append("user=" + credentials[0]); writer.append(String.format(
writer.append("\n"); "user=%s\npass=%s\n",
writer.append("pass=" + credentials[1]); credentials.getUsername(),
writer.append("\n"); credentials.getPassword()));
writer.close(); writer.close();
} catch (IOException e) { } catch (IOException e) {
@ -122,14 +139,20 @@ public class LocalCredentials {
return true; return true;
} }
/**
* Deletes the credentials file
* @return
*/
public boolean deleteCredentials() public boolean deleteCredentials()
{ {
credentialsFile.delete(); credentialsFile.delete();
return true; return true;
} }
private LocalCredentials() {} /**
* Returns a single instance to prevent creating and loading the files all over again when not much has changed
* @return {@link LocalCredentials} instance
*/
public static LocalCredentials getInstance() public static LocalCredentials getInstance()
{ {
return localCredentials; return localCredentials;

View File

@ -12,16 +12,21 @@ import org.jsoup.Connection.Method;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import xyz.thastertyn.Scrape.Downloader; import xyz.thastertyn.Scrape.Downloader;
import xyz.thastertyn.Types.Credentials;
public class Login { public class Login {
private String Jsessionid = null; private String Jsessionid = null;
// Check for session expiration /**
private long start; * Tries logging into SPSE Jecna. If it succeeds it also saves the JSessionId into {@link Downloader} for ease of use. If it fails to login, the program is as good as not running at all
private long lastCheck; * @param credentials {@link Credentials} for username anas password
* @throws UnknownHostException Most likely no internet connection, spsejecna.cz could not be recognised as a host
public void loginJecna(String user, String pass) throws UnknownHostException, IOException, CredentialException, TimeoutException * @throws IOException by Jsoup
* @throws CredentialException The username or password is incorrect
* @throws TimeoutException Connection timed out, most likely a very slow internet
*/
public void loginJecna(Credentials credentials) throws UnknownHostException, IOException, CredentialException, TimeoutException
{ {
//#region JSESSIONID //#region JSESSIONID
Connection.Response response = Jsoup.connect("https://www.spsejecna.cz") Connection.Response response = Jsoup.connect("https://www.spsejecna.cz")
@ -34,35 +39,32 @@ public class Login {
//#endregion //#endregion
//#region Token3 //#region Token3
String token3 = Downloader.download("https://www.spsejecna.cz/user/role?role=student") String token3 = Downloader.getConnection("https://www.spsejecna.cz/user/role?role=student")
.get() .get()
.select("input[name=token3]") .select("input[name=token3]")
.attr("value"); .attr("value");
//#endregion //#endregion
//#region Login //#region Login
Downloader.download("https://www.spsejecna.cz/user/login") Downloader.getConnection("https://www.spsejecna.cz/user/login")
.method(Connection.Method.POST) .method(Connection.Method.POST)
.header("Content-Type", "application/x-www-form-urlencoded") .header("Content-Type", "application/x-www-form-urlencoded")
.header("Origin", "https://www.spsejecna.cz") .header("Origin", "https://www.spsejecna.cz")
.data("token3", token3) .data("token3", token3)
.data("user", user) .data("user", credentials.getUsername())
.data("pass", pass) .data("pass", credentials.getPassword())
.data("submit", "P%C5%99ihl%C3%A1sit+se") .data("submit", "P%C5%99ihl%C3%A1sit+se")
.followRedirects(true) .followRedirects(true)
.execute(); .execute();
//#endregion //#endregion
Document test = Downloader.download("https://www.spsejecna.cz/score/student") Document test = Downloader.getConnection("https://www.spsejecna.cz/score/student")
.get(); .get();
if(test.toString().contains("Pro pokračování se přihlaste do systému")) if(test.toString().contains("Pro pokračování se přihlaste do systému"))
{ {
throw new CredentialException("Incorrect username or password"); throw new CredentialException("Incorrect username or password");
} }
start = System.currentTimeMillis() / 1000L;
lastCheck = start;
} }
public void loginJidelna(String user, String pass) throws UnknownHostException, IOException public void loginJidelna(String user, String pass) throws UnknownHostException, IOException

View File

@ -10,8 +10,12 @@ import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
import com.googlecode.lanterna.gui2.dialogs.MessageDialog; import com.googlecode.lanterna.gui2.dialogs.MessageDialog;
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton; import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton;
import xyz.thastertyn.Tuples.Triplet; import xyz.thastertyn.Types.Credentials;
import xyz.thastertyn.Types.InputtedCredentials;
/**
* Merges the functionality of {@link LocalCredentials}, {@link CredentialsInput}, and {@link Login}
*/
public class LoginController { public class LoginController {
private WindowBasedTextGUI textGUI; private WindowBasedTextGUI textGUI;
@ -24,58 +28,72 @@ public class LoginController {
this.textGUI = textGUI; this.textGUI = textGUI;
} }
public void login() /**
* Picks appropriate method for logging in
* <ul>
* <li> Locally stored credentials exist, then try using them. If they arent in good shape, use GUI
* <li> Use GUI and ask the user for credentials
* </ul>
*/
public void login(boolean forceGUI)
{ {
String[] credentials; Credentials credentials;
if(localCredentials.checkForExistingCredentials()) // Credentials exist if(!forceGUI && localCredentials.checkForExistingCredentials())
{ {
credentials = localCredentials.getCredentialsFile(); credentials = localCredentials.getCredentialsFile();
}else{ }else{
Triplet<String, String, Boolean> data = loginUsingGui(); // Failed to get credentials to log in, get them from user credentials = loginUsingGui();
credentials = new String[] {data.getValue0(), data.getValue1()};
if(data.getValue2())
{
localCredentials.saveCredentials(credentials);
}
} }
loginUsingCredentials(credentials); useCredentials(credentials);
} }
public Triplet<String, String, Boolean> loginUsingGui() public Credentials loginUsingGui()
{ {
dialog = new CredentialsInput(); dialog = new CredentialsInput();
return dialog.showDialog(textGUI); // Failed to get credentials to log in, get them from user InputtedCredentials credentials = dialog.showDialog(textGUI);
if(credentials.save())
{
localCredentials.saveCredentials(credentials.getCredentials());
} }
private void loginUsingCredentials(String[] credentials) return credentials.getCredentials();
}
/**
* Credentials were successfully obtained and are used for logging in, although a lot can go wrong
* @param credentials
*/
private void useCredentials(Credentials credentials)
{ {
try { try {
login.loginJecna(credentials[0], credentials[1]); login.loginJecna(credentials);
}catch (TimeoutException e) }catch (TimeoutException e)
{ {
MessageDialog.showMessageDialog(textGUI, "Timeout", "The attempt to connect took too long.", MessageDialogButton.Retry, MessageDialog.showMessageDialog(textGUI, "Timeout", "The attempt to connect took too long. The app will quit", MessageDialogButton.OK);
MessageDialogButton.Abort); System.exit(0);
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
MessageDialog.showMessageDialog(textGUI, "No Internet connection", MessageDialog.showMessageDialog(textGUI, "No Internet connection. The app will quit",
e.getMessage(), e.getMessage(),
MessageDialogButton.OK); MessageDialogButton.OK);
login(); System.exit(0);
} catch (CredentialException e) } catch (CredentialException e)
{ {
MessageDialog.showMessageDialog(textGUI, "Incorrect username or password", MessageDialog.showMessageDialog(textGUI, "Incorrect username or password",
e.getMessage(), e.getMessage(),
MessageDialogButton.OK); MessageDialogButton.OK);
login(); // The credentials were most likely tampered with after save, just get rid of them and save them another time
LocalCredentials.getInstance().deleteCredentials();
login(true);
} catch (IOException e) } catch (IOException e)
{ {
MessageDialog.showMessageDialog(textGUI, "There was an error", MessageDialog.showMessageDialog(textGUI, "There was an error. The app will quit",
e.getMessage(), e.getMessage(),
MessageDialogButton.Retry); MessageDialogButton.Retry);
login(); System.exit(0);
} }
} }
} }

View File

@ -0,0 +1,123 @@
package xyz.thastertyn.Scrape;
import java.io.IOException;
import java.time.LocalDate;
import java.time.Month;
import java.util.ArrayList;
import java.util.Arrays;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import xyz.thastertyn.Types.Choice;
import xyz.thastertyn.Types.Option;
import xyz.thastertyn.Types.Options;
public class AbsenceList extends JecnaScrape {
private ArrayList<xyz.thastertyn.Types.AbsenceList> data;
private Choice currentChoice;
private Options schoolYearOptions;
@Override
public void download() throws IOException
{
parse("https://www.spsejecna.cz/absence/student");
}
@Override
public void download(Choice choice) throws IOException {
currentChoice = choice;
parse(String.format(
"https://www.spsejecna.cz/absence/student?schoolYearId=%s",
choice.getChoices().get(0)));
}
private void parse(String url) throws IOException
{
data = new ArrayList<>();
schoolYearOptions = new Options("Skolni R.");
Document absenceListHTMLDocument = Downloader.download(url);
Elements absenceLists = absenceListHTMLDocument.select("table.absence-list").select("tbody").select("tr");
for(Element e : absenceLists)
{
String date = e.child(0).text();
String text = e.child(1).text();
data.add(
new xyz.thastertyn.Types.AbsenceList(
date, text));
}
Elements options = absenceListHTMLDocument.select("form.listConfigure").select("select[id=schoolYearId]").select("option");
for(Element e : options)
{
boolean isDefault = e.hasAttr("selected");
schoolYearOptions.addOption(new Option(e.text(), e.attr("value"), isDefault));
}
currentChoice = new Choice(Arrays.asList(schoolYearOptions.getOptions().get(0).getValue()));
}
/**
* Convert text to a {@link LocalDate} and try to guess the year
* @param text to be parsed
* @return {@link LocalDate} parsed
*/
// private LocalDate parseDate(String text)
// {
// int year = 0, month = 0, day = 0;
// String[] split = text.split("\\.");
// day = Integer.parseInt(split[0]);
// month = Integer.parseInt(split[1]);
// if(currentChoice == null)
// {
// // Pick the current year
// int currYear = LocalDate.now().getYear();
// int currMonth = LocalDate.now().getMonthValue();
// if(month > currMonth && currMonth < 8)
// {
// year = currYear;
// }else if(month < currMonth && currMonth > 8)
// {
// year = currYear + 1;
// }else if(month < currMonth && currMonth < 8)
// {
// year = currYear;
// }
// }else{
// year = Integer.parseInt(currentChoice.getChoices().get(0));
// }
// return LocalDate.of(year, Month.of(month), day);
// }
@Override
public Options[] getOptions() {
return new Options[] {schoolYearOptions};
}
public ArrayList<xyz.thastertyn.Types.AbsenceList> getData()
{
return data;
}
@Override
public String toString()
{
return (!data.isEmpty()) ? data.toString() : null;
}
}

View File

@ -5,7 +5,7 @@ import java.io.IOException;
import xyz.thastertyn.Types.Choice; import xyz.thastertyn.Types.Choice;
import xyz.thastertyn.Types.Options; import xyz.thastertyn.Types.Options;
public class Jidelna extends JecnaScrape { public class Canteen extends JecnaScrape {
//public void foo() //public void foo()
//{ //{

View File

@ -1,24 +1,50 @@
package xyz.thastertyn.Scrape; package xyz.thastertyn.Scrape;
import java.io.IOException;
import org.jsoup.Connection; import org.jsoup.Connection;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
public class Downloader { public class Downloader {
private static String JsessionId; private static String JsessionId;
public static Connection download(String url) /**
* Provide a general Jsoup Connection for simplicity with some predefined values and JSessionId already set
* @param url
* @return HTML Document downloaded. If it fails, null
*/
public static Document download(String url) throws IOException
{ {
Connection c = Jsoup.connect(url) return Jsoup.connect(url)
.header("Connection", "keep-alive")
.cookie("role", "student")
.cookie("JSESSIONID", JsessionId)
.timeout(10000)
.get();
}
/**
* Returns a basic {@link Connection} with some predefined headers
* @param url
* @return {@link Connection} with timeout for 10s, and headers needed for downloading from spsejecna.cz
*/
public static Connection getConnection(String url)
{
return Jsoup.connect(url)
.header("Connection", "keep-alive") .header("Connection", "keep-alive")
.cookie("role", "student") .cookie("role", "student")
.cookie("JSESSIONID", JsessionId) .cookie("JSESSIONID", JsessionId)
.timeout(10000); .timeout(10000);
return c;
} }
public static void setJSessionId(String JsId) /**
* Sets JsessionId
* @param newJsessionId JsessionId
*/
public static void setJSessionId(String newJsessionId)
{ {
JsessionId = JsId; JsessionId = newJsessionId;
} }
} }

View File

@ -5,9 +5,28 @@ import java.io.IOException;
import xyz.thastertyn.Types.Choice; import xyz.thastertyn.Types.Choice;
import xyz.thastertyn.Types.Options; import xyz.thastertyn.Types.Options;
/**
* Abstract class to wrap around everything that scrapes some data. Most of them also have getData or something similiar,
* but the data types are just so vastly different, they couldnt be wrapped as abstract method and abstract class
*/
public abstract class JecnaScrape { public abstract class JecnaScrape {
/**
* returns possible options, like school year or half year
* @return {@link Options} array, for when there are more options available
*/
public abstract Options[] getOptions(); public abstract Options[] getOptions();
/**
* Downloads the chosen url, given the {@link Choice} which includes all the needed data
* @param choice of any possible data like school year or half year
* @throws IOException like any other download method, in case no internet, timeout, etc.
*/
public abstract void download(Choice choice) throws IOException; public abstract void download(Choice choice) throws IOException;
/**
* Downloads the default url, without any parameters
* @throws IOException in case of timeout, no internet, etc.
*/
public abstract void download() throws IOException; public abstract void download() throws IOException;
} }

View File

@ -0,0 +1,129 @@
package xyz.thastertyn.Scrape;
import java.io.IOException;
import java.util.ArrayList;
import java.util.regex.Pattern;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import xyz.thastertyn.Types.Choice;
import xyz.thastertyn.Types.FinalMark;
import xyz.thastertyn.Types.Option;
import xyz.thastertyn.Types.Options;
import xyz.thastertyn.Types.Subject;
import xyz.thastertyn.Types.Mark;
public class Marks extends JecnaScrape {
// schoolYear, schoolYearId
private ArrayList<Subject> subjects;
// int znaci id roku, boolean jestli je jen prvni nebo i druhe pololeti
private Options schoolYearOptions;
private Options schoolHalfYearOptions;
public void download() throws IOException
{
download("https://www.spsejecna.cz/score/student");
}
@Override
public void download(Choice choice) throws IOException
{
download(String.format(
"https://www.spsejecna.cz/score/student?schoolYearId=%s&schoolYearHalfId=%s",
choice.getChoices().get(0),
choice.getChoices().get(1)));
}
private void download(String url) throws IOException
{
schoolHalfYearOptions = new Options("Pololeti");
schoolYearOptions = new Options("Skolni R.");
subjects = new ArrayList<>();
Document marksHTMLDocument = Downloader.download(url);
// Subjects stored as <tr>
Elements[] subjectRowsHTML = marksHTMLDocument
.select("table.score")
.select("tr")
.stream()
.map(Element::children)
.toArray(Elements[]::new);
int subjectIndex = 0;
for(int i = 1; i < subjectRowsHTML.length; i++)
{
String fullSubjectName = subjectRowsHTML[i].get(0).text();
// Attempt to shorten the subject name to the string in brackets
String shortSubjectName = Pattern
.compile("\\((.*?)\\)")
.matcher(fullSubjectName)
.results()
.findFirst()
.map(m -> m.group(1))
.orElse(fullSubjectName);
if(subjectRowsHTML[i].get(2).childrenSize() == 0) // Subject doesn't have final mark yet
{
subjects.add(new Subject(shortSubjectName));
}else{
String finalMark = subjectRowsHTML[i].get(2).select("a.scoreFinal").text();
subjects.add(new Subject(shortSubjectName, FinalMark.fromValue(finalMark)));
}
for(Element znamkaElement : subjectRowsHTML[i].get(1).select("a.score"))
{
int mark;
boolean isSmall = false;
String markText = znamkaElement.select("span.value").text();
mark = markText.matches("\\d") ?
Integer.parseInt(markText)
:
-1; // Most likely N (Nehodnocen)
// Small mark will be counted with smaller weight, compared to normal one
isSmall = znamkaElement.hasClass("scoreSmall");
subjects.get(subjectIndex).addMark(new Mark(mark, isSmall, markText));
}
subjectIndex++;
}
Element optionsPanel = marksHTMLDocument.selectFirst("form.listConfigure");
Elements schoolYears = optionsPanel.select("select[id=schoolYearId]").select("option");
Elements halfYears = optionsPanel.select("select[id=schoolYearHalfId]").select("option");
for(Element e : schoolYears)
{
boolean isDefault = e.hasAttr("selected");
schoolYearOptions.addOption(new Option(e.text(), e.attr("value"), isDefault));
}
for(Element e : halfYears)
{
boolean isDefault = e.hasAttr("selected");
schoolHalfYearOptions.addOption(new Option(e.text(), e.attr("value"), isDefault));
}
}
public ArrayList<Subject> getSubjects()
{
return subjects;
}
public Options[] getOptions()
{
return new Options[] {schoolYearOptions, schoolHalfYearOptions};
}
}

View File

@ -1,118 +0,0 @@
package xyz.thastertyn.Scrape;
import java.io.IOException;
import java.time.LocalDate;
import java.time.Month;
import java.util.ArrayList;
import java.util.Arrays;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import xyz.thastertyn.Types.Choice;
import xyz.thastertyn.Types.Option;
import xyz.thastertyn.Types.Options;
public class OmluvnyList extends JecnaScrape {
private ArrayList<xyz.thastertyn.Types.OmluvnyList> data;
private Choice currentChoice;
private Options schoolYearOptions;
@Override
public void download() throws IOException
{
download("https://www.spsejecna.cz/absence/student");
}
@Override
public void download(Choice choice) throws IOException {
currentChoice = choice;
download(String.format(
"https://www.spsejecna.cz/absence/student?schoolYearId=%s",
choice.getChoices().get(0)));
}
private void download(String url) throws IOException
{
data = new ArrayList<>();
schoolYearOptions = new Options("Skolni R.");
Document omluvnyListDokumentHTML = Downloader.download(url).get();
Elements omluvneListy = omluvnyListDokumentHTML.select("table.absence-list").select("tbody").select("tr");
for(Element e : omluvneListy)
{
String date = e.child(0).text();
String text = e.child(1).text();
data.add(
new xyz.thastertyn.Types.OmluvnyList(
parseDate(date), text));
}
Elements options = omluvnyListDokumentHTML.select("form.listConfigure").select("select[id=schoolYearId]").select("option");
for(Element e : options)
{
boolean isDefault = e.hasAttr("selected");
schoolYearOptions.addOption(new Option(e.text(), e.attr("value"), isDefault));
}
currentChoice = new Choice(Arrays.asList(schoolYearOptions.getOptions().get(0).getValue()));
}
private LocalDate parseDate(String text)
{
int year = 0, month = 0, day = 0;
String[] split = text.split("\\.");
month = Integer.parseInt(split[0]);
day = Integer.parseInt(split[1]);
if(currentChoice == null)
{
// Pick the current year
int currYear = LocalDate.now().getYear();
int currMonth = LocalDate.now().getMonthValue();
if(month > currMonth && currMonth < 8)
{
year = currYear;
}else if(month < currMonth && currMonth > 8)
{
year = currYear + 1;
}else if(month < currMonth && currMonth < 8)
{
year = currYear;
}
}else{
year = Integer.parseInt(currentChoice.getChoices().get(0));
}
return LocalDate.of(year, Month.of(month), day);
}
@Override
public Options[] getOptions() {
return new Options[] {schoolYearOptions};
}
public ArrayList<xyz.thastertyn.Types.OmluvnyList> getData()
{
return data;
}
@Override
public String toString()
{
return (!data.isEmpty()) ? data.toString() : null;
}
}

View File

@ -11,17 +11,17 @@ import org.jsoup.select.Elements;
import xyz.thastertyn.Types.Choice; import xyz.thastertyn.Types.Choice;
import xyz.thastertyn.Types.Options; import xyz.thastertyn.Types.Options;
public class Sdeleni extends JecnaScrape { public class Reports extends JecnaScrape {
ArrayList<xyz.thastertyn.Types.Sdeleni> sdeleniList = new ArrayList<>(); ArrayList<xyz.thastertyn.Types.Reports> reportList = new ArrayList<>();
public void download() throws UnknownHostException, IOException public void download() throws UnknownHostException, IOException
{ {
Document sdeleniDoc = Downloader.download("https://www.spsejecna.cz/user-student/record-list").get(); Document reportDoc = Downloader.download("https://www.spsejecna.cz/user-student/record-list");
Elements sdeleni = sdeleniDoc.select("ul.list li"); Elements report = reportDoc.select("ul.list li");
for(Element e : sdeleni) for(Element e : report)
{ {
boolean isPositive = false; boolean isPositive = false;
String label = ""; String label = "";
@ -29,15 +29,15 @@ public class Sdeleni extends JecnaScrape {
Elements spans = e.select("li").select("a.item").select("span"); Elements spans = e.select("li").select("a.item").select("span");
isPositive = spans.get(0).hasClass("sprite-icon-tick-16"); isPositive = spans.get(0).hasClass("sprite-icon-tick-16");
label = spans.get(1).text(); label = spans.get(1).select("span.label").text();
sdeleniList.add(new xyz.thastertyn.Types.Sdeleni(label, isPositive)); reportList.add(new xyz.thastertyn.Types.Reports(label, isPositive));
} }
} }
public ArrayList<xyz.thastertyn.Types.Sdeleni> getSdeleni() public ArrayList<xyz.thastertyn.Types.Reports> getSdeleni()
{ {
return sdeleniList; return reportList;
} }
@Override @Override

View File

@ -1,7 +1,6 @@
package xyz.thastertyn.Scrape; package xyz.thastertyn.Scrape;
import java.io.IOException; import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -13,25 +12,14 @@ import org.jsoup.select.Elements;
import xyz.thastertyn.Types.Choice; import xyz.thastertyn.Types.Choice;
import xyz.thastertyn.Types.Option; import xyz.thastertyn.Types.Option;
import xyz.thastertyn.Types.Options; import xyz.thastertyn.Types.Options;
import xyz.thastertyn.Types.Timetable;
/** public class Timetable extends JecnaScrape {
* Jeden radek v rozvrhu
*/
public class Rozvrh extends JecnaScrape {
private Timetable timetable; private xyz.thastertyn.Types.Timetable timetable;
private Options schoolYearOptions; private Options schoolYearOptions;
private Options timetableOptions; private Options timetableOptions;
/**
* Stahne rozvrh z www.spsejecna.cz a dale ho zpracuje do formy
* se kterou da pracovat
* @param Jsessionid ze stranek
* @throws UnknownHostException kdyz neni pripojeni k internetu
* @throws IOException ostatni exceptiony nejsou dulezite, tak jsou zahrnuty v jednom
*/
@Override @Override
public void download() throws IOException public void download() throws IOException
{ {
@ -51,32 +39,32 @@ public class Rozvrh extends JecnaScrape {
schoolYearOptions = new Options("Skolni R."); schoolYearOptions = new Options("Skolni R.");
timetableOptions = new Options("Obdobi"); timetableOptions = new Options("Obdobi");
timetable = new Timetable(); timetable = new xyz.thastertyn.Types.Timetable();
Document rozvrhDokumentHTML = Downloader.download(url).get(); Document timetableHTMLDocument = Downloader.download(url);
Elements[] radkyRozvrhuHTML = rozvrhDokumentHTML Elements[] timetableHTMLRow = timetableHTMLDocument
.select("table.timetable") .select("table.timetable")
.select("tr") .select("tr")
.stream() .stream()
.map(Element::children) .map(Element::children)
.toArray(Elements[]::new); .toArray(Elements[]::new);
for(int i = 0; i < 5; i++) for(int i = 0; i < 5; i++) // Days
{ {
for(int j = 0; j < 10; j++) for(int j = 0; j < 10; j++) // Individual hours
{ {
String predmet = radkyRozvrhuHTML[i+1].get(j+1).select("span.subject").text(); String subject = timetableHTMLRow[i+1].get(j+1).select("span.subject").text();
// Subjects like CEL are thrice, even though everyone has them, make it single // Subjects like CEL are thrice, even though everyone has them, make it single
String[] split = predmet.split(" "); String[] split = subject.split(" ");
HashSet<String> set = new HashSet<>(Arrays.asList(split)); HashSet<String> set = new HashSet<>(Arrays.asList(split));
String pr = String.join("/", set); String subj = String.join("/", set);
timetable.get(i).set(j, pr); timetable.get(i).set(j, subj);
} }
} }
Element optionsPanel = rozvrhDokumentHTML.selectFirst("form.listConfigure"); Element optionsPanel = timetableHTMLDocument.selectFirst("form.listConfigure");
Elements schoolYear = optionsPanel.select("select[id=schoolYearId]").select("option"); Elements schoolYear = optionsPanel.select("select[id=schoolYearId]").select("option");
Elements timetableId = optionsPanel.select("select[id=timetableId]").select("option"); Elements timetableId = optionsPanel.select("select[id=timetableId]").select("option");
@ -89,20 +77,21 @@ public class Rozvrh extends JecnaScrape {
for(Element e : timetableId) for(Element e : timetableId)
{ {
String text = Pattern // Try to extract the precise school years to make the text shorter
String optionDisplayText = Pattern
.compile("(Od .* do .*)") .compile("(Od .* do .*)")
.matcher(e.text()) .matcher(e.text())
.results() .results()
.findFirst() .findFirst()
.map(m -> m.group(1)) .map(m -> m.group(1))
.orElse(e.text()); .orElse(e.text()); // Just use the whole thing if it doesn't follow the pattern
boolean isDefault = e.hasAttr("selected"); boolean isDefault = e.hasAttr("selected"); // To prevent the last option added be picked as default always
timetableOptions.addOption(new Option(text, e.attr("value"), isDefault)); timetableOptions.addOption(new Option(optionDisplayText, e.attr("value"), isDefault));
} }
} }
public Timetable getRozvrh() public xyz.thastertyn.Types.Timetable getRozvrh()
{ {
return timetable; return timetable;
} }

View File

@ -1,130 +0,0 @@
package xyz.thastertyn.Scrape;
import java.io.IOException;
import java.util.ArrayList;
import java.util.regex.Pattern;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import xyz.thastertyn.Types.Choice;
import xyz.thastertyn.Types.FinalMark;
import xyz.thastertyn.Types.Option;
import xyz.thastertyn.Types.Options;
import xyz.thastertyn.Types.Predmet;
import xyz.thastertyn.Types.Znamka;
public class Znamky extends JecnaScrape {
// schoolYear, schoolYearId
private ArrayList<Predmet> predmety;
// int znaci id roku, boolean jestli je jen prvni nebo i druhe pololeti
private Options schoolYearOptions;
private Options schoolHalfYearOptions;
public void download() throws IOException
{
download("https://www.spsejecna.cz/score/student");
}
@Override
public void download(Choice choice) throws IOException
{
download(String.format(
"https://www.spsejecna.cz/score/student?schoolYearId=%s&schoolYearHalfId=%s",
choice.getChoices().get(0),
choice.getChoices().get(1)));
}
private void download(String url) throws IOException
{
schoolHalfYearOptions = new Options("Pololeti");
schoolYearOptions = new Options("Skolni R.");
predmety = new ArrayList<>();
Document znamkyDokumentHTML = Downloader.download(url).get();
// Predmety ulozene v <tr>
Elements[] radkyPredmetuHTML = znamkyDokumentHTML
.select("table.score")
.select("tr")
.stream()
.map(Element::children)
.toArray(Elements[]::new);
int subjectIndex = 0;
for(int i = 1; i < radkyPredmetuHTML.length; i++)
{
String plnyNazevPredmetu = radkyPredmetuHTML[i].get(0).text();
String jmenoPredmetu = Pattern
.compile("\\((.*?)\\)")
.matcher(plnyNazevPredmetu)
.results()
.findFirst()
.map(m -> m.group(1))
.orElse(plnyNazevPredmetu);
if(radkyPredmetuHTML[i].get(2).childrenSize() == 0) // Predmet jeste nema vyslednou znamku
{
predmety.add(new Predmet(jmenoPredmetu));
}else{
String vyslednaZnamka = radkyPredmetuHTML[i].get(2).select("a.scoreFinal").text();
predmety.add(new Predmet(jmenoPredmetu, FinalMark.fromValue(vyslednaZnamka)));
}
for(Element znamkaElement : radkyPredmetuHTML[i].get(1).select("a.score"))
{
int znamka;
boolean jeMala = false;
String textZnamky = znamkaElement.select("span.value").text();
znamka = textZnamky.matches("\\d") ?
Integer.parseInt(textZnamky)
:
-1; // Nejspis se jedna o N (Nehodnocen)
// Mala znamka se bude pocitat jako polovicni vaha
jeMala = znamkaElement.hasClass("scoreSmall");
predmety.get(subjectIndex).addZnamka(new Znamka(znamka, jeMala, textZnamky));
}
predmety.get(subjectIndex).calculateFinalMark();
subjectIndex++;
}
Element optionsPanel = znamkyDokumentHTML.selectFirst("form.listConfigure");
Elements skolniRoky = optionsPanel.select("select[id=schoolYearId]").select("option");
Elements pololeti = optionsPanel.select("select[id=schoolYearHalfId]").select("option");
for(Element e : skolniRoky)
{
boolean isDefault = e.hasAttr("selected");
schoolYearOptions.addOption(new Option(e.text(), e.attr("value"), isDefault));
}
for(Element e : pololeti)
{
boolean isDefault = e.hasAttr("selected");
schoolHalfYearOptions.addOption(new Option(e.text(), e.attr("value"), isDefault));
}
}
public ArrayList<Predmet> getPredmety()
{
return predmety;
}
public Options[] getOptions()
{
return new Options[] {schoolYearOptions, schoolHalfYearOptions};
}
}

View File

@ -1,54 +0,0 @@
package xyz.thastertyn.Tuples;
/**
* Ekvitalent Tuplu, ktery neni zabudovan v jave
*/
public class Pair<T1, T2> {
private T1 value0;
private T2 value1;
public Pair(T1 value0, T2 value1)
{
this.value0 = value0;
this.value1 = value1;
}
public Pair()
{
}
public void put(T1 value0, T2 value1)
{
this.value0 = value0;
this.value1 = value1;
}
public T1 getValue0()
{
return value0;
}
public T2 getValue1()
{
return value1;
}
public void setValue0(T1 value0)
{
this.value0 = value0;
}
public void setValue1(T2 value1)
{
this.value1 = value1;
}
@Override
public String toString()
{
return "[" + value0 + ", " + value1 + "]";
}
}

View File

@ -1,66 +0,0 @@
package xyz.thastertyn.Tuples;
/**
* Ekvitalent Tuplu, ktery neni zabudovan v jave
*/
public class Triplet<T1, T2, T3> {
private T1 value0;
private T2 value1;
private T3 value2;
public Triplet(T1 value0, T2 value1, T3 value2)
{
this.value0 = value0;
this.value1 = value1;
this.value2 = value2;
}
public Triplet()
{
}
public void put(T1 value0, T2 value1, T3 value2)
{
this.value0 = value0;
this.value1 = value1;
this.value2 = value2;
}
public T1 getValue0()
{
return value0;
}
public T2 getValue1()
{
return value1;
}
public T3 getValue2()
{
return value2;
}
public void setValue0(T1 value0)
{
this.value0 = value0;
}
public void setValue1(T2 value1)
{
this.value1 = value1;
}
public void setValue2(T3 value2)
{
this.value2 = value2;
}
@Override
public String toString()
{
return "[" + value0 + ", " + value1 + "," + value2 + "]";
}
}

View File

@ -0,0 +1,31 @@
package xyz.thastertyn.Types;
public class AbsenceList {
private String date;
private String description;
public AbsenceList(String date, String description)
{
this.date = date;
this.description = description;
}
public String getDate() {
return date;
}
public void setDate(String datum) {
this.date = datum;
}
public String getDescription() {
return description;
}
public void setDescription(String popis) {
this.description = popis;
}
}

View File

@ -2,6 +2,9 @@ package xyz.thastertyn.Types;
import java.util.List; import java.util.List;
/**
* A choice parsed from dropdowns on the site. Usually contains something like school year or half year id
*/
public class Choice { public class Choice {
private List<String> choices; private List<String> choices;

View File

@ -0,0 +1,23 @@
package xyz.thastertyn.Types;
/**
* Stores username and password
*/
public class Credentials {
private String username;
private String password;
public Credentials(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}

View File

@ -1,5 +1,8 @@
package xyz.thastertyn.Types; package xyz.thastertyn.Types;
/**
* Stores a single day from the timetable as an array of strings
*/
public class DayOfTimetable { public class DayOfTimetable {
private String[] subjects = new String[10]; private String[] subjects = new String[10];

View File

@ -1,5 +1,8 @@
package xyz.thastertyn.Types; package xyz.thastertyn.Types;
/**
* Stores the final mark from a subject
*/
public class FinalMark{ public class FinalMark{
public static final FinalMark VYBORNY = new FinalMark(1, "1"); public static final FinalMark VYBORNY = new FinalMark(1, "1");
public static final FinalMark CHVALITEBNY = new FinalMark(2, "2"); public static final FinalMark CHVALITEBNY = new FinalMark(2, "2");
@ -7,6 +10,8 @@ public class FinalMark{
public static final FinalMark DOSTATECNY = new FinalMark(4, "4"); public static final FinalMark DOSTATECNY = new FinalMark(4, "4");
public static final FinalMark NEDOSTATECNY = new FinalMark(5, "5"); public static final FinalMark NEDOSTATECNY = new FinalMark(5, "5");
public static final FinalMark NOTHING = new FinalMark(0, "");
public static final FinalMark NEHODNOCEN = new FinalMark(-1, "N"); public static final FinalMark NEHODNOCEN = new FinalMark(-1, "N");
public static final FinalMark NAPOMENUT_ZA_NEKLASIFIKACI = new FinalMark(-2, "N?"); public static final FinalMark NAPOMENUT_ZA_NEKLASIFIKACI = new FinalMark(-2, "N?");

View File

@ -0,0 +1,30 @@
package xyz.thastertyn.Types;
/**
* Wrapper around credentials with the change of having a boolean for whether the user wants to save the credentials or not
*/
public class InputtedCredentials extends Credentials {
private boolean save;
public InputtedCredentials(String username, String password, boolean save)
{
super(username, password);
this.save = save;
}
public InputtedCredentials(Credentials credentials, boolean save)
{
super(credentials.getUsername(), credentials.getPassword());
this.save = save;
}
public Credentials getCredentials()
{
return new Credentials(getUsername(), getPassword());
}
public boolean save() {
return save;
}
}

View File

@ -0,0 +1,34 @@
package xyz.thastertyn.Types;
/**
* A single mark. holds numeric value, weight - small = 1, normal 2
*/
public class Mark {
private int mark;
private String markAsText;
private int weight;
public Mark(int mark, boolean isSmall, String markAsText)
{
this.mark = mark;
this.weight = (isSmall) ? 1 : 2;
this.markAsText = markAsText;
}
public int getMark()
{
return mark;
}
public int getWeight()
{
return weight;
}
@Override
public String toString()
{
return markAsText;
}
}

View File

@ -1,33 +0,0 @@
package xyz.thastertyn.Types;
import java.time.LocalDate;
public class OmluvnyList {
private LocalDate datum;
private String popis;
public OmluvnyList(LocalDate datum, String popis)
{
this.datum = datum;
this.popis = popis;
}
public LocalDate getDatum() {
return datum;
}
public void setDatum(LocalDate datum) {
this.datum = datum;
}
public String getPopis() {
return popis;
}
public void setPopis(String popis) {
this.popis = popis;
}
}

View File

@ -1,12 +1,16 @@
package xyz.thastertyn.Types; package xyz.thastertyn.Types;
/**
* A single option. Holds a text description, the thing displayed and a string value, the one used in url
*/
public class Option { public class Option {
private String text; private String displayText;
private String value; private String value;
private boolean isDefault; private boolean isDefault;
public Option(String text, String value, boolean isDefault) { public Option(String text, String value, boolean isDefault) {
this.text = text; this.displayText = text;
this.value = value; this.value = value;
this.isDefault = isDefault; this.isDefault = isDefault;
} }
@ -18,6 +22,6 @@ public class Option {
} }
@Override @Override
public String toString() { public String toString() {
return text; return displayText;
} }
} }

View File

@ -2,6 +2,9 @@ package xyz.thastertyn.Types;
import java.util.ArrayList; import java.util.ArrayList;
/**
* Holds all possible a single dropdown on the website can have.
*/
public class Options { public class Options {
private ArrayList<Option> options = new ArrayList<>(); private ArrayList<Option> options = new ArrayList<>();

View File

@ -1,67 +0,0 @@
package xyz.thastertyn.Types;
import java.util.ArrayList;
public class Predmet {
private ArrayList<Znamka> marks = new ArrayList<>();
private String subjectName = "";
private boolean isFinal = false;
private FinalMark finalMark = FinalMark.UNKNOWN;
public Predmet(String subjectName)
{
this.subjectName = subjectName;
}
public Predmet(String subjectName, FinalMark finalMark)
{
this.subjectName = subjectName;
this.finalMark = finalMark;
isFinal = true;
}
public void addZnamka(Znamka newMark)
{
marks.add(newMark);
}
public void calculateFinalMark()
{
int total = marks
.stream()
.mapToInt((z) -> z.getZnamka() * z.getVaha())
.sum();
int weight = marks
.stream()
.mapToInt((z) -> z.getVaha())
.sum();
double finalMark = ((double) total / weight);
this.finalMark = new FinalMark(finalMark);
}
public FinalMark getFinalMark()
{
return finalMark;
}
public boolean isFinal()
{
return isFinal;
}
public String getSubjectName()
{
return subjectName;
}
public ArrayList<Znamka> getMarks()
{
return marks;
}
}

View File

@ -1,11 +1,14 @@
package xyz.thastertyn.Types; package xyz.thastertyn.Types;
public class Sdeleni { /**
* Reports for parents. Holds a string of text and a boolean for positivity to determine color for display
*/
public class Reports {
private String text; private String text;
private boolean isPositive; private boolean isPositive;
public Sdeleni(String text, boolean isPositive) public Reports(String text, boolean isPositive)
{ {
this.text = text; this.text = text;
this.isPositive = isPositive; this.isPositive = isPositive;

View File

@ -0,0 +1,80 @@
package xyz.thastertyn.Types;
import java.util.ArrayList;
/**
* Subject for marks. Holds a name, all its marks and the final mark
*/
public class Subject {
private ArrayList<Mark> marks = new ArrayList<>();
private String subjectName = "";
private boolean isFinal = false;
private FinalMark finalMark = FinalMark.UNKNOWN;
public Subject(String subjectName)
{
this.subjectName = subjectName;
}
public Subject(String subjectName, FinalMark finalMark)
{
this.subjectName = subjectName;
this.finalMark = finalMark;
isFinal = true;
}
public void addMark(Mark newMark)
{
marks.add(newMark);
}
private void calculateFinalMark()
{
if(marks.isEmpty())
{
this.finalMark = FinalMark.NOTHING;
}else{
int total = marks
.stream()
.mapToInt((z) -> z.getMark() * z.getWeight())
.sum();
int weight = marks
.stream()
.mapToInt((z) -> z.getWeight())
.sum();
double finalMark = ((double) total / weight);
this.finalMark = new FinalMark(finalMark);
}
}
public FinalMark getFinalMark()
{
if(!isFinal)
{
calculateFinalMark();
}
return finalMark;
}
public boolean isFinal()
{
return isFinal;
}
public String getSubjectName()
{
return subjectName;
}
public ArrayList<Mark> getMarks()
{
return marks;
}
}

View File

@ -1,5 +1,8 @@
package xyz.thastertyn.Types; package xyz.thastertyn.Types;
/**
* Whole timetable. holds an array of {@link DayOfTimetable} for each day
*/
public class Timetable { public class Timetable {
private DayOfTimetable[] timetable = new DayOfTimetable[5]; private DayOfTimetable[] timetable = new DayOfTimetable[5];

View File

@ -1,30 +0,0 @@
package xyz.thastertyn.Types;
public class Znamka {
private int znamka;
private String znamkaJakoText;
private int vaha;
public Znamka(int znamka, boolean malaZnamka, String znamkaJakoText/*, String text, String datum*/)
{
this.znamka = znamka;
this.vaha = (malaZnamka) ? 1 : 2;
this.znamkaJakoText = znamkaJakoText;
}
public int getZnamka()
{
return znamka;
}
public int getVaha()
{
return vaha;
}
public String getText()
{
return znamkaJakoText;
}
}

View File

@ -10,11 +10,11 @@ import com.googlecode.lanterna.gui2.Panel;
import xyz.thastertyn.UserInterface.Listeners.UpdateListener; import xyz.thastertyn.UserInterface.Listeners.UpdateListener;
public class OmluvnyList extends JecnaContent{ public class AbsenceList extends JecnaContent{
private xyz.thastertyn.Scrape.OmluvnyList omluvnyList = new xyz.thastertyn.Scrape.OmluvnyList(); private xyz.thastertyn.Scrape.AbsenceList omluvnyList = new xyz.thastertyn.Scrape.AbsenceList();
public OmluvnyList(UpdateListener listener) public AbsenceList(UpdateListener listener)
{ {
super(listener); super(listener);
this.mainPanel = new Panel() this.mainPanel = new Panel()
@ -27,15 +27,15 @@ public class OmluvnyList extends JecnaContent{
protected void setGUI() protected void setGUI()
{ {
this.mainPanel.removeAllComponents(); this.mainPanel.removeAllComponents();
ArrayList<xyz.thastertyn.Types.OmluvnyList> a = omluvnyList.getData(); ArrayList<xyz.thastertyn.Types.AbsenceList> a = omluvnyList.getData();
Panel content = new Panel() Panel content = new Panel()
.setLayoutManager(new LinearLayout(Direction.VERTICAL)) .setLayoutManager(new LinearLayout(Direction.VERTICAL))
.addTo(mainPanel); .addTo(mainPanel);
for(xyz.thastertyn.Types.OmluvnyList omluvnyList : a) for(xyz.thastertyn.Types.AbsenceList omluvnyList : a)
{ {
content.addComponent(new Label(omluvnyList.getDatum() + " - " + omluvnyList.getPopis())); content.addComponent(new Label(omluvnyList.getDate() + " - " + omluvnyList.getDescription()));
} }
} }
} }

View File

@ -1,5 +1,5 @@
package xyz.thastertyn.UserInterface.Content; package xyz.thastertyn.UserInterface.Content;
public class Jidelna { public class Canteen {
} }

View File

@ -23,6 +23,9 @@ public abstract class JecnaContent {
protected UpdateListener listener; protected UpdateListener listener;
protected JecnaScrape scraper; protected JecnaScrape scraper;
/**
* Prepares it's content to be display by gettings the panel from {@link #getPanel()}
*/
protected abstract void setGUI(); protected abstract void setGUI();
public JecnaContent(UpdateListener listener) public JecnaContent(UpdateListener listener)
@ -30,12 +33,18 @@ public abstract class JecnaContent {
this.listener = listener; this.listener = listener;
} }
/**
* Call its scraper class to download content using the provided choice
* @param choice
* @throws IOException in case download fails in some way
*/
protected void download(Choice choice) throws IOException protected void download(Choice choice) throws IOException
{ {
if(choice != null) if(choice != null)
{ {
scraper.download(choice); scraper.download(choice);
setGUI(); setGUI();
mainPanel.setPreferredSize(mainPanel.calculatePreferredSize());
listener.updatePanel(); listener.updatePanel();
}else{ }else{
scraper.download(); scraper.download();
@ -45,11 +54,20 @@ public abstract class JecnaContent {
hasStarted = true; hasStarted = true;
} }
/**
* Tells scraper to download the default content and parse it
* @throws IOException
*/
public void downloadDefault() throws IOException public void downloadDefault() throws IOException
{ {
download(null); download(null);
} }
/**
* Creates a {@link OptionsDialog} to display its options
* @param textGUI where to display the dialog
* @throws IOException upon ok, conent is immediatelly downloaded using {@link #download( choice_from_dialog )}
*/
public void showOptions(WindowBasedTextGUI textGUI) throws IOException { public void showOptions(WindowBasedTextGUI textGUI) throws IOException {
if(scraper.getOptions() == null) if(scraper.getOptions() == null)
@ -67,16 +85,27 @@ public abstract class JecnaContent {
} }
} }
/**
* Returns the panel where the content should be held and already arranged for display
* @return
*/
public Panel getPanel() public Panel getPanel()
{ {
return this.mainPanel; return this.mainPanel;
} }
/**
* Check if something has been downloaded already
* @return true or false depending on the download state
*/
public boolean hasStarted() public boolean hasStarted()
{ {
return this.hasStarted; return this.hasStarted;
} }
/**
* Returns a label with the title text like "Známky", etc.
*/
public Label getLabel() public Label getLabel()
{ {
return this.borderLabel; return this.borderLabel;

View File

@ -14,11 +14,11 @@ import com.googlecode.lanterna.gui2.LinearLayout;
import com.googlecode.lanterna.gui2.Panel; import com.googlecode.lanterna.gui2.Panel;
import xyz.thastertyn.Types.FinalMark; import xyz.thastertyn.Types.FinalMark;
import xyz.thastertyn.Types.Predmet; import xyz.thastertyn.Types.Subject;
import xyz.thastertyn.Types.Znamka; import xyz.thastertyn.Types.Mark;
import xyz.thastertyn.UserInterface.Listeners.UpdateListener; import xyz.thastertyn.UserInterface.Listeners.UpdateListener;
public class Znamky extends JecnaContent { public class Marks extends JecnaContent {
private final TextColor.RGB VYBORNY = new TextColor.RGB(85,212,0); private final TextColor.RGB VYBORNY = new TextColor.RGB(85,212,0);
private final TextColor.RGB CHVALITEBNY = new TextColor.RGB(196,224,80); private final TextColor.RGB CHVALITEBNY = new TextColor.RGB(196,224,80);
@ -39,16 +39,16 @@ public class Znamky extends JecnaContent {
true, true,
false); false);
private xyz.thastertyn.Scrape.Znamky znamky = new xyz.thastertyn.Scrape.Znamky(); private xyz.thastertyn.Scrape.Marks marks = new xyz.thastertyn.Scrape.Marks();
public Znamky(UpdateListener listener) public Marks(UpdateListener listener)
{ {
super(listener); super(listener);
this.mainPanel = new Panel() this.mainPanel = new Panel()
.setLayoutManager(new GridLayout(3)); .setLayoutManager(new GridLayout(3));
this.borderLabel = new Label("Znamky"); this.borderLabel = new Label("Znamky");
super.scraper = this.znamky; super.scraper = this.marks;
} }
@Override @Override
@ -56,8 +56,9 @@ public class Znamky extends JecnaContent {
{ {
mainPanel.removeAllComponents(); mainPanel.removeAllComponents();
ArrayList<Predmet> subjects = znamky.getPredmety(); ArrayList<Subject> subjects = marks.getSubjects();
// Colors are used a lot and instead of referencing them in lengthy lines, this seems shorter
HashMap<Integer, SimpleTheme> colors = new HashMap<>(); HashMap<Integer, SimpleTheme> colors = new HashMap<>();
colors.put(1, new SimpleTheme(ANSI.BLACK, VYBORNY)); colors.put(1, new SimpleTheme(ANSI.BLACK, VYBORNY));
colors.put(2, new SimpleTheme(ANSI.BLACK, CHVALITEBNY)); colors.put(2, new SimpleTheme(ANSI.BLACK, CHVALITEBNY));
@ -67,51 +68,50 @@ public class Znamky extends JecnaContent {
colors.put(-1, new SimpleTheme(ANSI.WHITE, NEHODNOCEN)); colors.put(-1, new SimpleTheme(ANSI.WHITE, NEHODNOCEN));
colors.put(-2, new SimpleTheme(ANSI.WHITE, NEHODNOCEN)); colors.put(-2, new SimpleTheme(ANSI.WHITE, NEHODNOCEN));
// Sloupec pro jmena predmetu // Column for subject names
Panel jmemaPredmetu = new Panel() Panel subjectNames = new Panel()
.setLayoutManager(new LinearLayout(Direction.VERTICAL)) .setLayoutManager(new LinearLayout(Direction.VERTICAL))
.setLayoutData(ALIGN_LEFT) .setLayoutData(ALIGN_LEFT)
.addTo(mainPanel); .addTo(mainPanel);
// Sloupec pro znamky z predmetu // Column for marks
Panel znamky = new Panel() Panel marks = new Panel()
.setLayoutManager(new LinearLayout(Direction.VERTICAL)) .setLayoutManager(new LinearLayout(Direction.VERTICAL))
.setLayoutData(ALIGN_LEFT) .setLayoutData(ALIGN_LEFT)
.addTo(mainPanel); .addTo(mainPanel);
// Sloupec pro vyslednou znamku // Column for final mark
Panel vysledneZnamky = new Panel() Panel finalMarks = new Panel()
.setLayoutManager(new LinearLayout(Direction.VERTICAL)) .setLayoutManager(new LinearLayout(Direction.VERTICAL))
.setLayoutData(ALIGN_RIGHT) .setLayoutData(ALIGN_RIGHT)
.addTo(mainPanel); .addTo(mainPanel);
for(Predmet predmet : subjects) for(Subject subject : subjects)
{ {
Panel jednotliveZnamky = new Panel() Panel individualMarks = new Panel()
.setLayoutManager(new LinearLayout(Direction.HORIZONTAL)) .setLayoutManager(new LinearLayout(Direction.HORIZONTAL))
.addTo(znamky); .addTo(marks);
if(predmet.getMarks().isEmpty()) if(subject.getMarks().isEmpty())
{ {
jednotliveZnamky.addComponent(new Label("")); individualMarks.addComponent(new Label(""));
} }
for(Znamka znamka : predmet.getMarks()) for(Mark mark : subject.getMarks())
{ {
Label znamkaLabel = new Label(znamka.getText()); Label markLabel = new Label(mark.toString());
znamkaLabel.setTheme(colors.get(znamka.getZnamka())); markLabel.setTheme(colors.get(mark.getMark()));
jednotliveZnamky.addComponent(znamkaLabel); individualMarks.addComponent(markLabel);
} }
FinalMark finalMark = predmet.getFinalMark(); FinalMark finalMark = subject.getFinalMark();
Label vysl = new Label(finalMark.toString()); Label vysl = new Label(finalMark.toString());
vysl.setTheme(colors.get((int) Math.round(finalMark.getValue()))); vysl.setTheme(colors.get((int) Math.round(finalMark.getValue())));
jmemaPredmetu.addComponent(new Label(predmet.getSubjectName())); subjectNames.addComponent(new Label(subject.getSubjectName()));
vysledneZnamky.addComponent(vysl); finalMarks.addComponent(vysl);
} }
} }
} }

View File

@ -11,18 +11,18 @@ import com.googlecode.lanterna.gui2.Panel;
import xyz.thastertyn.UserInterface.Listeners.UpdateListener; import xyz.thastertyn.UserInterface.Listeners.UpdateListener;
public class Sdeleni extends JecnaContent { public class Reports extends JecnaContent {
private xyz.thastertyn.Scrape.Sdeleni sdeleni = new xyz.thastertyn.Scrape.Sdeleni(); private xyz.thastertyn.Scrape.Reports reports = new xyz.thastertyn.Scrape.Reports();
public Sdeleni(UpdateListener listener) public Reports(UpdateListener listener)
{ {
super(listener); super(listener);
this.mainPanel = new Panel().setLayoutManager(new GridLayout(1) this.mainPanel = new Panel().setLayoutManager(new GridLayout(1)
.setLeftMarginSize(1) .setLeftMarginSize(1)
.setRightMarginSize(1)); .setRightMarginSize(1));
this.borderLabel = new Label("Sdeleni R."); this.borderLabel = new Label("Sdeleni R.");
super.scraper = this.sdeleni; super.scraper = this.reports;
} }
@Override @Override
@ -30,26 +30,25 @@ public class Sdeleni extends JecnaContent {
{ {
mainPanel.removeAllComponents(); mainPanel.removeAllComponents();
ArrayList<xyz.thastertyn.Types.Sdeleni> sdeleniList = sdeleni.getSdeleni(); ArrayList<xyz.thastertyn.Types.Reports> reportsList = reports.getSdeleni();
for(xyz.thastertyn.Types.Sdeleni sdeleni : sdeleniList) for(xyz.thastertyn.Types.Reports reports : reportsList)
{ {
Panel row = new Panel().setLayoutManager(new LinearLayout(Direction.HORIZONTAL)); Panel row = new Panel().setLayoutManager(new LinearLayout(Direction.HORIZONTAL));
Label checkmark = new Label("");
Label text = new Label(reports.getText());
Label check = new Label(""); if(reports.isPositive())
Label text = new Label(sdeleni.getText());
if(sdeleni.isPositive())
{ {
check.setForegroundColor(ANSI.GREEN); checkmark.setForegroundColor(ANSI.GREEN);
check.setText(""); checkmark.setText("");
}else{ }else{
check.setForegroundColor(ANSI.RED); checkmark.setForegroundColor(ANSI.RED);
check.setText(""); checkmark.setText("");
} }
row.addComponent(check) row.addComponent(checkmark)
.addComponent(text) .addComponent(text)
.addTo(mainPanel); .addTo(mainPanel);
} }

View File

@ -7,12 +7,11 @@ import com.googlecode.lanterna.gui2.Label;
import com.googlecode.lanterna.gui2.Panel; import com.googlecode.lanterna.gui2.Panel;
import com.googlecode.lanterna.gui2.table.Table; import com.googlecode.lanterna.gui2.table.Table;
import xyz.thastertyn.Types.Timetable;
import xyz.thastertyn.UserInterface.Listeners.UpdateListener; import xyz.thastertyn.UserInterface.Listeners.UpdateListener;
public class Rozvrh extends JecnaContent { public class Timetable extends JecnaContent {
private xyz.thastertyn.Scrape.Rozvrh rozvrh = new xyz.thastertyn.Scrape.Rozvrh(); private xyz.thastertyn.Scrape.Timetable rozvrh = new xyz.thastertyn.Scrape.Timetable();
private String[] labels = {"Den", "7:30-8:15", "8:25-9:10", "9:20-10:05", "10:20-11:05", "11:15-12:00", "12:10-12:55", "13:05-13:50", "14:00-14:45", "14:55-15:40", "15:50-16:35"}; private String[] labels = {"Den", "7:30-8:15", "8:25-9:10", "9:20-10:05", "10:20-11:05", "11:15-12:00", "12:10-12:55", "13:05-13:50", "14:00-14:45", "14:55-15:40", "15:50-16:35"};
//private String[] labels = {"Den", "1.", "2.", "3.", "4.", "5.", "6.", "7.", "8.", "9.", "10."}; //private String[] labels = {"Den", "1.", "2.", "3.", "4.", "5.", "6.", "7.", "8.", "9.", "10."};
@ -21,7 +20,7 @@ public class Rozvrh extends JecnaContent {
Table<String> table; Table<String> table;
public Rozvrh(UpdateListener listener) public Timetable(UpdateListener listener)
{ {
super(listener); super(listener);
this.mainPanel = new Panel(); this.mainPanel = new Panel();
@ -34,7 +33,7 @@ public class Rozvrh extends JecnaContent {
{ {
mainPanel.removeAllComponents(); mainPanel.removeAllComponents();
table = new Table<>(labels); table = new Table<>(labels);
Timetable timetable = rozvrh.getRozvrh(); xyz.thastertyn.Types.Timetable timetable = rozvrh.getRozvrh();
for(int day = 0; day < 5; day++) for(int day = 0; day < 5; day++)
{ {

View File

@ -62,7 +62,7 @@ public class EscapeDialog extends DialogWindow {
{ {
close(); close();
LoginController controller = new LoginController(textGUI); LoginController controller = new LoginController(textGUI);
controller.loginUsingGui(); controller.login(true);
resetListener.reset(); resetListener.reset();
} }

View File

@ -1,5 +1,8 @@
package xyz.thastertyn.UserInterface.Listeners; package xyz.thastertyn.UserInterface.Listeners;
/**
* Interface for when accounts have been switched and a complete wipe of content is needed
*/
public interface ContentResetListener { public interface ContentResetListener {
public void reset(); public void reset();
} }

View File

@ -19,20 +19,24 @@ import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType; import com.googlecode.lanterna.input.KeyType;
import xyz.thastertyn.UserInterface.Content.JecnaContent; import xyz.thastertyn.UserInterface.Content.JecnaContent;
import xyz.thastertyn.UserInterface.Content.OmluvnyList; import xyz.thastertyn.UserInterface.Content.AbsenceList;
import xyz.thastertyn.UserInterface.Content.Rozvrh; import xyz.thastertyn.UserInterface.Content.Timetable;
import xyz.thastertyn.UserInterface.Content.Sdeleni; import xyz.thastertyn.UserInterface.Content.Reports;
import xyz.thastertyn.UserInterface.Content.Znamky; import xyz.thastertyn.UserInterface.Content.Marks;
import xyz.thastertyn.UserInterface.Dialogs.EscapeDialog; import xyz.thastertyn.UserInterface.Dialogs.EscapeDialog;
/**
* Holds all the content displayables and moves between them when needed on tab, or shift tab press
*/
// The biggest and greatest.
public class WindowSwitchListener implements WindowListener, UpdateListener, ContentResetListener { public class WindowSwitchListener implements WindowListener, UpdateListener, ContentResetListener {
private WindowBasedTextGUI textGUI; private WindowBasedTextGUI textGUI;
private JecnaContent[] contents = { private JecnaContent[] contents = {
new Rozvrh(this), new Timetable(this),
new Znamky(this), // This being first doesn't resize properly for some reason new Marks(this), // This being first doesn't resize properly for some reason
new Sdeleni(this), new Reports(this),
new OmluvnyList(this) new AbsenceList(this)
}; };
private Label[] tabs = new Label[contents.length]; private Label[] tabs = new Label[contents.length];
@ -105,32 +109,40 @@ public class WindowSwitchListener implements WindowListener, UpdateListener, Con
} }
} }
/**
* If a panel is smaller than the bar with labels, make it at least the bar size
*/
private void setPanelPreferedSize() private void setPanelPreferedSize()
{ {
int tabColumns = tabsPanel contents[current].getPanel().setSize(contents[current].getPanel().calculatePreferredSize()); // Fix for reports not being big enough
int labelTabColumnSize = tabsPanel
.getSize() .getSize()
.getColumns(); .getColumns();
int currentColumns = contents[current] int currentPanelColumnSize = contents[current]
.getPanel() .getPanel()
.getSize() .getSize()
.getColumns(); .getColumns();
if(currentColumns < tabColumns) if(currentPanelColumnSize < labelTabColumnSize)
{ {
int currentPanelRowSize = contents[current].getPanel().getPreferredSize().getRows();
holderPanel.addComponent( holderPanel.addComponent(
contents[current] contents[current]
.getPanel() .getPanel()
.setPreferredSize( .setPreferredSize(new TerminalSize(
new TerminalSize( labelTabColumnSize,
tabColumns, currentPanelRowSize))); // Pretty janky, but basically just create a new TerminalSize with height the same and the width of label bar
contents[current].getPanel().getPreferredSize().getRows())));
return; return;
} }
holderPanel.addComponent(contents[current].getPanel()); holderPanel.addComponent(contents[current].getPanel());
} }
/**
* Gets all labels again and properly sets their borders to single or double depending on {@link #current}
*/
private void updateLabels() private void updateLabels()
{ {
for(int i = 0; i < tabs.length; i++) for(int i = 0; i < tabs.length; i++)
@ -144,6 +156,9 @@ public class WindowSwitchListener implements WindowListener, UpdateListener, Con
} }
} }
/**
* Calls the {@link #current}'s showOptions with a textGui
*/
private void showOptions() private void showOptions()
{ {
try{ try{
@ -163,7 +178,7 @@ public class WindowSwitchListener implements WindowListener, UpdateListener, Con
case Tab: case Tab:
next(); next();
break; break;
case ReverseTab: case ReverseTab: // Shift + Tab
previous(); previous();
break; break;
case Character: case Character:
@ -196,24 +211,26 @@ public class WindowSwitchListener implements WindowListener, UpdateListener, Con
window.invalidate(); window.invalidate();
} }
@Override @Override
public void updatePanel() { public void updatePanel() {
holderPanel.removeAllComponents(); holderPanel.removeAllComponents();
contents[current].getPanel().setPreferredSize(contents[current].getPanel().calculatePreferredSize());
setPanelPreferedSize(); setPanelPreferedSize();
holderPanel.addComponent(contents[current].getPanel()); holderPanel.addComponent(contents[current].getPanel());
} }
/**
* On user change should wipe everything
*/
@Override @Override
public void reset() public void reset()
{ {
contents = new JecnaContent[] { contents = new JecnaContent[] {
new Rozvrh(this), new Timetable(this),
new Znamky(this), new Marks(this),
new Sdeleni(this), new Reports(this),
new OmluvnyList(this) new AbsenceList(this)
}; };
loadPanel(); loadPanel();
} }
} }

View File

@ -28,7 +28,14 @@ public class MainWindow {
private Window window; private Window window;
private MultiWindowTextGUI textGUI; private MultiWindowTextGUI textGUI;
/**
* Create:
* <ul>
* <li> All the main components like Terminal, and Screen </li>
* <li> {@link LoginController} and try logging in using any of its methods </li>
* <li> {@link WindowSwitchListener} for the purpose of switching between tabs using {@code Tab} and {@code ReverseTab} (Shift + Tab) </li>
* </ul>
*/
public void run() public void run()
{ {
try { try {
@ -47,23 +54,25 @@ public class MainWindow {
textGUI = new MultiWindowTextGUI(screen, new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLACK_BRIGHT)); textGUI = new MultiWindowTextGUI(screen, new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLACK_BRIGHT));
//#endregion //#endregion
// Create panel to hold components // Create panel to hold all components
final Panel mainPanel = new Panel(); final Panel mainPanel = new Panel();
mainPanel.setLayoutManager(new LinearLayout(Direction.VERTICAL)); mainPanel.setLayoutManager(new LinearLayout(Direction.VERTICAL));
window.setComponent(mainPanel); window.setComponent(mainPanel);
// Create a secondary panel, as working with the mainPanel is quite buggy
Panel content = new Panel(); Panel content = new Panel();
mainPanel.addComponent(content.withBorder(Borders.singleLine("Jecnak"))); mainPanel.addComponent(content.withBorder(Borders.singleLine("Jecnak")));
// Try logging in using any method
LoginController controller = new LoginController(textGUI); LoginController controller = new LoginController(textGUI);
controller.login(); controller.login(false);
// Create a WindowListener for tab and shift tab for moving between tabs
window.addWindowListener(new WindowSwitchListener(content, textGUI)); window.addWindowListener(new WindowSwitchListener(content, textGUI));
textGUI.addWindowAndWait(window); textGUI.addWindowAndWait(window);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }

View File

@ -1,9 +1,14 @@
package xyz.thastertyn; package xyz.thastertyn;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import org.junit.Test; import org.junit.Test;
import xyz.thastertyn.Types.FinalMark;
import xyz.thastertyn.Types.Mark;
import xyz.thastertyn.Types.Subject;
/** /**
* Unit test for simple App. * Unit test for simple App.
*/ */
@ -19,21 +24,50 @@ public class AppTest
} }
@Test @Test
public void hasJavaAbove11() public void noMarksPresentTest()
{ {
// https://stackoverflow.com/questions/2591083/getting-java-version-at-runtime#2591122 // No marks are present on start
String version = System.getProperty("java.version"); Subject subject = new Subject("Test Subject");
int v;
if(version.startsWith("1.")) assertTrue(subject.getMarks().isEmpty());
{
version = version.substring(2, 3);
}else {
int dot = version.indexOf(".");
if(dot != -1) { version = version.substring(0, dot); }
} }
v = Integer.parseInt(version);
assertTrue("Java >= 11 is required", (v >= 11)); @Test
public void correctAverage()
{
Subject subject = new Subject("Test");
// Average is calculated properly
subject.addMark(new Mark(1, false, "1"));
subject.addMark(new Mark(3, false, "3"));
assertTrue(subject.getFinalMark().getValue() == 2.0);
}
@Test
public void correctSmallMarkAverage()
{
// Small marks are calculated properly
Subject subject = new Subject("Test");
subject.addMark(new Mark(1, false, "1"));
subject.addMark(new Mark(3, false, "3"));
subject.addMark(new Mark(5, true, "5"));
subject.addMark(new Mark(5, true, "5"));
assertTrue(subject.getFinalMark().getValue() == 3.0);
}
@Test
public void marksDontChangeFinalMark()
{
// Once a final mark is given, no additional marks should change it
Subject subject = new Subject("Test", FinalMark.NEDOSTATECNY);
subject.addMark(new Mark(1, false, "a"));
subject.addMark(new Mark(1, false, "a"));
subject.addMark(new Mark(1, false, "a"));
subject.addMark(new Mark(1, false, "a"));
assertEquals(FinalMark.NEDOSTATECNY, subject.getFinalMark());
} }
} }