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: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ docker run --name postgres_dev -d -p 5432:5432 -e POSTGRES_DB=brn -e POSTGRES_PA
```bash
docker run --name postgres_dev -d -p 5432:5432 -e POSTGRES_DB=brn -e POSTGRES_PASSWORD=admin -e POSTGRES_USER=admin postgres:13
```
if you want container start automatically on system boot you must use --restart=always option

### Back-end Kotlin Part:
1. Run command 'gradle build' from main project folder to build project with tests.
Expand Down Expand Up @@ -172,7 +173,7 @@ docker rm $(docker ps -a -q) # Remove all stopped containers
https://github.com/Brain-up/brn/wiki/Kotlin-request-dto-validation-with-annotations

### Flyway scripts naming
use `V2yearmonthday_taskNumber`
use `V2yyyymmdd_taskNumber`
for example `V220210804_899`.

### Branches:
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/com/epam/brn/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.springframework.scheduling.annotation.EnableScheduling
@SpringBootApplication
@EnableScheduling
class Application

fun main(args: Array<String>) {
SpringApplication.run(Application::class.java, *args)
}
1 change: 1 addition & 0 deletions src/main/kotlin/com/epam/brn/config/AwsConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class AwsConfig(
val baseFileUrl: String = ""

fun instant(): OffsetDateTime = Instant.now().atOffset(ZoneOffset.UTC)

fun uuid(): String = UUID.randomUUID().toString()

private lateinit var accessKeyId: String
Expand Down
10 changes: 4 additions & 6 deletions src/main/kotlin/com/epam/brn/config/SwaggerConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,16 @@ class SwaggerConfig {
fun rolesAllowedCustomizer(): OperationCustomizer? = OperationCustomizer { operation, handlerMethod ->
var allowedRoles: Array<String>? = null
var rolesAllowedAnnotation = handlerMethod.getMethodAnnotation(RolesAllowed::class.java)
if (rolesAllowedAnnotation != null) {
if (rolesAllowedAnnotation != null)
allowedRoles = rolesAllowedAnnotation.value
} else {
else
rolesAllowedAnnotation = handlerMethod.method.declaringClass.getAnnotation(RolesAllowed::class.java)
if (rolesAllowedAnnotation != null)
allowedRoles = rolesAllowedAnnotation.value
}
if (rolesAllowedAnnotation != null) allowedRoles = rolesAllowedAnnotation.value

val sb = StringBuilder("Roles: ")
if (allowedRoles != null)
sb.append("**${allowedRoles.joinToString(",")}**")
else
else
sb.append("**PUBLIC**")

operation.description?.let {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.epam.brn.config

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration

@Configuration
class UserDetailControllerConfig(
@Value("\${brn.user.analytics.use.new.version}")
val isUseNewAnalyticsService: Boolean,
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.epam.brn.controller

import com.epam.brn.config.UserDetailControllerConfig
import com.epam.brn.dto.HeadphonesDto
import com.epam.brn.dto.UserAccountDto
import com.epam.brn.dto.request.UserAccountChangeRequest
Expand All @@ -8,6 +9,7 @@ import com.epam.brn.enums.BrnRole
import com.epam.brn.service.DoctorService
import com.epam.brn.service.UserAccountService
import com.epam.brn.service.UserAnalyticsService
import com.epam.brn.service.UserAnalyticsServiceV1
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.data.domain.Pageable
Expand Down Expand Up @@ -35,7 +37,9 @@ import javax.annotation.security.RolesAllowed
class UserDetailController(
private val userAccountService: UserAccountService,
private val doctorService: DoctorService,
private val config: UserDetailControllerConfig,
private val userAnalyticsService: UserAnalyticsService,
private val userAnalyticsServiceV1: UserAnalyticsServiceV1,
) {
@GetMapping
@Operation(summary = "Get all users with/without analytic data")
Expand All @@ -47,7 +51,10 @@ class UserDetailController(
): ResponseEntity<Any> {
val users =
if (withAnalytics)
userAnalyticsService.getUsersWithAnalytics(pageable, role)
if (config.isUseNewAnalyticsService)
userAnalyticsServiceV1.getUsersWithAnalytics(pageable, role)
else
userAnalyticsService.getUsersWithAnalytics(pageable, role)
else
userAccountService.getUsers(pageable, role)
return ResponseEntity.ok().body(BrnResponse(data = users))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ data class UserWithAnalyticsResponse(
var active: Boolean = true,
var firstDone: LocalDateTime? = null, // generally first done exercise
var lastDone: LocalDateTime? = null, // generally last done exercise
var lastWeek: List<DayStudyStatistics> = emptyList(),
var lastWeek: MutableList<DayStudyStatistics> = arrayListOf(),
var studyDaysInCurrentMonth: Int = 0, // amount of days in current month when user made any exercises
var diagnosticProgress: Map<AudiometryType, Boolean> = mapOf(AudiometryType.SIGNALS to true), // todo fill by user
var doneExercises: Int = 0, // for all time
Expand Down
51 changes: 51 additions & 0 deletions src/main/kotlin/com/epam/brn/job/UserAnalyticsJob.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.epam.brn.job

import org.apache.logging.log4j.kotlin.logger
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
@ConditionalOnProperty(name = ["brn.user.analytics.job.enabled"], havingValue = "true")
class UserAnalyticsJob(
private val jdbcTemplate: JdbcTemplate,
) {
private val log = logger()

@Scheduled(cron = "@midnight")
@Transactional
fun fillUserAnalytics() {
try {
log.info("start filling study analytics table...")
val rowsCount = jdbcTemplate.update(FILL_USER_ANALYTICS_SQL)
log.info("filling study analytics table was finished successfully. total $rowsCount rows inserted")
} catch (e: Exception) {
log.error("Some error occurred on fill statistics tables: ${e.message}", e)
}
}
}

private const val FILL_USER_ANALYTICS_SQL: String = """
TRUNCATE TABLE user_analytics CASCADE;
INSERT INTO user_analytics (user_id, first_done, last_done, spent_time,
done_exercises, study_days, role_name)
SELECT s.user_id,
min(start_time),
max(start_time),
coalesce(sum(spent_time_in_seconds), 0),
count(distinct exercise_id),
(SELECT distinct count(distinct date_trunc('day', s1.start_time))
FROM study_history s1
WHERE s1.user_id = s.user_id
AND s1.start_time between date_trunc('month', current_date)
AND current_date),
r.name
FROM study_history s,
user_roles ur,
role r
WHERE s.user_id = ur.user_id
AND ur.role_id = r.id
GROUP BY s.user_id, r.name;
"""
47 changes: 47 additions & 0 deletions src/main/kotlin/com/epam/brn/model/UserAnalytics.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.epam.brn.model

import java.time.LocalDateTime
import javax.persistence.Column
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Index
import javax.persistence.Entity
import javax.persistence.Table
import javax.persistence.Id

@Entity
@Table(indexes = [Index(name = "user_analytics_ix_role_name", columnList = "role_name")])
class UserAnalytics(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val userId: Long,
val firstDone: LocalDateTime?,
val lastDone: LocalDateTime?,
val spentTime: Long?,
val doneExercises: Int?,
val studyDays: Int?,
@Column(name = "role_name")
val roleName: String,
) {
override fun toString(): String =
"UserAnalytics(id=$id, userId=$userId, firstDone=$firstDone, lastDone=$lastDone, spentTime=$spentTime, doneExercises=$doneExercises, studyDays=$studyDays, roleName='$roleName')"

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as UserAnalytics

if (id != other.id) return false
if (userId != other.userId) return false

return true
}

override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + userId.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.epam.brn.model.projection

import com.epam.brn.enums.BrnGender
import java.time.LocalDateTime

/**
* StudyAnalyticsView.
*
* @author Andrey Samoylov
*/
interface UsersWithAnalyticsView {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have 'dto' package for such dto meanings
lets be in one style and rename it UsersWithAnalyticsDto and move it there. ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no. UsersWithAnalyticsView - is a projection and located in projections package

val id: Long
val userId: String
val fullName: String
val email: String
val bornYear: Int?
val gender: BrnGender
var active: Boolean
val firstDone: LocalDateTime
val lastDone: LocalDateTime
var lastVisit: LocalDateTime
val doneExercises: Int
val spentTime: Long
val studyDays: Int
}
34 changes: 34 additions & 0 deletions src/main/kotlin/com/epam/brn/repo/UserAnalyticsRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.epam.brn.repo

import com.epam.brn.model.UserAnalytics
import com.epam.brn.model.projection.UsersWithAnalyticsView
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query

interface UserAnalyticsRepository : JpaRepository<UserAnalytics, Long> {
@Query(
"""
select u.id as id,
u.userId as userId,
u.fullName as fullName,
u.email as email,
u.bornYear as bornYear,
u.gender as gender,
u.active as active,
u.lastVisit as lastVisit,
a.firstDone as firstDone,
a.lastDone as lastDone,
a.doneExercises as doneExercises,
a.spentTime as spentTime,
a.studyDays as studyDays
from UserAnalytics a
join UserAccount u on a.userId = u.id
where a.roleName=:roleName
""",
)
fun getUserAnalytics(
pageable: Pageable,
roleName: String,
): List<UsersWithAnalyticsView>
}
3 changes: 2 additions & 1 deletion src/main/kotlin/com/epam/brn/service/TaskService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ class TaskService(
private fun processAnswerOptions(task: Task) {
task.answerOptions
.forEach { resource ->
if (!resource.pictureFileUrl.isNullOrEmpty())
if (!resource.pictureFileUrl.isNullOrEmpty()) {
resource.pictureFileUrl = cloudService.baseFileUrl() + "/" + resource.pictureFileUrl
}
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/main/kotlin/com/epam/brn/service/UserAccountService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,57 @@ import org.springframework.data.domain.Pageable

interface UserAccountService {
fun findUserByEmail(email: String): UserAccountDto

fun createUser(firebaseUserRecord: UserRecord): UserAccountDto

fun getCurrentUser(): UserAccount

fun findUserById(id: Long): UserAccount

fun getCurrentUserId(): Long

fun getCurrentUserRoles(): Set<String>

fun getCurrentUserDto(): UserAccountDto

fun findUserDtoById(id: Long): UserAccountDto

fun findUserDtoByUuid(uuid: String): UserAccountDto?

fun getUsers(
pageable: Pageable,
role: String,
): List<UserAccountDto>

fun updateAvatarForCurrentUser(avatarUrl: String): UserAccountDto

fun updateCurrentUser(userChangeRequest: UserAccountChangeRequest): UserAccountDto

fun addHeadphonesToUser(
userId: Long,
headphonesDto: HeadphonesDto,
): HeadphonesDto

fun addHeadphonesToCurrentUser(headphones: HeadphonesDto): HeadphonesDto

fun deleteHeadphonesForCurrentUser(headphonesId: Long)

fun getAllHeadphonesForUser(userId: Long): Set<HeadphonesDto>

fun getAllHeadphonesForCurrentUser(): Set<HeadphonesDto>

fun updateDoctorForPatient(
userId: Long,
doctorId: Long,
): UserAccount

fun removeDoctorFromPatient(userId: Long): UserAccount

fun getPatientsForDoctor(doctorId: Long): List<UserAccountDto>

fun markVisitForCurrentUser()

fun deleteAutoTestUsers(): Long

fun deleteAutoTestUserByEmail(email: String): Long
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ interface UserAnalyticsService {
pageable: Pageable,
role: String,
): List<UserWithAnalyticsResponse>

fun prepareAudioStreamForUser(
exerciseId: Long,
audioFileMetaData: AudioFileMetaData,
): InputStream

fun prepareAudioFileMetaData(
exerciseId: Long,
audioFileMetaData: AudioFileMetaData,
Expand Down
11 changes: 11 additions & 0 deletions src/main/kotlin/com/epam/brn/service/UserAnalyticsServiceV1.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.epam.brn.service

import com.epam.brn.dto.response.UserWithAnalyticsResponse
import org.springframework.data.domain.Pageable

interface UserAnalyticsServiceV1 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it is reasonable to design one interface for both UserAnalyticsServices?
and chose the realization on start application step.?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserAnalyticsServiceV1 - temporary solution, switched using brn.user.analytics.use.new.version toggle in application.properties

fun getUsersWithAnalytics(
pageable: Pageable,
role: String,
): List<UserWithAnalyticsResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,18 @@ class UserAnalyticsServiceImpl(
val startOfCurrentMonth = now.withDayOfMonth(1).with(LocalTime.MIN)

users.onEach { user ->
user.lastWeek = userDayStatisticsService.getStatisticsForPeriod(from, to, user.id)
user.lastWeek = userDayStatisticsService.getStatisticsForPeriod(from, to, user.id).toMutableList()
user.studyDaysInCurrentMonth =
countWorkDaysForMonth(
userDayStatisticsService.getStatisticsForPeriod(startOfCurrentMonth, now, user.id),
)

val userStatistic = studyHistoryRepository.getStatisticsByUserAccountId(user.id)
val userStatistics = studyHistoryRepository.getStatisticsByUserAccountId(user.id)
user.apply {
this.firstDone = userStatistic.firstStudy
this.lastDone = userStatistic.lastStudy
this.spentTime = userStatistic.spentTime.toDuration(DurationUnit.SECONDS)
this.doneExercises = userStatistic.doneExercises
this.firstDone = userStatistics.firstStudy
this.lastDone = userStatistics.lastStudy
this.spentTime = userStatistics.spentTime.toDuration(DurationUnit.SECONDS)
this.doneExercises = userStatistics.doneExercises
}
}
return users
Expand Down
Loading
Loading