Merge remote-tracking branch 'origin/master' into AddBranchToCurriculum
All checks were successful
Build and test backend / Build-backend (pull_request) Successful in 1m25s
Build and test FrontEnd / Build-frontend (pull_request) Successful in 25s

This commit is contained in:
Wawilski 2024-04-21 21:51:14 +02:00
commit 9cd54bdae9
15 changed files with 219 additions and 92 deletions

View File

@ -80,7 +80,7 @@ public class ForumController {
public ResponseEntity<Topic> postTopicToForum(@RequestHeader("Authorization") String token, @PathVariable long id, @RequestBody Topic data){ public ResponseEntity<Topic> postTopicToForum(@RequestHeader("Authorization") String token, @PathVariable long id, @RequestBody Topic data){
User u = authServ.getUserFromToken(token); User u = authServ.getUserFromToken(token);
Forum f = forumRepo.findById(id).orElse(null); Forum f = forumRepo.findById(id).orElse(null);
if(!(f.getWriters().contains(u) || u.getRole() == Role.Admin)){ if(!(f.getWriters().contains(u) || f.getCourse().getOwner().equals(u) || u.getRole() == Role.Admin)){
return new UnauthorizedResponse<>(null); return new UnauthorizedResponse<>(null);
} }
forumServ.createTopic(f, data); forumServ.createTopic(f, data);

View File

@ -0,0 +1,61 @@
package ovh.herisson.Clyde.EndPoints;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import lombok.AllArgsConstructor;
import ovh.herisson.Clyde.Repositories.NotificationRepository;
import ovh.herisson.Clyde.Responses.UnauthorizedResponse;
import ovh.herisson.Clyde.Services.AuthenticatorService;
import ovh.herisson.Clyde.Tables.Notification;
import ovh.herisson.Clyde.Tables.User;
import ovh.herisson.Clyde.Tables.Notification.Status;
@RestController
@AllArgsConstructor
@CrossOrigin(originPatterns = "*", allowCredentials = "true")
public class NotificationController {
private AuthenticatorService authServ;
private NotificationRepository notifRepo;
@GetMapping("/notifications")
public ResponseEntity<List<Notification>> getNotifications(@RequestHeader("Authorization") String token){
User u = authServ.getUserFromToken(token);
if(u == null){
return new UnauthorizedResponse<>(null);
}
ArrayList<Notification> ret = new ArrayList<>();
for (Notification n : u.getNotifications()) {
if(!n.getStatus().equals(Status.Archived)){
ret.add(n);
}
}
return new ResponseEntity<>(ret, HttpStatus.OK);
}
@PostMapping("/notifications/{id}")
public ResponseEntity<Notification> archiveNotification(@RequestHeader("Authorization") String token, @PathVariable long id){
User u = authServ.getUserFromToken(token);
Notification n = notifRepo.findById(id).orElse(null);
if(u == null || n.getUser() != u){
return new UnauthorizedResponse<>(null);
}
n.setStatus(Status.Archived);
notifRepo.save(n);
return new ResponseEntity<>(HttpStatus.OK);
}
}

View File

@ -0,0 +1,8 @@
package ovh.herisson.Clyde.Repositories;
import org.springframework.data.repository.CrudRepository;
import ovh.herisson.Clyde.Tables.Notification;
public interface NotificationRepository extends CrudRepository<Notification, Long> {}

View File

@ -17,6 +17,8 @@ import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.util.JSONPObject; import com.fasterxml.jackson.databind.util.JSONPObject;
import ovh.herisson.Clyde.Repositories.Msg.DiscussionRepository; import ovh.herisson.Clyde.Repositories.Msg.DiscussionRepository;
import ovh.herisson.Clyde.Services.UserService;
import ovh.herisson.Clyde.Tables.Notification;
import ovh.herisson.Clyde.Tables.User; import ovh.herisson.Clyde.Tables.User;
import ovh.herisson.Clyde.Tables.Msg.Discussion; import ovh.herisson.Clyde.Tables.Msg.Discussion;
import ovh.herisson.Clyde.Tables.Msg.Message; import ovh.herisson.Clyde.Tables.Msg.Message;
@ -26,6 +28,8 @@ public class DiscussionService {
@Autowired @Autowired
private DiscussionRepository discRepo; private DiscussionRepository discRepo;
@Autowired
private UserService userServ;
public Discussion create(String name, User author){ public Discussion create(String name, User author){
return discRepo.save(new Discussion(name, author)); return discRepo.save(new Discussion(name, author));
@ -42,6 +46,9 @@ public class DiscussionService {
* Create a message and link it to it's discussion * Create a message and link it to it's discussion
*/ */
public Discussion CreateMessage(Discussion disc, Message msg){ public Discussion CreateMessage(Discussion disc, Message msg){
for(User u: disc.getMembers()){
userServ.Notify(u, new Notification("msg.notification.new", msg.getContent(), "/#/msg"));
}
disc.addMessage(msg); disc.addMessage(msg);
return discRepo.save(disc); return discRepo.save(disc);
} }

View File

@ -0,0 +1,9 @@
package ovh.herisson.Clyde.Services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class NotificationService {
}

View File

@ -4,6 +4,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ovh.herisson.Clyde.Tables.RegNoGenerator; import ovh.herisson.Clyde.Tables.RegNoGenerator;
import ovh.herisson.Clyde.Repositories.UserRepository; import ovh.herisson.Clyde.Repositories.UserRepository;
import ovh.herisson.Clyde.Tables.Notification;
import ovh.herisson.Clyde.Tables.Role; import ovh.herisson.Clyde.Tables.Role;
import ovh.herisson.Clyde.Tables.User; import ovh.herisson.Clyde.Tables.User;
import java.util.*; import java.util.*;
@ -139,4 +140,10 @@ public class UserService {
public void delete(User user) { public void delete(User user) {
userRepo.delete(user); userRepo.delete(user);
} }
public void Notify(User u, Notification n){
n.setUser(u);
u.getNotifications().add(n);
userRepo.save(u);
}
} }

View File

@ -28,10 +28,11 @@ public class Course {
private User owner; private User owner;
//// Extension Messagerie ///// //// Extension Messagerie /////
@OneToMany(cascade = CascadeType.ALL) @OneToMany(mappedBy = "course", cascade = CascadeType.ALL)
private List<Forum> forums; private List<Forum> forums;
public void addForum(Forum f){ public void addForum(Forum f){
f.setCourse(this);
forums.add(f); forums.add(f);
} }
/////////////////////////////// ///////////////////////////////

View File

@ -2,6 +2,8 @@ package ovh.herisson.Clyde.Tables.Msg;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.Data; import lombok.Data;
import ovh.herisson.Clyde.Tables.Course; import ovh.herisson.Clyde.Tables.Course;
@ -16,6 +18,7 @@ public class Forum {
private int id; private int id;
@ManyToOne @ManyToOne
@JsonIgnore
private Course course; private Course course;
private String name; private String name;

View File

@ -0,0 +1,53 @@
package ovh.herisson.Clyde.Tables;
import java.util.Date;
import org.hibernate.annotations.CreationTimestamp;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.annotation.Nullable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@Entity
public class Notification {
public enum Status {
Unread,
Read,
Archived
}
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String subject;
private String body;
private Status status = Status.Unread;
private String link;
@ManyToOne
@JsonIgnore
private User user;
@CreationTimestamp
private Date creation;
public Notification(String subject, @Nullable String body, @Nullable String link){
this.subject = subject;
this.body = body;
this.link = link;
}
}

View File

@ -2,18 +2,24 @@ package ovh.herisson.Clyde.Tables;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction; import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
import ovh.herisson.Clyde.Tables.Msg.Discussion; import ovh.herisson.Clyde.Tables.Msg.Discussion;
import ovh.herisson.Clyde.Tables.Msg.Message; import ovh.herisson.Clyde.Tables.Msg.Message;
import ovh.herisson.Clyde.Tables.Notification.Status;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity @Entity
@Table(name = "Users") @Table(name = "Users")
@NoArgsConstructor
@Data
public class User { public class User {
@Id @Id
@GenericGenerator(name = "userGen", type = ovh.herisson.Clyde.Tables.RegNoGenerator.class) @GenericGenerator(name = "userGen", type = ovh.herisson.Clyde.Tables.RegNoGenerator.class)
@ -32,13 +38,19 @@ public class User {
@JsonIgnore @JsonIgnore
private String password; private String password;
@JsonIgnore
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Notification> notifications;
////// Extension Messagerie ///// ////// Extension Messagerie /////
@JsonIgnore
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL) @OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
private List<Message> msgs; private List<Message> msgs;
/////////////////////////////////
@JsonIgnore
@ManyToMany( mappedBy = "members" ) @ManyToMany( mappedBy = "members" )
private List<Discussion> discussions; private List<Discussion> discussions;
/////////////////////////////////
public User(String lastName, String firstName, String email, String address, public User(String lastName, String firstName, String email, String address,
String country, Date birthDate, String profilePictureUrl, Role role, String password) String country, Date birthDate, String profilePictureUrl, Role role, String password)
@ -70,84 +82,4 @@ public class User {
this.role = Role.Student; this.role = Role.Student;
this.identityCardUrl = identityCardUrl; this.identityCardUrl = identityCardUrl;
} }
public User() {}
public Long getRegNo(){
return this.regNo;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public Date getBirthDate() {
return birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public String getProfilePictureUrl(){return this.profilePictureUrl;}
public void setProfilePictureUrl(String profilePictureUrl){
this.profilePictureUrl = profilePictureUrl;
}
public ovh.herisson.Clyde.Tables.Role getRole() {
return role;
}
public void setRole(ovh.herisson.Clyde.Tables.Role role) {
this.role = role;
}
public String getPassword(){
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setIdentityCardUrl(String identityCardUrl) {
this.identityCardUrl = identityCardUrl;
}
public String getIdentityCardUrl() {
return identityCardUrl;
}
} }

View File

@ -121,6 +121,7 @@ Curriculum=curriculum
Credits=Credits Credits=Credits
InscriptionService=I.S. InscriptionService=I.S.
faculty=Faculty faculty=Faculty
msg.notification.new=You have a new message
forum.create=Create forum forum.create=Create forum
forum.create.name=New forum's name forum.create.name=New forum's name
forum.post.create.name=New post's title forum.post.create.name=New post's title

View File

@ -121,6 +121,7 @@ Curriculum=Cursus
Credits=Credits Credits=Credits
InscriptionService=S.I. InscriptionService=S.I.
faculty=Faculté faculty=Faculté
msg.notification.new=Vous avez un nouveau message!
forum.create=Créer un forum forum.create=Créer un forum
forum.create.name=Nom du forum forum.create.name=Nom du forum
forum.post.create.name=Titre du post forum.post.create.name=Titre du post

View File

@ -3,6 +3,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import i18n, { setLang } from './i18n.js' import i18n, { setLang } from './i18n.js'
import { isLogged } from '@/rest/Users.js' import { isLogged } from '@/rest/Users.js'
import { notifications, fetchNotifications, archiveNotification } from '@/rest/notifications.js'
import { appList, currentView } from '@/rest/apps.js' import { appList, currentView } from '@/rest/apps.js'
var prevURL; var prevURL;
@ -14,16 +15,20 @@ window.onhashchange = function() {
} }
const Logged = ref(isLogged()); const Logged = ref(isLogged());
if(Logged.value){
fetchNotifications();
}
window.addEventListener('hashchange', () => { window.addEventListener('hashchange', () => {
if((location.hash === "#/home" && prevURL === "#/login") || (location.hash === "#/home" && prevURL === "#/profil")){ if((location.hash === "#/home" && prevURL === "#/login") || (location.hash === "#/home" && prevURL === "#/profil")){
window.location.reload(); window.location.reload();
} }
}); });
const home=ref(i18n("app.home")) const home=ref(i18n("app.home"))
const notifications=ref(i18n("app.notifications"))
const settings=ref(i18n("app.settings")) const settings=ref(i18n("app.settings"))
const login=ref(i18n("app.login")) const login=ref(i18n("app.login"))
const active=ref(false) const active=ref(false)
const notification = ref(false)
const apps = ref([]) const apps = ref([])
appList().then(e => apps.value = e) appList().then(e => apps.value = e)
@ -44,14 +49,17 @@ window.addEventListener('hashchange', () => {
</a></li> </a></li>
<li style="float: right;" title=login> <li style="float: right;" title=login>
<a class="icon" href="#/login"> <a class="icon" href="#/login">
<div class="fa-solid fa-user" :style="Logged ? 'color: orange' : 'haha'" style="margin-top: 7px; margin-bottom: 3px; "></div> <div class="fa-solid fa-user" :style="Logged ? 'color: red' : ''" style="margin-top: 7px; margin-bottom: 3px; "></div>
</a></li> </a></li>
<li style="float: right;" title=notifications> <li style="float: right;" title=notifications @click="notification = !notification">
<a class="icon" href="#Notifications"> <a class="icon">
<div class="fa-solid fa-bell" style="margin-top: 7px; margin-bottom: 3px;"></div> <div class="fa-solid fa-bell" :style="notifications.length != 0 ? 'color:orange': '' " style="margin-top: 7px; margin-bottom: 3px;"></div>
<ul v-if=notification id="notification">
<li v-for="notif in notifications" @click="archiveNotification(notif.id)"> {{ i18n(notif.subject) }} - {{ notif.body }}</li>
</ul>
</a></li> </a></li>
<li @click="active=!active" class="option"style="float: right;" title=settings> <li @click="active=!active" class="option"style="float: right;" title=settings>
<a class="icon" > <a class="icon">
<div class="fa-solid fa-gear" style="margin-top: 7px; margin-bottom: 3px;"></div> <div class="fa-solid fa-gear" style="margin-top: 7px; margin-bottom: 3px;"></div>
<div v-if="active" class="dropdown"> <div v-if="active" class="dropdown">
<div class="dropdown-content">{{i18n("app.language")}}</div> <div class="dropdown-content">{{i18n("app.language")}}</div>
@ -215,8 +223,6 @@ window.addEventListener('hashchange', () => {
background-color: black; background-color: black;
border-radius:6px; border-radius:6px;
color:white; color:white;
transform: translate(0px ,1px);
} }
ul.vertical:hover { ul.vertical:hover {
@ -251,6 +257,32 @@ window.addEventListener('hashchange', () => {
content: url("./assets/angry_clyde.png") content: url("./assets/angry_clyde.png")
} }
#notification{
position: absolute;
top: 61px;
right: 0;
background-color: white;
width: 300px;
height: 600px;
border-radius: 10px;
margin: 10px;
}
#notification > li{
color: black;
list-style: none;
font-size: 0.4em;
display: block;
background-color: #00FF00A0;
margin: 1px;
border-radius: 42px;
padding: 10px;
}
#notification > li:hover{
background-color: #00FF0000
}
</style> </style>
<!-- vim:set noet sts=0 sw=4 ts=2: --> <!-- vim:set noet sts=0 sw=4 ts=2: -->

View File

@ -15,7 +15,7 @@ import { fetchedPost, fetchPost, sendAnswer } from '@/rest/forum.js'
import { getSelf } from '@/rest/Users.js' import { getSelf } from '@/rest/Users.js'
const Role = (await getSelf()).role; const Role = (await getSelf()).role;
const courses = Role === 'Admin' || Role === 'Secretary' ? await reactive(getCourses()) : await reactive(getUserActualCourses()); const courses = Role === 'Admin' || Role === 'Secretary' || Role === 'Teacher' ? await reactive(getCourses(Role)) : await reactive(getUserActualCourses());
const selectedCourse = ref(); const selectedCourse = ref();
const selectedForum = ref(); const selectedForum = ref();

View File

@ -0,0 +1,12 @@
import { ref } from 'vue'
import { restGet, restPost } from '@/rest/restConsumer.js'
export const notifications = ref([]);
export function fetchNotifications(){
restGet("/notifications").then( e => notifications.value = e );
}
export function archiveNotification(id){
restPost("/notifications/" + id).then( e => fetchNotifications() );
}