Add asset checkout, quantity/uniqueness, contacts, and QR labels
Checkout:
- assets.AssignUser + POST /app/assets/{id}/checkout assign/unassign an
asset's user independent of the full edit form. New shared
#checkoutModal, wired from a "Check Out..." context-menu action, backed
by GET /app/assets/{id} and GET /app/users/options JSON endpoints.
Quantity / uniqueness:
- Item types gain an IsUnique flag (checked by default, matching prior
behavior). Assets gain a Quantity column, fixed at 1 for unique types
and adjustable via +/- steppers (POST /app/assets/{id}/quantity) for
non-unique ones. Flipping a type back to unique clamps existing assets'
quantity back to 1. CSV import/export gained a Quantity column.
Migration 0003_quantity.sql.
Contacts:
- New models.RoleContact: an assignable person who cannot sign in
(User.CanLogin). Migration 0004_contacts.sql rebuilds the users table
(SQLite can't alter a CHECK constraint in place) to allow the role.
Users form/list updated; contact creation auto-generates an unusable
password since the login path is blocked by role regardless.
QR asset tags and printable labels:
- Each asset's QR code (via github.com/skip2/go-qrcode, pure Go) encodes
an absolute /scan/<tag> URL; GET /scan/{tag} resolves it and redirects
into /inventory?asset=<id>, which app.js picks up on load to
auto-select that asset - works from a phone camera or a USB
scanner-as-keyboard-wedge pointed at the address bar.
- GET /labels renders a print-ready sheet (right-click an asset for one
label, a folder for every asset in it) with a template switcher between
Avery 5160 (verified dimensions) and 61526 asset tag labels (best-effort
layout, flagged as such on the page).
Also refreshed README to reflect the right-click-first tree UX and all of
the above.
This commit is contained in:
@@ -1,12 +1,38 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"inventree/internal/models"
|
||||
)
|
||||
|
||||
// parseRole maps a form's role value to a models.Role, defaulting to
|
||||
// RoleUser for anything unrecognized.
|
||||
func parseRole(s string) models.Role {
|
||||
switch s {
|
||||
case "admin":
|
||||
return models.RoleAdmin
|
||||
case "contact":
|
||||
return models.RoleContact
|
||||
default:
|
||||
return models.RoleUser
|
||||
}
|
||||
}
|
||||
|
||||
// randomPassword generates an unguessable password for contact accounts,
|
||||
// which can never sign in (see models.User.CanLogin) but still need some
|
||||
// non-empty password_hash in storage.
|
||||
func randomPassword() (string, error) {
|
||||
b := make([]byte, 24)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
type usersListData struct {
|
||||
User *models.User
|
||||
Users []models.User
|
||||
@@ -21,6 +47,28 @@ func (a *App) handleUsersList(w http.ResponseWriter, r *http.Request) {
|
||||
a.Render(w, http.StatusOK, "users_list", usersListData{User: currentUser(r), Users: list})
|
||||
}
|
||||
|
||||
// handleUserOptions returns enabled users as JSON, for pickers like the
|
||||
// asset checkout modal (a currently-assigned but since-disabled user still
|
||||
// displays correctly elsewhere; they just won't appear as a new choice).
|
||||
func (a *App) handleUserOptions(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := a.Users.List(r.Context())
|
||||
if err != nil {
|
||||
jsonErr(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
type option struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
out := make([]option, 0, len(list))
|
||||
for _, u := range list {
|
||||
if u.Enabled {
|
||||
out = append(out, option{ID: u.ID, Username: u.Username})
|
||||
}
|
||||
}
|
||||
jsonOK(w, out)
|
||||
}
|
||||
|
||||
type userFormData struct {
|
||||
User *models.User
|
||||
EditUser *models.User
|
||||
@@ -36,15 +84,20 @@ func (a *App) handleUserCreate(w http.ResponseWriter, r *http.Request) {
|
||||
writeErr(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
role := models.RoleUser
|
||||
if r.FormValue("role") == "admin" {
|
||||
role = models.RoleAdmin
|
||||
}
|
||||
role := parseRole(r.FormValue("role"))
|
||||
u := &models.User{Username: r.FormValue("username"), Role: role, Enabled: r.FormValue("enabled") == "on"}
|
||||
password := r.FormValue("password")
|
||||
if password == "" {
|
||||
a.Render(w, http.StatusBadRequest, "users_form", userFormData{User: currentUser(r), EditUser: u, Error: "Password is required."})
|
||||
return
|
||||
if role != models.RoleContact {
|
||||
a.Render(w, http.StatusBadRequest, "users_form", userFormData{User: currentUser(r), EditUser: u, Error: "Password is required."})
|
||||
return
|
||||
}
|
||||
generated, err := randomPassword()
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
password = generated
|
||||
}
|
||||
if err := a.Users.Create(r.Context(), u, password); err != nil {
|
||||
a.Render(w, http.StatusBadRequest, "users_form", userFormData{User: currentUser(r), EditUser: u, Error: err.Error()})
|
||||
@@ -83,10 +136,7 @@ func (a *App) handleUserUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
u.Username = r.FormValue("username")
|
||||
u.Role = models.RoleUser
|
||||
if r.FormValue("role") == "admin" {
|
||||
u.Role = models.RoleAdmin
|
||||
}
|
||||
u.Role = parseRole(r.FormValue("role"))
|
||||
u.Enabled = r.FormValue("enabled") == "on"
|
||||
if err := a.Users.Update(r.Context(), u); err != nil {
|
||||
a.Render(w, http.StatusBadRequest, "users_form", userFormData{User: currentUser(r), EditUser: u, Error: err.Error()})
|
||||
|
||||
Reference in New Issue
Block a user