Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ attachments
*.patch


/src/main/resources/application-secrets.properties
/src/test/resources/application-secrets-test.properties
/secrets.env
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM openjdk:17 AS build
WORKDIR /workspace/app

COPY target/jira-1.0.jar app-jira.jar
COPY resources ./resources

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app-jira.jar"]
42 changes: 12 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
## [REST API](http://localhost:8080/doc)

## Концепция:

- Spring Modulith
- [Spring Modulith: достигли ли мы зрелости модульности](https://habr.com/ru/post/701984/)
- [Introducing Spring Modulith](https://spring.io/blog/2022/10/21/introducing-spring-modulith)
- [Spring Modulith - Reference documentation](https://docs.spring.io/spring-modulith/docs/current-SNAPSHOT/reference/html/)

```
url: jdbc:postgresql://localhost:5432/jira
username: jira
password: JiraRush
```

- Есть 2 общие таблицы, на которых не fk
- _Reference_ - справочник. Связь делаем по _code_ (по id нельзя, тк id привязано к окружению-конкретной базе)
- _UserBelong_ - привязка юзеров с типом (owner, lead, ...) к объекту (таска, проект, спринт, ...). FK вручную будем
проверять

## Аналоги

- https://java-source.net/open-source/issue-trackers

## Тестирование

- https://habr.com/ru/articles/259055/

Список выполненных задач:
...
List of completed tasks:

1. Familiarized with the project structure (onboarding).
2. Removed social networks: VK and Yandex.
3. Moved sensitive information to a separate properties file.
4. Refactored tests to use an in-memory database (H2) instead of PostgreSQL during testing.
5. Wrote tests for all public methods of the `ProfileRestController`.
6. Refactored the method `com.javarush.jira.bugtracking.attachment.FileUtil#upload` to use a modern approach for working with the file system.
7. Added new functionality: tagging tasks (REST API + service implementation).
8. Implemented time tracking: calculated how long a task remained in the "In Progress" and "Testing" states.
9. Created a Dockerfile for the main server.
10. Created a docker-compose file to run the server container alongside the database and NGINX.
2 changes: 1 addition & 1 deletion config/_application-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ app:
host-url: http://localhost
spring:
datasource:
url: jdbc:postgresql://localhost:5432/jira
url: jdbc:postgresql://localhost:5433/jira
username: jira
password: JiraRush

92 changes: 52 additions & 40 deletions config/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,40 +1,52 @@
# https://losst.ru/ustanovka-nginx-ubuntu-16-04
# https://pai-bx.com/wiki/nginx/2332-useful-redirects-in-nginx/#1
# sudo iptables -A INPUT ! -s 127.0.0.1 -p tcp -m tcp --dport 8080 -j DROP
server {
listen 80;

# https://www.digitalocean.com/community/tutorials/how-to-optimize-nginx-configuration
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 2048;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
root /opt/jirarush/resources;

if ($request_uri ~ ';') {return 404;}

# proxy_cookie_flags ~ secure samesite=none;

# static
location /static/ {
expires 30d;
access_log off;
}
location /robots.txt {
access_log off;
}

location ~ (/$|/view/|/ui/|/oauth2/) {
expires 0m;
proxy_pass http://localhost:8080;
proxy_connect_timeout 30s;
}
location ~ (/api/|/doc|/swagger-ui/|/v3/api-docs/) {
proxy_pass http://localhost:8080;
proxy_connect_timeout 150s;
}
location / {
try_files /view/404.html = 404;
}
}
# Основний блок налаштувань
user nginx;
worker_processes 1;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

# Ваш серверний блок
server {
listen 80;

# https://www.digitalocean.com/community/tutorials/how-to-optimize-nginx-configuration
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 2048;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
root /opt/jirarush/resources;

if ($request_uri ~ ';') { return 404; }

# Статичні файли
location /static/ {
expires 30d;
access_log off;
}

location /robots.txt {
access_log off;
}

location ~ (/$|/view/|/ui/|/oauth2/) {
expires 0m;
proxy_pass http://localhost:8080;
proxy_connect_timeout 30s;
}

location ~ (/api/|/doc|/swagger-ui/|/v3/api-docs/) {
proxy_pass http://localhost:8080;
proxy_connect_timeout 150s;
}

location / {
try_files /view/404.html = 404;
}
}
}
47 changes: 47 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
services:
db:
image: postgres
container_name: postgres
env_file:
- ./secrets.env
ports:
- "5433:5432"
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- app_network

app:
build:
context: .
dockerfile: Dockerfile
container_name: app
env_file:
- ./secrets.env
ports:
- "8080:8080"
depends_on:
- db
networks:
- app_network

nginx:
image: nginx:latest
container_name: nginx
volumes:
- ./config/nginx.conf:/etc/nginx/nginx.conf
- ./resources/static:/opt/jirarush/resources/static
ports:
- "80:80"
depends_on:
- app
networks:
- app_network

networks:
app_network:
driver: bridge

volumes:
pgdata:
driver: local
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>

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

<build>
Expand Down
8 changes: 0 additions & 8 deletions resources/static/fontawesome/css/all.css
Original file line number Diff line number Diff line change
Expand Up @@ -9955,10 +9955,6 @@ readers do not read off random characters that represent icons */
content: "\f3bc";
}

.fa-yandex:before {
content: "\f413";
}

.fa-readme:before {
content: "\f4d5";
}
Expand Down Expand Up @@ -10183,10 +10179,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";
}
Expand Down
2 changes: 1 addition & 1 deletion resources/static/fontawesome/css/all.min.css

Large diffs are not rendered by default.

8 changes: 0 additions & 8 deletions resources/view/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,6 @@ <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-dark btn-lg me-2" href="/oauth2/authorization/github" type="button">
<i class="fa-brands fa-github"></i>
</a>
Expand Down
8 changes: 0 additions & 8 deletions resources/view/unauth/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,6 @@ <h3 class="mb-3">Registration</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-dark btn-lg me-2" href="/oauth2/authorization/github" type="button">
<i class="fa-brands fa-github"></i>
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,14 +22,15 @@ 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());
}
Path path = Path.of(directoryPath);
try {
Files.createDirectories(path);

Path filePath = path.resolve(fileName);

Files.write(filePath, multipartFile.getBytes());
} catch (IOException ex) {
throw new IllegalRequestDataException("Failed to upload file: " + multipartFile.getOriginalFilename());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.javarush.jira.bugtracking.task;

import com.javarush.jira.common.BaseRepository;
import jakarta.validation.constraints.Size;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Transactional(readOnly = true)
public interface ActivityRepository extends BaseRepository<Activity> {
Expand All @@ -13,4 +15,6 @@ public interface ActivityRepository extends BaseRepository<Activity> {

@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<Activity> findAllComments(long taskId);

Optional<Activity> findActivityByTaskIdAndStatusCode(long taskId, @Size(min = 2, max = 32) String statusCode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());
Expand Down Expand Up @@ -73,4 +79,33 @@ private void updateTaskIfRequired(long taskId, String activityStatus, String act
}
}
}

public Duration progressTaskDuration(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 testingTaskDuration(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(done, readyForReview);
}

private LocalDateTime getActivityUpdatedTime(long taskId, String statusCode) {
return handler.getRepository()
.findActivityByTaskIdAndStatusCode(taskId, statusCode)
.map(Activity::getUpdated)
.orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading