diff --git a/.gitignore b/.gitignore
index cd38e2e7b..7f9709787 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,6 @@ target
logs
attachments
*.patch
+.env
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..507a3b56d
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,9 @@
+FROM eclipse-temurin:17-jdk
+
+WORKDIR /app
+
+COPY target/*.jar app.jar
+
+EXPOSE 8080
+
+ENTRYPOINT ["java", "-jar", "app.jar"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 719b268f5..4d2411b15 100644
--- a/README.md
+++ b/README.md
@@ -27,4 +27,11 @@
- https://habr.com/ru/articles/259055/
Список выполненных задач:
-...
\ No newline at end of file
+
+1. Розібратися зі структурою проєкту (onboarding).
+2. Видалити соціальні мережі: vk, yandex. Easy task
+3. Винести чутливу інформацію до окремого проперті файлу.
+6. Зробити рефакторинг методу com.javarush.jira.bugtracking.attachment.FileUtil#upload.
+7. Додати новий функціонал: додавання тегів до завдання.
+8. Додати підрахунок часу: скільки завдання перебувало у роботі та тестуванні.
+9. Написати Dockerfile для основного сервера
diff --git a/pom.xml b/pom.xml
index 4556ac639..c555db575 100644
--- a/pom.xml
+++ b/pom.xml
@@ -152,6 +152,12 @@
true
+
+ me.paulschwarz
+ spring-dotenv
+ 4.0.0
+
+
diff --git a/resources/static/fontawesome/css/all.css b/resources/static/fontawesome/css/all.css
index af5980828..f71679f4c 100644
--- a/resources/static/fontawesome/css/all.css
+++ b/resources/static/fontawesome/css/all.css
@@ -8603,9 +8603,6 @@ readers do not read off random characters that represent icons */
content: "\f3e8";
}
-.fa-vk:before {
- content: "\f189";
-}
.fa-untappd:before {
content: "\f405";
@@ -9955,10 +9952,6 @@ readers do not read off random characters that represent icons */
content: "\f3bc";
}
-.fa-yandex:before {
- content: "\f413";
-}
-
.fa-readme:before {
content: "\f4d5";
}
@@ -10183,9 +10176,6 @@ readers do not read off random characters that represent icons */
content: "\f7c6";
}
-.fa-yandex-international:before {
- content: "\f414";
-}
.fa-cc-amex:before {
content: "\f1f3";
diff --git a/resources/view/login.html b/resources/view/login.html
index 8765ca8ff..8570fad46 100644
--- a/resources/view/login.html
+++ b/resources/view/login.html
@@ -48,14 +48,7 @@ Sign in
type="button">
-
-
-
-
-
-
+
diff --git a/resources/view/unauth/register.html b/resources/view/unauth/register.html
index 2ba955045..52a892bd3 100644
--- a/resources/view/unauth/register.html
+++ b/resources/view/unauth/register.html
@@ -77,14 +77,6 @@ Registration
type="button">
-
-
-
-
-
-
diff --git a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java
index 6cffbe175..083de0c79 100644
--- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java
+++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java
@@ -7,10 +7,7 @@
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.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -24,15 +21,14 @@ public static void upload(MultipartFile multipartFile, String directoryPath, Str
if (multipartFile.isEmpty()) {
throw new IllegalRequestDataException("Select a file to upload.");
}
+ try {
+ Path dirPath = Paths.get(directoryPath);
+ Files.createDirectories(dirPath);
- 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());
- }
+ Path filePath = dirPath.resolve(fileName);
+ Files.write(filePath, multipartFile.getBytes());
+ } catch (IOException ex) {
+ throw new IllegalRequestDataException("Failed to upload file " + multipartFile.getOriginalFilename());
}
}
diff --git a/src/main/java/com/javarush/jira/bugtracking/task/ActivityRepository.java b/src/main/java/com/javarush/jira/bugtracking/task/ActivityRepository.java
index 3ce8a9386..ee073e636 100644
--- a/src/main/java/com/javarush/jira/bugtracking/task/ActivityRepository.java
+++ b/src/main/java/com/javarush/jira/bugtracking/task/ActivityRepository.java
@@ -5,6 +5,7 @@
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
+import java.util.Optional;
@Transactional(readOnly = true)
public interface ActivityRepository extends BaseRepository {
@@ -13,4 +14,6 @@ public interface ActivityRepository extends BaseRepository {
@Query("SELECT a FROM Activity a JOIN FETCH a.author WHERE a.taskId =:taskId AND a.comment IS NOT NULL ORDER BY a.updated DESC")
List findAllComments(long taskId);
+
+ Optional findFirstByTaskIdAndStatusCodeOrderByUpdatedAsc (long taskId, String statusCode);
}
diff --git a/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java b/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java
index 7938541bb..4fb739293 100644
--- a/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java
+++ b/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java
@@ -8,6 +8,8 @@
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;
@@ -19,6 +21,10 @@ public class ActivityService {
private final Handlers.ActivityHandler handler;
+ public static final String IN_PROGRESS = "in_progress";
+ public static final String READY_FOR_REVIEW = "ready_for_review";
+ public static final String DONE = "done";
+
private static void checkBelong(HasAuthorId activity) {
if (activity.getAuthorId() != AuthUser.authId()) {
throw new DataConflictException("Activity " + activity.getId() + " doesn't belong to " + AuthUser.get());
@@ -73,4 +79,31 @@ private void updateTaskIfRequired(long taskId, String activityStatus, String act
}
}
}
+
+ public Duration getProgressDuration(Task task) {
+ LocalDateTime readyForReview = getActivityUpdatedTime(task.id(), READY_FOR_REVIEW);
+ LocalDateTime inProgress = getActivityUpdatedTime(task.id(), IN_PROGRESS);
+
+ if (readyForReview == null || inProgress == null) {
+ return Duration.ZERO;
+ }
+ return Duration.between(inProgress, readyForReview);
+ }
+
+ public Duration getTestingDuration(Task task) {
+ LocalDateTime done = getActivityUpdatedTime(task.id(), DONE);
+ LocalDateTime readyForReview = getActivityUpdatedTime(task.id(), READY_FOR_REVIEW);
+
+ if (readyForReview == null || done == null) {
+ return Duration.ZERO;
+ }
+ return Duration.between(readyForReview, done);
+ }
+
+ public LocalDateTime getActivityUpdatedTime(long taskId, String statusCode) {
+ return handler.getRepository()
+ .findFirstByTaskIdAndStatusCodeOrderByUpdatedAsc(taskId, statusCode)
+ .map(Activity::getUpdated)
+ .orElse(null);
+ }
}
diff --git a/src/main/java/com/javarush/jira/bugtracking/task/Task.java b/src/main/java/com/javarush/jira/bugtracking/task/Task.java
index 6c9f4d96e..29d85b727 100644
--- a/src/main/java/com/javarush/jira/bugtracking/task/Task.java
+++ b/src/main/java/com/javarush/jira/bugtracking/task/Task.java
@@ -14,6 +14,7 @@
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -68,7 +69,7 @@ public class Task extends TitleEntity implements HasCode {
@ElementCollection(fetch = FetchType.LAZY)
@JoinColumn()
@OnDelete(action = OnDeleteAction.CASCADE)
- private Set<@Size(min = 2, max = 32) String> tags = Set.of();
+ private Set<@Size(min = 2, max = 32) String> tags = new HashSet<>();
// history of comments and task fields changing
@OneToMany(mappedBy = "taskId", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java
index b53f7ff37..4fa2523af 100644
--- a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java
+++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java
@@ -156,4 +156,10 @@ public TaskTreeNode(TaskTo taskTo) {
this(taskTo, new LinkedList<>());
}
}
+
+ @PostMapping("/{id}/tag")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void addTag(@PathVariable long id, @RequestBody String tag) {
+ taskService.addTag(id, tag);
+ }
}
diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java
index e6f385548..945b01702 100644
--- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java
+++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java
@@ -10,6 +10,7 @@
import com.javarush.jira.bugtracking.task.to.TaskToExt;
import com.javarush.jira.bugtracking.task.to.TaskToFull;
import com.javarush.jira.common.error.DataConflictException;
+import com.javarush.jira.common.error.IllegalRequestDataException;
import com.javarush.jira.common.error.NotFoundException;
import com.javarush.jira.common.util.Util;
import com.javarush.jira.login.AuthUser;
@@ -140,4 +141,20 @@ private void checkAssignmentActionPossible(long id, String userType, boolean ass
throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode()));
}
}
+
+ @Transactional
+ public void addTag(long id, String tag) {
+ if (tag == null || tag.isEmpty()) {
+ throw new IllegalRequestDataException("Tag must not be null or empty");
+ }
+ if (tag.length() > 32) {
+ throw new IllegalRequestDataException("Tag must not exceed 32 characters");
+ }
+ Task task = handler.getRepository().getExisted(id);
+ if (task.getTags().contains(tag)) {
+ throw new IllegalRequestDataException("Tag already exists for this task");
+ }
+ task.getTags().add(tag);
+ handler.getRepository().saveAndFlush(task);
+ }
}
diff --git a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java b/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java
deleted file mode 100644
index e8e05be05..000000000
--- a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.javarush.jira.login.internal.sociallogin.handler;
-
-import org.springframework.stereotype.Component;
-
-import java.util.List;
-import java.util.Map;
-
-@Component("vk")
-public class VkOAuth2UserDataHandler implements OAuth2UserDataHandler {
- @Override
- public String getFirstName(OAuth2UserData oAuth2UserData) {
- return getAttribute(oAuth2UserData, "first_name");
- }
-
- @Override
- public String getLastName(OAuth2UserData oAuth2UserData) {
- return getAttribute(oAuth2UserData, "last_name");
- }
-
- @Override
- public String getEmail(OAuth2UserData oAuth2UserData) {
- return oAuth2UserData.getData("email");
- }
-
- private String getAttribute(OAuth2UserData oAuth2UserData, String name) {
- List