Merge remote-tracking branch 'remotes/origin/dev' into 69-take-part-conf
# Conflicts: # src/main/resources/db/changelog-master.xmlmerge-requests/56/head
commit
713e50043e
@ -1,50 +1,46 @@
|
||||
## Краткое описание задачи
|
||||
```
|
||||
Что требуется сделать
|
||||
```
|
||||
|
||||
## `Опционально` Список верстаемых страниц
|
||||
```
|
||||
Будут затронуты страницы:
|
||||
* page1.html
|
||||
* page2.html
|
||||
* page3.html
|
||||
```
|
||||
|
||||
## `Опционально` Список затрагиваемых модулей
|
||||
```
|
||||
При реализации задачи потребуется также реализовать методы контроллера
|
||||
```
|
||||
|
||||
## `Опционально` Список реализуемых функций
|
||||
```
|
||||
После выполнения задачи станет доступным:
|
||||
* просмотр `entity_name`
|
||||
* редактирование `entity_name`
|
||||
* валидация `entity_name`
|
||||
```
|
||||
|
||||
## `Опционально` Сценарии работы
|
||||
```
|
||||
Сценарий просмотра:
|
||||
1. Зайти на главную страницу приложения
|
||||
2. Перейти в раздел `section_name`
|
||||
3. Перейти к списку `entity_name`
|
||||
4. Выбрать нужную `entity_name` и нажать на нее
|
||||
|
||||
Сценарий редактирования:
|
||||
1. Зайти на главную страницу приложения
|
||||
2. Перейти в раздел `section_name`
|
||||
3. Перейти к списку `entity_name`
|
||||
4. Выбрать нужную `entity_name` и нажать на нее
|
||||
5. Внести нужные правки в поля и сохранить
|
||||
```
|
||||
|
||||
## Описание конечного результата, дающего возможность проверки выполнения задачи: компоненты проекта, сценарии работы
|
||||
```
|
||||
* Сверстаны страницы page1.hml, page2.hml, page3.hml
|
||||
* Реализован контроллер для обслуживания страниц
|
||||
* Сохранение в БД еще не реализовано
|
||||
* Валидация происходит по полям `field1, field2`
|
||||
* Сценарий просмотра проверяется при ручном внечении записей в БД
|
||||
```
|
||||
## Краткое описание задачи
|
||||
|
||||
Что требуется сделать
|
||||
|
||||
|
||||
## `Опционально` Список верстаемых страниц
|
||||
|
||||
Будут затронуты страницы:
|
||||
* page1.html
|
||||
* page2.html
|
||||
* page3.html
|
||||
|
||||
## `Опционально` Список затрагиваемых модулей
|
||||
|
||||
При реализации задачи потребуется также реализовать методы контроллера
|
||||
|
||||
|
||||
## `Опционально` Список реализуемых функций
|
||||
|
||||
После выполнения задачи станет доступным:
|
||||
* просмотр `entity_name`
|
||||
* редактирование `entity_name`
|
||||
* валидация `entity_name`
|
||||
|
||||
## `Опционально` Сценарии работы
|
||||
|
||||
Сценарий просмотра:
|
||||
1. Зайти на главную страницу приложения
|
||||
2. Перейти в раздел `section_name`
|
||||
3. Перейти к списку `entity_name`
|
||||
4. Выбрать нужную `entity_name` и нажать на нее
|
||||
|
||||
Сценарий редактирования:
|
||||
1. Зайти на главную страницу приложения
|
||||
2. Перейти в раздел `section_name`
|
||||
3. Перейти к списку `entity_name`
|
||||
4. Выбрать нужную `entity_name` и нажать на нее
|
||||
5. Внести нужные правки в поля и сохранить
|
||||
|
||||
## Описание конечного результата, дающего возможность проверки выполнения задачи: компоненты проекта, сценарии работы
|
||||
|
||||
* Сверстаны страницы page1.hml, page2.hml, page3.hml
|
||||
* Реализован контроллер для обслуживания страниц
|
||||
* Сохранение в БД еще не реализовано
|
||||
* Валидация происходит по полям `field1, field2`
|
||||
* Сценарий просмотра проверяется при ручном внечении записей в БД
|
||||
|
@ -1,7 +0,0 @@
|
||||
package ru.ulstu.core.error;
|
||||
|
||||
public class XlsLoadException extends Exception {
|
||||
public XlsLoadException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package ru.ulstu.core.error;
|
||||
|
||||
public class XlsParseException extends Exception {
|
||||
public XlsParseException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package ru.ulstu.core.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.ulstu.core.model.TreeDto;
|
||||
import ru.ulstu.core.model.TreeEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@Service
|
||||
public class TreeService<T extends TreeEntity> {
|
||||
public TreeDto getTree(String rootName, List<T> rootItems) {
|
||||
return addChildNode(new TreeDto(rootName), rootItems, element -> true);
|
||||
}
|
||||
|
||||
public TreeDto getTree(String rootName, List<T> rootItems, Predicate<T> filterPredicate) {
|
||||
return addChildNode(new TreeDto(rootName), rootItems, filterPredicate);
|
||||
}
|
||||
|
||||
private TreeDto addChildNode(TreeDto currentRoot, List<T> children, Predicate<T> filterPredicate) {
|
||||
if (children != null) {
|
||||
children.stream()
|
||||
.filter(filterPredicate)
|
||||
.forEach(item -> {
|
||||
TreeDto newNode = new TreeDto(item);
|
||||
currentRoot.getChildren().add(addChildNode(newNode, item.getChildren(), filterPredicate));
|
||||
});
|
||||
}
|
||||
return currentRoot;
|
||||
}
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
package ru.ulstu.core.service;
|
||||
|
||||
import org.apache.poi.POIXMLDocument;
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
import org.apache.poi.ss.util.RegionUtil;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import ru.ulstu.core.error.XlsParseException;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class XlsDocumentBuilder {
|
||||
private static final int DEFAULT_SHEET_NUM = 0;
|
||||
private File documentFile;
|
||||
private Workbook workBook;
|
||||
private Sheet currentSheet;
|
||||
|
||||
/**
|
||||
* Constructor for reading and writing data from/to *.[xls|xlsx] document
|
||||
*
|
||||
* @param file contains existing document for reading or new document to save
|
||||
*/
|
||||
public XlsDocumentBuilder(File file) throws IOException, XlsParseException {
|
||||
this.documentFile = file;
|
||||
if (file.exists()) {
|
||||
workBook = getWorkBook(file);
|
||||
currentSheet = workBook.getSheetAt(DEFAULT_SHEET_NUM);
|
||||
} else {
|
||||
workBook = new XSSFWorkbook();
|
||||
currentSheet = workBook.createSheet();
|
||||
}
|
||||
}
|
||||
|
||||
private Workbook getWorkBook(File file) throws XlsParseException, IOException {
|
||||
InputStream inputStream = new PushbackInputStream(new FileInputStream(file), 4096);
|
||||
if (isXlsx(inputStream)) {
|
||||
return new XSSFWorkbook(inputStream);
|
||||
} else if (isExcel(inputStream)) {
|
||||
return new HSSFWorkbook(inputStream);
|
||||
}
|
||||
throw new XlsParseException("Wrong document format");
|
||||
}
|
||||
|
||||
/**
|
||||
* Change active sheet to write or read data
|
||||
*
|
||||
* @param index index of sheet to activate
|
||||
*/
|
||||
public XlsDocumentBuilder setActiveSheet(int index) {
|
||||
workBook.setActiveSheet(index);
|
||||
currentSheet = workBook.getSheetAt(index);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new sheet in document and set it active
|
||||
*
|
||||
* @param sheetName
|
||||
*/
|
||||
public XlsDocumentBuilder insertNewSheet(String sheetName) {
|
||||
currentSheet = workBook.createSheet(sheetName);
|
||||
workBook.setActiveSheet(getSheetCount() - 1);
|
||||
return this;
|
||||
}
|
||||
|
||||
public XlsDocumentBuilder insertNewSheet(String sheetName, int order) {
|
||||
insertNewSheet(sheetName);
|
||||
workBook.setSheetOrder(sheetName, order);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of sheet in document
|
||||
*
|
||||
* @return sheets count
|
||||
*/
|
||||
public int getSheetCount() {
|
||||
return workBook.getNumberOfSheets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of rows in sheet
|
||||
*
|
||||
* @return rows count
|
||||
*/
|
||||
public int getRowCount() {
|
||||
return currentSheet.getLastRowNum();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of columns in sheet
|
||||
*
|
||||
* @return columns count
|
||||
*/
|
||||
public int getColumnCount() {
|
||||
Row row = currentSheet.getRow(getRowCount());
|
||||
if (row == null) {
|
||||
return 0;
|
||||
}
|
||||
return row.getLastCellNum() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns converted to string representation of cell value
|
||||
*
|
||||
* @param rowIndex row index of current sheet
|
||||
* @param colIndex column index of current sheet
|
||||
* @return string value of cell
|
||||
*/
|
||||
public String getCellAsString(int rowIndex, int colIndex) {
|
||||
Cell cell = currentSheet.getRow(rowIndex).getCell(colIndex);
|
||||
cell.setCellType(Cell.CELL_TYPE_STRING);
|
||||
return cell.getStringCellValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets string cell value
|
||||
*
|
||||
* @param rowIndex row index of current sheet
|
||||
* @param colIndex column index of current sheet
|
||||
*/
|
||||
public XlsDocumentBuilder setCellValue(int rowIndex, int colIndex, String value) {
|
||||
if (currentSheet.getRow(rowIndex) == null) {
|
||||
currentSheet.createRow(rowIndex);
|
||||
}
|
||||
if (currentSheet.getRow(rowIndex).getCell(colIndex) == null) {
|
||||
currentSheet.getRow(rowIndex).createCell(colIndex);
|
||||
}
|
||||
Cell cell = currentSheet.getRow(rowIndex).getCell(colIndex);
|
||||
cell.setCellValue(value);
|
||||
setColumnAutosize(colIndex, colIndex);
|
||||
return this;
|
||||
}
|
||||
|
||||
public XlsDocumentBuilder setCellValue(int rowIndex, int colIndex, int value) {
|
||||
return setCellValue(rowIndex, colIndex, String.valueOf(value));
|
||||
}
|
||||
|
||||
public XlsDocumentBuilder setCellValue(int rowIndex, int colIndex, long value) {
|
||||
return setCellValue(rowIndex, colIndex, String.valueOf(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current file
|
||||
*/
|
||||
public XlsDocumentBuilder save() throws IOException {
|
||||
OutputStream out = new FileOutputStream(documentFile);
|
||||
workBook.write(out);
|
||||
return this;
|
||||
}
|
||||
|
||||
private boolean isExcel(InputStream i) throws IOException {
|
||||
return (POIFSFileSystem.hasPOIFSHeader(i) || POIXMLDocument.hasOOXMLHeader(i));
|
||||
}
|
||||
|
||||
private boolean isXlsx(InputStream i) throws IOException {
|
||||
return POIXMLDocument.hasOOXMLHeader(i);
|
||||
}
|
||||
|
||||
public int getActiveSheetIndex() {
|
||||
return workBook.getActiveSheetIndex();
|
||||
}
|
||||
|
||||
public XlsDocumentBuilder mergeCells(int rowFrom, int rowTo, int colFrom, int colTo) {
|
||||
currentSheet.addMergedRegion(new CellRangeAddress(rowFrom, rowTo, colFrom, colTo));
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setRegionBorderWithMedium(int rowFrom, int rowTo, int colFrom, int colTo) {
|
||||
for (int row = rowFrom; row < rowTo; row++) {
|
||||
for (int col = colFrom; col <= colTo; col++) {
|
||||
CellRangeAddress cellRangeAddress = new CellRangeAddress(row, row, col, col);
|
||||
RegionUtil.setBorderBottom(CellStyle.BORDER_THIN, cellRangeAddress, currentSheet, workBook);
|
||||
RegionUtil.setBorderLeft(CellStyle.BORDER_THIN, cellRangeAddress, currentSheet, workBook);
|
||||
RegionUtil.setBorderRight(CellStyle.BORDER_THIN, cellRangeAddress, currentSheet, workBook);
|
||||
RegionUtil.setBorderTop(CellStyle.BORDER_THIN, cellRangeAddress, currentSheet, workBook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public XlsDocumentBuilder setColumnAutosize(int from, int to) {
|
||||
for (int col = from; col <= to; col++) {
|
||||
currentSheet.autoSizeColumn(col, true);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public XlsDocumentBuilder setRowAutosize(int from, int to) {
|
||||
CellStyle style = workBook.createCellStyle();
|
||||
style.setWrapText(true);
|
||||
for (int row = from; row <= to; row++) {
|
||||
for (int col = 0; col <= currentSheet.getRow(row).getLastCellNum(); col++) {
|
||||
if (currentSheet.getRow(row).getCell(col) != null) {
|
||||
currentSheet.getRow(row).getCell(col).setCellStyle(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public XlsDocumentBuilder deleteSheet(int index) {
|
||||
workBook.removeSheetAt(index);
|
||||
return this;
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package ru.ulstu.grant.model;
|
||||
|
||||
public class GrantStatusDto {
|
||||
private final String id;
|
||||
private final String name;
|
||||
|
||||
public GrantStatusDto(Grant.GrantStatus status) {
|
||||
this.id = status.name();
|
||||
this.name = status.getStatusName();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package ru.ulstu.paper.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.ulstu.file.service.FileService;
|
||||
import ru.ulstu.paper.model.PaperDto;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
|
||||
@Service
|
||||
public class LatexService {
|
||||
private String errorMessage;
|
||||
private File pdfFile;
|
||||
private FileService fileService;
|
||||
private final String pdfLatexError = "Errors occurred while executing pdfLaTeX.";
|
||||
private final String bibtexError = "Errors occurred while executing bibtex.";
|
||||
|
||||
public LatexService(FileService fileService) {
|
||||
this.fileService = fileService;
|
||||
}
|
||||
|
||||
public byte[] generatePdfFromLatexFile(PaperDto paper) throws IOException, InterruptedException {
|
||||
fileService.createLatexAttachs(paper);
|
||||
File tex = fileService.createLatexFile(paper);
|
||||
|
||||
if (!generate(paper.getTitle(), tex.getParentFile())) {
|
||||
throw new IOException(errorMessage);
|
||||
}
|
||||
|
||||
return Files.readAllBytes(pdfFile.toPath());
|
||||
}
|
||||
|
||||
private int startProcess(String[] args, File dir, String message) throws IOException, InterruptedException {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(args);
|
||||
processBuilder.redirectErrorStream(true);
|
||||
processBuilder.directory(dir);
|
||||
|
||||
Process process = processBuilder.start();
|
||||
InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream());
|
||||
|
||||
try (BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
|
||||
while ((bufferedReader.readLine()) != null) ;
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
errorMessage = message + " Exit value of the process: " + exitCode;
|
||||
}
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
private boolean generate(String filename, File dir) throws IOException, InterruptedException {
|
||||
startProcess(new String[]{"pdflatex", filename, "--interaction=nonstopmode"}, dir, pdfLatexError);
|
||||
startProcess(new String[]{"bibtex", filename}, dir, bibtexError);
|
||||
if (startProcess(new String[]{"pdflatex", filename, "--interaction=nonstopmode"}, dir, pdfLatexError) != 0)
|
||||
return false;
|
||||
return checkPdf(filename, dir);
|
||||
}
|
||||
|
||||
private boolean checkPdf(String filename, File dir) {
|
||||
pdfFile = new File(dir.getAbsolutePath() + File.separator + filename + ".pdf");
|
||||
|
||||
if (pdfFile.isFile()) return true;
|
||||
else {
|
||||
errorMessage = "The pdf file could not be created.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package ru.ulstu.project.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import ru.ulstu.project.model.Project;
|
||||
import ru.ulstu.project.model.ProjectDto;
|
||||
import ru.ulstu.project.service.ProjectService;
|
||||
import springfox.documentation.annotations.ApiIgnore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller()
|
||||
@RequestMapping(value = "/projects")
|
||||
@ApiIgnore
|
||||
public class ProjectController {
|
||||
private final ProjectService projectService;
|
||||
|
||||
public ProjectController(ProjectService projectService) {
|
||||
this.projectService = projectService;
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
public void getDashboard(ModelMap modelMap) {
|
||||
modelMap.put("projects", projectService.findAllDto());
|
||||
}
|
||||
|
||||
@GetMapping("/projects")
|
||||
public void getProjects(ModelMap modelMap) {
|
||||
modelMap.put("projects", projectService.findAllDto());
|
||||
}
|
||||
|
||||
@GetMapping("/project")
|
||||
public void getProject(ModelMap modelMap, @RequestParam(value = "id") Integer id) {
|
||||
if (id != null && id > 0) {
|
||||
modelMap.put("projectDto", projectService.findOneDto(id));
|
||||
} else {
|
||||
modelMap.put("projectDto", new ProjectDto());
|
||||
}
|
||||
}
|
||||
|
||||
@ModelAttribute("allStatuses")
|
||||
public List<Project.ProjectStatus> getProjectStatuses() {
|
||||
return projectService.getProjectStatuses();
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="masha" id="20190323_000000-1">
|
||||
<addColumn tableName="paper">
|
||||
<column name="latex_text" type="varchar"/>
|
||||
</addColumn>
|
||||
<addColumn tableName="file">
|
||||
<column name="is_latex_attach" type="boolean"/>
|
||||
</addColumn>
|
||||
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -0,0 +1,26 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="tanya" id="20190402_000000-1">
|
||||
<addColumn tableName="grants">
|
||||
<column name="leader_id" type="integer"></column>
|
||||
</addColumn>
|
||||
<addForeignKeyConstraint baseTableName="grants" baseColumnNames="leader_id"
|
||||
constraintName="fk_grants_leader_id" referencedTableName="users"
|
||||
referencedColumnNames="id"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="tanya" id="20190402_000000-2">
|
||||
<createTable tableName="grants_authors">
|
||||
<column name="grant_id" type="integer"/>
|
||||
<column name="authors_id" type="integer"/>
|
||||
</createTable>
|
||||
<addForeignKeyConstraint baseTableName="grants_authors" baseColumnNames="grant_id"
|
||||
constraintName="fk_grants_grants_authors" referencedTableName="grants"
|
||||
referencedColumnNames="id"/>
|
||||
<addForeignKeyConstraint baseTableName="grants_authors" baseColumnNames="authors_id"
|
||||
constraintName="fk_user_grants_authors" referencedTableName="users"
|
||||
referencedColumnNames="id"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -0,0 +1,13 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="tanya" id="20190404_000000-1">
|
||||
<addColumn tableName="users">
|
||||
<column name="birth_date" type="timestamp"></column>
|
||||
</addColumn>
|
||||
<addColumn tableName="users">
|
||||
<column name="degree" type="varchar(255)"></column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="anton" id="20190418_000000-1">
|
||||
<addColumn tableName="project">
|
||||
<column name="status" type="varchar(255)"/>
|
||||
</addColumn>
|
||||
<addColumn tableName="project">
|
||||
<column name="description" type="varchar(255)"/>
|
||||
</addColumn>
|
||||
<addColumn tableName="project">
|
||||
<column name="grant_id" type="integer"/>
|
||||
</addColumn>
|
||||
<addForeignKeyConstraint baseTableName="project" baseColumnNames="grant_id"
|
||||
constraintName="fk_project_grant_id" referencedTableName="grants"
|
||||
referencedColumnNames="id"/>
|
||||
<addColumn tableName="project">
|
||||
<column name="repository" type="varchar(255)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,12 @@
|
||||
.div-deadline-date {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.form-deadline-date{
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.div-deadline-description{
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
#files-list .row > div:nth-child(6) {
|
||||
#files-list .row > div:nth-child(7) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
</head>
|
||||
<body>
|
||||
<body>
|
||||
<div th:fragment="filesList (isLatexAttach)" th:remove="tag">
|
||||
<th:block th:each="file, rowStat : *{files}">
|
||||
|
||||
<span th:if="${file.isLatexAttach == isLatexAttach}" th:remove="tag">
|
||||
|
||||
<div class="row" th:id="|files${rowStat.index}|"
|
||||
th:style="${file.deleted} ? 'display: none;' :''">
|
||||
<input type="hidden" th:field="*{files[__${rowStat.index}__].id}"/>
|
||||
<input type="hidden"
|
||||
th:field="*{files[__${rowStat.index}__].deleted}"/>
|
||||
<input type="hidden"
|
||||
th:field="*{files[__${rowStat.index}__].name}"/>
|
||||
<input type="hidden"
|
||||
th:field="*{files[__${rowStat.index}__].tmpFileName}"/>
|
||||
<input type="hidden"
|
||||
th:field="*{files[__${rowStat.index}__].isLatexAttach}"/>
|
||||
<div class="col-2">
|
||||
<a class="btn btn-danger float-right"
|
||||
th:onclick="|$('#files${rowStat.index}\\.deleted').val('true'); $('#files${rowStat.index}').hide(); |">
|
||||
<span aria-hidden="true"><i class="fa fa-times"/></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<a th:onclick="${file.id==null} ? 'downloadFile('+${file.tmpFileName}+',null,\''+${file.name}+'\')':
|
||||
'downloadFile(null,'+${file.id}+',\''+${file.name}+'\')' "
|
||||
href="javascript:void(0)"
|
||||
th:text="*{files[__${rowStat.index}__].name}">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorator="default" xmlns:th="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" layout:fragment="content">
|
||||
<section id="services">
|
||||
<div class="container">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading text-uppercase">Проекты</h2>
|
||||
<div th:replace="projects/fragments/projectNavigationFragment"/>
|
||||
</div>
|
||||
<div class="row justify-content-center" id="dashboard">
|
||||
<th:block>
|
||||
<div/>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:fragment="headerfiles">
|
||||
<meta charset="UTF-8"/>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="projectLine (project)" class="row text-left project-row" style="background-color: white;">
|
||||
<div class="col">
|
||||
<span th:replace="projects/fragments/projectStatusFragment :: projectStatus(projectStatus=${project.status})"/>
|
||||
<a th:href="@{'project?id='+${project.id}}">
|
||||
<span class="h6" th:text="${project.title}"/>
|
||||
<span class="text-muted" th:text="${project.description}"/>
|
||||
</a>
|
||||
<input class="id-class" type="hidden" th:value="${project.id}"/>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:fragment="headerfiles">
|
||||
<meta charset="UTF-8"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-sm-12 col-md-12 col-lg-4 col-xl-3">
|
||||
<a href="./projects" class="btn btn-light toolbar-button"><i class="fa fa-list-alt"></i>
|
||||
Список</a>
|
||||
</div>
|
||||
<div class="col-12 col-sm-12 col-md-12 col-lg-4 col-xl-3">
|
||||
<a href="./dashboard" class="btn btn-light toolbar-button"><i class="fa fa-newspaper-o"
|
||||
aria-hidden="true"></i> Панель
|
||||
управления</a>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-12 col-md-12 col-lg-4 col-xl-3">
|
||||
<a href="./project?id=0" class="btn btn-light toolbar-button"><i class="fa fa-plus-circle"
|
||||
aria-hidden="true"></i>
|
||||
Добавить проект</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:fragment="headerfiles">
|
||||
<meta charset="UTF-8"/>
|
||||
</head>
|
||||
<body>
|
||||
<span th:fragment="paperStatus (paperStatus)" class="fa-stack fa-1x">
|
||||
<th:block th:switch="${projectStatus.name()}">
|
||||
<div th:case="'APPLICATION'">
|
||||
<i class="fa fa-circle fa-stack-2x text-draft"></i>
|
||||
</div>
|
||||
<div th:case="'ON_COMPETITION'">
|
||||
<i class="fa fa-circle fa-stack-2x text-review"></i>
|
||||
</div>
|
||||
<div th:case="'SUCCESSFUL_PASSAGE'">
|
||||
<i class="fa fa-circle fa-stack-2x text-accepted"></i>
|
||||
</div>
|
||||
<div th:case="'IN_WORK'">
|
||||
<i class="fa fa-circle fa-stack-2x text-primary"></i>
|
||||
</div>
|
||||
<div th:case="'COMPLETED'">
|
||||
<i class="fa fa-circle fa-stack-2x text-success"></i>
|
||||
</div>
|
||||
<div th:case="'FAILED'">
|
||||
<i class="fa fa-circle fa-stack-2x text-failed"></i>
|
||||
</div>
|
||||
</th:block>
|
||||
<i class="fa fa-file-text-o fa-stack-1x fa-inverse"></i>
|
||||
</span>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorator="default" xmlns:th="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container" layout:fragment="content">
|
||||
|
||||
<section id="paper">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading text-uppercase">Редактирование проекта</h2>
|
||||
<div th:replace="projects/fragments/projectNavigationFragment"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<form id="project-form" method="post"
|
||||
th:action="@{'/projects/project?id='+ *{id == null ? '' : id} + ''}"
|
||||
th:object="${projectDto}">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<input type="hidden" name="id" th:field="*{id}"/>
|
||||
<div class="form-group">
|
||||
<label for="title">Название:</label>
|
||||
<input class="form-control" id="title" type="text"
|
||||
placeholder="Название проекта"
|
||||
th:field="*{title}"/>
|
||||
<p th:if="${#fields.hasErrors('title')}" th:errors="*{title}"
|
||||
class="alert alert-danger">Incorrect title</p>
|
||||
<p class="help-block text-danger"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="status">Статус:</label>
|
||||
<select class="form-control" th:field="*{status}" id="status">
|
||||
<option th:each="status : ${allStatuses}" th:value="${status}"
|
||||
th:text="${status.statusName}">Status
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Описание:</label>
|
||||
<textarea class="form-control" rows="3" id="description"
|
||||
th:field="*{description}"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="createGrant">Добавить грант:</label>
|
||||
<div th:if="*{grant} == null">
|
||||
<input type="submit" id="createGrant" name="createGrant" class="btn btn-primary"
|
||||
value="Добавить грант"/>
|
||||
</div>
|
||||
<input type="hidden" th:field="*{grant.id}"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="repository">Ссылка на репозиторий:</label>
|
||||
<input class="form-control" id="repository" type="text"
|
||||
placeholder="http://"
|
||||
th:field="*{repository}"/>
|
||||
<p th:if="${#fields.hasErrors('repository')}" th:errors="*{repository}"
|
||||
class="alert alert-danger">Incorrect repository link</p>
|
||||
<p class="help-block text-danger"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Дедлайны показателей:</label>
|
||||
<div class="row">
|
||||
<input type="hidden"/>
|
||||
<div class="col-6">
|
||||
<input type="date" class="form-control" name="deadline"/>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<input class="form-control" type="text" placeholder="Описание"/>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<a class="btn btn-danger float-right"><span
|
||||
aria-hidden="true"><i class="fa fa-times"/></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p th:if="${#fields.hasErrors('deadlines')}" th:errors="*{deadlines}"
|
||||
class="alert alert-danger">Incorrect title</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" id="addDeadline" name="addDeadline" class="btn btn-primary"
|
||||
value="Добавить
|
||||
дедлайн"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="loader">Загрузить файл:</label>
|
||||
<div id="loader">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
<div class="col-lg-12">
|
||||
<div class="form-group">
|
||||
<button id="sendMessageButton" name="save" class="btn btn-success text-uppercase"
|
||||
type="submit">
|
||||
Сохранить
|
||||
</button>
|
||||
<button id="cancelButton" class="btn btn-default text-uppercase"
|
||||
href="/projects/projects">
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script type="text/javascript" src="/js/file-loader.js"></script>
|
||||
<script>
|
||||
/*<![CDATA[*/
|
||||
$(document).ready(function () {
|
||||
new FileLoader({
|
||||
div: "loader",
|
||||
url: urlFileUpload,
|
||||
maxSize: 2,
|
||||
extensions: ["doc", "docx", "xls", "jpg", "png", "pdf", "txt"],
|
||||
callback: function (response) {
|
||||
showFeedbackMessage("Файл успешно загружен");
|
||||
console.debug(response);
|
||||
}
|
||||
});
|
||||
$('.selectpicker').selectpicker();
|
||||
});
|
||||
/*]]>*/
|
||||
|
||||
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorator="default" xmlns:th="">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" layout:fragment="content">
|
||||
<form id="projects-form" method="post" th:action="@{'/projects/projects'}">
|
||||
<input th:type="hidden" name="projectDeleteId" id="projectDeleteId"/>
|
||||
<section id="projects">
|
||||
<div class="container">
|
||||
<div class="row" id="project-list">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading text-uppercase">Проекты</h2>
|
||||
<div th:replace="projects/fragments/projectNavigationFragment"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-9 col-sm-12">
|
||||
<th:block th:each="project : ${projects}">
|
||||
<div th:replace="projects/fragments/projectLineFragment :: projectLine(project=${project})"/>
|
||||
</th:block>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,52 @@
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import core.PageObject;
|
||||
import core.TestTemplate;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import paper.PaperPage;
|
||||
import paper.PapersDashboardPage;
|
||||
import paper.PapersPage;
|
||||
import ru.ulstu.NgTrackerApplication;
|
||||
import ru.ulstu.configuration.ApplicationProperties;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = NgTrackerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
public class IndexPageTest extends TestTemplate {
|
||||
private final Map<PageObject, List<String>> navigationHolder = ImmutableMap.of(
|
||||
new PapersPage(), Arrays.asList("СТАТЬИ", "/papers/papers"),
|
||||
new PaperPage(), Arrays.asList("РЕДАКТИРОВАНИЕ СТАТЬИ", "/papers/paper?id=0"),
|
||||
new PapersDashboardPage(), Arrays.asList("СТАТЬИ", "/papers/dashboard")
|
||||
);
|
||||
|
||||
@Autowired
|
||||
private ApplicationProperties applicationProperties;
|
||||
|
||||
@Test
|
||||
public void testStartApplication() {
|
||||
getContext().goTo(applicationProperties.getBaseUrl());
|
||||
Assertions
|
||||
.assertThat(getContext().getTitle())
|
||||
.isEqualTo("NG-Tracker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModulesNavigation() {
|
||||
navigationHolder.entrySet()
|
||||
.stream()
|
||||
.forEach(navigationEntry -> {
|
||||
getContext().goTo(applicationProperties.getBaseUrl() + navigationEntry.getValue().get(1));
|
||||
PageObject pageObject = getContext().initPage(navigationEntry.getKey());
|
||||
Assertions
|
||||
.assertThat(pageObject.getSubTitle())
|
||||
.isEqualToIgnoringCase(navigationEntry.getValue().get(0));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package context;
|
||||
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
|
||||
public class ChromeContext extends Context {
|
||||
private final static String WINDOWS_DRIVER = "chromedriver.exe";
|
||||
private final static String LINUX_DRIVER = "chromedriver";
|
||||
private final static String DRIVER_TYPE = "webdriver.chrome.driver";
|
||||
|
||||
@Override
|
||||
protected void createDriver() {
|
||||
final ChromeOptions chromeOptions = new ChromeOptions();
|
||||
chromeOptions.addArguments("--headless");
|
||||
driver = new ChromeDriver(chromeOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDriverExecutable(boolean isWindows) {
|
||||
return isWindows ? WINDOWS_DRIVER : LINUX_DRIVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDriverType() {
|
||||
return DRIVER_TYPE;
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package context;
|
||||
|
||||
import core.PageObject;
|
||||
import org.openqa.selenium.Dimension;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
//import org.openqa.selenium.support.PageFactory;
|
||||
|
||||
public abstract class Context {
|
||||
private final static String DRIVER_LOCATION = "drivers/%s";
|
||||
|
||||
protected WebDriver driver;
|
||||
|
||||
protected WebDriver getDriver() {
|
||||
if (driver != null) {
|
||||
return driver;
|
||||
}
|
||||
throw new IllegalStateException("WebDriver is not initialized");
|
||||
}
|
||||
|
||||
public void start() {
|
||||
System.setProperty(getDriverType(), getDriverExecutablePath());
|
||||
|
||||
createDriver();
|
||||
// это плохая инструкция для автотестов, т.к. лучше задавать для конкретного элемента или кейса
|
||||
getDriver().manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (driver != null) {
|
||||
driver.quit();
|
||||
}
|
||||
}
|
||||
|
||||
public void goTo(String url) {
|
||||
getDriver().get(url);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return getDriver().getTitle();
|
||||
}
|
||||
|
||||
public <T extends PageObject> T initPage(T pageObject) {
|
||||
return (T) pageObject.setDriver(getDriver());
|
||||
}
|
||||
|
||||
protected abstract void createDriver();
|
||||
|
||||
protected abstract String getDriverType();
|
||||
|
||||
protected abstract String getDriverExecutable(boolean windows);
|
||||
|
||||
private String getDriverExecutablePath() {
|
||||
return Context.class.getClassLoader().getResource(
|
||||
String.format(DRIVER_LOCATION, getDriverExecutable(isWindows()))).getFile();
|
||||
}
|
||||
|
||||
private boolean isWindows() {
|
||||
return System.getProperty("os.name").toLowerCase().contains("windows");
|
||||
}
|
||||
|
||||
public void setSize(Dimension dimension) {
|
||||
driver.manage().window().setSize(dimension);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package context;
|
||||
|
||||
import org.openqa.selenium.firefox.FirefoxDriver;
|
||||
|
||||
public class FirefoxContext extends Context {
|
||||
private final static String WINDOWS_DRIVER = "geckodriver.exe";
|
||||
private final static String LINUX_DRIVER = "geckodriver";
|
||||
private final static String DRIVER_TYPE = "webdriver.gecko.driver";
|
||||
|
||||
@Override
|
||||
protected void createDriver() {
|
||||
driver = new FirefoxDriver();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDriverExecutable(boolean isWindows) {
|
||||
return isWindows ? WINDOWS_DRIVER : LINUX_DRIVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDriverType() {
|
||||
return DRIVER_TYPE;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package core;
|
||||
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
public abstract class PageObject {
|
||||
protected WebDriver driver;
|
||||
|
||||
public abstract String getSubTitle();
|
||||
|
||||
public PageObject setDriver(WebDriver driver) {
|
||||
this.driver = driver;
|
||||
return this;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package core;
|
||||
|
||||
import context.ChromeContext;
|
||||
import context.Context;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.openqa.selenium.Dimension;
|
||||
|
||||
public abstract class TestTemplate {
|
||||
private static Context context;
|
||||
|
||||
public static Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
context = new ChromeContext();
|
||||
context.start();
|
||||
context.setSize(new Dimension(1600, 900));
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void quit() {
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package paper;
|
||||
|
||||
import core.PageObject;
|
||||
import org.openqa.selenium.By;
|
||||
|
||||
public class PaperPage extends PageObject {
|
||||
|
||||
public String getSubTitle() {
|
||||
return driver.findElement(By.tagName("h2")).getText();
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package paper;
|
||||
|
||||
import core.PageObject;
|
||||
import org.openqa.selenium.By;
|
||||
|
||||
public class PapersDashboardPage extends PageObject {
|
||||
|
||||
public String getSubTitle() {
|
||||
return driver.findElement(By.tagName("h2")).getText();
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package paper;
|
||||
|
||||
import core.PageObject;
|
||||
import org.openqa.selenium.By;
|
||||
|
||||
public class PapersPage extends PageObject {
|
||||
|
||||
public String getSubTitle() {
|
||||
return driver.findElement(By.tagName("h2")).getText();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue