Skip to content
Snippets Groups Projects
Commit fb2819b0 authored by Marnuri Nitish -'s avatar Marnuri Nitish -
Browse files

Merge branch '243-add-news-to-news-feed' into 'release/Sprint-3'

Resolve "As an admin user, I want to be able to add new news to the news feed so that i can share latest news with the community."

See merge request c24108486/team-11-polish-community-group!36
parents d1957d10 e98b59a0
No related branches found
No related tags found
No related merge requests found
Showing
with 535 additions and 61 deletions
package polish_community_group_11.polish_community.news.controllers; package polish_community_group_11.polish_community.news.controllers;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import polish_community_group_11.polish_community.information.models.DBInfo;
import polish_community_group_11.polish_community.information.models.DBInfoImpl;
import polish_community_group_11.polish_community.news.models.News;
import polish_community_group_11.polish_community.news.models.NewsImpl;
import polish_community_group_11.polish_community.news.services.NewsService; import polish_community_group_11.polish_community.news.services.NewsService;
import java.sql.SQLException; import java.sql.SQLException;
...@@ -21,6 +28,22 @@ public class NewsController { ...@@ -21,6 +28,22 @@ public class NewsController {
public ModelAndView getNews() throws SQLException { public ModelAndView getNews() throws SQLException {
ModelAndView modelAndView = new ModelAndView("news/newsList"); ModelAndView modelAndView = new ModelAndView("news/newsList");
modelAndView.addObject("newsList",newsService.getAllNews()); modelAndView.addObject("newsList",newsService.getAllNews());
modelAndView.addObject("news" , new NewsImpl());
return modelAndView;
}
@PostMapping("/news/add")
public ModelAndView addInformation(@Valid @ModelAttribute("news") NewsImpl news,
BindingResult bindingResult, Model model) throws SQLException {
ModelAndView modelAndView = new ModelAndView("news/addNews");
if(bindingResult.hasErrors()){
modelAndView.addObject(model.asMap());
}
else{
News newsToAdd = news;
newsService.addNews(newsToAdd);
modelAndView = new ModelAndView("redirect:/news");
}
return modelAndView; return modelAndView;
} }
} }
package polish_community_group_11.polish_community.news.dao; package polish_community_group_11.polish_community.news.dao;
import polish_community_group_11.polish_community.information.models.DBInfo;
import polish_community_group_11.polish_community.news.models.News; import polish_community_group_11.polish_community.news.models.News;
import java.sql.SQLException; import java.sql.SQLException;
...@@ -8,7 +9,11 @@ import java.util.List; ...@@ -8,7 +9,11 @@ import java.util.List;
public interface NewsRepository { public interface NewsRepository {
public List<News> getAllNews() throws SQLException; public List<News> getAllNews() throws SQLException;
void addNews(News news) throws SQLException;
News getNewsById(int id) throws SQLException; News getNewsById(int id) throws SQLException;
void updateNews(News news) throws SQLException; void updateNews(News news) throws SQLException;
void deleteNews(int id) throws SQLException; void deleteNews(int id) throws SQLException;
} }
...@@ -6,11 +6,13 @@ import org.springframework.dao.EmptyResultDataAccessException; ...@@ -6,11 +6,13 @@ import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import polish_community_group_11.polish_community.information.models.DBInfo;
import polish_community_group_11.polish_community.news.models.News; import polish_community_group_11.polish_community.news.models.News;
import polish_community_group_11.polish_community.news.models.NewsImpl; import polish_community_group_11.polish_community.news.models.NewsImpl;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.DateTimeException; import java.time.DateTimeException;
import java.time.LocalDate;
import java.util.List; import java.util.List;
@Repository // Marks the class as a repository in the Spring context, responsible for data access logic @Repository // Marks the class as a repository in the Spring context, responsible for data access logic
...@@ -81,6 +83,32 @@ public class NewsRepositoryImpl implements NewsRepository { ...@@ -81,6 +83,32 @@ public class NewsRepositoryImpl implements NewsRepository {
} }
} }
public void addNews(News news) throws SQLException{
String dbInsertSql =
"insert into news " +
"(news_title, news_summary, news_source, " +
"news_link, news_image_url, user_id, news_upload_date)" +
" values (?,?,?,?,?,?,?)";
try {
int x = jdbc.update(dbInsertSql,
news.getNews_title(),
news.getNews_summary(),
news.getNews_source(),
news.getNews_link(),
news.getNews_image_url(),
1,
LocalDate.now()
);
}catch (DataAccessException e) {
throw new SQLException("Failed to insert new information record", e);
}
}
@Override @Override
public News getNewsById(int id) throws SQLException { public News getNewsById(int id) throws SQLException {
String sql = "SELECT * FROM news WHERE news_id = ?"; String sql = "SELECT * FROM news WHERE news_id = ?";
......
package polish_community_group_11.polish_community.news.models; package polish_community_group_11.polish_community.news.models;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate; import java.time.LocalDate;
@Data @Data
@NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class NewsImpl implements News{ public class NewsImpl implements News{
private int news_id; private int news_id;
@NotEmpty(message = "News Title should not be empty!")
private String news_title; private String news_title;
@NotEmpty(message = "News summary should not be empty!")
private String news_summary; private String news_summary;
private String news_source; private String news_source;
private String news_link; private String news_link;
......
package polish_community_group_11.polish_community.news.services; package polish_community_group_11.polish_community.news.services;
import polish_community_group_11.polish_community.information.models.DBInfo;
import polish_community_group_11.polish_community.news.models.News; import polish_community_group_11.polish_community.news.models.News;
import java.sql.SQLException; import java.sql.SQLException;
...@@ -8,6 +9,10 @@ import java.util.List; ...@@ -8,6 +9,10 @@ import java.util.List;
public interface NewsService { public interface NewsService {
public List<News> getAllNews() throws SQLException; public List<News> getAllNews() throws SQLException;
// Save new news article
void addNews(News news) throws SQLException;
News getNewsById(int id) throws SQLException; News getNewsById(int id) throws SQLException;
void updateNews(News news) throws SQLException; void updateNews(News news) throws SQLException;
void deleteNews(int id) throws SQLException; void deleteNews(int id) throws SQLException;
......
...@@ -22,6 +22,12 @@ public class NewsServiceImpl implements NewsService { ...@@ -22,6 +22,12 @@ public class NewsServiceImpl implements NewsService {
} }
@Override @Override
public void addNews(News news)throws SQLException {
newsRepository.addNews(news);
}
public News getNewsById(int id) throws SQLException { public News getNewsById(int id) throws SQLException {
return newsRepository.getNewsById(id); return newsRepository.getNewsById(id);
} }
...@@ -36,4 +42,5 @@ public class NewsServiceImpl implements NewsService { ...@@ -36,4 +42,5 @@ public class NewsServiceImpl implements NewsService {
public void deleteNews(int id) throws SQLException { public void deleteNews(int id) throws SQLException {
newsRepository.deleteNews(id); newsRepository.deleteNews(id);
} }
} }
/* Basic reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Modal Styling */
.modal {
display: none; /* Hidden by default */
align-items: center;
justify-content: center;
position: fixed;
z-index: 9999; /* Sit on top */
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden; /* Prevent scrolling of the modal itself */
background-color: rgba(0, 0, 0, 0.4); /* Black with opacity */
}
/* Overlay */
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7); /* Dark background */
z-index: 1; /* Behind the modal content */
}
/* Modal Content */
.modal-content {
position: fixed; /* Ensure content stays above the overlay */
background-color: #fff;
margin: auto; /* Centered horizontally and vertically */
padding: 20px;
border-radius: 8px;
width: 80%;
max-width: 600px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 2; /* Make sure modal content stays above overlay */
max-height: 80vh; /* Prevents scrolling by limiting height */
overflow: auto; /* Allow internal scrolling if necessary but prevents overall scrolling */
}
/* Close button */
.close-btn {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close-btn:hover,
.close-btn:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
/* Button to open modal */
.openModalBtn {
padding: 10px 15px;
background-color: var(--primary-color);
color: var(--secondary-color);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.openModalBtn:hover {
background-color: var(--border-color);
color:var(--text-color);
}
/* Form Styling */
h2 {
text-align: center;
margin-bottom: 20px;
}
form {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 600px;
margin: 0 auto;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
font-size: 14px;
color: #555;
margin-bottom: 5px;
}
input[type="text"], textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
outline: none;
transition: border 0.3s ease;
}
input[type="text"]:focus, textarea:focus {
border-color: #007bff;
}
textarea {
resize: vertical;
}
.form-buttons {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
}
button {
padding: 10px 15px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.cancelButton {
background-color: black;
color: #fff;
}
.submitButton {
background-color: black;
color: #fff;
}
button:hover {
opacity: 0.9;
}
/* Responsive Design */
@media (max-width: 600px) {
form {
width: 90%;
padding: 15px;
}
.form-buttons {
flex-direction: column;
gap: 10px;
}
.submitButton, .cancelButton {
width: 100%;
text-align: center;
}
.modal-content {
width: 90%; /* Ensure modal is responsive */
}
}
...@@ -3,14 +3,17 @@ body { ...@@ -3,14 +3,17 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
background-color: white; background-color: white;
color: #5D001E; color: var(--text-color);
} }
.heading {
background-image: linear-gradient(to right, #9a1750 20%, #e3afbc); .general-headings-layout{
color: #e3e2df; display: flex;
text-align: center; /*margin-top: 5%;*/
padding: 1% 0; align-items: center;
justify-content: space-between;
/*margin-bottom: 20px;*/
margin: 5% 8% 0 8%;
} }
h1 { h1 {
...@@ -27,8 +30,8 @@ h1 { ...@@ -27,8 +30,8 @@ h1 {
} }
.news-card { .news-card {
background-image: linear-gradient(to right, #e3e2df, white); background-image: linear-gradient(to right, var(--border-color), var(--secondary-color));
box-shadow: 0px 0px 15px 2px #e3afbc; box-shadow: 0px 0px 15px 2px var(--border-color);
padding: 1.5rem; padding: 1.5rem;
width: 100%; width: 100%;
max-width: 90%; max-width: 90%;
...@@ -71,7 +74,7 @@ h1 { ...@@ -71,7 +74,7 @@ h1 {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: 0.9rem; font-size: 0.9rem;
color: #777; color: var(--primary-color);
} }
.small-cards { .small-cards {
...@@ -85,7 +88,7 @@ h1 { ...@@ -85,7 +88,7 @@ h1 {
flex: 1 1 calc(45% - 1rem); flex: 1 1 calc(45% - 1rem);
max-width: calc(45% - 1rem); max-width: calc(45% - 1rem);
padding: 1rem; padding: 1rem;
background-image: linear-gradient(to right, #e3e2df, white); background-image: linear-gradient(to right, var(--border-color), var(--secondary-color));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
...@@ -114,17 +117,62 @@ h1 { ...@@ -114,17 +117,62 @@ h1 {
text-decoration: underline; text-decoration: underline;
} }
/*create news*/
/* Modal background */
.create-new-modal {
display: none; /* Initially hidden */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
background-color: rgba(0, 0, 0, 0.4); /* Semi-transparent background */
}
/* Modal content */
.modal-content {
background-color: #fff;
margin: 15% auto; /* Center the modal */
padding: 20px;
border: 1px solid #888;
width: 80%; /* Set a width */
max-width: 600px;
}
/* Close button */
.close-modal {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
/* Change color on hover */
.close-modal:hover,
.close-modal:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
.form-buttons {
display: flex;
justify-content: space-between;
padding-top: 15px;
}
.modify-btn { .modify-btn {
display: inline-block; display: inline-block;
margin-top: 5px; margin-top: 5px;
padding: 5px 10px; padding: 5px 10px;
background-color: #007BFF; background-color: var(--primary-color);
color: white; color:var(--secondary-color);
text-decoration: none; text-decoration: none;
border-radius: 5px; border-radius: 5px;
font-size: 14px; font-size: 14px;
} }
.modify-btn:hover { .modify-btn:hover {
background-color: #0056b3; background-color: var(--border-color);
color:var(--primary-color)
} }
// Get the modal and button
const modal = document.getElementById("modal");
const closeBtn = document.getElementsByClassName("close-btn")[0];
const cancelButton = document.getElementById("cancelButton");
const overlay = document.getElementById("overlay");
// Close the modal
closeBtn.onclick = function() {
modal.style.display = "none";
}
// Close the modal if the overlay is clicked
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
}
}
// Close the modal when the user clicks the "Cancel" button
cancelButton.addEventListener("click", function() {
modal.style.display = "none";
});
// Close the modal when the user clicks outside of the modal content
overlay.addEventListener("click", function() {
modal.style.display = "none";
});
\ No newline at end of file
...@@ -33,3 +33,20 @@ document.addEventListener("DOMContentLoaded", () => { ...@@ -33,3 +33,20 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
} }
}); });
// // This will handle the modal opening when the Add News tile is clicked
function openNewsForm() {
const openModalBtn = document.getElementById("openModalBtn");
// Open the modal
openModalBtn.onclick = function() {
modal.style.display = "flex";
}
const newsFormSection = document.getElementById('newsForm');
if(newsFormSection.style.display == 'none'){
newsFormSection.style.display = 'block';
}else
newsFormSection.style.display = 'none';
}
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/layout}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add News</title>
<link rel="stylesheet" href="/css/news/addNews.css">
<script src="/js/news/addNews.js" defer></script>
</head>
<section th:fragment="addNewsForm">
<!-- Modal structure -->
<div id="modal" class="modal">
<div class="overlay" id="overlay"></div>
<div class="modal-content" >
<span class="close-btn">&times;</span>
<form id="newsForm" th:action="@{/news/add}" th:method="post" th:object="${news}">
<h2>Create News</h2>
<div class="form-group">
<label th:for="newsTitle">News Title</label>
<input type="text" id="newsTitle" name="newsTitle" th:field="*{news_title}" placeholder="Enter news title" required>
</div>
<div class="form-group">
<label th:for="newsDescription">Description</label>
<textarea id="newsDescription" name="newsDescription" th:field="*{news_summary}" placeholder="Briefly describe this news article..." rows="4" required></textarea>
</div>
<div class="form-group">
<label th:for="newsImageUrl">Image URL (optional)</label>
<input type="text" id="newsImageUrl" name="newsImageUrl" th:field="*{news_image_url}" placeholder="Enter image URL">
</div>
<div class="form-group">
<label th:for="newsSource">Source</label>
<input type="text" id="newsSource" name="newsSource" th:field="*{news_source}" placeholder="Enter source" required>
</div>
<div class="form-group">
<label th:for="newsLink">News Link</label>
<input type="text" name="newsLink" th:field="*{news_link}" placeholder="Enter Link" required>
</div>
<div class="form-buttons">
<button type="button" class="cancelButton" id="cancelButton">Cancel</button>
<button class="submitButton" type="submit">Create News</button>
</div>
</form>
</div>
</div>
</section>
</html>
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" <html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}"> layout:decorate="~{layout/layout}">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>News Page</title> <title>News Page</title>
<link rel="stylesheet" href="/css/news/newsStyles.css"> <link rel="stylesheet" href="/css/news/newsStyles.css">
<link rel="stylesheet" href="/css/news/addNews.css">
<script src="/js/news/addNews.js" defer></script>
<script src="/js/news/newsScripts.js" defer></script>
</head> </head>
<body class="content"> <section layout:fragment="content" >
<header class="heading"> <div class="general-headings-layout">
<h1>Latest News</h1>
</header> <h1>Community News</h1>
<main class="news-container" layout:fragment="content">
<button onclick="openNewsForm()" id="openModalBtn" class="openModalBtn">Add News</button>
</div>
<main class="news-container">
<!-- Main news card --> <!-- Main news card -->
<div class="news-card main-card"> <div class="news-card main-card">
<div class="main-card-container"> <div class="main-card-container">
...@@ -26,29 +33,32 @@ ...@@ -26,29 +33,32 @@
</div> </div>
<div class="card-footer"> <div class="card-footer">
<p class="source">Source: <span th:text="${newsList[0].getNews_source()}"></span></p> <p class="source">Source: <span th:text="${newsList[0].getNews_source()}"></span></p>
<p class="date" th:text="${newsList[0].getNews_upload_date()}"></p>
<a th:href="@{/editNews/{id}(id=${newsList[0].getNews_id()})}" class="modify-btn modify-link">Modify</a> <a th:href="@{/editNews/{id}(id=${newsList[0].getNews_id()})}" class="modify-btn modify-link">Modify</a>
<p class="date" th:text="${newsList[0].getNews_upload_date()}"></p>
</div> </div>
</div> </div>
<!-- Smaller news cards --> <!-- Smaller news cards -->
<div class="small-cards"> <div class="small-cards">
<div class="news-card small-card" th:each="news : ${newsList}" th:if="${newsStat.index != 0}"> <div class="news-card small-card" th:each="news,newsStat : ${newsList}" th:if="${newsStat.index != 0}">
<a th:href="${news.getNews_link()}"><h3 th:text="${news.getNews_title()}"></h3></a> <a th:href="${news.getNews_link()}"><h3 th:text="${news.getNews_title()}"></h3></a>
<p class="summary" th:text="${news.getNews_summary()}"></p> <p class="summary" th:text="${news.getNews_summary()}"></p>
<div class="card-footer"> <div class="card-footer">
<p class="source">Source: <span th:text="${news.getNews_source()}"></span></p> <p class="source">Source: <span th:text="${news.getNews_source()}"></span></p>
<p class="date" th:text="${news.getNews_upload_date()}"></p> <p class="date" th:text="${news.getNews_upload_date()}"></p>
<a th:href="@{/editNews/{id}(id=${news.getNews_id()})}" class="modify-btn modify-link">Modify</a> <a th:href="@{/editNews/{id}(id=${news.getNews_id()})}" class="modify-btn modify-link">Modify</a>
</div> </div>
</div> </div>
</div> </div>
<!-- Add News Button -->
<div>
<!-- Empty section to be replaced by the fragment -->
<section id="newsForm" style="display: none;" >
<!-- Fragment will be loaded here dynamically -->
<div class="news-card small-card" th:replace="news/addNews :: addNewsForm"></div>
</section>
</div>
</main> </main>
<script src="/js/news/newsScripts.js"></script> </section>
</body>
</html> </html>
package polish_community_group_11.polish_community.news;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import polish_community_group_11.polish_community.news.models.NewsImpl;
import polish_community_group_11.polish_community.news.services.NewsService;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
@SpringBootTest
@AutoConfigureMockMvc
public class AddNewsTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webApplicationContext;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private NewsService newsService;
private NewsImpl validNews;
private NewsImpl invalidNews;
@BeforeEach
public void setup() {
validNews = new NewsImpl();
validNews.setNews_title("Valid News Title");
validNews.setNews_summary("Summary of valid news");
validNews.setNews_link("https://www.msn.com/en-in/news/India/currency-notes-in-rajya-sabha-diversion-tactics-alleges-congress-bjp-seeks-probe/ar-AA1vnPFP?ocid=BingNewsSerp");
}
@Test
public void testAddNewsValid() throws Exception {
// Mock the NewsService to simulate saving news
doNothing().when(newsService).addNews(any(NewsImpl.class));
// Perform the POST request for adding valid news
mockMvc.perform(post("/news/add")
.param("news_title", validNews.getNews_title())
.param("news_summary", validNews.getNews_summary())
.param("news_link", validNews.getNews_link()))
.andExpect(status().is3xxRedirection()) // Expect redirect (successful)
.andExpect(view().name("redirect:/news")) // Should redirect to /news
.andExpect(model().attributeDoesNotExist("news")); // No validation errors on "news"
// Verify that the service method was called once
verify(newsService, times(1)).addNews(any(NewsImpl.class));
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment