Can now comment on events

This commit is contained in:
neviyn 2021-04-02 20:36:16 +01:00
parent ac06033c7d
commit bf5b800f7d
6 changed files with 156 additions and 47 deletions

View File

@ -2,7 +2,6 @@ package uk.co.neviyn.projectplanner
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime
import javax.persistence.CascadeType import javax.persistence.CascadeType
import javax.persistence.Column import javax.persistence.Column
import javax.persistence.Entity import javax.persistence.Entity
@ -69,6 +68,8 @@ class Event(
@ManyToMany(mappedBy = "events") @ManyToMany(mappedBy = "events")
@JsonIgnore @JsonIgnore
var tags: MutableSet<Tag> = mutableSetOf(), var tags: MutableSet<Tag> = mutableSetOf(),
@OneToMany(mappedBy = "event")
var comments: MutableList<Comment> = mutableListOf(),
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
) )
@ -82,12 +83,13 @@ class Comment(
@JsonIgnore @JsonIgnore
var event: Event? = null, var event: Event? = null,
@ManyToMany(mappedBy = "comments") @ManyToMany(mappedBy = "comments")
@JsonIgnore
var tags: MutableSet<Tag> = mutableSetOf(), var tags: MutableSet<Tag> = mutableSetOf(),
var created: LocalDateTime = LocalDateTime.MIN, var created: Instant = Instant.MIN,
var comment: String = "INVALID", var comment: String = "INVALID",
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
) ) {
fun toFlatComment(): FlatComment = FlatComment(created, comment, user!!.username)
}
@Entity @Entity
class Tag( class Tag(

View File

@ -1,6 +1,7 @@
package uk.co.neviyn.projectplanner package uk.co.neviyn.projectplanner
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseBody import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.server.ResponseStatusException
import java.time.Instant import java.time.Instant
import javax.persistence.EntityManager import javax.persistence.EntityManager
import javax.transaction.Transactional import javax.transaction.Transactional
@ -95,11 +97,11 @@ class HtmlController @Autowired constructor(val userRepository: UserRepository,
@Controller @Controller
@RequestMapping("/project/{id}") @RequestMapping("/project/{id}")
class ProjectController @Autowired constructor(val projectRepository: ProjectRepository, val userRepository: UserRepository, val eventRepository: EventRepository) { class ProjectController @Autowired constructor(val projectRepository: ProjectRepository, val userRepository: UserRepository, val eventRepository: EventRepository, val commentRepository: CommentRepository) {
@GetMapping("") @GetMapping("")
@PreAuthorize("hasPermission(#id, 'Long', '')") @PreAuthorize("hasPermission(#id, 'Long', '')")
fun getProject(@PathVariable id: Long, model: Model) : String { fun getProject(@PathVariable id: Long, model: Model): String {
val project = projectRepository.findById(id).get() val project = projectRepository.findById(id).get()
val nonMembers = userRepository.findByIdNotIn(project.members.map { it.id!! }) val nonMembers = userRepository.findByIdNotIn(project.members.map { it.id!! })
model.addAttribute("project", project) model.addAttribute("project", project)
@ -169,5 +171,23 @@ class ProjectController @Autowired constructor(val projectRepository: ProjectRep
eventRepository.deleteById(e.id) eventRepository.deleteById(e.id)
} }
@GetMapping("/eventcomments/{eventID}")
@PreAuthorize("hasPermission(#id, 'Long', '')")
@ResponseBody
fun getCommentsForEvent(@PathVariable id: Long, @PathVariable eventID: Long): List<FlatComment> {
val project = projectRepository.findById(id).get()
val event = eventRepository.findById(eventID).get()
if (event.project != project) throw ResponseStatusException(HttpStatus.FORBIDDEN, "This comment does not belong to this project")
return event.comments.map { it.toFlatComment() }
}
@PostMapping("/addcomment/{eventID}")
@PreAuthorize("hasPermission(#id, 'Long', '')")
@ResponseBody
fun addCommentToEvent(@PathVariable id: Long, @PathVariable eventID: Long, @RequestBody c: NewComment, @AuthenticationPrincipal userDetails: CustomUserDetails) {
val event = eventRepository.findById(eventID).get()
val comment = Comment(user = userDetails.user, event = event, created = Instant.now(), comment = c.comment)
event.comments.add(comment)
commentRepository.save(comment)
}
} }

View File

@ -11,3 +11,7 @@ data class NewEvent(val title: String, val description: String, val start: Insta
data class EditedEvent(val id: Long, val title: String, val description: String, val start: Instant, val end: Instant) data class EditedEvent(val id: Long, val title: String, val description: String, val start: Instant, val end: Instant)
data class EventID(val id: Long) data class EventID(val id: Long)
data class FlatComment(val created: Instant, val comment: String, val username: String)
data class NewComment(val comment: String)

View File

@ -53,17 +53,17 @@ create unique index if not exists event_id_uindex
create table if not exists projectplanner.comment create table if not exists projectplanner.comment
( (
id bigserial not null id bigserial not null
constraint comment_pk constraint comment_pk
primary key primary key,
event_id bigserial not null
constraint comment_event_fk constraint comment_event_fk
references projectplanner.event references projectplanner.event,
user_id bigserial not null
constraint comment_user_fk constraint comment_user_fk
references projectplanner."user", references projectplanner."user",
event_id bigserial not null, created timestamp not null,
user_id bigserial not null, comment text not null
created timestamp not null,
comment text not null
); );
create unique index if not exists comment_id_uindex create unique index if not exists comment_id_uindex

View File

@ -1,6 +1,9 @@
let DateTime = luxon.DateTime;
let calendar; let calendar;
let addModal; let addModal;
let editModal; let editModal;
let memberList;
let commentList;
window.onload = function () { window.onload = function () {
flatpickr("#startTimeInput", {enableTime: true}) flatpickr("#startTimeInput", {enableTime: true})
flatpickr("#endTimeInput", {enableTime: true}) flatpickr("#endTimeInput", {enableTime: true})
@ -71,15 +74,24 @@ window.onload = function () {
document.getElementById('descriptionEdit').value = eventInfo.event.extendedProps.description document.getElementById('descriptionEdit').value = eventInfo.event.extendedProps.description
startTimeEdit.setDate(eventInfo.event.start) startTimeEdit.setDate(eventInfo.event.start)
endTimeEdit.setDate(eventInfo.event.end) endTimeEdit.setDate(eventInfo.event.end)
setComments(eventInfo.event.id)
editModal.show() editModal.show()
}, },
events: window.location.origin + window.location.pathname + "/events" events: window.location.origin + window.location.pathname + "/events"
}); });
calendar.render(); calendar.render();
const options = { try {
valueNames: ['username'], const options = {
}; valueNames: ['username'],
new List('user-list', options); };
memberList = new List('user-list', options);
} catch (e) {
}
const commentOptions = {
valueNames: ['comment', 'created', 'username'],
item: '<blockquote class="blockquote mb-0"><p class="comment"></p><footer class="blockquote-footer"><span class="username"></span> | <span class="created"></span></footer></blockquote>'
}
commentList = new List('comment-list', commentOptions)
}; };
function addEvent() { function addEvent() {
@ -102,3 +114,37 @@ function editEvent() {
if (myEvent.description !== description) myEvent.setExtendedProp('description', description) if (myEvent.description !== description) myEvent.setExtendedProp('description', description)
myEvent.setDates(start, end) myEvent.setDates(start, end)
} }
async function setComments(eventID) {
let data = await fetch(window.location.origin + window.location.pathname + "/eventcomments/" + eventID, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8',
'X-CSRF-TOKEN': csrf_token
}
}).then(x => {
return x.json()
})
commentList.clear()
data.forEach(function (x) {
x.created = DateTime.fromISO(x.created).toLocaleString(DateTime.DATETIME_FULL)
commentList.add(x)
})
}
function saveComment() {
let id = document.getElementById('idEdit').value
let newComment = {
comment: document.getElementById('addComment').value
}
fetch(window.location.origin + window.location.pathname + "/addcomment/" + id, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
'X-CSRF-TOKEN': csrf_token
},
body: JSON.stringify(newComment)
})
// Refresh comments after adding one
setComments(id)
}

View File

@ -4,15 +4,21 @@
<title th:text="${project.title} + ' | Project Planner'">Project Planner</title> <title th:text="${project.title} + ' | Project Planner'">Project Planner</title>
</head> </head>
<head> <head>
<style>
.flatpickr {
background-color: white !important;
}
</style>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/main.min.css" rel="stylesheet"> <link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/main.min.css" rel="stylesheet">
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/main.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/main.min.js"></script>
<script crossorigin="anonymous" src="https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script> <script crossorigin="anonymous" src="https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet"> <link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet">
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/flatpickr"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/luxon@1.26.0/build/global/luxon.min.js"></script>
<script th:inline="javascript"> <script th:inline="javascript">
let csrf_token = /*[[${_csrf.token}]]*/ 'csrf_token' let csrf_token = /*[[${_csrf.token}]]*/ 'csrf_token'
</script> </script>
<script type="text/javascript" th:src="@{/js/project.js}"></script> <script th:src="@{/js/project.js}" type="text/javascript"></script>
<title></title> <title></title>
</head> </head>
<body> <body>
@ -91,7 +97,7 @@
</div> </div>
<!-- Modal for editing existing events --> <!-- Modal for editing existing events -->
<div class="modal" id="editEventModal" tabindex="-1"> <div class="modal" id="editEventModal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog modal-xl">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Edit Event</h5> <h5 class="modal-title">Edit Event</h5>
@ -99,33 +105,64 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input id="idEdit" type="hidden"/> <input id="idEdit" type="hidden"/>
<div class="form-floating mb-3"> <div class="container-fluid">
<input class="form-control" id="titleEdit" type="text"> <div class="row">
<label for="titleEdit">Event Title</label> <div class="col-10">
</div> <div class="form-floating mb-3">
<div class="form-floating mb-3"> <input class="form-control" id="titleEdit" type="text">
<textarea class="form-control h-100" id="descriptionEdit" rows="3"></textarea> <label for="titleEdit">Event Title</label>
<label for="descriptionEdit">Event Description</label> </div>
</div> <div class="form-floating mb-3">
<div class="form-floating mb-3"> <input class="form-control flatpickr" id="startTimeEdit" type="text">
<input class="form-control flatpickr" id="startTimeEdit" type="text"> <label for="startTimeEdit">Start Time</label>
<label for="startTimeEdit">Start Time</label> </div>
</div> <div class="form-floating mb-3">
<div class="form-floating mb-3"> <input class="form-control flatpickr" id="endTimeEdit" type="text">
<input class="form-control flatpickr" id="endTimeEdit" type="text"> <label for="endTimeEdit">End Time</label>
<label for="endTimeEdit">End Time</label> </div>
</div> </div>
</div> <div class="col-2 d-flex flex-column">
<div class="modal-footer justify-content-between"> <button class="btn btn-danger mb-3"
<button class="btn btn-danger" onclick="calendar.getEventById(document.getElementById('idEdit').value).remove();editModal.hide()"
onclick="calendar.getEventById(document.getElementById('idEdit').value).remove();editModal.hide()" type="button">
type="button"> Delete Event
Delete Event </button>
</button> <button class="btn btn-primary mb-3" onclick="editEvent();editModal.hide()" type="button">
<div> Save
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Close</button> Changes
<button class="btn btn-primary" onclick="editEvent();editModal.hide()" type="button">Save Changes </button>
</button> <button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Close</button>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-floating mb-3">
<textarea class="form-control h-100" id="descriptionEdit" rows="3"></textarea>
<label for="descriptionEdit">Event Description</label>
</div>
</div>
</div>
<!-- Show comments -->
<div class="row mb-3">
<div class="col" id="comment-list">
<h6>Comments</h6>
<div class="list">
</div>
</div>
</div>
<!-- Add comment -->
<div class="row">
<div class="col">
<div class="form-floating mb-3">
<textarea class="form-control h-100" id="addComment" rows="2"></textarea>
<label for="addComment">Comment</label>
</div>
</div>
<div class="col-1 d-inline-flex align-items-center justify-content-center">
<button class="btn btn-primary" onclick="saveComment();" type="button">Save
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>