Skip to content

Commit

Permalink
Adds Todos, so far only manual.
Browse files Browse the repository at this point in the history
  • Loading branch information
vega113 committed Nov 18, 2023
1 parent 4597b1c commit f656d58
Show file tree
Hide file tree
Showing 51 changed files with 1,453 additions and 164 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/qodana_code_quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Qodana
on:
workflow_dispatch:
pull_request:
push:
branches:
- master
- feature/add-todo-list

jobs:
qodana:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2023.2
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
3 changes: 2 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@
- [X] Change emotion intensity to have 5 steps, like: low, moderate, high... and replace the color cue with words and a tooltip that will explain in details the intensity definitions.
- [X] Idea for a chart - evolution of emotions over time: negative/neutral/positive as there can be more than one emotion per day. this will provide more detailed view on what happened over time.
- [ ] Allow emotions to be publicly shared
- [ ] allow to create an emotion for another person
- [ ] allow to create an emotion for another person
- [ ] Fix the future handling of the database execution, need to wrap with Future instead of returning the result wrapped into immediate future
3 changes: 1 addition & 2 deletions app/Module.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import auth.{JwtService, JwtServiceImpl}
import com.google.inject.AbstractModule
import dao.{DatabaseExecutionContext, DatabaseExecutionContextImpl}
import liquibase.LiquibaseRunner
import play.api.libs.concurrent.AkkaGuiceSupport

class Module extends AbstractModule with AkkaGuiceSupport {
override def configure(): Unit = {
bind(classOf[LiquibaseRunner]).asEagerSingleton()
bind(classOf[ShutdownHook]).asEagerSingleton()
}
}
24 changes: 24 additions & 0 deletions app/ShutdownHook.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import org.slf4j.Logger

import scala.concurrent.Future
import play.api.inject.ApplicationLifecycle

import javax.inject._

class ShutdownHook @Inject()(lifecycle: ApplicationLifecycle) {
val logger: Logger = org.slf4j.LoggerFactory.getLogger("ShutdownHook")

lifecycle.addStopHook { () =>
logger.info("ShutdownHook is running")
val threadMXBean = java.lang.management.ManagementFactory.getThreadMXBean
val threadInfos = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds, 100)
val stackTrace = threadInfos.map(threadInfo => {
s"""
'${threadInfo.getThreadName}': ${threadInfo.getThreadState}
at ${threadInfo.getStackTrace.mkString("\n at ")}
"""
}).mkString("\n")
logger.info(s"Thread dump:\n$stackTrace")
Future.successful(())
}
}
2 changes: 1 addition & 1 deletion app/auth/JwtService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class JwtServiceImpl @Inject()(dateTimeService: DateTimeService) extends JwtServ
} match {
case Success(user) => Some(user)
case Failure(failure) =>
logger.warn(s"Error validating token: ${token}", failure.getMessage)
logger.warn(s"Error validating token: $token, message: {}", failure.getMessage)
None
}
} match {
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/EmotionRecordController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class EmotionRecordController @Inject()(cc: ControllerComponents,
map(emotionRecordService.generateLineChartTrendDataSetForEmotionTypesTriggers).
map(emotionRecords => Ok(Json.toJson(emotionRecords)))
case Failure(_) =>
logger.info(s"failed to parse date for findRecordsByDayByUserIdForMonth monthStart: ${monthStart}, monthEnd: $monthEnd")
logger.info(s"failed to parse date for findRecordsByDayByUserIdForMonth monthStart: $monthStart, monthEnd: $monthEnd")
Future.successful(BadRequest(Json.obj("message" -> "Invalid date format")))
}
}
Expand Down Expand Up @@ -189,7 +189,7 @@ class EmotionRecordController @Inject()(cc: ControllerComponents,
tagData => {
logger.info("adding tag: " + tagData.tagName)
emotionRecordService.findByIdForUser(tagData.emotionRecordId, token.user.userId).flatMap {
case Some(_) => tagService.add(tagData.emotionRecordId, tagData.tagName).map {
case Some(_) => tagService.insert(tagData.emotionRecordId, tagData.tagName).map {
case true => Ok
case false => BadRequest(Json.obj("message" -> s"Invalid tag name: ${tagData.tagName}"))
}
Expand Down
24 changes: 20 additions & 4 deletions app/controllers/NoteController.scala
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
package controllers

import auth.AuthenticatedAction
import dao.model.{EmotionDetectionResult, EmotionFromNoteResult, EmotionRecord, Note, Trigger}
import liquibase.LiquibaseRunner
import dao.model.Note
import org.slf4j.{Logger, LoggerFactory}
import play.api.libs.json.{JsError, JsValue, Json}
import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents}
import service.{EmotionDetectionService, EmotionRecordService, NoteService, TagService}
import service.{EmotionDetectionService, EmotionRecordService, NoteService, NoteTodoService}

import javax.inject.Inject
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import net.logstash.logback.argument.StructuredArguments._

import scala.util.Success


class NoteController @Inject()(cc: ControllerComponents,
noteService: NoteService,
tagService: TagService,
emotionRecordService: EmotionRecordService,
emotionDetectionService: EmotionDetectionService,
noteTodoService: NoteTodoService,
authenticatedAction: AuthenticatedAction)
extends AbstractController(cc){

Expand Down Expand Up @@ -50,6 +53,7 @@ class NoteController @Inject()(cc: ControllerComponents,
}

def detectEmotion(): Action[JsValue] = Action(parse.json) andThen authenticatedAction async { implicit token =>
logger.info("Detecting emotion")
token.body.validate[Note].fold(
errors => {
Future.successful(BadRequest(Json.obj("message" -> JsError.toJson(errors))))
Expand All @@ -60,4 +64,16 @@ class NoteController @Inject()(cc: ControllerComponents,
}
})
}

def acceptTodo(noteTodoId: Long):Action[AnyContent] =
Action andThen authenticatedAction async { implicit token =>
noteTodoService.acceptNoteTodo(token.user.userId, noteTodoId).flatMap {
case true =>
logger.info("Accepted note todo {}", value("noteTodoId", noteTodoId))
Future.successful(Ok)
case false =>
logger.error("Failed to accept note todo {}", value("noteTodoId", noteTodoId))
Future.successful(BadRequest(Json.obj("message" -> s"Invalid note todo id: $noteTodoId")))
}
}
}
49 changes: 49 additions & 0 deletions app/controllers/UserTodoController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package controllers

import auth.AuthenticatedAction
import org.slf4j.{Logger, LoggerFactory}
import play.api.libs.json.Json
import play.api.mvc.{Action, AnyContent, ControllerComponents}
import service.UserTodoService

import javax.inject.Inject
import scala.concurrent.ExecutionContext.Implicits.global

class UserTodoController @Inject()(cc: ControllerComponents, userTodoService: UserTodoService,
authenticatedAction: AuthenticatedAction)
extends EmoBaseController(cc, authenticatedAction) {

private val logger: Logger = LoggerFactory.getLogger(classOf[UserTodoController])

def fetchUserTodos(page: Int, size: Int): Action[AnyContent] = { //TODO: Add pagination
authenticatedActionWithUser { implicit token =>
logger.info("Fetching user todos")
userTodoService.fetchByUserId(token.userId).map(userTodos => Ok(Json.toJson(userTodos)))
}
}

def complete(userTodoId: Long, isDone: Boolean): Action[AnyContent] = {
authenticatedActionWithUser { implicit token =>
if (isDone) {
logger.info("Completing user todo {}", Map("userTodoId" -> userTodoId))
userTodoService.complete(token.userId, userTodoId).map(userTodos => Ok(Json.toJson(userTodos)))
} else {
logger.info("Uncompleting user todo {}", Map("userTodoId" -> userTodoId))
userTodoService.uncomplete(token.userId, userTodoId).map(userTodos => Ok(Json.toJson(userTodos)))
}

}
}

def archive(userTodoId: Long, isArchived: Boolean): Action[AnyContent] = {
authenticatedActionWithUser { implicit token =>
if (isArchived) {
logger.info("Archiving user todo {}", Map("userTodoId" -> userTodoId))
userTodoService.archive(token.userId, userTodoId).map(userTodos => Ok(Json.toJson(userTodos)))
} else {
logger.info("Unarchiving user todo {}", Map("userTodoId" -> userTodoId))
userTodoService.unarchive(token.userId, userTodoId).map(userTodos => Ok(Json.toJson(userTodos)))
}
}
}
}
16 changes: 15 additions & 1 deletion app/dao/EmotionRecordDao.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ class EmotionRecordDao @Inject()(emotionRecordSubEmotionDao: EmotionRecordSubEmo
idOpt
}

def insert2(emotionRecord: EmotionRecord)(implicit connection: Connection): Option[Long] = {
val idOpt: Option[Long] = SQL(
"""
INSERT INTO emotion_records (emotion_type, emotion_id, user_id, intensity, created)
VALUES ({emotionType}, {emotionId}, {userId}, {intensity}, {created})
""").on("userId" -> emotionRecord.userId.getOrElse(throw new RuntimeException("User id is required.")),
"emotionType" -> emotionRecord.emotionType,
"emotionId" -> emotionRecord.emotion.flatMap(_.id),
"intensity" -> emotionRecord.intensity,
"created" -> emotionRecord.created)
.executeInsert()
idOpt
}

private def insertSubLists(emotionRecord: EmotionRecord, id: Long)(implicit connection: Connection) = {
for {
subEmotion <- emotionRecord.subEmotions
Expand Down Expand Up @@ -108,7 +122,7 @@ class EmotionRecordDao @Inject()(emotionRecordSubEmotionDao: EmotionRecordSubEmo
} yield {
noteDao.insert(id, note)
}
tagDao.insert(id, emotionRecord.tags)
tagDao.insert(id, emotionRecord.tags.toSet)
}

def update(emotionRecord: EmotionRecord)(implicit connection: Connection): Int = {
Expand Down
14 changes: 12 additions & 2 deletions app/dao/NoteDao.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@ import service.DateTimeService
import java.sql.Connection
import javax.inject.Inject

class NoteDao @Inject()(dateTimeService: DateTimeService) {
class NoteDao @Inject()(dateTimeService: DateTimeService, noteTodoDao: NoteTodoDao) {
def findAllByEmotionRecordId(id: Long)(implicit connection: Connection): List[Note] = {
SQL("SELECT * FROM notes inner join emotion_record_notes on id = note_id WHERE emotion_record_id = {id}").on("id" -> id).as(Note.parser.*)
}

def findAllNotDeletedByEmotionRecordId(id: Long)(implicit connection: Connection): List[Note] = {
SQL("SELECT * FROM notes inner join emotion_record_notes on id = note_id WHERE emotion_record_id = {id} and is_deleted = false").on("id" -> id).as(Note.parser.*)
val notes: List[Note] =
SQL("SELECT * FROM notes inner join emotion_record_notes on id = note_id" +
" WHERE emotion_record_id = {id} and is_deleted = false").on("id" -> id).as(Note.parser.*)
for {
note <- notes
noteId <- note.id
} yield {
note.copy(
todos = noteTodoDao.findNoteTodosByNoteId(noteId)
)
}
}


Expand Down
84 changes: 84 additions & 0 deletions app/dao/NoteTodoDao.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package dao

import anorm.{SQL, SqlParser}
import dao.model.{Note, NoteTodo}
import service.DateTimeService

import java.sql.Connection
import javax.inject.Inject

class NoteTodoDao @Inject()() {
def findById(id: Long)(implicit connection: Connection): Option[NoteTodo] = {
SQL("SELECT * FROM note_todos WHERE id = {id}").on("id" -> id).as(NoteTodo.parser.singleOpt)
}

def verifyNoteTodoBelongsToUser(userId: Long, noteTodoId: Long)(implicit connection: Connection): Boolean = {
SQL(
"""
SELECT emotion_records.user_id
FROM note_todos
INNER JOIN note_note_todos ON note_todos.id = note_note_todos.note_todo_id
INNER JOIN notes ON note_note_todos.note_id = notes.id
INNER JOIN emotion_record_notes ON notes.id = emotion_record_notes.note_id
INNER JOIN emotion_records ON emotion_record_notes.emotion_record_id = emotion_records.id
WHERE note_todos.id = {noteTodoId} AND emotion_records.user_id = {userId}
""").on(
"noteTodoId" -> noteTodoId,
"userId" -> userId
).as(SqlParser.scalar[Long].singleOpt).isDefined
}

def acceptNoteTodo(noteTodoId: Long)(implicit connection: Connection): Boolean = {
val updated: Int = SQL(
"""
UPDATE note_todos
SET is_accepted = true
WHERE id = {noteTodoId}
""").on(
"noteTodoId" -> noteTodoId
).executeUpdate()
updated == 1
}


def insert(noteId: Long, todo: NoteTodo)(implicit connection: Connection): Option[Long] = {
val todoId: Option[Long] = SQL("""
INSERT INTO note_todos (title, description, is_accepted, is_ai)
VALUES ({title}, {description}, {is_accepted}, {is_ai})
""").on(
"title" -> todo.title,
"description" -> todo.description,
"is_accepted" -> todo.isAccepted,
"is_ai" -> todo.isAi
).executeInsert()

for {
id <- todoId
} yield {
linkTodoToNote (id, noteId)
}
todoId
}

private def linkTodoToNote(todoId: Long, noteId: Long)(implicit connection: Connection): Int = {
SQL(
"""
INSERT INTO note_note_todos (note_id, note_todo_id)
VALUES ({noteId}, {noteTodoId})
""").on(
"noteTodoId" -> todoId,
"noteId" -> noteId
).executeUpdate()
}

def findNoteTodosByNoteId(noteId: Long)(implicit connection: Connection): Option[List[NoteTodo]] = {
val todos = SQL("SELECT * FROM note_todos inner join note_note_todos on id = note_todo_id WHERE note_id = " +
"{noteId}").on("noteId" -> noteId).as(NoteTodo.parser.*)
todos.size match {
case 0 => None
case _ => Some(todos)
}
}
}


4 changes: 2 additions & 2 deletions app/dao/TagDao.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import java.sql.Connection

class TagDao {

def add(emotionRecordId: Long, tagName: String)(implicit connection: Connection): Int = {
def insert(emotionRecordId: Long, tagName: String)(implicit connection: Connection): Int = {
if (!checkTagExistsForEmotionRecordByTagName(emotionRecordId, tagName)) {
val tagId = SQL(
"""
Expand Down Expand Up @@ -38,7 +38,7 @@ class TagDao {
SQL("SELECT * FROM tags inner join emotion_record_tags on tag_id = id WHERE emotion_record_id = {id}").on("id" -> id).as(Tag.parser.*)
}

def insert(emotionRecordId: Long, tags: List[Tag])(implicit connection: Connection): List[Long] = {
def insert(emotionRecordId: Long, tags: Set[Tag])(implicit connection: Connection): Set[Long] = {
tags.map { tag =>
val tagId: Long = SQL(
"""
Expand Down
16 changes: 13 additions & 3 deletions app/dao/TriggerDao.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ class TriggerDao {
SQL("SELECT * FROM triggers WHERE id = {id}").on("id" -> id).as(Trigger.parser.singleOpt)
}

def insert(trigger: Trigger)(implicit connection: Connection): Option[Long] = {
SQL("INSERT INTO triggers (user_id, description) VALUES ({userId}, {description})")
def insert(trigger: Trigger, emotionRecordId: Long)(implicit connection: Connection): Option[Long] = {
val triggerId: Option[Long] = SQL("INSERT INTO triggers (user_id, description) VALUES ({userId}, {description})")
.on("userId" -> trigger.createdByUser, "description" -> trigger.description)
.executeInsert()
triggerId match {
case Some(id) => linkTriggerToEmotionRecord(id, emotionRecordId)
}
triggerId
}

def update(trigger: Trigger)(implicit connection: Connection): Int = {
Expand All @@ -30,5 +34,11 @@ class TriggerDao {
SQL("DELETE FROM triggers WHERE id = {id}").on("id" -> id).executeUpdate()
}


def linkTriggerToEmotionRecord(triggerId: Long, emotionRecordId: Long)(implicit connection: Connection): Long = {
SQL(
"""
INSERT INTO emotion_record_triggers (parent_trigger_id, parent_emotion_record_id)
VALUES ({triggerId}, {emotionRecordId})""").
on("triggerId" -> triggerId, "emotionRecordId" -> emotionRecordId).executeUpdate()
}
}
Loading

0 comments on commit f656d58

Please sign in to comment.