Skip to content
Open

Task #85

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ target
logs
attachments
*.patch
secrets.properties
secrets-test.properties


9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM eclipse-temurin:17-jdk-alpine

WORKDIR /app

COPY target/jira-1.0.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,11 @@
- https://habr.com/ru/articles/259055/

Список выполненных задач:
...
- Видалити соціальні мережі: vk, yandex;
- Винести чутливу інформацію до окремого проперті файлу;
- Написати тести для всіх публічних методів контролера ProfileRestController;
- Зробити рефакторинг методу com.javarush.jira.bugtracking.attachment.FileUtil#upload, щоб він використовував сучасний підхід для роботи з файловою системою;
- Додати новий функціонал: додавання тегів до завдання (REST API + реалізація на сервісі);
- Додати підрахунок часу: скільки завдання перебувало у роботі та тестуванні;
- Написати Dockerfile для основного сервера;
- Додати локалізацію мінімум двома мовами для шаблонів листів (mails) та стартовою сторінки index.html.
36 changes: 35 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<java.version>17</java.version>
<springdoc.version>2.0.2</springdoc.version>
<mapstruct.version>1.5.3.Final</mapstruct.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
Expand Down Expand Up @@ -88,6 +89,13 @@
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
Expand All @@ -112,7 +120,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
<version>${lombok-mapstruct-binding.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
Expand Down Expand Up @@ -166,6 +174,7 @@
</excludes>
</configuration>
</plugin>

<plugin>
<!--https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven -->
<groupId>org.apache.maven.plugins</groupId>
Expand All @@ -174,6 +183,31 @@
<argLine>-Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>

<plugin>
<!--https://projectlombok.org/setup/maven-->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

Expand Down
18 changes: 12 additions & 6 deletions resources/mails/email-confirmation.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<title>JiraRush - подтверждение почты</title>
<title th:text="#{mail.confirm.title}">JiraRush - подтверждение почты</title>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
</head>
<body>
<p th:text="'Привет, ' + ${user.firstName} + '.'"/>
<p>Чтобы завершить настройку учетной записи и начать пользоваться JiraRush, подтвердите, что вы правильно указали вашу
<p th:text="#{mail.confirm.greeting(${user.firstName})}"></p>
<p th:text="#{mail.confirm.text}">Чтобы завершить настройку учетной записи и начать пользоваться JiraRush, подтвердите, что вы правильно указали вашу
электронную почту.</p>
<a th:href="${confirmationUrl}">Подтвердить почту</a>
<a th:href="${confirmationUrl}" th:text="#{mail.confirm.button}">Подтвердить почту</a>
<nav th:fragment="menu">
<span th:text="#{language.text}">Язык:</span>
<a href="?lang=en">EN</a>
<a href="?lang=uk">UK</a>
<a href="?lang=ru">RU</a>
</nav>
</body>
</html>
</html>
16 changes: 11 additions & 5 deletions resources/mails/password-reset.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:fragment="head">
<title>JiraRush - установить новый пароль</title>
<title th:text="#{mail.reset.title}">JiraRush - установить новый пароль</title>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
</head>
<body>
<p th:text="'Привет, ' + ${user.firstName} + '.'"/>
<p th:text="'Мы получили запрос на установку нового пароля JiraRush для учетной записи: ' + ${user.email} + '.'"/>
<a th:href="${resetUrl}">Установить пароль</a>
<p th:text="#{mail.reset.greeting(${user.firstName})}"></p>
<p th:text="#{mail.reset.text(${user.email})}"></p>
<a th:href="${resetUrl}" th:text="#{mail.reset.button}">Установить пароль</a>
<nav th:fragment="menu">
<span th:text="#{language.text}">Язык:</span>
<a href="?lang=en">EN</a>
<a href="?lang=uk">UK</a>
<a href="?lang=ru">RU</a>
</nav>
</body>
</html>
15 changes: 11 additions & 4 deletions resources/view/index.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
<!DOCTYPE html>
<html lang="ru" xmlns:th="http://www.thymeleaf.org">
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<!--/*@thymesVar id="authUser" type="com.javarush.jira.login.AuthUser"*/-->

<th:block th:replace="~{layout/main::page(title='JiraRush: mini bugtracking system',appMain=~{::appMain})}">
<th:block th:replace="~{layout/main::page(title=#{index.title},appMain=~{::appMain})}">
<appMain>
<h1>JiraRush Home page</h1>
<h1 th:text="#{index.heading}">JiraRush Home page</h1>

<div th:if="${authUser} != null">
<form action="/ui/logout" method="post">
<button class="btn btn-primary btn-lg mt-3" type="submit">Logout</button>
<button class="btn btn-primary btn-lg mt-3" type="submit" th:text="#{index.logout}">Logout</button>
</form>
</div>
<nav th:fragment="menu">
<span th:text="#{language.text}">Language:</span>
<a href="?lang=en">EN</a>
<a href="?lang=uk">UK</a>
<a href="?lang=ru">RU</a>
</nav>
</appMain>
</th:block>
16 changes: 8 additions & 8 deletions resources/view/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ <h3 class="mb-3">Sign in</h3>
type="button">
<i class="fa-brands fa-google"></i>
</a>
<a class="btn btn-primary btn-lg me-2" href="/oauth2/authorization/vk" style="padding-left: 17px; padding-right: 17px;"
type="button">
<i class="fa-brands fa-vk"></i>
</a>
<a class="btn btn-danger btn-lg me-2" href="/oauth2/authorization/yandex" style="padding-left: 21px; padding-right: 21px;"
type="button">
<i class="fa-brands fa-yandex"></i>
</a>
<!-- <a class="btn btn-primary btn-lg me-2" href="/oauth2/authorization/vk" style="padding-left: 17px; padding-right: 17px;"-->
<!-- type="button">-->
<!-- <i class="fa-brands fa-vk"></i>-->
<!-- </a>-->
<!-- <a class="btn btn-danger btn-lg me-2" href="/oauth2/authorization/yandex" style="padding-left: 21px; padding-right: 21px;"-->
<!-- type="button">-->
<!-- <i class="fa-brands fa-yandex"></i>-->
<!-- </a>-->
<a class="btn btn-dark btn-lg me-2" href="/oauth2/authorization/github" type="button">
<i class="fa-brands fa-github"></i>
</a>
Expand Down
1 change: 0 additions & 1 deletion src/main/java/com/javarush/jira/JiraRushApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
@EnableConfigurationProperties(AppProperties.class)
@EnableCaching
public class JiraRushApplication {

public static void main(String[] args) {
SpringApplication.run(JiraRushApplication.class, args);
}
Expand Down
19 changes: 10 additions & 9 deletions src/main/java/com/javarush/jira/bugtracking/Handlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,8 @@
import com.javarush.jira.bugtracking.task.ActivityRepository;
import com.javarush.jira.bugtracking.task.Task;
import com.javarush.jira.bugtracking.task.TaskRepository;
import com.javarush.jira.bugtracking.task.mapper.ActivityMapper;
import com.javarush.jira.bugtracking.task.mapper.TaskExtMapper;
import com.javarush.jira.bugtracking.task.mapper.TaskFullMapper;
import com.javarush.jira.bugtracking.task.mapper.TaskMapper;
import com.javarush.jira.bugtracking.task.to.ActivityTo;
import com.javarush.jira.bugtracking.task.to.TaskTo;
import com.javarush.jira.bugtracking.task.to.TaskToExt;
import com.javarush.jira.bugtracking.task.to.TaskToFull;
import com.javarush.jira.bugtracking.task.mapper.*;
import com.javarush.jira.bugtracking.task.to.*;
import com.javarush.jira.common.BaseHandler;
import com.javarush.jira.common.BaseMapper;
import com.javarush.jira.common.BaseRepository;
Expand Down Expand Up @@ -80,6 +74,13 @@ public TaskExtHandler(TaskRepository repository, TaskExtMapper mapper) {
}
}

@Component
public static class TaskTagsHandler extends UserBelongHandler<Task, TaskToTags, TaskRepository, TaskTagsMapper> {
public TaskTagsHandler(TaskRepository repository, TaskTagsMapper mapper) {
super(repository, mapper);
}
}

@Component
public static class ActivityHandler extends BaseHandler<Activity, ActivityTo, ActivityRepository, ActivityMapper> {
public ActivityHandler(ActivityRepository repository, ActivityMapper mapper) {
Expand Down Expand Up @@ -126,4 +127,4 @@ public void createUserBelong(long id, ObjectType type, long userId, String userT
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@
import org.springframework.core.io.UrlResource;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.*;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

@UtilityClass
public class FileUtil {
Expand All @@ -25,14 +23,24 @@ public static void upload(MultipartFile multipartFile, String directoryPath, Str
throw new IllegalRequestDataException("Select a file to upload.");
}

File dir = new File(directoryPath);
if (dir.exists() || dir.mkdirs()) {
File file = new File(directoryPath + fileName);
try (OutputStream outStream = new FileOutputStream(file)) {
outStream.write(multipartFile.getBytes());
} catch (IOException ex) {
throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename());
if (directoryPath.isBlank()) {
throw new IllegalRequestDataException("Directory path is empty");
}

if (fileName.isBlank()) {
throw new IllegalRequestDataException("File name is empty");
}

try (InputStream in = multipartFile.getInputStream()) {
Path dir = Files.createDirectories(Paths.get(directoryPath));

Path file = dir.resolve(fileName);
if (!Files.exists(file)) {
Files.createFile(file);
}
Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ex) {
throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import com.javarush.jira.bugtracking.Handlers;
import com.javarush.jira.bugtracking.task.to.ActivityTo;
import com.javarush.jira.common.error.DataConflictException;
import com.javarush.jira.common.error.NotFoundException;
import com.javarush.jira.login.AuthUser;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;

import static com.javarush.jira.bugtracking.task.TaskUtil.getLatestValue;
Expand Down Expand Up @@ -73,4 +76,55 @@ private void updateTaskIfRequired(long taskId, String activityStatus, String act
}
}
}

public Duration getTimeInWork(long taskId) {
Task task = taskRepository.getExisted(taskId);
List<Activity> activities = handler.getRepository().findAllByTaskIdOrderByUpdatedDesc(task.id());

LocalDateTime timeInProgress = getTimeTaskByStatus("in_progress", activities);
if (timeInProgress == null) {
throw new NotFoundException(
"Task with id=" + task.id() + " does not have status 'in_progress'"
);
}

LocalDateTime timeReadyForReview = getTimeTaskByStatus("ready_for_review", activities);
if (timeReadyForReview == null) {
throw new NotFoundException(
"Task with id=" + task.id() + " does not have status 'ready_for_review'"
);
}

return Duration.between(timeInProgress, timeReadyForReview);
}

public Duration getTimeInTesting(long taskId) {
Task task = taskRepository.getExisted(taskId);
List<Activity> activities = handler.getRepository().findAllByTaskIdOrderByUpdatedDesc(task.id());

LocalDateTime timeReadyForReview = getTimeTaskByStatus("ready_for_review", activities);
if (timeReadyForReview == null) {
throw new NotFoundException(
"Task with id=" + task.id() + " does not have status 'ready_for_review'"
);
}

LocalDateTime timeDone = getTimeTaskByStatus("done", activities);
if (timeDone == null) {
throw new NotFoundException(
"Task with id=" + task.id() + " does not have status 'done'"
);
}

return Duration.between(timeReadyForReview, timeDone);
}

private LocalDateTime getTimeTaskByStatus(String status, List<Activity> activities) {
for (Activity activity : activities) {
if (status.equals(activity.getStatusCode())) {
return activity.getUpdated();
}
}
return null;
}
}
Loading