Compare commits

..

1 Commits

Author SHA1 Message Date
77c24704c6 #13 -- add model, repository and service for members 2022-04-26 16:30:07 +04:00
18 changed files with 169 additions and 46 deletions

View File

@ -1,9 +1,2 @@
# seminar # seminar
JDK: https://download.oracle.com/java/17/archive/jdk-17.0.6_windows-x64_bin.exe
IDE: IntelliJIDEA community version https://www.jetbrains.com/ru-ru/idea/download/#section=windows
To run: run project and open http://localhost:8080/
Demo: http://seminar.athene.tech

View File

@ -48,7 +48,6 @@ dependencies {
implementation group: 'org.webjars', name: 'font-awesome', version: '4.7.0' implementation group: 'org.webjars', name: 'font-awesome', version: '4.7.0'
implementation group: 'org.webjars', name: 'momentjs', version: '2.24.0' implementation group: 'org.webjars', name: 'momentjs', version: '2.24.0'
implementation group: 'org.webjars', name: 'bootstrap-glyphicons', version: 'bdd2cbfba0' implementation group: 'org.webjars', name: 'bootstrap-glyphicons', version: 'bdd2cbfba0'
implementation group: 'org.webjars', name: 'summernote', version: '0.8.10'
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test' testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test'
} }

View File

@ -70,7 +70,8 @@ public class MeetingController {
@PostMapping("saveMeeting") @PostMapping("saveMeeting")
@Secured({UserRoleConstants.ADMIN}) @Secured({UserRoleConstants.ADMIN})
public String saveNews(@Valid @ModelAttribute Meeting meeting, BindingResult result) { public String saveNews(@Valid @ModelAttribute Meeting meeting,
BindingResult result) {
if (result.hasErrors()) { if (result.hasErrors()) {
return "editMeeting"; return "editMeeting";
} }

View File

@ -0,0 +1,40 @@
package ru.ulstu.members;
import org.springframework.web.multipart.MultipartFile;
import ru.ulstu.model.BaseEntity;
import javax.persistence.Entity;
import javax.persistence.Transient;
@Entity
public class Member extends BaseEntity {
private String name;
private String imageFileName;
@Transient
private MultipartFile imageFile;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getImageFileName() {
return imageFileName;
}
public void setImageFileName(String imageFileName) {
this.imageFileName = imageFileName;
}
public MultipartFile getImageFile() {
return imageFile;
}
public void setImageFile(MultipartFile imageFile) {
this.imageFile = imageFile;
}
}

View File

@ -0,0 +1,6 @@
package ru.ulstu.members;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MembersRepository extends JpaRepository<Member, Integer> {
}

View File

@ -0,0 +1,37 @@
package ru.ulstu.members;
import org.springframework.stereotype.Service;
import ru.ulstu.files.FileSystemStorageService;
import ru.ulstu.files.FileUtil;
import java.io.IOException;
@Service
public class MembersService {
private final MembersRepository membersRepository;
public MembersService(MembersRepository membersRepository) {
this.membersRepository = membersRepository;
}
public Member save(Member member) throws IOException {
String fileName = System.currentTimeMillis() + "";
if (!member.getImageFile().isEmpty()) {
member.setImageFileName(fileName);
FileUtil.saveFile(FileSystemStorageService.UPLOAD_DIR, fileName, member.getImageFile());
} else {
member.setImageFileName(member.getImageFileName().isEmpty() ? "user.png" : member.getImageFileName());
}
if (member.getId() != null && (member.getId() != 0)) {
return membersRepository.save(member);
} else {
return create(member);
}
}
public Member create(Member member) {
return membersRepository.save(member);
}
}

View File

@ -1,12 +1,14 @@
package ru.ulstu.news; package ru.ulstu.news;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.multipart.MultipartFile;
import ru.ulstu.meeting.Meeting; import ru.ulstu.meeting.Meeting;
import ru.ulstu.model.BaseEntity; import ru.ulstu.model.BaseEntity;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Lob; import javax.persistence.Lob;
import javax.persistence.OneToOne; import javax.persistence.OneToOne;
import javax.persistence.Transient;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import java.util.Date; import java.util.Date;
@ -24,6 +26,11 @@ public class News extends BaseEntity {
@NotEmpty(message = "Текст новости не может быть пустым") @NotEmpty(message = "Текст новости не может быть пустым")
private String text; private String text;
private String imageFileName;
@Transient
private MultipartFile imageFile;
@OneToOne(mappedBy = "news") @OneToOne(mappedBy = "news")
private Meeting meeting; private Meeting meeting;
@ -60,6 +67,22 @@ public class News extends BaseEntity {
return text; return text;
} }
public String getImageFileName() {
return imageFileName;
}
public void setImageFileName(String imageFileName) {
this.imageFileName = imageFileName;
}
public MultipartFile getImageFile() {
return imageFile;
}
public void setImageFile(MultipartFile imageFile) {
this.imageFile = imageFile;
}
public Meeting getMeeting() { public Meeting getMeeting() {
return meeting; return meeting;
} }
@ -67,4 +90,10 @@ public class News extends BaseEntity {
public void setMeeting(Meeting meeting) { public void setMeeting(Meeting meeting) {
this.meeting = meeting; this.meeting = meeting;
} }
public String getPreview() {
return text != null && text.length() > MAX_NEWS_TEXT_PREVIEW_LENGTH
? text.substring(0, MAX_NEWS_TEXT_PREVIEW_LENGTH) + "..."
: text;
}
} }

View File

@ -21,6 +21,7 @@ import ru.ulstu.model.OffsetablePageRequest;
import ru.ulstu.model.UserRoleConstants; import ru.ulstu.model.UserRoleConstants;
import javax.validation.Valid; import javax.validation.Valid;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -70,7 +71,8 @@ public class NewsController {
@PostMapping("saveNews") @PostMapping("saveNews")
@Secured({UserRoleConstants.ADMIN}) @Secured({UserRoleConstants.ADMIN})
public String saveNews(@Valid @ModelAttribute News news, BindingResult result) { public String saveNews(@Valid @ModelAttribute News news,
BindingResult result) throws IOException {
if (result.hasErrors()) { if (result.hasErrors()) {
return "editNews"; return "editNews";
} }

View File

@ -3,10 +3,13 @@ package ru.ulstu.news;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ru.ulstu.files.FileSystemStorageService;
import ru.ulstu.files.FileUtil;
import ru.ulstu.meeting.Meeting; import ru.ulstu.meeting.Meeting;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -34,7 +37,15 @@ public class NewsService {
return newsRepository.save(news); return newsRepository.save(news);
} }
public void save(News news) { public void save(News news) throws IOException {
String fileName = System.currentTimeMillis() + "";
if (!news.getImageFile().isEmpty()) {
news.setImageFileName(fileName);
FileUtil.saveFile(FileSystemStorageService.UPLOAD_DIR, fileName, news.getImageFile());
} else {
news.setImageFileName(news.getImageFileName().isEmpty() ? "logo.png" : news.getImageFileName());
}
if (news.getId() != null && (news.getId() != 0)) { if (news.getId() != null && (news.getId() != 0)) {
newsRepository.save(news); newsRepository.save(news);
} else { } else {

View File

@ -3,11 +3,8 @@ spring.main.banner-mode=off
logging.level.tech.athene=DEBUG logging.level.tech.athene=DEBUG
server.port=8080 server.port=8080
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
spring.servlet.multipart.max-file-size=2048MB spring.servlet.multipart.max-file-size=100000000
spring.servlet.multipart.max-request-size=2048MB spring.servlet.multipart.max-request-size=100000000
server.tomcat.max-http-form-post-size=2048MB
#server.tomcat.max-swallow-size=2048MB
#server.max-http-header-size=2048MB
# go to http://localhost:8080/h2-console # go to http://localhost:8080/h2-console
spring.datasource.url=jdbc:h2:file:./data/db spring.datasource.url=jdbc:h2:file:./data/db
spring.datasource.driverClassName=org.h2.Driver spring.datasource.driverClassName=org.h2.Driver
@ -16,3 +13,4 @@ spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false

View File

@ -6,11 +6,6 @@
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<title th:text="#{messages.app-name}">app-name</title> <title th:text="#{messages.app-name}">app-name</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script type="text/javascript" src="/webjars/jquery/3.6.0/jquery.min.js"></script>
<script type="text/javascript" src="/webjars/bootstrap/4.3.0/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/webjars/bootstrap/4.3.0/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/4.7.0/css/font-awesome.min.css"/>
<link rel="stylesheet" href="/css/main.css"/>
</head> </head>
<body> <body>
<div class="body-container"> <div class="body-container">
@ -88,7 +83,9 @@
<div><img src="https://mc.yandex.ru/watch/88139282" style="position:absolute; left:-9999px;" alt=""/></div> <div><img src="https://mc.yandex.ru/watch/88139282" style="position:absolute; left:-9999px;" alt=""/></div>
</noscript> </noscript>
<!-- /Yandex.Metrika counter --> <!-- /Yandex.Metrika counter -->
<script type="text/javascript" src="/webjars/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="/webjars/bootstrap/4.3.0/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/css/main.css"/>
</div> </div>
<footer> <footer>
2022 2022

View File

@ -32,15 +32,14 @@
</form> </form>
<script type="text/javascript" src="/webjars/momentjs/2.24.0/moment.js"></script> <script type="text/javascript" src="/webjars/momentjs/2.24.0/moment.js"></script>
<script type="text/javascript" src="/webjars/momentjs/2.24.0/locale/ru.js"></script> <script type="text/javascript" src="/webjars/momentjs/2.24.0/locale/ru.js"></script>
<script type="text/javascript" src="/webjars/bootstrap/4.3.0/js/bootstrap.bundle.min.js"></script>
<script type="text/javascript" src="/webjars/bootstrap-select/1.13.8/js/bootstrap-select.min.js"></script> <script type="text/javascript" src="/webjars/bootstrap-select/1.13.8/js/bootstrap-select.min.js"></script>
<script type="text/javascript" <script type="text/javascript"
src="/webjars/bootstrap-datetimepicker/2.4.4/js/bootstrap-datetimepicker.js"></script> src="/webjars/bootstrap-datetimepicker/2.4.4/js/bootstrap-datetimepicker.js"></script>
<script type="text/javascript" src="/webjars/summernote/0.8.10/summernote-bs4.js"></script>
<link rel="stylesheet" href="/webjars/bootstrap-select/1.13.8/css/bootstrap-select.min.css"/> <link rel="stylesheet" href="/webjars/bootstrap-select/1.13.8/css/bootstrap-select.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/4.7.0/css/font-awesome.min.css"/> <link rel="stylesheet" href="/webjars/font-awesome/4.7.0/css/font-awesome.min.css"/>
<link rel="stylesheet" href="/webjars/bootstrap-glyphicons/bdd2cbfba0/css/bootstrap-glyphicons.css"/> <link rel="stylesheet" href="/webjars/bootstrap-glyphicons/bdd2cbfba0/css/bootstrap-glyphicons.css"/>
<link rel="stylesheet" href="/webjars/bootstrap-datetimepicker/2.4.4/css/bootstrap-datetimepicker.css"/> <link rel="stylesheet" href="/webjars/bootstrap-datetimepicker/2.4.4/css/bootstrap-datetimepicker.css"/>
<link rel="stylesheet" href="/webjars/summernote/0.8.10/summernote-bs4.css"/>
<script> <script>
$(function () { $(function () {
$("#date").datetimepicker({ $("#date").datetimepicker({
@ -48,9 +47,7 @@
locale: 'ru' locale: 'ru'
}); });
}); });
$('#text').summernote({
height: 300
});
</script> </script>
</div> </div>
</html> </html>

View File

@ -8,6 +8,7 @@
<input type="hidden" th:field="*{id}"> <input type="hidden" th:field="*{id}">
<input type="hidden" th:field="*{date}"> <input type="hidden" th:field="*{date}">
<input type="hidden" th:field="*{version}"> <input type="hidden" th:field="*{version}">
<input type="hidden" th:field="*{imageFileName}">
<div class="form-group"> <div class="form-group">
<label for="title">Заголовок</label> <label for="title">Заголовок</label>
<input type="text" class="form-control" id="title" th:field="*{title}" placeholder="Заголовок новости"> <input type="text" class="form-control" id="title" th:field="*{title}" placeholder="Заголовок новости">
@ -21,26 +22,26 @@
<p th:if="${#fields.hasErrors('text')}" th:class="${#fields.hasErrors('text')}? error"> <p th:if="${#fields.hasErrors('text')}" th:class="${#fields.hasErrors('text')}? error">
Не может быть пустым</p> Не может быть пустым</p>
</div> </div>
<div class="form-group">
<label for="image">Изображение</label>
<input type="file" name="imageFile" class="form-control" th:field="*{imageFile}" id="image"
placeholder="Фото новости"/>
<p th:if="${#fields.hasErrors('imageFile')}" th:class="${#fields.hasErrors('imageFile')} ? error">
Ошибка</p>
</div>
<button type="submit" class="btn btn-outline-dark">Сохранить</button> <button type="submit" class="btn btn-outline-dark">Сохранить</button>
<a href="javascript:history.back()" class="btn btn-outline-dark">Отмена</a> <a href="javascript:history.back()" class="btn btn-outline-dark">Отмена</a>
</form> </form>
<script type="text/javascript" src="/webjars/momentjs/2.24.0/moment.js"></script> <script type="text/javascript" src="/webjars/momentjs/2.24.0/moment.js"></script>
<script type="text/javascript" src="/webjars/momentjs/2.24.0/locale/ru.js"></script> <script type="text/javascript" src="/webjars/momentjs/2.24.0/locale/ru.js"></script>
<script type="text/javascript" src="/webjars/bootstrap/4.3.0/js/bootstrap.bundle.min.js"></script>
<script type="text/javascript" src="/webjars/bootstrap-select/1.13.8/js/bootstrap-select.min.js"></script> <script type="text/javascript" src="/webjars/bootstrap-select/1.13.8/js/bootstrap-select.min.js"></script>
<script type="text/javascript" <script type="text/javascript"
src="/webjars/bootstrap-datetimepicker/2.4.4/js/bootstrap-datetimepicker.js"></script> src="/webjars/bootstrap-datetimepicker/2.4.4/js/bootstrap-datetimepicker.js"></script>
<script type="text/javascript" src="/webjars/summernote/0.8.10/summernote-bs4.js"></script>
<link rel="stylesheet" href="/webjars/bootstrap-select/1.13.8/css/bootstrap-select.min.css"/> <link rel="stylesheet" href="/webjars/bootstrap-select/1.13.8/css/bootstrap-select.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/4.7.0/css/font-awesome.min.css"/> <link rel="stylesheet" href="/webjars/font-awesome/4.7.0/css/font-awesome.min.css"/>
<link rel="stylesheet" href="/webjars/bootstrap-glyphicons/bdd2cbfba0/css/bootstrap-glyphicons.css"/> <link rel="stylesheet" href="/webjars/bootstrap-glyphicons/bdd2cbfba0/css/bootstrap-glyphicons.css"/>
<link rel="stylesheet" href="/webjars/bootstrap-datetimepicker/2.4.4/css/bootstrap-datetimepicker.css"/> <link rel="stylesheet" href="/webjars/bootstrap-datetimepicker/2.4.4/css/bootstrap-datetimepicker.css"/>
<link rel="stylesheet" href="/webjars/summernote/0.8.10/summernote-bs4.css"/>
<script>
$('#text').summernote({
height: 300
});
</script>
</div> </div>
</html> </html>

View File

@ -4,7 +4,10 @@
<div class="container" layout:fragment="content"> <div class="container" layout:fragment="content">
<div th:each="n : ${news}" class="news"> <div th:each="n : ${news}" class="news">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-4">
<img class="news-image" th:src="@{'/files/' + ${n.imageFileName}}"/>
</div>
<div class="col-md-8">
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
<a th:if="${n.meeting == null}" th:href="@{'/news/news/' + ${n.id}}" class="link-dark"><h5 <a th:if="${n.meeting == null}" th:href="@{'/news/news/' + ${n.id}}" class="link-dark"><h5
@ -13,13 +16,13 @@
class="link-dark"><h5 th:text="${n.title}"/></a> class="link-dark"><h5 th:text="${n.title}"/></a>
</div> </div>
</div> </div>
<div th:if="${n.meeting == null}" th:utext="${n.text}" class="news-item"></div> <div th:if="${n.meeting == null}" th:text="${n.preview}" class="news-item"></div>
<div th:if="${n.meeting != null}" th:text="${'Тема заседания: ' + n.meeting.title}" <div th:if="${n.meeting != null}" th:text="${'Тема заседания: ' + n.meeting.title}"
class="news-item"></div> class="news-item"></div>
<div th:if="${n.meeting != null}" <div th:if="${n.meeting != null}"
th:text="${'Дата: ' + #calendars.format(n.meeting.date, 'dd.MM.yyyy HH:mm')}" th:text="${'Дата: ' + #calendars.format(n.meeting.date, 'dd.MM.yyyy HH:mm')}"
class="news-item"></div> class="news-item"></div>
<div th:if="${n.meeting != null}" th:utext="${n.meeting.text}" class="news-item"></div> <div th:if="${n.meeting != null}" th:text="${n.meeting.text}" class="news-item"></div>
</div> </div>
</div> </div>
<div th:text="${'Опубликовано: ' + #calendars.format(n.date, 'dd.MM.yyyy HH:mm')}" <div th:text="${'Опубликовано: ' + #calendars.format(n.date, 'dd.MM.yyyy HH:mm')}"

View File

@ -7,7 +7,6 @@
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
<a th:href="@{'/meetings/meetings/' + ${m.id}}" class="link-dark"><h5 th:text="${m.title}"/></a> <a th:href="@{'/meetings/meetings/' + ${m.id}}" class="link-dark"><h5 th:text="${m.title}"/></a>
<div th:text="${'Дата заседания: ' + #calendars.format(m.date, 'dd.MM.yyyy HH:mm')}"></div>
</div> </div>
<div sec:authorize="hasRole('ROLE_ADMIN')" class="col-md-2" style="text-align: right"> <div sec:authorize="hasRole('ROLE_ADMIN')" class="col-md-2" style="text-align: right">
<a th:href="@{'/meetings/editMeeting/' + ${m.id}}" class="link-dark"> <a th:href="@{'/meetings/editMeeting/' + ${m.id}}" class="link-dark">
@ -30,5 +29,6 @@
th:text=${pageNumber} th:text=${pageNumber}
th:class="${pageNumber == meetings.number+1} ? active"></a> th:class="${pageNumber == meetings.number+1} ? active"></a>
</div> </div>
</div> </div>
</html> </html>

View File

@ -5,7 +5,10 @@
<div class="container" layout:fragment="content"> <div class="container" layout:fragment="content">
<div th:each="n : ${news}" class="news"> <div th:each="n : ${news}" class="news">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-4">
<img class="news-image" th:src="@{'/files/' + ${n.imageFileName}}"/>
</div>
<div class="col-md-8">
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
<a th:if="${n.meeting == null}" th:href="@{'/news/news/' + ${n.id}}" class="link-dark"><h5 <a th:if="${n.meeting == null}" th:href="@{'/news/news/' + ${n.id}}" class="link-dark"><h5
@ -23,13 +26,13 @@
</a> </a>
</div> </div>
</div> </div>
<div th:if="${n.meeting == null}" th:utext="${n.text}" class="news-item"></div> <div th:if="${n.meeting == null}" th:text="${n.preview}" class="news-item"></div>
<div th:if="${n.meeting != null}" th:text="${'Тема заседания: ' + n.meeting.title}" <div th:if="${n.meeting != null}" th:text="${'Тема заседания: ' + n.meeting.title}"
class="news-item"></div> class="news-item"></div>
<div th:if="${n.meeting != null}" <div th:if="${n.meeting != null}"
th:text="${'Дата: ' + #calendars.format(n.meeting.date, 'dd.MM.yyyy HH:mm')}" th:text="${'Дата: ' + #calendars.format(n.meeting.date, 'dd.MM.yyyy HH:mm')}"
class="news-item"></div> class="news-item"></div>
<div th:if="${n.meeting != null}" th:utext="${n.meeting.text}" class="news-item"></div> <div th:if="${n.meeting != null}" th:text="${n.meeting.text}" class="news-item"></div>
</div> </div>
</div> </div>
<div th:text="${'Опубликовано: ' + #calendars.format(n.date, 'dd.MM.yyyy HH:mm')}" <div th:text="${'Опубликовано: ' + #calendars.format(n.date, 'dd.MM.yyyy HH:mm')}"

View File

@ -4,9 +4,12 @@
layout:decorate="~{default}"> layout:decorate="~{default}">
<div class="container" layout:fragment="content"> <div class="container" layout:fragment="content">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-4">
<img class="news-image" src="/img/logo.svg"/>
</div>
<div class="col-md-8">
<h5 th:text="${meeting.title}"/> <h5 th:text="${meeting.title}"/>
<div th:utext="${meeting.text}" class="news-item"></div> <div th:text="${meeting.text}" class="news-item"></div>
</div> </div>
</div> </div>
<a href="javascript:history.back()" class="btn btn-outline-dark" style="text-align: right">Назад</a> <a href="javascript:history.back()" class="btn btn-outline-dark" style="text-align: right">Назад</a>

View File

@ -4,9 +4,12 @@
layout:decorate="~{default}"> layout:decorate="~{default}">
<div class="container" layout:fragment="content"> <div class="container" layout:fragment="content">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-4">
<img class="news-image" th:src="@{'/files/' + ${news.imageFileName}}"/>
</div>
<div class="col-md-8">
<h5 th:text="${news.title}"/> <h5 th:text="${news.title}"/>
<div th:utext="${news.text}" class="news-item"></div> <div th:text="${news.text}" class="news-item"></div>
</div> </div>
</div> </div>
<div th:text="${'Опубликовано: ' + #calendars.format(news.date, 'dd.MM.yyyy HH:mm')}" <div th:text="${'Опубликовано: ' + #calendars.format(news.date, 'dd.MM.yyyy HH:mm')}"