Compare commits

..

11 Commits

Author SHA1 Message Date
746da497de #5 -- Add rule only if variables are defined
All checks were successful
CI fuzzy controller / container-test-job (push) Successful in 1m1s
2025-02-21 11:45:38 +04:00
df451f01ce #5 -- Fix layout
All checks were successful
CI fuzzy controller / container-test-job (push) Successful in 1m45s
2025-02-21 11:40:49 +04:00
202d8cd0d3 #5 -- Fix validation
All checks were successful
CI fuzzy controller / container-test-job (push) Successful in 1m10s
2025-02-21 10:40:47 +04:00
c33680fd66 #5 -- Fix table identity strategy
Some checks failed
CI fuzzy controller / container-test-job (push) Failing after 11s
2025-02-21 10:40:03 +04:00
47059bc2de #5 -- Add rule edit link
All checks were successful
CI fuzzy controller / container-test-job (push) Successful in 1m4s
2025-02-20 18:53:39 +04:00
4449902185 #5 -- Validate edit
All checks were successful
CI fuzzy controller / container-test-job (push) Successful in 2m28s
2025-02-20 16:14:55 +04:00
972284fad3 #5 -- Fix save variables
All checks were successful
CI fuzzy controller / container-test-job (push) Successful in 1m11s
2025-02-20 13:52:14 +04:00
fb01034fc8 #5 -- Fix variables
All checks were successful
CI fuzzy controller / container-test-job (push) Successful in 57s
2025-02-19 17:44:51 +04:00
3723881909 #5 -- Add variables and terms to db
All checks were successful
CI fuzzy controller / container-test-job (push) Successful in 1m2s
2025-02-19 17:02:37 +04:00
11fc4e46d1 #5 -- Show and edit rules
All checks were successful
CI fuzzy controller / container-test-job (push) Successful in 1m47s
2025-02-19 14:33:29 +04:00
486981c0ff #5 -- Rule model as a string
All checks were successful
CI fuzzy controller / container-test-job (push) Successful in 1m3s
2025-02-18 17:54:30 +04:00
36 changed files with 628 additions and 354 deletions

View File

@ -1,3 +1,4 @@
{ {
"java.configuration.updateBuildConfiguration": "interactive" "java.configuration.updateBuildConfiguration": "interactive",
"java.compile.nullAnalysis.mode": "disabled"
} }

View File

@ -4,7 +4,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
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.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;

View File

@ -12,7 +12,7 @@ import java.io.Serializable;
@MappedSuperclass @MappedSuperclass
public abstract class BaseEntity implements Serializable, Comparable<BaseEntity> { public abstract class BaseEntity implements Serializable, Comparable<BaseEntity> {
@Id @Id
@GeneratedValue(strategy = GenerationType.TABLE) @GeneratedValue(strategy = GenerationType.SEQUENCE)
private Integer id; private Integer id;
@Version @Version

View File

@ -12,6 +12,7 @@ import ru.ulstu.fc.project.model.Project;
import ru.ulstu.fc.project.model.ProjectForm; import ru.ulstu.fc.project.model.ProjectForm;
import ru.ulstu.fc.project.service.ProjectRulesService; import ru.ulstu.fc.project.service.ProjectRulesService;
import ru.ulstu.fc.project.service.ProjectService; import ru.ulstu.fc.project.service.ProjectService;
import ru.ulstu.fc.project.service.ProjectVariableService;
import ru.ulstu.fc.user.model.UserRoleConstants; import ru.ulstu.fc.user.model.UserRoleConstants;
@Controller @Controller
@ -21,11 +22,14 @@ import ru.ulstu.fc.user.model.UserRoleConstants;
public class ProjectController { public class ProjectController {
private final ProjectService projectService; private final ProjectService projectService;
private final ProjectRulesService projectRulesService; private final ProjectRulesService projectRulesService;
private final ProjectVariableService projectVariableService;
public ProjectController(ProjectService projectService, public ProjectController(ProjectService projectService,
ProjectRulesService projectRulesService) { ProjectRulesService projectRulesService,
ProjectVariableService projectVariableService) {
this.projectService = projectService; this.projectService = projectService;
this.projectRulesService = projectRulesService; this.projectRulesService = projectRulesService;
this.projectVariableService = projectVariableService;
} }
@GetMapping("list") @GetMapping("list")
@ -42,6 +46,7 @@ public class ProjectController {
: new Project())); : new Project()));
model.addAttribute("rules", projectRulesService.getByProjectId(id)); model.addAttribute("rules", projectRulesService.getByProjectId(id));
model.addAttribute("variables", projectVariableService.getByProjectId(id));
return "project/edit"; return "project/edit";
} }

View File

@ -1,19 +1,20 @@
package ru.ulstu.fc.project.model; package ru.ulstu.fc.project.model;
import java.util.Date;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import ru.ulstu.fc.core.model.BaseEntity; import ru.ulstu.fc.core.model.BaseEntity;
import ru.ulstu.fc.user.model.User; import ru.ulstu.fc.user.model.User;
import java.util.Date;
@Entity @Entity
public class Project extends BaseEntity { public class Project extends BaseEntity {
@NotEmpty(message = "Текст новости не может быть пустым") @NotEmpty(message = "Текст новости не может быть пустым")
private String name; private String name;
private Date createDate = new Date(); private Date createDate = new Date();
@ManyToOne @ManyToOne(cascade = CascadeType.MERGE)
private User user; private User user;
public Project() { public Project() {

View File

@ -1,12 +0,0 @@
package ru.ulstu.fc.project.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.ulstu.fc.rule.model.Rule;
public interface RuleRepository extends JpaRepository<Rule, Integer> {
List<Rule> findByProjectId(Integer projectId);
}

View File

@ -1,21 +1,28 @@
package ru.ulstu.fc.project.service; package ru.ulstu.fc.project.service;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ru.ulstu.fc.project.repository.RuleRepository; import ru.ulstu.fc.rule.repository.FuzzyRuleRepository;
import ru.ulstu.fc.rule.model.Rule; import ru.ulstu.fc.rule.model.FuzzyRule;
@Service @Service
public class ProjectRulesService { public class ProjectRulesService {
private final RuleRepository ruleRepository; private final FuzzyRuleRepository ruleRepository;
private final ProjectService projectService;
public ProjectRulesService(RuleRepository ruleRepository) { public ProjectRulesService(FuzzyRuleRepository ruleRepository,
ProjectService projectService) {
this.ruleRepository = ruleRepository; this.ruleRepository = ruleRepository;
this.projectService = projectService;
} }
public List<Rule> getByProjectId(Integer projectId) { public List<FuzzyRule> getByProjectId(Integer projectId) {
return ruleRepository.findByProjectId(projectId); if (projectId == null || projectId == 0) {
return Collections.emptyList();
}
return ruleRepository.findByProject(projectService.getById(projectId));
} }
} }

View File

@ -0,0 +1,28 @@
package ru.ulstu.fc.project.service;
import java.util.Collections;
import java.util.List;
import org.springframework.stereotype.Service;
import ru.ulstu.fc.rule.repository.VariableRepository;
import ru.ulstu.fc.rule.model.Variable;
@Service
public class ProjectVariableService {
private final VariableRepository variableRepository;
private final ProjectService projectService;
public ProjectVariableService(VariableRepository variableRepository,
ProjectService projectService) {
this.variableRepository = variableRepository;
this.projectService = projectService;
}
public List<Variable> getByProjectId(Integer projectId) {
if (projectId == null || projectId == 0) {
return Collections.emptyList();
}
return variableRepository.findByProject(projectService.getById(projectId));
}
}

View File

@ -7,10 +7,9 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import ru.ulstu.fc.rule.model.Antecedent;
import ru.ulstu.fc.rule.model.InferenceForm; import ru.ulstu.fc.rule.model.InferenceForm;
import ru.ulstu.fc.rule.model.FuzzyTerm;
import ru.ulstu.fc.rule.model.Variable; import ru.ulstu.fc.rule.model.Variable;
import ru.ulstu.fc.rule.model.VariableValue;
import ru.ulstu.fc.rule.service.FuzzyInferenceService; import ru.ulstu.fc.rule.service.FuzzyInferenceService;
import java.util.Arrays; import java.util.Arrays;
@ -45,21 +44,21 @@ public class InferenceMvcController {
return "index"; return "index";
} }
private List<VariableValue> getAgeValues() { private List<FuzzyTerm> getAgeValues() {
Variable var = new Variable("Age"); Variable var = new Variable("Age");
var.getValues().addAll(Arrays.asList( var.getFuzzyTerms().addAll(Arrays.asList(
new VariableValue("молодой", 30.0), new FuzzyTerm("молодой", 30.0),
new VariableValue("средний", 45.0), new FuzzyTerm("средний", 45.0),
new VariableValue("старый", 60.0))); new FuzzyTerm("старый", 60.0)));
return var.getValues(); return var.getFuzzyTerms();
} }
private List<VariableValue> getIncomeValues() { private List<FuzzyTerm> getIncomeValues() {
Variable var = new Variable("Income"); Variable var = new Variable("Income");
var.getValues().addAll(Arrays.asList( var.getFuzzyTerms().addAll(Arrays.asList(
new VariableValue("небольшой", 20000.0), new FuzzyTerm("небольшой", 20000.0),
new VariableValue("средний", 90000.0), new FuzzyTerm("средний", 90000.0),
new VariableValue("высокий", 200000.0))); new FuzzyTerm("высокий", 200000.0)));
return var.getValues(); return var.getFuzzyTerms();
} }
} }

View File

@ -2,21 +2,22 @@ package ru.ulstu.fc.rule.controller;
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.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; 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 ru.ulstu.fc.rule.model.Rule; import jakarta.validation.Valid;
import ru.ulstu.fc.rule.model.RuleForm; import ru.ulstu.fc.rule.model.FuzzyRuleForm;
import ru.ulstu.fc.rule.service.RuleService; import ru.ulstu.fc.rule.service.FuzzyRuleService;
@Controller @Controller
@RequestMapping("rule") @RequestMapping("rule")
public class RuleController { public class RuleController {
private final RuleService ruleService; private final FuzzyRuleService ruleService;
public RuleController(RuleService ruleService) { public RuleController(FuzzyRuleService ruleService) {
this.ruleService = ruleService; this.ruleService = ruleService;
} }
@ -24,25 +25,28 @@ public class RuleController {
public String edit(@PathVariable(value = "projectId") Integer projectId, public String edit(@PathVariable(value = "projectId") Integer projectId,
@PathVariable(value = "ruleId") Integer id, Model model) { @PathVariable(value = "ruleId") Integer id, Model model) {
model.addAttribute("projectId", projectId); model.addAttribute("projectId", projectId);
model.addAttribute("rule", model.addAttribute("fuzzyRuleForm",
new RuleForm((id != null && id != 0) (id != null && id != 0)
? ruleService.getById(id) ? new FuzzyRuleForm(id, ruleService.getById(id))
: new Rule())); : new FuzzyRuleForm(id, projectId));
return "rule/edit"; return "rule/edit";
} }
@PostMapping(value = "save", params = "save") @PostMapping(value = "save", params = "save")
public String save(RuleForm ruleForm, Model model) { public String save(@Valid FuzzyRuleForm fuzzyRuleForm, BindingResult bindingResult, Model model) {
model.addAttribute("rule", ruleService.save(ruleForm)); if (bindingResult.hasErrors()) {
return "redirect:/project/edit/" + ruleForm.getProjectId(); model.addAttribute("projectId", fuzzyRuleForm.getProjectId());
return "rule/edit";
}
ruleService.save(fuzzyRuleForm);
return "redirect:/project/edit/" + fuzzyRuleForm.getProjectId();
} }
@PostMapping(value = "save", params = "delete") @PostMapping(value = "save", params = "delete")
public String delete(RuleForm ruleForm) { public String delete(FuzzyRuleForm fuzzyRuleForm) {
if (ruleForm != null && ruleForm.getId() != null) { if (fuzzyRuleForm != null && fuzzyRuleForm.getId() != null) {
ruleService.delete(ruleForm); ruleService.delete(fuzzyRuleForm);
} }
return "redirect:/project/edit/" + ruleForm.getProjectId(); return "redirect:/project/edit/" + fuzzyRuleForm.getProjectId();
} }
} }

View File

@ -0,0 +1,52 @@
package ru.ulstu.fc.rule.controller;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.validation.Valid;
import ru.ulstu.fc.rule.model.VariableForm;
import ru.ulstu.fc.rule.service.VariableService;
@Controller
@RequestMapping("var")
public class VariableController {
private final VariableService variableService;
public VariableController(VariableService variableService) {
this.variableService = variableService;
}
@GetMapping("/edit/{projectId}/{varId}")
public String edit(@PathVariable(value = "projectId") Integer projectId,
@PathVariable(value = "varId") Integer id, Model model) {
model.addAttribute("projectId", projectId);
model.addAttribute("variableForm",
(id != null && id != 0)
? new VariableForm(id, variableService.getById(id))
: new VariableForm(id, projectId));
return "var/edit";
}
@PostMapping(value = "save", params = "save")
public String save(@Valid VariableForm variableForm, BindingResult result, Model model) {
if (result.hasErrors()) {
model.addAttribute("projectId", variableForm.getProjectId());
return "var/edit";
}
variableService.save(variableForm);
return "redirect:/project/edit/" + variableForm.getProjectId();
}
@PostMapping(value = "save", params = "delete")
public String delete(VariableForm variableForm) {
if (variableForm != null && variableForm.getId() != null) {
variableService.delete(variableForm);
}
return "redirect:/project/edit/" + variableForm.getProjectId();
}
}

View File

@ -1,25 +0,0 @@
package ru.ulstu.fc.rule.model;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import ru.ulstu.fc.core.model.BaseEntity;
@Entity
public class Antecedent extends BaseEntity {
@ManyToOne
private Variable variable;
private String value;
public Variable getVariable() {
return variable;
}
public void setVariable(Variable variable) {
this.variable = variable;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@ -1,29 +0,0 @@
package ru.ulstu.fc.rule.model;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import ru.ulstu.fc.core.model.BaseEntity;
@Entity
public class Consequent extends BaseEntity {
@ManyToOne
private Variable variable;
private String value;
public Variable getVariable() {
return variable;
}
public void setVariable(Variable variable) {
this.variable = variable;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@ -0,0 +1,36 @@
package ru.ulstu.fc.rule.model;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import ru.ulstu.fc.core.model.BaseEntity;
import ru.ulstu.fc.project.model.Project;
@Entity
public class FuzzyRule extends BaseEntity {
@ManyToOne
@NotNull
private Project project;
@Size(min = 5, max = 250, message = "Длина от 5 до 250 символов")
private String content;
public FuzzyRule() {
}
public Project getProject() {
return project;
}
public void setProject(Project project) {
this.project = project;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}

View File

@ -0,0 +1,52 @@
package ru.ulstu.fc.rule.model;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class FuzzyRuleForm {
private Integer id;
@NotNull
private Integer projectId;
@Size(min = 5, max = 250, message = "Длина от 5 до 250 символов")
private String content;
public FuzzyRuleForm() {
}
public FuzzyRuleForm(Integer id, Integer projectId) {
this.id = id;
this.projectId = projectId;
}
public FuzzyRuleForm(Integer id, FuzzyRule fuzzyRule) {
this.id = fuzzyRule.getId();
this.projectId = fuzzyRule.getProject().getId();
this.content = fuzzyRule.getContent();
}
public Integer getProjectId() {
return projectId;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setProjectId(Integer projectId) {
this.projectId = projectId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}

View File

@ -0,0 +1,34 @@
package ru.ulstu.fc.rule.model;
import jakarta.persistence.Entity;
import ru.ulstu.fc.core.model.BaseEntity;
@Entity
public class FuzzyTerm extends BaseEntity {
private String description;
private Double crispValue;
public FuzzyTerm() {
}
public FuzzyTerm(String description, Double value) {
this.description = description;
this.crispValue = value;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Double getCrispValue() {
return crispValue;
}
public void setCrispValue(Double crispValue) {
this.crispValue = crispValue;
}
}

View File

@ -1,47 +0,0 @@
package ru.ulstu.fc.rule.model;
import java.util.List;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import ru.ulstu.fc.core.model.BaseEntity;
import ru.ulstu.fc.project.model.Project;
@Entity
public class Rule extends BaseEntity {
@ManyToOne
private Project project;
@OneToMany
private List<Antecedent> antecedents; //TODO: AND / OR?
@ManyToOne
private Consequent consequent;
public Rule() {
}
public Project getProject() {
return project;
}
public void setProject(Project project) {
this.project = project;
}
public List<Antecedent> getAntecedents() {
return antecedents;
}
public void setAntecedents(List<Antecedent> antecedents) {
this.antecedents = antecedents;
}
public Consequent getConsequent() {
return consequent;
}
public void setConsequent(Consequent consequent) {
this.consequent = consequent;
}
}

View File

@ -1,32 +0,0 @@
package ru.ulstu.fc.rule.model;
public class RuleForm {
private Integer id;
private Integer projectId;
public RuleForm() {
}
public RuleForm(Rule rule) {
this.projectId = (rule == null || rule.getProject() == null)
? null
: rule.getProject().getId();
}
public Integer getProjectId() {
return projectId;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setProjectId(Integer projectId) {
this.projectId = projectId;
}
}

View File

@ -3,33 +3,73 @@ package ru.ulstu.fc.rule.model;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import ru.ulstu.fc.core.model.BaseEntity; import ru.ulstu.fc.core.model.BaseEntity;
import ru.ulstu.fc.project.model.Project;
@Entity @Entity
public class Variable extends BaseEntity { public class Variable extends BaseEntity {
@Size(min = 3, max = 250, message = "Длина должна быть от 3 до 250")
private String name; private String name;
@OneToMany
private List<VariableValue> values = new ArrayList<>(); @ManyToOne
@NotNull
private Project project;
private boolean input = true;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "variable_id", unique = true)
private List<FuzzyTerm> fuzzyTerms = new ArrayList<>();
public Variable() { public Variable() {
} }
public Variable(String name, List<VariableValue> values) {
this.name = name;
this.values = values;
}
public Variable(String name) { public Variable(String name) {
this.name = name; this.name = name;
} }
public Variable(String name, List<FuzzyTerm> fuzzyTerms) {
this.name = name;
this.fuzzyTerms = fuzzyTerms;
}
public String getName() { public String getName() {
return name; return name;
} }
public List<VariableValue> getValues() { public void setName(String name) {
return values; this.name = name;
}
public List<FuzzyTerm> getFuzzyTerms() {
return fuzzyTerms;
}
public void setFuzzyTerms(List<FuzzyTerm> fuzzyTerms) {
this.fuzzyTerms = fuzzyTerms;
}
public Project getProject() {
return project;
}
public void setProject(Project project) {
this.project = project;
}
public boolean isInput() {
return input;
}
public void setInput(boolean input) {
this.input = input;
} }
} }

View File

@ -0,0 +1,61 @@
package ru.ulstu.fc.rule.model;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class VariableForm {
private Integer id;
@NotNull
private Integer projectId;
@Size(min = 3, max = 250, message = "Длина должна быть от 3 до 250")
private String name;
private boolean input = true;
public VariableForm() {
}
public VariableForm(Integer id, Integer projectId) {
this.id = id;
this.projectId = projectId;
}
public VariableForm(Integer id, Variable variable) {
this.id = id;
this.projectId = variable.getProject().getId();
this.name = variable.getName();
this.input = variable.isInput();
}
public Integer getProjectId() {
return projectId;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setProjectId(Integer projectId) {
this.projectId = projectId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isInput() {
return input;
}
public void setInput(boolean input) {
this.input = input;
}
}

View File

@ -1,34 +0,0 @@
package ru.ulstu.fc.rule.model;
import jakarta.persistence.Entity;
import ru.ulstu.fc.core.model.BaseEntity;
@Entity
public class VariableValue extends BaseEntity {
private String fuzzyTerm;
private Double value;
public VariableValue() {
}
public VariableValue(String fuzzyTerm, Double value) {
this.fuzzyTerm = fuzzyTerm;
this.value = value;
}
public String getFuzzyTerm() {
return fuzzyTerm;
}
public void setFuzzyTerm(String fuzzyTerm) {
this.fuzzyTerm = fuzzyTerm;
}
public Double getValue() {
return value;
}
public void setValue(Double value) {
this.value = value;
}
}

View File

@ -0,0 +1,13 @@
package ru.ulstu.fc.rule.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.ulstu.fc.project.model.Project;
import ru.ulstu.fc.rule.model.FuzzyRule;
public interface FuzzyRuleRepository extends JpaRepository<FuzzyRule, Integer> {
List<FuzzyRule> findByProject(Project project);
}

View File

@ -0,0 +1,13 @@
package ru.ulstu.fc.rule.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.ulstu.fc.project.model.Project;
import ru.ulstu.fc.rule.model.Variable;
public interface VariableRepository extends JpaRepository<Variable, Integer> {
List<Variable> findByProject(Project project);
}

View File

@ -15,8 +15,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ru.ulstu.fc.rule.model.OutputValue; import ru.ulstu.fc.rule.model.OutputValue;
import ru.ulstu.fc.rule.model.FuzzyTerm;
import ru.ulstu.fc.rule.model.Variable; import ru.ulstu.fc.rule.model.Variable;
import ru.ulstu.fc.rule.model.VariableValue;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -45,15 +45,15 @@ public class FuzzyInferenceService {
final InputVariable input = new InputVariable(); final InputVariable input = new InputVariable();
input.setName(variable.getName()); input.setName(variable.getName());
input.setDescription(""); input.setDescription("");
input.setRange(0, variable.getValues().get(variable.getValues().size() - 1).getValue()); input.setRange(0, variable.getFuzzyTerms().get(variable.getFuzzyTerms().size() - 1).getCrispValue());
input.setEnabled(true); input.setEnabled(true);
input.setLockValueInRange(false); input.setLockValueInRange(false);
double prev = 0; double prev = 0;
for (int i = 0; i < variable.getValues().size(); i++) { for (int i = 0; i < variable.getFuzzyTerms().size(); i++) {
Triangle term = new Triangle(variable.getValues().get(i).getFuzzyTerm(), Triangle term = new Triangle(variable.getFuzzyTerms().get(i).getDescription(),
prev, prev,
variable.getValues().get(i).getValue(), variable.getFuzzyTerms().get(i).getCrispValue(),
variable.getValues().get(i).getValue() + variable.getValues().get(i).getValue() - prev); variable.getFuzzyTerms().get(i).getCrispValue() + variable.getFuzzyTerms().get(i).getCrispValue() - prev);
prev = term.getVertexB(); prev = term.getVertexB();
input.addTerm(term); input.addTerm(term);
} }
@ -64,19 +64,19 @@ public class FuzzyInferenceService {
final OutputVariable output = new OutputVariable(); final OutputVariable output = new OutputVariable();
output.setName(variable.getName()); output.setName(variable.getName());
output.setDescription(""); output.setDescription("");
output.setRange(0, variable.getValues().get(variable.getValues().size() - 1).getValue()); output.setRange(0, variable.getFuzzyTerms().get(variable.getFuzzyTerms().size() - 1).getCrispValue());
output.setEnabled(true); output.setEnabled(true);
output.setAggregation(new Maximum()); output.setAggregation(new Maximum());
output.setDefuzzifier(new WeightedAverage()); output.setDefuzzifier(new WeightedAverage());
output.setDefaultValue(Double.NaN); output.setDefaultValue(Double.NaN);
output.setLockValueInRange(false); output.setLockValueInRange(false);
double prev = 0; double prev = 0;
for (int i = 0; i < variable.getValues().size(); i++) { for (int i = 0; i < variable.getFuzzyTerms().size(); i++) {
Triangle term = new Triangle( Triangle term = new Triangle(
variable.getValues().get(i).getFuzzyTerm(), variable.getFuzzyTerms().get(i).getDescription(),
prev, prev,
variable.getValues().get(i).getValue(), variable.getFuzzyTerms().get(i).getCrispValue(),
variable.getValues().get(i).getValue() + variable.getValues().get(i).getValue() - prev); variable.getFuzzyTerms().get(i).getCrispValue() + variable.getFuzzyTerms().get(i).getCrispValue() - prev);
prev = term.getVertexB(); prev = term.getVertexB();
output.addTerm(term); output.addTerm(term);
} }
@ -98,6 +98,7 @@ public class FuzzyInferenceService {
mamdani.setImplication(new AlgebraicProduct()); mamdani.setImplication(new AlgebraicProduct());
mamdani.setActivation(new General()); mamdani.setActivation(new General());
rules.forEach(r -> mamdani.addRule(Rule.parse(r, engine))); rules.forEach(r -> mamdani.addRule(Rule.parse(r, engine)));
mamdani.addRule(new Rule());
return mamdani; return mamdani;
} }
@ -130,20 +131,20 @@ public class FuzzyInferenceService {
public List<OutputValue> getFuzzyInference(Map<String, Double> vals) { public List<OutputValue> getFuzzyInference(Map<String, Double> vals) {
return getFuzzyInference(getDemoRules(), vals, return getFuzzyInference(getDemoRules(), vals,
List.of(new Variable("возраст", List.of( List.of(new Variable("возраст", List.of(
new VariableValue("молодой", 35.0), new FuzzyTerm("молодой", 35.0),
new VariableValue("средний", 60.0), new FuzzyTerm("средний", 60.0),
new VariableValue("старый", 100.0)) new FuzzyTerm("старый", 100.0))
), ),
new Variable("доход", List.of( new Variable("доход", List.of(
new VariableValue("небольшой", 35000.0), new FuzzyTerm("небольшой", 35000.0),
new VariableValue("средний", 100000.0), new FuzzyTerm("средний", 100000.0),
new VariableValue("высокий", 500000.0)) new FuzzyTerm("высокий", 500000.0))
) )
), ),
new Variable("кредит", List.of( new Variable("кредит", List.of(
new VariableValue("небольшой", 20000.0), new FuzzyTerm("небольшой", 20000.0),
new VariableValue("средний", 100000.0), new FuzzyTerm("средний", 100000.0),
new VariableValue("большой", 1000000.0))) new FuzzyTerm("большой", 1000000.0)))
); );
} }

View File

@ -0,0 +1,42 @@
package ru.ulstu.fc.rule.service;
import org.springframework.stereotype.Service;
import ru.ulstu.fc.rule.repository.FuzzyRuleRepository;
import ru.ulstu.fc.project.service.ProjectService;
import ru.ulstu.fc.rule.model.FuzzyRule;
import ru.ulstu.fc.rule.model.FuzzyRuleForm;
@Service
public class FuzzyRuleService {
private final FuzzyRuleRepository ruleRepository;
private final ProjectService projectService;
public FuzzyRuleService(FuzzyRuleRepository ruleRepository, ProjectService projectService) {
this.ruleRepository = ruleRepository;
this.projectService = projectService;
}
public FuzzyRule getById(Integer id) {
return ruleRepository
.findById(id)
.orElseThrow(() -> new RuntimeException("Rule not found by id"));
}
public Object save(FuzzyRuleForm ruleForm) {
FuzzyRule rule;
if (ruleForm.getId() == null || ruleForm.getId() == 0) {
rule = new FuzzyRule();
} else {
rule = getById(ruleForm.getId());
}
rule.setProject(projectService.getById(ruleForm.getProjectId()));
rule.setContent(ruleForm.getContent());
return ruleRepository.save(rule);
}
public void delete(FuzzyRuleForm ruleForm) {
getById(ruleForm.getId());
ruleRepository.deleteById(ruleForm.getId());
}
}

View File

@ -1,43 +0,0 @@
package ru.ulstu.fc.rule.service;
import org.springframework.stereotype.Service;
import ru.ulstu.fc.project.repository.RuleRepository;
import ru.ulstu.fc.project.service.ProjectService;
import ru.ulstu.fc.rule.model.Rule;
import ru.ulstu.fc.rule.model.RuleForm;
@Service
public class RuleService {
private final RuleRepository ruleRepository;
private final ProjectService projectService;
public RuleService(RuleRepository ruleRepository, ProjectService projectService) {
this.ruleRepository = ruleRepository;
this.projectService = projectService;
}
public Rule getById(Integer id) {
return ruleRepository
.findById(id)
.orElseThrow(() -> new RuntimeException("Rule not found by id"));
}
public Object save(RuleForm ruleForm) {
if (ruleForm.getId() == null) {
Rule rule = new Rule();
rule.setProject(projectService.getById(ruleForm.getProjectId()));
// rule.set...
return ruleRepository.save(rule);
}
Rule dbRule = getById(ruleForm.getId());
dbRule.setProject(projectService.getById(ruleForm.getProjectId()));
// dbRule.set ...
return ruleRepository.save(dbRule);
}
public void delete(RuleForm ruleForm) {
getById(ruleForm.getId());
ruleRepository.deleteById(ruleForm.getId());
}
}

View File

@ -0,0 +1,43 @@
package ru.ulstu.fc.rule.service;
import org.springframework.stereotype.Service;
import ru.ulstu.fc.project.service.ProjectService;
import ru.ulstu.fc.rule.model.Variable;
import ru.ulstu.fc.rule.model.VariableForm;
import ru.ulstu.fc.rule.repository.VariableRepository;
@Service
public class VariableService {
private final VariableRepository variableRepository;
private final ProjectService projectService;
public VariableService(VariableRepository variableRepository, ProjectService projectService) {
this.variableRepository = variableRepository;
this.projectService = projectService;
}
public Variable getById(Integer id) {
return variableRepository
.findById(id)
.orElseThrow(() -> new RuntimeException("Variable not found by id"));
}
public Variable save(VariableForm variableForm) {
Variable variable;
if (variableForm.getId() == null || variableForm.getId() == 0) {
variable = new Variable();
} else {
variable = getById(variableForm.getId());
}
variable.setProject(projectService.getById(variableForm.getProjectId()));
variable.setName(variableForm.getName());
variable.setInput(variableForm.isInput());
return variableRepository.save(variable);
}
public void delete(VariableForm ruleForm) {
getById(ruleForm.getId());
variableRepository.deleteById(ruleForm.getId());
}
}

View File

@ -7,7 +7,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;

View File

@ -1,21 +1,20 @@
package ru.ulstu.fc.user.model; package ru.ulstu.fc.user.model;
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable; import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import ru.ulstu.fc.config.Constants; import ru.ulstu.fc.config.Constants;
import ru.ulstu.fc.core.model.BaseEntity; import ru.ulstu.fc.core.model.BaseEntity;
import java.util.HashSet;
import java.util.Set;
@Entity @Entity
@Table(name = "is_users") @Table(name = "is_users")
public class User extends BaseEntity { public class User extends BaseEntity {

View File

@ -1,19 +1,18 @@
package ru.ulstu.fc.user.service; package ru.ulstu.fc.user.service;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;
public final class IpAddressResolver { public final class IpAddressResolver {
private static final String CLIENT_IP_HEADER = "Client-IP"; //private static final String CLIENT_IP_HEADER = "Client-IP";
private static final String FORWARDED_FOR_HEADER = "X-Forwarded-For"; //private static final String FORWARDED_FOR_HEADER = "X-Forwarded-For";
public static String getRemoteAddr(HttpServletRequest request) { public static String getRemoteAddr(HttpServletRequest request) {
String headerClientIp = request.getHeader(""); String headerClientIp = request.getHeader("");
String headerXForwardedFor = request.getHeader(HttpServletRequest.FORM_AUTH); String headerXForwardedFor = request.getHeader(HttpServletRequest.FORM_AUTH);
if (StringUtils.isEmpty(request.getRemoteAddr()) && !StringUtils.isEmpty(headerClientIp)) { if (request.getRemoteAddr().isEmpty() && !headerClientIp.isEmpty()) {
return headerClientIp; return headerClientIp;
} }
if (!StringUtils.isEmpty(headerXForwardedFor)) { if (!headerXForwardedFor.isEmpty()) {
return headerXForwardedFor; return headerXForwardedFor;
} }
return request.getRemoteAddr(); return request.getRemoteAddr();

View File

@ -1,21 +1,21 @@
package ru.ulstu.fc.user.service; package ru.ulstu.fc.user.service;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import ru.ulstu.fc.config.Constants; import ru.ulstu.fc.config.Constants;
import java.io.IOException;
@Component @Component
public class UserSessionLoginHandler extends SavedRequestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { public class UserSessionLoginHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private final Logger log = LoggerFactory.getLogger(UserSessionLoginHandler.class); private final Logger log = LoggerFactory.getLogger(UserSessionLoginHandler.class);
private final UserSessionService userSessionService; private final UserSessionService userSessionService;

View File

@ -1,21 +1,21 @@
package ru.ulstu.fc.user.service; package ru.ulstu.fc.user.service;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.stereotype.Component;
import ru.ulstu.fc.config.Constants; import ru.ulstu.fc.config.Constants;
import java.io.IOException;
@Component @Component
public class UserSessionLogoutHandler extends SimpleUrlLogoutSuccessHandler implements LogoutSuccessHandler { public class UserSessionLogoutHandler extends SimpleUrlLogoutSuccessHandler {
private final Logger log = LoggerFactory.getLogger(UserSessionLogoutHandler.class); private final Logger log = LoggerFactory.getLogger(UserSessionLogoutHandler.class);
private final UserSessionService userSessionService; private final UserSessionService userSessionService;

View File

@ -29,7 +29,7 @@
data-width="90%"> data-width="90%">
<option th:each="ageValue : ${ageValues}" <option th:each="ageValue : ${ageValues}"
th:value="${ageValue.value}" th:value="${ageValue.value}"
th:utext="${ageValue.fuzzyTerm}"> th:utext="${ageValue.description}">
</option> </option>
</select> </select>
</div> </div>
@ -44,7 +44,7 @@
data-width="90%"> data-width="90%">
<option th:each="incomeValue : ${incomeValues}" <option th:each="incomeValue : ${incomeValues}"
th:value="${incomeValue.value}" th:value="${incomeValue.value}"
th:utext="${incomeValue.fuzzyTerm}"> th:utext="${incomeValue.description}">
</option> </option>
</select> </select>
</div> </div>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd"> <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html <html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.w3.org/1999/xhtml"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.w3.org/1999/xhtml"
layout:decorate="~{default}"> layout:decorate="~{default}">
<head> <head>
<title>Редактирование проекта</title> <title>Редактирование проекта</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
@ -12,14 +12,8 @@
<input type="hidden" th:field="*{id}"> <input type="hidden" th:field="*{id}">
<div class="form-group"> <div class="form-group">
<label for="name">Название</label> <label for="name">Название</label>
<input th:field="*{name}" <input th:field="*{name}" id="name" type="text" required class="form-control" placeholder="Название">
id="name" <p th:if="${#fields.hasErrors('name')}" th:class="${#fields.hasErrors('name')}? error">
type="text"
required
class="form-control"
placeholder="Название">
<p th:if="${#fields.hasErrors('name')}"
th:class="${#fields.hasErrors('name')}? error">
Не может быть пустым Не может быть пустым
</p> </p>
</div> </div>
@ -27,22 +21,38 @@
<label th:text="'Дата создания: ' + ${#dates.format(project.createDate, 'dd.MM.yyyy HH:mm')}"></label> <label th:text="'Дата создания: ' + ${#dates.format(project.createDate, 'dd.MM.yyyy HH:mm')}"></label>
</div> </div>
<button name="save" type="submit" class="btn btn-outline-dark">Сохранить</button> <button name="save" type="submit" class="btn btn-outline-dark">Сохранить</button>
<button name="delete" <button name="delete" type="submit" class="btn btn-outline-dark" onclick="return confirm('Удалить запись?')">
type="submit"
class="btn btn-outline-dark"
onclick="return confirm('Удалить запись?')">
Удалить Удалить
</button> </button>
<a href="/project/list" class="btn btn-outline-dark">Отмена</a> <a href="/project/list" class="btn btn-outline-dark">Отмена</a>
</form> </form>
<hr /> <hr />
<div class="row">
<div class="col col-md-6">
<h4> Список переменных</h4>
<div class="form-group">
<div class="row" th:each="v, iter : ${variables}">
<div class="col col-md-12">
<a th:href="@{'/var/edit/' + ${projectId}+'/'+${v.id}}">
<span class="badge badge-light" th:text="${iter.index+1} + '. ' + ${v.name}"></span>
</a>
</div>
</div>
</div>
<a th:href="@{'/var/edit/' + ${projectId}+'/0'}" class="btn btn-outline-dark">Добавить преременную</a>
</div>
<div class="col col-md-6">
<h4> Список правил</h4> <h4> Список правил</h4>
<div class="form-group">
<div class="row" th:each="r, iter : ${rules}"> <div class="row" th:each="r, iter : ${rules}">
<div class="col col-md-12"> <div class="col col-md-12">
<span class="badge badge-light" th:text="${iter} + ' Если'"></span> <a th:href="@{'/rule/edit/' + ${projectId}+'/'+${r.id}}">
<span class="badge badge-light" th:text="${iter.index+1} + '. ' + ${r.content}"></span>
</a>
</div> </div>
<div class="col col-md-2 offset-md-3"> </div>
<!-- <div class="col col-md-2 offset-md-3">
<span class="badge badge-primary">Переменная</span> <span class="badge badge-primary">Переменная</span>
</div> </div>
<div class="col col-md-2"> <div class="col col-md-2">
@ -66,8 +76,11 @@
</div> </div>
<div class="col col-md-1"> <div class="col col-md-1">
<span class="badge badge-danger">И / ИЛИ</span> <span class="badge badge-danger">И / ИЛИ</span>
</div>-->
</div>
<a th:href="@{'/rule/edit/' + ${projectId}+'/0'}" th:if="${not #lists.isEmpty(variables)}"
class="btn btn-outline-dark">Добавить правило</a>
</div> </div>
</div> </div>
<a th:href="@{'/rule/edit/' + ${projectId}+'/0'}" class="btn btn-outline-dark">Добавить правило</a>
</div>
</html> </html>

View File

@ -8,12 +8,21 @@
</head> </head>
<div class="container" layout:fragment="content"> <div class="container" layout:fragment="content">
<h3>Редактирование правила:</h3> <h3>Редактирование правила:</h3>
<form th:action="@{/rule/save}" th:object="${rule}" method="post"> <form th:action="@{/rule/save}" th:object="${fuzzyRuleForm}" method="post">
<input type="hidden" th:field="${projectId}"> <input type="hidden" th:field="*{projectId}">
<input type="hidden" th:field="*{id}"> <input type="hidden" th:field="*{id}">
<div class="form-group"> <div class="form-group">
<label for="name">Название</label> <label for="content">Правило</label>
<input th:field="*{content}"
id="content"
type="text"
required
class="form-control"
placeholder="Правило">
<p th:if="${#fields.hasErrors('content')}"
th:class="${#fields.hasErrors('content')}? error">
Не может быть пустым
</p>
</div> </div>
<button name="save" type="submit" class="btn btn-outline-dark">Сохранить</button> <button name="save" type="submit" class="btn btn-outline-dark">Сохранить</button>

View File

@ -0,0 +1,46 @@
<!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}">
<head>
<title>Редактирование переменной</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<div class="container" layout:fragment="content">
<h3>Редактирование переменной:</h3>
<form th:action="@{/var/save}" th:object="${variableForm}" method="post">
<input type="hidden" th:field="*{projectId}">
<input type="hidden" th:field="*{id}">
<div class="form-group">
<label for="name">Переменная</label>
<input th:field="*{name}"
id="name"
type="text"
required
class="form-control"
placeholder="Переменная">
<p th:if="${#fields.hasErrors('name')}"
th:class="${#fields.hasErrors('name')} ? error">
Не может быть пустым
</p>
</div>
<div class="form-group">
<input th:field="*{input}"
id="input"
type="checkbox">
<label for="input">Является входной переменной</label>
</div>
<button name="save" type="submit" class="btn btn-outline-dark">Сохранить</button>
<button name="delete"
type="submit"
class="btn btn-outline-dark"
th:if="*{id != 0}"
onclick="return confirm('Удалить запись?')">
Удалить
</button>
<a th:href="@{'/project/edit/' + ${projectId}}" class="btn btn-outline-dark">Отмена</a>
</form>
</div>
</html>