Can now comment on events
This commit is contained in:
parent
ac06033c7d
commit
bf5b800f7d
@ -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(
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
@ -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)
|
@ -55,13 +55,13 @@ 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,
|
|
||||||
user_id bigserial not null,
|
|
||||||
created timestamp not null,
|
created timestamp not null,
|
||||||
comment text not null
|
comment text not null
|
||||||
);
|
);
|
||||||
|
@ -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();
|
||||||
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
valueNames: ['username'],
|
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)
|
||||||
|
}
|
@ -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,14 +105,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input id="idEdit" type="hidden"/>
|
<input id="idEdit" type="hidden"/>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-10">
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<input class="form-control" id="titleEdit" type="text">
|
<input class="form-control" id="titleEdit" type="text">
|
||||||
<label for="titleEdit">Event Title</label>
|
<label for="titleEdit">Event Title</label>
|
||||||
</div>
|
</div>
|
||||||
<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 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>
|
||||||
@ -116,16 +121,48 @@
|
|||||||
<label for="endTimeEdit">End Time</label>
|
<label for="endTimeEdit">End Time</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer justify-content-between">
|
<div class="col-2 d-flex flex-column">
|
||||||
<button class="btn btn-danger"
|
<button class="btn btn-danger mb-3"
|
||||||
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>
|
||||||
<div>
|
<button class="btn btn-primary mb-3" onclick="editEvent();editModal.hide()" type="button">
|
||||||
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Close</button>
|
Save
|
||||||
<button class="btn btn-primary" onclick="editEvent();editModal.hide()" type="button">Save Changes
|
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>
|
||||||
|
Loading…
Reference in New Issue
Block a user