Merge remote-tracking branch 'origin/master'

# Conflicts:
#	data/db.mv.db
#	src/main/resources/application.properties
This commit is contained in:
Anton Romanov 2022-04-03 22:22:39 +04:00
commit 305f78777c
45 changed files with 815 additions and 49 deletions

View File

@ -1,23 +1,17 @@
/*
* Copyright (C) 2021 Anton Romanov - All Rights Reserved
* You may use, distribute and modify this code, please write to: romanov73@gmail.com.
*
*/
plugins { plugins {
id 'java' id 'java'
id 'io.spring.dependency-management' version '1.0.9.RELEASE' id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'org.springframework.boot' version '2.3.3.RELEASE' id 'org.springframework.boot' version '2.6.4'
id "org.sonarqube" version "2.7"
} }
jar { jar {
archivesBaseName='seminar' archivesBaseName = 'seminar'
} }
repositories { repositories {
maven { maven {
url "http://repo.athene.tech/repository/maven-central/" url "http://repo.athene.tech/repository/maven-central/"
allowInsecureProtocol(true)
} }
} }
@ -39,20 +33,23 @@ dependencies {
implementation group: 'org.springframework.boot', name:'spring-boot-starter-data-jpa' implementation group: 'org.springframework.boot', name:'spring-boot-starter-data-jpa'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security'
implementation group: 'org.slf4j', name: 'slf4j-api', version: versionSLF4J implementation group: 'org.slf4j', name: 'slf4j-api', version: versionSLF4J
implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect' implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect', version: '3.1.0'
implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-springsecurity5' implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-springsecurity5'
implementation group: 'com.h2database', name:'h2' implementation group: 'com.h2database', name:'h2'
implementation group: 'javax.xml.bind', name:'jaxb-api' implementation group: 'javax.xml.bind', name: 'jaxb-api'
implementation group: 'org.javassist', name:'javassist' implementation group: 'org.javassist', name: 'javassist'
implementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: versionJetty implementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: versionJetty
implementation group: 'org.webjars', name: 'jquery', version: '3.6.0' implementation group: 'org.webjars', name: 'jquery', version: '3.6.0'
implementation group: 'org.webjars', name: 'bootstrap', version: '4.6.0' implementation group: 'org.webjars', name: 'bootstrap', version: '4.3.0'
implementation group: 'org.webjars', name: 'bootstrap-select', version: '1.13.8' implementation group: 'org.webjars', name: 'bootstrap-select', version: '1.13.8'
implementation group: 'org.webjars', name: 'bootstrap-datetimepicker', version: '2.4.4'
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: 'bootstrap-glyphicons', version: 'bdd2cbfba0'
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test' testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test'
} }

Binary file not shown.

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip

View File

@ -3,10 +3,12 @@ package ru.ulstu;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import ru.ulstu.user.UserService; import ru.ulstu.user.UserService;
@SpringBootApplication @SpringBootApplication
@EnableConfigurationProperties
public class SeminarApplication { public class SeminarApplication {
private final UserService userService; private final UserService userService;

View File

@ -24,6 +24,8 @@ public class MvcConfiguration implements WebMvcConfigurer {
registry.addViewController("/loginError"); registry.addViewController("/loginError");
registry.addViewController("/index"); registry.addViewController("/index");
registry.addViewController("/admin"); registry.addViewController("/admin");
registry.addViewController("/organizers");
registry.addViewController("/docs");
registry.addViewController("/editNews"); registry.addViewController("/editNews");
} }

View File

@ -45,7 +45,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
log.debug("Security enabled"); log.debug("Security enabled");
http.authorizeRequests() http.authorizeRequests()
.antMatchers("/").permitAll() .antMatchers("/").permitAll()
.antMatchers("/login", "/index", "/news/**", "/h2-console/*", "/h2-console").permitAll() .antMatchers("/login", "/index", "/news/**", "/meetings/**", "/files/**", "/docs/**",
"/public/**", "/organizers", "/webjars/**", "/h2-console/*", "/h2-console").permitAll()
.antMatchers("/swagger-ui.html").hasAuthority(UserRoleConstants.ADMIN) .antMatchers("/swagger-ui.html").hasAuthority(UserRoleConstants.ADMIN)
.anyRequest().authenticated() .anyRequest().authenticated()
.and() .and()

View File

@ -6,7 +6,7 @@
package ru.ulstu.configuration; package ru.ulstu.configuration;
import nz.net.ultraq.thymeleaf.LayoutDialect; import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect; import org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect;

View File

@ -9,7 +9,7 @@ package ru.ulstu.controller;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import ru.ulstu.service.NewsService; import ru.ulstu.news.NewsService;
@Controller @Controller
public class IndexController { public class IndexController {

View File

@ -0,0 +1,80 @@
package ru.ulstu.files;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
@Service
public class FileSystemStorageService implements StorageService {
public static final String UPLOAD_DIR = "upload";
private final Path rootLocation = Paths.get(UPLOAD_DIR);
@Override
public void store(MultipartFile file) {
try {
if (file.isEmpty()) {
throw new StorageException("Failed to store empty file " + file.getOriginalFilename());
}
Files.copy(file.getInputStream(), this.rootLocation.resolve(file.getOriginalFilename()));
} catch (IOException e) {
throw new StorageException("Failed to store file " + file.getOriginalFilename(), e);
}
}
@Override
public Stream<Path> loadAll() {
try {
return Files.walk(this.rootLocation, 1)
.filter(path -> !path.equals(this.rootLocation))
.map(path -> this.rootLocation.relativize(path));
} catch (IOException e) {
throw new StorageException("Failed to read stored files", e);
}
}
@Override
public Path load(String filename) {
return rootLocation.resolve(filename);
}
@Override
public Resource loadAsResource(String filename) {
try {
Path file = load(filename);
Resource resource = new UrlResource(file.toUri());
if (resource.exists() || resource.isReadable()) {
return resource;
} else {
throw new StorageFileNotFoundException("Could not read file: " + filename);
}
} catch (MalformedURLException e) {
throw new StorageFileNotFoundException("Could not read file: " + filename, e);
}
}
@Override
public void deleteAll() {
FileSystemUtils.deleteRecursively(rootLocation.toFile());
}
@Override
public void init() {
try {
Files.createDirectory(rootLocation);
} catch (IOException e) {
throw new StorageException("Could not initialize storage", e);
}
}
}

View File

@ -0,0 +1,29 @@
package ru.ulstu.files;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
public class FileUtil {
public static void saveFile(String uploadDir, String fileName,
MultipartFile multipartFile) throws IOException {
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
try (InputStream inputStream = multipartFile.getInputStream()) {
Path filePath = uploadPath.resolve(fileName);
Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ioe) {
throw new IOException("Could not save image file: " + fileName, ioe);
}
}
}

View File

@ -0,0 +1,28 @@
package ru.ulstu.files;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class FilesController {
private final StorageService storageService;
public FilesController(StorageService storageService) {
this.storageService = storageService;
}
@GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
Resource file = storageService.loadAsResource((filename == null || filename.equals("null") || filename.isEmpty())
? "logo.png"
: filename);
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
"filename=\"" + file.getFilename() + "\"").body(file);
}
}

View File

@ -0,0 +1,12 @@
package ru.ulstu.files;
public class StorageException extends RuntimeException {
public StorageException(String message) {
super(message);
}
public StorageException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,12 @@
package ru.ulstu.files;
public class StorageFileNotFoundException extends StorageException {
public StorageFileNotFoundException(String message) {
super(message);
}
public StorageFileNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,23 @@
package ru.ulstu.files;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Path;
import java.util.stream.Stream;
public interface StorageService {
void init();
void store(MultipartFile file);
Stream<Path> loadAll();
Path load(String filename);
Resource loadAsResource(String filename);
void deleteAll();
}

View File

@ -0,0 +1,85 @@
package ru.ulstu.meeting;
import org.springframework.format.annotation.DateTimeFormat;
import ru.ulstu.model.BaseEntity;
import ru.ulstu.news.News;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.OneToOne;
import javax.persistence.Transient;
import javax.validation.constraints.NotEmpty;
import java.util.Date;
@Entity
public class Meeting extends BaseEntity {
@NotEmpty(message = "Заголовок не может быть пустым")
private String title;
@DateTimeFormat(pattern = "dd.MM.yyyy HH:mm")
private Date date;
@Lob
@NotEmpty(message = "Текст заседания не может быть пустым")
private String text;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "news_id", referencedColumnName = "id")
private News news;
@Transient
private Integer newsId;
public Meeting() {
}
public Meeting(String title, String text, Date date) {
this.title = title;
this.date = date;
this.text = text;
}
public String getTitle() {
return title;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public void setTitle(String title) {
this.title = title;
}
public void setText(String text) {
this.text = text;
}
public String getText() {
return text;
}
public News getNews() {
return news;
}
public void setNews(News news) {
this.news = news;
}
public Integer getNewsId() {
return newsId == null
? (news != null) ? news.getId() : null
: newsId;
}
public void setNewsId(Integer newsId) {
this.newsId = newsId;
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright (C) 2021 Anton Romanov - All Rights Reserved
* You may use, distribute and modify this code, please write to: romanov73@gmail.com.
*
*/
package ru.ulstu.meeting;
import org.springframework.data.domain.Page;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import ru.ulstu.model.OffsetablePageRequest;
import ru.ulstu.model.UserRoleConstants;
import javax.validation.Valid;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Controller
@RequestMapping("meetings")
public class MeetingController {
private final static int DEFAULT_PAGE_SIZE = 10;
private final MeetingService meetingService;
public MeetingController(MeetingService meetingService) {
this.meetingService = meetingService;
}
@GetMapping("/meetings")
public String listMeetings(Model model,
@RequestParam Optional<Integer> page,
@RequestParam Optional<Integer> size) {
int currentPage = page.orElse(1);
int pageSize = size.orElse(DEFAULT_PAGE_SIZE);
Page<Meeting> meetingsPage = meetingService.getMeetings(new OffsetablePageRequest(currentPage - 1, pageSize));
model.addAttribute("meetings", meetingsPage);
int totalPages = meetingsPage.getTotalPages();
if (totalPages > 0) {
List<Integer> pageNumbers = IntStream.rangeClosed(1, totalPages)
.boxed()
.collect(Collectors.toList());
model.addAttribute("pageNumbers", pageNumbers);
}
return "meetings";
}
@GetMapping("/editMeeting/{meetingId}")
@Secured({UserRoleConstants.ADMIN})
public String editMeeting(@PathVariable(value = "meetingId") Integer id, Model model) {
model.addAttribute("meeting", (id != null && id != 0) ? meetingService.getById(id) : new Meeting());
return "editMeeting";
}
@GetMapping("/meetings/{meetingId}")
public String viewMeeting(@PathVariable(value = "meetingId") Integer id, Model model) {
model.addAttribute("meeting", id != null ? meetingService.getById(id) : new Meeting());
return "viewMeeting";
}
@PostMapping("saveMeeting")
@Secured({UserRoleConstants.ADMIN})
public String saveNews(@Valid @ModelAttribute Meeting meeting,
BindingResult result) {
if (result.hasErrors()) {
return "editMeeting";
}
meetingService.save(meeting);
return "redirect:/meetings/meetings";
}
@GetMapping("deleteMeeting/{meetingId}")
@Secured({UserRoleConstants.ADMIN})
public String delete(@PathVariable(value = "meetingId") Integer id) {
meetingService.delete(id);
return "redirect:/meetings/meetings";
}
}

View File

@ -0,0 +1,9 @@
package ru.ulstu.meeting;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
interface MeetingRepository extends JpaRepository<Meeting, Integer> {
Page<Meeting> findByOrderByDateDesc(Pageable pageable);
}

View File

@ -0,0 +1,58 @@
package ru.ulstu.meeting;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import ru.ulstu.news.News;
import ru.ulstu.news.NewsService;
import javax.transaction.Transactional;
import javax.validation.constraints.NotNull;
@Service
public class MeetingService {
private final MeetingRepository meetingRepository;
private final NewsService newsService;
public MeetingService(MeetingRepository meetingRepository,
NewsService newsService) {
this.meetingRepository = meetingRepository;
this.newsService = newsService;
}
@Transactional
public Meeting create(Meeting meeting) {
meeting = meetingRepository.save(meeting);
News news = newsService.create("Объявление о заседании семинара", meeting);
meeting.setNews(news);
return save(meeting);
}
public Meeting save(Meeting meeting) {
if (meeting.getNewsId() != null) {
meeting.setNews(newsService.getById(meeting.getNewsId()));
}
return (meeting.getId() != null && (meeting.getId() != 0))
? meetingRepository.save(meeting)
: create(meeting);
}
public Meeting getById(@NotNull Integer id) {
return meetingRepository
.findById(id)
.orElseThrow(() -> new RuntimeException("Запись о заседании не найдена"));
}
@Transactional
public void delete(Integer id) {
Meeting meeting = meetingRepository.getById(id);
if (meeting.getNews() != null) {
newsService.delete(meeting.getNews().getId());
}
meetingRepository.deleteById(id);
}
public Page<Meeting> getMeetings(Pageable pageable) {
return meetingRepository.findByOrderByDateDesc(pageable);
}
}

View File

@ -4,6 +4,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import java.io.Serializable; import java.io.Serializable;
import java.util.Optional;
public class OffsetablePageRequest implements Pageable, Serializable { public class OffsetablePageRequest implements Pageable, Serializable {
private final int offset; private final int offset;
@ -31,11 +32,26 @@ public class OffsetablePageRequest implements Pageable, Serializable {
return sort; return sort;
} }
@Override
public Sort getSortOr(Sort sort) {
return Pageable.super.getSortOr(sort);
}
@Override @Override
public int getPageSize() { public int getPageSize() {
return count; return count;
} }
@Override
public boolean isPaged() {
return Pageable.super.isPaged();
}
@Override
public boolean isUnpaged() {
return Pageable.super.isUnpaged();
}
@Override @Override
public int getPageNumber() { public int getPageNumber() {
return offset / count; return offset / count;
@ -51,6 +67,11 @@ public class OffsetablePageRequest implements Pageable, Serializable {
return offset > 0; return offset > 0;
} }
@Override
public Optional<Pageable> toOptional() {
return Pageable.super.toOptional();
}
@Override @Override
public Pageable next() { public Pageable next() {
return new OffsetablePageRequest(getOffset() + getPageSize(), getPageSize(), getSort()); return new OffsetablePageRequest(getOffset() + getPageSize(), getPageSize(), getSort());
@ -70,6 +91,11 @@ public class OffsetablePageRequest implements Pageable, Serializable {
return new OffsetablePageRequest(0, getPageSize(), getSort()); return new OffsetablePageRequest(0, getPageSize(), getSort());
} }
@Override
public Pageable withPage(int pageNumber) {
return null;
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) { if (this == obj) {

View File

@ -1,9 +1,14 @@
package ru.ulstu.model; 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.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.Transient;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import java.util.Date; import java.util.Date;
@ -21,6 +26,14 @@ 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")
private Meeting meeting;
public News() { public News() {
} }
@ -54,6 +67,30 @@ 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() {
return meeting;
}
public void setMeeting(Meeting meeting) {
this.meeting = meeting;
}
public String getPreview() { public String getPreview() {
return text != null && text.length() > MAX_NEWS_TEXT_PREVIEW_LENGTH return text != null && text.length() > MAX_NEWS_TEXT_PREVIEW_LENGTH
? text.substring(0, MAX_NEWS_TEXT_PREVIEW_LENGTH) + "..." ? text.substring(0, MAX_NEWS_TEXT_PREVIEW_LENGTH) + "..."

View File

@ -4,9 +4,10 @@
* *
*/ */
package ru.ulstu.controller; package ru.ulstu.news;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
@ -16,11 +17,11 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
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 ru.ulstu.model.News;
import ru.ulstu.model.OffsetablePageRequest; import ru.ulstu.model.OffsetablePageRequest;
import ru.ulstu.service.NewsService; 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;
@ -56,6 +57,7 @@ public class NewsController {
} }
@GetMapping("/editNews/{newsId}") @GetMapping("/editNews/{newsId}")
@Secured({UserRoleConstants.ADMIN})
public String editNews(@PathVariable(value = "newsId") Integer id, Model model) { public String editNews(@PathVariable(value = "newsId") Integer id, Model model) {
model.addAttribute("news", (id != null && id != 0) ? newsService.getById(id) : new News()); model.addAttribute("news", (id != null && id != 0) ? newsService.getById(id) : new News());
return "editNews"; return "editNews";
@ -68,16 +70,18 @@ public class NewsController {
} }
@PostMapping("saveNews") @PostMapping("saveNews")
public String saveNews(@Valid @ModelAttribute News news, BindingResult result) { @Secured({UserRoleConstants.ADMIN})
public String saveNews(@Valid @ModelAttribute News news,
BindingResult result) throws IOException {
if (result.hasErrors()) { if (result.hasErrors()) {
return "editNews"; return "editNews";
} }
newsService.save(news); newsService.save(news);
return "redirect:/news/news"; return "redirect:/news/news";
} }
@GetMapping("deleteNews/{newsId}") @GetMapping("deleteNews/{newsId}")
@Secured({UserRoleConstants.ADMIN})
public String delete(@PathVariable(value = "newsId") Integer id) { public String delete(@PathVariable(value = "newsId") Integer id) {
newsService.delete(id); newsService.delete(id);
return "redirect:/news/news"; return "redirect:/news/news";

View File

@ -1,9 +1,8 @@
package ru.ulstu.repository; 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.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import ru.ulstu.model.News;
import java.util.List; import java.util.List;

View File

@ -1,12 +1,15 @@
package ru.ulstu.service; 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.model.News; import ru.ulstu.files.FileSystemStorageService;
import ru.ulstu.repository.NewsRepository; import ru.ulstu.files.FileUtil;
import ru.ulstu.meeting.Meeting;
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;
@ -19,15 +22,30 @@ public class NewsService {
} }
public void create(String title, String text) { public void create(String title, String text) {
newsRepository.save(new News(title, new Date(), text)); create(new News(title, new Date(), text));
} }
public void create(News news) { @Transactional
public News create(String title, Meeting meeting) {
News news = new News(title, new Date(), "Новость о заседении семинара");
news.setMeeting(meeting);
return create(news);
}
public News create(News news) {
news.setDate(new Date()); news.setDate(new Date());
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

@ -2,6 +2,7 @@ package ru.ulstu.user;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
@ -24,6 +25,8 @@ public class UserService implements UserDetailsService {
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository; private final UserRepository userRepository;
private final UserRoleRepository userRoleRepository; private final UserRoleRepository userRoleRepository;
@Value("${admin-password}")
private String adminPassword;
public UserService(PasswordEncoder passwordEncoder, public UserService(PasswordEncoder passwordEncoder,
UserRepository userRepository, UserRepository userRepository,
@ -63,7 +66,6 @@ public class UserService implements UserDetailsService {
public void initDefaultAdmin() { public void initDefaultAdmin() {
String adminLogin = "admin"; String adminLogin = "admin";
String adminPassword = "adminadmin";
if (getUserByLogin(adminLogin) == null) { if (getUserByLogin(adminLogin) == null) {
UserRole adminRole = userRoleRepository.save(new UserRole(UserRoleConstants.ADMIN)); UserRole adminRole = userRoleRepository.save(new UserRole(UserRoleConstants.ADMIN));
createUser(new User(adminLogin, adminPassword, Set.of(adminRole))); createUser(new User(adminLogin, adminPassword, Set.of(adminRole)));

View File

@ -1,7 +1,10 @@
admin-password=admin
spring.main.banner-mode=off 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=100000000
spring.servlet.multipart.max-request-size=100000000
# 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

Binary file not shown.

Binary file not shown.

View File

@ -4,5 +4,8 @@
<a href="/news/editNews/0" class="btn btn-outline-dark"> <a href="/news/editNews/0" class="btn btn-outline-dark">
<i class="fa fa-plus-square" aria-hidden="true">Добавить новость</i> <i class="fa fa-plus-square" aria-hidden="true">Добавить новость</i>
</a> </a>
<a href="/meetings/editMeeting/0" class="btn btn-outline-dark">
<i class="fa fa-plus-square" aria-hidden="true">Добавить заседание</i>
</a>
</div> </div>
</html> </html>

View File

@ -7,11 +7,17 @@
<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/jquery/3.6.0/jquery.min.js"></script>
<script type="text/javascript" src="/webjars/bootstrap/4.6.0/js/bootstrap.bundle.min.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/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>
<link rel="stylesheet" href="/webjars/bootstrap/4.6.0/css/bootstrap.min.css"/> <script type="text/javascript"
src="/webjars/bootstrap-datetimepicker/2.4.4/js/bootstrap-datetimepicker.js"></script>
<link rel="stylesheet" href="/webjars/bootstrap/4.3.0/css/bootstrap.min.css"/>
<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-datetimepicker/2.4.4/css/bootstrap-datetimepicker.css"/>
<link rel="stylesheet" href="/css/main.css"/> <link rel="stylesheet" href="/css/main.css"/>
</head> </head>
<body> <body>
@ -30,10 +36,16 @@
<a class="nav-link" href="/news/news">Новости</a> <a class="nav-link" href="/news/news">Новости</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/news">Заседания</a> <a class="nav-link" href="/meetings/meetings">Заседания</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/news">Отчеты</a> <a class="nav-link" href="/organizers">Организаторы</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/docs">Документы</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/reports">Отчеты</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/admin">Администратору</a> <a class="nav-link" href="/admin">Администратору</a>

View File

@ -0,0 +1,23 @@
<!--
~ Copyright (C) 2021 Anton Romanov - All Rights Reserved
~ You may use, distribute and modify this code, please write to: romanov73@gmail.com.
~
-->
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<div class="container" layout:fragment="content">
<ul class="list-group">
<li class="list-group-item">
<a class="link-secondary" href="/docs/polozh.docx">
Положение
</a>
</li>
<li class="list-group-item">
<a class="link-secondary" href="/docs/plan2022.docx">
План заседаний на 2022 год.
</a>
</li>
</ul>
</div>
</html>

View File

@ -0,0 +1,42 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.w3.org/1999/xhtml"
layout:decorate="~{default}">
<div class="container" layout:fragment="content">
<h3>Редактирование заседания:</h3>
<form action="#" th:action="@{/meetings/saveMeeting}" th:object="${meeting}" method="post">
<input type="hidden" th:field="*{id}">
<input type="hidden" th:field="*{version}">
<input type="hidden" th:field="*{newsId}">
<div class="form-group">
<label for="title">Тема заседания</label>
<input type="text" class="form-control" id="title" th:field="*{title}" placeholder="Тема заседания">
<p th:if="${#fields.hasErrors('title')}" th:class="${#fields.hasErrors('title')}? error">
Не может быть пустым</p>
</div>
<div class="form-group">
<label for="text">Текст о заседании</label>
<textarea class="form-control" id="text" th:field="*{text}" placeholder="Текст о заседании"
style="height: 300px"></textarea>
<p th:if="${#fields.hasErrors('text')}" th:class="${#fields.hasErrors('text')}? error">
Не может быть пустым</p>
</div>
<div class="form-group">
<label for="date">Дата заседания</label>
<input type="text" id="date" class="form-control" data-target="#date" th:field="*{date}"/>
<p th:if="${#fields.hasErrors('date')}" th:class="${#fields.hasErrors('date')}? error">
Не может быть пустым</p>
</div>
<button type="submit" class="btn btn-outline-dark">Сохранить</button>
<a href="javascript:history.back()" class="btn btn-outline-dark">Отмена</a>
</form>
<script>
$(function () {
$("#date").datetimepicker({
format: 'dd.mm.yyyy hh:ii',
locale: 'ru'
});
});
</script>
</div>
</html>

View File

@ -4,10 +4,11 @@
layout:decorate="~{default}"> layout:decorate="~{default}">
<div class="container" layout:fragment="content"> <div class="container" layout:fragment="content">
<h3>Редактирование новости:</h3> <h3>Редактирование новости:</h3>
<form action="#" th:action="@{/news/saveNews}" th:object="${news}" method="post"> <form action="#" th:action="@{/news/saveNews}" th:object="${news}" method="post" enctype="multipart/form-data">
<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,6 +22,14 @@
<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>

View File

@ -5,15 +5,24 @@
<div th:each="n : ${news}" class="news"> <div th:each="n : ${news}" class="news">
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<img class="news-image" src="/img/logo.svg"/> <img class="news-image" th:src="@{'/files/' + ${n.imageFileName}}"/>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
<a th:href="@{'/news/news/' + ${n.id}}" class="link-dark"><h5 th:text="${n.title}"/></a> <a th:if="${n.meeting == null}" th:href="@{'/news/news/' + ${n.id}}" class="link-dark"><h5
th:text="${n.title}"/></a>
<a th:if="${n.meeting != null}" th:href="@{'/meetings/meetings/' + ${n.meeting.id}}"
class="link-dark"><h5 th:text="${n.title}"/></a>
</div> </div>
</div> </div>
<div th:text="${n.preview}" 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}"
class="news-item"></div>
<div th:if="${n.meeting != null}"
th:text="${'Дата: ' + #calendars.format(n.meeting.date, 'dd.MM.yyyy HH:mm')}"
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

@ -0,0 +1,34 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
layout:decorate="~{default}">
<div class="container" layout:fragment="content">
<div th:each="m : ${meetings}" class="news">
<div class="row">
<div class="col-md-10">
<a th:href="@{'/meetings/meetings/' + ${m.id}}" class="link-dark"><h5 th:text="${m.title}"/></a>
</div>
<div sec:authorize="hasRole('ROLE_ADMIN')" class="col-md-2" style="text-align: right">
<a th:href="@{'/meetings/editMeeting/' + ${m.id}}" class="link-dark">
<i class="fa fa-pencil" aria-hidden="true"></i>
</a>
<a th:href="@{'/meetings/deleteMeeting/' + ${m.id}}" class="link-dark"
onclick="return confirm('Удалить запись о заседании?')">
<i class="fa fa-trash" aria-hidden="true"></i>
</a>
</div>
</div>
<hr/>
</div>
<div th:if="${meetings.totalPages > 0}" class="pagination">
<span style="float: left; padding: 5px 5px;">Страницы:</span>
</div>
<div th:if="${meetings.totalPages > 0}" class="pagination"
th:each="pageNumber : ${pageNumbers}">
<a th:href="@{/news/news(size=${meetings.size}, page=${pageNumber})}"
th:text=${pageNumber}
th:class="${pageNumber == meetings.number+1} ? active"></a>
</div>
</div>
</html>

View File

@ -6,15 +6,18 @@
<div th:each="n : ${news}" class="news"> <div th:each="n : ${news}" class="news">
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<img class="news-image" src="/img/logo.svg"/> <img class="news-image" th:src="@{'/files/' + ${n.imageFileName}}"/>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
<a th:href="@{'/news/news/' + ${n.id}}" class="link-dark"><h5 th:text="${n.title}"/></a> <a th:if="${n.meeting == null}" th:href="@{'/news/news/' + ${n.id}}" class="link-dark"><h5
th:text="${n.title}"/></a>
<a th:if="${n.meeting != null}" th:href="@{'/meetings/meetings/' + ${n.meeting.id}}"
class="link-dark"><h5 th:text="${n.title}"/></a>
</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="@{'/news/editNews/' + ${n.id}}" class="link-dark"> <a th:if="${n.meeting == null}" th:href="@{'/news/editNews/' + ${n.id}}" class="link-dark">
<i class="fa fa-pencil" aria-hidden="true"></i> <i class="fa fa-pencil" aria-hidden="true"></i>
</a> </a>
<a th:href="@{'/news/deleteNews/' + ${n.id}}" class="link-dark" <a th:href="@{'/news/deleteNews/' + ${n.id}}" class="link-dark"
@ -23,7 +26,13 @@
</a> </a>
</div> </div>
</div> </div>
<div th:text="${n.preview}" 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}"
class="news-item"></div>
<div th:if="${n.meeting != null}"
th:text="${'Дата: ' + #calendars.format(n.meeting.date, 'dd.MM.yyyy HH:mm')}"
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

@ -0,0 +1,29 @@
<!--
~ Copyright (C) 2021 Anton Romanov - All Rights Reserved
~ You may use, distribute and modify this code, please write to: romanov73@gmail.com.
~
-->
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<div class="container" layout:fragment="content">
<h3>Состав организаторов семинара</h3>
<ul>
<li>
Ярушкина Н.Г., профессор, д.т.н., профессор
</li>
<li>
Мошкин В.С., доцент, к.т.н.
</li>
<li>
Романов А.А., зав. кафедрой, доцент, к.т.н., доцент
</li>
<li>
Гуськов Г.Ю., доцент, к.т.н.
</li>
<li>
Филиппов А.А., доцент, к.т.н.
</li>
</ul>
</div>
</html>

View File

@ -0,0 +1,23 @@
<!--
~ Copyright (C) 2021 Anton Romanov - All Rights Reserved
~ You may use, distribute and modify this code, please write to: romanov73@gmail.com.
~
-->
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.w3.org/1999/xhtml"
layout:decorate="~{default}">
<div class="container" layout:fragment="content">
<div class="row">
<div class="col-md-4">
<img class="news-image" src="/img/logo.svg"/>
</div>
<div class="col-md-8">
<h5 th:text="${meeting.title}"/>
<div th:text="${meeting.text}" class="news-item"></div>
</div>
</div>
<a href="javascript:history.back()" class="btn btn-outline-dark" style="text-align: right">Назад</a>
</div>
</html>

View File

@ -5,7 +5,7 @@
<div class="container" layout:fragment="content"> <div class="container" layout:fragment="content">
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<img class="news-image" src="/img/logo.svg"/> <img class="news-image" th:src="@{'/files/' + ${news.imageFileName}}"/>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<h5 th:text="${news.title}"/> <h5 th:text="${news.title}"/>

BIN
upload/1647261445926 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
upload/1647262081164 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

58
upload/1647262775259 Normal file
View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="1000"
height="1000"
viewBox="0 0 500 500"
xml:space="preserve"
id="svg45"
sodipodi:docname="IS_logo_black.svg"
inkscape:version="1.1.2 (b8e25be833, 2022-02-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview47"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
showgrid="false"
showborder="false"
inkscape:zoom="0.769"
inkscape:cx="500"
inkscape:cy="500"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg45" />
<desc
id="desc29">Created with Fabric.js 4.6.0</desc>
<defs
id="defs31">
</defs>
<path
id="path33"
style="fill:#000000;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
d="M 614.48828 177.41406 C 585.03403 177.64615 555.102 180 528 180 C 509.268 180 474.11592 173.20281 457.41992 181.83789 C 441.12592 190.26483 422.34 188.47467 404 190.33789 C 365.3278 194.26683 326.787 197.16411 288 199.82031 C 261.38 201.64331 230.50279 201.95901 212.77539 226.25781 C 196.16547 249.02541 217.5264 250.08661 219.7832 270.00781 C 223.4616 302.47861 220 337.3084 220 370 C 220 424.688 216.64862 482.378 228.69922 536 C 234.13282 560.178 241.07448 585.21445 258.58008 603.81445 C 286.96128 633.96645 327.183 628.83584 364 632.33984 C 434.958 639.09184 507.266 629.02169 578 624.17969 C 620.728 621.25169 671.094 620.42538 710 600.35938 C 785.886 561.21938 770.028 472.564 770 402 C 769.978 343.7938 783.18837 263.01536 746.73438 213.03516 C 735.77437 198.01096 714.65742 193.13757 698.10742 187.56055 C 672.91842 179.07202 643.94253 177.18197 614.48828 177.41406 z M 451.28516 278.24414 C 508.18157 277.94477 575.51534 292.60888 617.99609 321.03906 C 665.52009 352.84446 673.38009 409.60016 625.99609 446.66016 C 609.19809 459.79816 583.47409 470.56595 561.99609 465.75195 C 553.62409 463.87395 546.58609 456.87805 537.99609 456.49805 C 522.09609 455.79605 512.77609 463.8217 495.99609 456.5957 C 476.36009 448.1397 462.80717 414.5508 471.20117 394.2168 C 474.36317 386.5588 491.70145 379.35103 497.93945 387.20703 C 508.38745 400.36503 494.40964 432.52702 516.18164 440.79102 C 530.60964 446.26702 534.58192 411.00817 555.66992 414.32617 C 563.02792 415.48417 559.99497 424.656 562.04297 430 C 565.00297 437.722 572.19609 445.56767 579.99609 448.51367 C 613.82609 461.29167 642.2743 406.694 629.9043 380 C 614.3683 346.4774 578.65809 326.98059 545.99609 313.90039 C 482.01609 288.27899 396.70598 263.8648 347.51758 330 C 318.46498 369.0616 331.97353 426.708 360.51953 462 C 409.14233 522.116 497.53009 557.79392 573.99609 553.91992 C 589.34209 553.14192 620.65517 534.20281 630.95117 522.88281 C 639.49717 513.48281 642.08522 495.18492 661.69922 503.79492 C 677.95722 510.93492 656.70214 530.07166 649.99414 535.34766 C 623.11814 556.49766 583.00409 562 549.99609 562 C 468.35409 562 372.36956 530.03 324.10156 460 C 268.98216 380.028 324.58909 293.80382 413.99609 280.85742 C 425.58059 279.17993 438.15521 278.31323 451.28516 278.24414 z "
transform="scale(0.5)" />
<path
id="path39"
style="fill:#000000;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
d="M 668.33594 643.47656 C 662.50706 643.55654 656.40463 644.00634 650 644.84766 C 557.68 656.97566 466.83 659.8418 374 659.8418 C 334.445 659.8418 285.9012 644.14103 248 660.95703 C 236.5832 666.02303 220.35223 674.28205 214.67383 685.99805 C 208.78323 698.15205 227.05259 701.474 224.27539 714 C 219.13139 737.202 163.19081 780.43105 192.00977 796.62305 C 213.03631 808.43705 243.23832 802.31272 265.85352 808.63672 C 272.15212 810.39872 271.30588 817.50928 276.70508 819.36328 C 297.12228 826.37328 330.3592 820 352 820 L 522 820 C 564.108 820 614.34895 827.63112 655.62695 819.70312 C 662.58095 818.36712 661.85506 809.62887 670.03906 808.29688 C 720.71106 800.04488 767.32912 814.45956 813.70312 783.85156 C 829.97513 773.11156 793.83588 738.344 786.17188 726 C 755.44819 676.52238 724.68174 642.70346 668.33594 643.47656 z M 632.96094 696.08594 C 656.9772 696.33526 670.5987 710.58 684.6582 732 C 687.2762 735.988 699.90202 751.25736 696.16602 755.94336 C 692.75802 760.21936 680.742 757.31823 676 757.49023 C 655.976 758.22223 636.016 761.75419 616 761.99219 C 537.304 762.92819 456.724 769.21231 378 763.82031 C 355.2814 762.26631 332.628 759.24231 310 756.57031 C 305.7648 756.07031 289.0863 758.23892 287.8125 752.79492 C 284.9009 740.34892 309.79135 708.10719 322.00195 708.11719 C 423.72835 708.20119 520.668 711.33947 622 696.85547 C 625.88675 696.29997 629.53004 696.05032 632.96094 696.08594 z "
transform="scale(0.5)" />
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

0
upload/1647262911771 Normal file
View File

0
upload/1647262925015 Normal file
View File

BIN
upload/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB