Can create, edit, view, and delete assets
This commit is contained in:
@@ -2,6 +2,7 @@ package be.seeseepuff.pcinv.controllers;
|
||||
|
||||
import be.seeseepuff.pcinv.services.AssetService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@@ -26,8 +27,14 @@ public class WebController {
|
||||
private static final String DESCRIPTOR = "descriptor";
|
||||
/// The name of the model attribute that holds the list of assets.
|
||||
private static final String ASSETS = "assets";
|
||||
/// The name of the model attribute that holds the asset being viewed or edited.
|
||||
private static final String ASSET = "asset";
|
||||
/// The name of the model attribute that holds a list of all properties of all descriptors.
|
||||
private static final String PROPERTIES = "properties";
|
||||
/// The name of the model attribute that holds the action to be performed.
|
||||
private static final String ACTION = "action";
|
||||
/// The name of the model attribute that holds the current time in milliseconds.
|
||||
private static final String TIME = "time";
|
||||
|
||||
private final AssetService assetService;
|
||||
|
||||
@@ -36,6 +43,7 @@ public class WebController {
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String index(Model model) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors());
|
||||
model.addAttribute("asset_count", assetService.countAssets());
|
||||
return "index";
|
||||
@@ -46,6 +54,7 @@ public class WebController {
|
||||
*/
|
||||
@GetMapping("/browse")
|
||||
public String browse(Model model) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors());
|
||||
return "browse";
|
||||
}
|
||||
@@ -60,6 +69,7 @@ public class WebController {
|
||||
*/
|
||||
@GetMapping("/browse/{type}")
|
||||
public String browseType(Model model, @PathVariable String type) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
var tree = assetService.getAssetDescriptorTree(type);
|
||||
model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type));
|
||||
model.addAttribute(DESCRIPTORS, tree);
|
||||
@@ -76,20 +86,62 @@ public class WebController {
|
||||
*/
|
||||
@GetMapping("/view/{qr}")
|
||||
public String view(Model model, @PathVariable long qr) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
model.addAttribute(ACTION, "view");
|
||||
return renderView(model, qr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a page asking if the user wants to delete an asset.
|
||||
*
|
||||
* @param qr The QR code of the asset to delete.
|
||||
*/
|
||||
@GetMapping("/delete/{qr}")
|
||||
public String delete(Model model, @PathVariable long qr, @Param("confirm") boolean confirm) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
model.addAttribute(ACTION, "delete");
|
||||
if (confirm) {
|
||||
var asset = assetService.getAssetByQr(qr);
|
||||
if (asset == null) {
|
||||
return "redirect:/";
|
||||
}
|
||||
var type = asset.getAsset().getType();
|
||||
assetService.deleteAsset(qr);
|
||||
return "redirect:/browse/" + type;
|
||||
}
|
||||
return renderView(model, qr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the view for an asset based on its QR code.
|
||||
* If the asset does not exist, it redirects to the index page.
|
||||
*
|
||||
* @param model The model to add attributes to.
|
||||
* @param qr The QR code of the asset to view.
|
||||
* @return The view name for viewing the asset.
|
||||
*/
|
||||
private String renderView(Model model, long qr) {
|
||||
var asset = assetService.getAssetByQr(qr);
|
||||
if (asset == null) {
|
||||
return "redirect:/";
|
||||
}
|
||||
model.addAttribute("asset", asset);
|
||||
model.addAttribute(ASSET, asset);
|
||||
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(asset.getAsset().getType()));
|
||||
model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(asset.getAsset().getType()));
|
||||
return "view";
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public String search(@Param("qr") long qr) {
|
||||
return "redirect:/view/" + qr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a view where the user can create the type of asset to create.
|
||||
*/
|
||||
@GetMapping("/create")
|
||||
public String create(Model model) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors());
|
||||
return "create_select";
|
||||
}
|
||||
@@ -101,11 +153,51 @@ public class WebController {
|
||||
*/
|
||||
@GetMapping("/create/{type}")
|
||||
public String createType(Model model, @PathVariable String type) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
model.addAttribute(ACTION, "create");
|
||||
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(type));
|
||||
model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type));
|
||||
return "create_asset";
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a view where the user can edit an existing asset.
|
||||
*
|
||||
* @param qr The QR code of the asset to edit.
|
||||
*/
|
||||
@GetMapping("/edit/{qr}")
|
||||
public String edit(Model model, @PathVariable long qr) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
var asset = assetService.getAssetByQr(qr);
|
||||
if (asset == null) {
|
||||
throw new RuntimeException("Asset not found");
|
||||
}
|
||||
String assetType = asset.getAsset().getType();
|
||||
model.addAttribute(ACTION, "edit");
|
||||
model.addAttribute(ASSET, asset);
|
||||
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(assetType));
|
||||
model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(assetType));
|
||||
return "create_asset";
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually edits an asset based on the form data submitted.
|
||||
*
|
||||
* @param qr The QR code of the asset to edit.
|
||||
*/
|
||||
@PostMapping("/edit/{qr}")
|
||||
public String editPost(Model model, @PathVariable long qr, @RequestBody MultiValueMap<String, String> formData) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
var formMap = new HashMap<String, String>();
|
||||
formData.forEach((key, values) -> {
|
||||
if (!values.isEmpty()) {
|
||||
formMap.put(key, values.getFirst());
|
||||
}
|
||||
});
|
||||
var asset = assetService.editAsset(qr, formMap);
|
||||
return "redirect:/view/" + asset.getQr();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the form submission for creating an asset.
|
||||
*
|
||||
@@ -118,6 +210,7 @@ public class WebController {
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
|
||||
)
|
||||
public String createTypePost(Model model, @PathVariable String type, @RequestBody MultiValueMap<String, String> formData) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
var formMap = new HashMap<String, String>();
|
||||
formData.forEach((key, values) -> {
|
||||
if (!values.isEmpty()) {
|
||||
|
||||
@@ -41,6 +41,8 @@ public class AssetProperty {
|
||||
private final BiConsumer<Object, Object> setter;
|
||||
/// A getter function that can be used to get the value of the property from an asset.
|
||||
private final Function<Object, Object> getter;
|
||||
/// Whether the property is an input list.
|
||||
private final boolean inputList;
|
||||
|
||||
/**
|
||||
* Enum representing the possible types of asset properties.
|
||||
@@ -84,6 +86,7 @@ public class AssetProperty {
|
||||
.displayName(annotation.value())
|
||||
.type(type)
|
||||
.required(annotation.required())
|
||||
.inputList(property.isAnnotationPresent(InputList.class))
|
||||
.setter((obj, value) -> {
|
||||
try {
|
||||
property.setAccessible(true);
|
||||
@@ -167,7 +170,11 @@ public class AssetProperty {
|
||||
* @param asset The asset to get the property value from.
|
||||
* @return The value of the property.
|
||||
*/
|
||||
public Object getValue(Object asset) {
|
||||
@Nullable
|
||||
public Object getValue(@Nullable Object asset) {
|
||||
if (asset == null) {
|
||||
return null;
|
||||
}
|
||||
return getter.apply(asset);
|
||||
}
|
||||
|
||||
@@ -176,7 +183,11 @@ public class AssetProperty {
|
||||
*
|
||||
* @return The rendered value as a string.
|
||||
*/
|
||||
public String renderValue(Object asset) {
|
||||
@Nullable
|
||||
public String renderValue(@Nullable Object asset) {
|
||||
if (asset == null) {
|
||||
return null;
|
||||
}
|
||||
var value = getValue(asset);
|
||||
if (value == null) {
|
||||
return "Unknown";
|
||||
|
||||
15
src/main/java/be/seeseepuff/pcinv/meta/InputList.java
Normal file
15
src/main/java/be/seeseepuff/pcinv/meta/InputList.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package be.seeseepuff.pcinv.meta;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Indicates that any form for the given field should be rendered as a dropdown
|
||||
* list with optional manual text input.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface InputList {
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package be.seeseepuff.pcinv.models;
|
||||
|
||||
import be.seeseepuff.pcinv.meta.AssetInfo;
|
||||
import be.seeseepuff.pcinv.meta.InputList;
|
||||
import be.seeseepuff.pcinv.meta.Property;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@@ -21,7 +19,10 @@ import lombok.Setter;
|
||||
type = "asset",
|
||||
isVisible = false
|
||||
)
|
||||
@Table(name = "assets")
|
||||
@Table(
|
||||
name = "assets",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = "qr")
|
||||
)
|
||||
public class GenericAsset
|
||||
{
|
||||
public static final String TYPE = "asset";
|
||||
@@ -38,6 +39,7 @@ public class GenericAsset
|
||||
|
||||
/// The brand of the asset.
|
||||
@Property("Brand")
|
||||
@InputList
|
||||
private String brand;
|
||||
|
||||
/// The model of the asset
|
||||
|
||||
@@ -21,9 +21,11 @@ public interface AssetRepository<T extends Asset> {
|
||||
|
||||
T findByAsset(GenericAsset asset);
|
||||
|
||||
void deleteByAsset(GenericAsset asset);
|
||||
|
||||
void flush();
|
||||
|
||||
List<T> findAll();
|
||||
|
||||
Class<T> getAssetType();
|
||||
|
||||
long count();
|
||||
}
|
||||
|
||||
@@ -125,6 +125,31 @@ public class AssetService {
|
||||
return asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits an existing asset with the provided form data.
|
||||
*
|
||||
* @param qr The QR code of the asset to edit.
|
||||
* @param formData The form data containing the updated asset properties.
|
||||
* @return The updated asset.
|
||||
*/
|
||||
@Transactional
|
||||
public Asset editAsset(long qr, Map<String, String> formData) {
|
||||
var genericAsset = genericRepository.findByQr(qr);
|
||||
if (genericAsset == null) {
|
||||
throw new IllegalArgumentException("No asset found with QR code: " + qr);
|
||||
}
|
||||
|
||||
var assetType = genericAsset.getType();
|
||||
var assetDescriptor = getAssetDescriptor(assetType);
|
||||
var asset = getRepositoryFor(assetType).findByAsset(genericAsset);
|
||||
|
||||
fillIn(genericAsset, getAssetDescriptor(GenericAsset.TYPE), formData);
|
||||
fillIn(asset, assetDescriptor, formData);
|
||||
|
||||
genericRepository.saveAndFlush(genericAsset);
|
||||
return getRepositoryFor(assetType).saveAndFlushAsset(asset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the asset repository for the specified type.
|
||||
*
|
||||
@@ -148,11 +173,10 @@ public class AssetService {
|
||||
private void fillIn(Object asset, AssetDescriptor assetDescriptor, Map<String, String> formData) {
|
||||
for (var property : assetDescriptor.getProperties()) {
|
||||
var value = parseValue(assetDescriptor, property, formData);
|
||||
if (value != null) {
|
||||
property.setValue(asset, value);
|
||||
} else if (property.isRequired()) {
|
||||
if (value == null && property.isRequired()) {
|
||||
throw new IllegalArgumentException("Property '" + property.getName() + "' is required but not provided.");
|
||||
}
|
||||
property.setValue(asset, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,4 +223,18 @@ public class AssetService {
|
||||
throw new IllegalArgumentException("Unsupported property type: " + property.getType());
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteAsset(long qr) {
|
||||
var genericAsset = genericRepository.findByQr(qr);
|
||||
if (genericAsset == null) {
|
||||
throw new IllegalArgumentException("No asset found with QR code: " + qr);
|
||||
}
|
||||
var assetType = genericAsset.getType();
|
||||
var assetRepository = getRepositoryFor(assetType);
|
||||
assetRepository.deleteByAsset(genericAsset);
|
||||
assetRepository.flush();
|
||||
genericRepository.delete(genericAsset);
|
||||
genericRepository.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<body th:replace="~{fragments :: base(title='View '+${descriptor.pluralName}, content=~{::content})}">
|
||||
<div th:fragment="content">
|
||||
There are <span th:text="${assets.size()}"></span> <span th:text="${descriptor.pluralName}"></span> in the database.
|
||||
<table border="1">
|
||||
<table border="1" cellpadding="4">
|
||||
<tr bgcolor="#d3d3d3">
|
||||
<th th:each="p : ${properties}" th:text="${p.displayName}"></th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
<tr th:each="a : ${assets}">
|
||||
<td th:each="p : ${properties}"><a th:href="'/view/'+${a.getQr()}" th:text="${p.renderValue(a)}"></a></td>
|
||||
<td th:each="p : ${properties}">
|
||||
<a th:if="${p.name == 'qr'}" th:href="'/view/'+${a.getQr()}" th:text="${p.renderValue(a)}"></a>
|
||||
<span th:if="${p.name != 'qr'}" th:text="${p.renderValue(a)}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/edit">Edit</a>
|
||||
<a th:href="'/view/'+${a.getQr()}">View</a>
|
||||
<a th:href="'/edit/'+${a.getQr()}">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<body th:replace="~{fragments :: base(title='Create '+${descriptor.displayName}, content=~{::content})}">
|
||||
<div th:fragment="content">
|
||||
Create a <span th:text="${descriptor.displayName}"></span>
|
||||
<form th:action="'/create/'+${descriptor.getType()}" method="post">
|
||||
<form th:action="'/'+${action}+'/'+${asset != null ? asset.getQr() : descriptor.getType()}" method="post">
|
||||
<div th:each="d : ${descriptors}">
|
||||
<h2 th:text="${d.displayName}"></h2>
|
||||
<table border="1">
|
||||
<table border="1" cellpadding="4">
|
||||
<tr th:each="p : ${d.getProperties()}">
|
||||
<td><label th:text="${p.displayName}" th:for="${d.asString(p)}"></label></td>
|
||||
<td bgcolor="#d3d3d3"><b><label th:text="${p.displayName}" th:for="${d.asString(p)}"></label></b></td>
|
||||
<td th:switch="${p.type.nameOrEnum()}">
|
||||
<input th:case="STRING" type="text" th:id="${d.asString(p)}" th:name="${d.asString(p)}" th:placeholder="${p.displayName}" th:required="${p.required}"/>
|
||||
<input th:case="INTEGER" type="number" th:id="${d.asString(p)}" th:name="${d.asString(p)}" th:required="${p.required}"/>
|
||||
<input th:case="STRING" type="text" th:id="${d.asString(p)}" th:name="${d.asString(p)}" th:value="${p.getValue(asset)}" th:placeholder="${p.displayName}" th:required="${p.required}"/>
|
||||
<input th:case="INTEGER" type="number" th:id="${d.asString(p)}" th:name="${d.asString(p)}" th:value="${p.getValue(asset)}" th:required="${p.required}"/>
|
||||
<select th:case="enum" th:id="${d.asString(p)}" th:name="${d.asString(p)}">
|
||||
<option th:each="o : ${p.options}" th:value="${o.value}" th:text="${o.displayName}" th:selected="${o.defaultValue}">Good</option>
|
||||
<option th:each="o : ${p.options}" th:value="${o.value}" th:text="${o.displayName}" th:selected="${asset != null ? (p.getValue(asset) == o.enumConstant) : o.defaultValue}">Good</option>
|
||||
</select>
|
||||
<span th:case="CAPACITY">
|
||||
<input type="number" th:id="${d.asString(p)+'-value'}" th:name="${d.asString(p)+'-value'}" th:required="${p.required}"/>
|
||||
<select th:id="${d.asString(p)}+'-unit'" th:name="${d.asString(p)}+'-unit'">
|
||||
<option value="1">Bytes</option>
|
||||
<option th:if="${p.capacityAsSI}" value="1000">kB</option>
|
||||
<option th:if="${p.capacityAsIEC}" value="1024">KiB</option>
|
||||
<option th:if="${p.capacityAsSI}" value="1000000">MB</option>
|
||||
<option th:if="${p.capacityAsIEC}" value="1048576">MiB</option>
|
||||
<option th:if="${p.capacityAsSI}" value="1000000000">GB</option>
|
||||
<option th:if="${p.capacityAsIEC}" value="1073741824">GiB</option>
|
||||
<option th:if="${p.capacityAsSI}" value="1000000000000">TB</option>
|
||||
<option th:if="${p.capacityAsIEC}" value="1099511627776">TiB</option>
|
||||
<option th:if="${p.capacityAsSI}">kB</option>
|
||||
<option th:if="${p.capacityAsIEC}">KiB</option>
|
||||
<option th:if="${p.capacityAsSI}">MB</option>
|
||||
<option th:if="${p.capacityAsIEC}">MiB</option>
|
||||
<option th:if="${p.capacityAsSI}">GB</option>
|
||||
<option th:if="${p.capacityAsIEC}">GiB</option>
|
||||
<option th:if="${p.capacityAsSI}">TB</option>
|
||||
<option th:if="${p.capacityAsIEC}">TiB</option>
|
||||
</select>
|
||||
</span>
|
||||
<b th:case="*">Bad input type for <span th:text="${d.type}+'-'+${p.type}"></span></b>
|
||||
@@ -33,7 +33,8 @@
|
||||
</table>
|
||||
</div>
|
||||
<p>
|
||||
<input type="submit" value="Create">
|
||||
<input th:if="${action == 'create'}" type="submit" value="Create">
|
||||
<input th:if="${action == 'edit'}" type="submit" value="Save Changes">
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -13,5 +13,9 @@
|
||||
<hr>
|
||||
<div th:replace="${content}">
|
||||
</div>
|
||||
<hr>
|
||||
<p>
|
||||
<small>Rendered in <span th:text="${#execInfo.getNow().getTimeInMillis() - time}">25</span>ms on <span th:text="${#dates.format(#execInfo.getNow(), 'dd/MM/yyyy HH:mm')}"></span>.</small>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<div th:replace="fragments :: base(title='Home', content=~{::content})">
|
||||
<div th:fragment="content">
|
||||
<p>This system holds <span th:text="${asset_count}">5</span> assets.</p>
|
||||
<form action="/search" method="get">
|
||||
<label>
|
||||
Find an asset via QR code:
|
||||
<input type="number" name="qr" placeholder="Find by QR..." />
|
||||
</label>
|
||||
<input type="submit" value="Search" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<body th:replace="~{fragments :: base(title='View asset information', content=~{::content})}">
|
||||
<div th:fragment="content">
|
||||
View device details
|
||||
<h2 th:if="${action == 'view'}">Details of <span th:text="${descriptor.displayName}">Hard Disk Drive</span> <span th:text="${asset.getQr()}">21</span></h2>
|
||||
<h2 th:if="${action == 'delete'}">Are you sure you want to delete <span th:text="${descriptor.displayName}">Hard Disk Drive</span> <span th:text="${asset.getQr()}">21</span></h2>
|
||||
<div th:each="d : ${descriptors}">
|
||||
<h2 th:text="${d.displayName}"></h2>
|
||||
<table border="1">
|
||||
<table border="1" cellpadding="4">
|
||||
<tr th:each="p : ${d.properties}">
|
||||
<td><b th:text="${p.displayName}"></b></td>
|
||||
<td bgcolor="lightgray"><b th:text="${p.displayName}"></b></td>
|
||||
<td th:text="${p.renderValue(asset)}"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<p>
|
||||
<ul th:if="${action == 'view'}">
|
||||
<li><a th:href="'/edit/'+${asset.getQr()}">Edit</a></li>
|
||||
<li><a th:href="'/delete/'+${asset.getQr()}">Delete</a></li>
|
||||
<li><a th:href="'/browse/'+${descriptor.type}">Browse all <span th:text="${descriptor.pluralName}">Hard Drives</span></a></li>
|
||||
</ul>
|
||||
<ul th:if="${action == 'delete'}">
|
||||
<li><a th:href="'/view/'+${asset.getQr()}">No</a></li>
|
||||
<li><a th:href="'/delete/'+${asset.getQr()}+'?confirm=true'">Yes, delete it</a></li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user