Add default message priority for applications

Co-authored-by: Jannis Mattheis <contact@jmattheis.de>
This commit is contained in:
chrispruitt
2023-07-19 14:15:01 -04:00
committed by GitHub
parent aedc3e2ba6
commit 72bd8c8ba6
18 changed files with 210 additions and 35 deletions

View File

@@ -44,6 +44,10 @@ type ApplicationParams struct {
//
// example: Backup server for the interwebs
Description string `form:"description" query:"description" json:"description"`
// The default priority of messages sent by this application. Defaults to 0.
//
// example: 5
DefaultPriority int `form:"defaultPriority" query:"defaultPriority" json:"defaultPriority"`
}
// CreateApplication creates an application and returns the access token.
@@ -83,11 +87,12 @@ func (a *ApplicationAPI) CreateApplication(ctx *gin.Context) {
applicationParams := ApplicationParams{}
if err := ctx.Bind(&applicationParams); err == nil {
app := model.Application{
Name: applicationParams.Name,
Description: applicationParams.Description,
Token: auth.GenerateNotExistingToken(generateApplicationToken, a.applicationExists),
UserID: auth.GetUserID(ctx),
Internal: false,
Name: applicationParams.Name,
Description: applicationParams.Description,
DefaultPriority: applicationParams.DefaultPriority,
Token: auth.GenerateNotExistingToken(generateApplicationToken, a.applicationExists),
UserID: auth.GetUserID(ctx),
Internal: false,
}
if success := successOrAbort(ctx, 500, a.DB.CreateApplication(&app)); !success {
@@ -245,6 +250,7 @@ func (a *ApplicationAPI) UpdateApplication(ctx *gin.Context) {
if err := ctx.Bind(&applicationParams); err == nil {
app.Description = applicationParams.Description
app.Name = applicationParams.Name
app.DefaultPriority = applicationParams.DefaultPriority
if success := successOrAbort(ctx, 500, a.DB.UpdateApplication(app)); !success {
return

View File

@@ -92,7 +92,7 @@ func (s *ApplicationSuite) Test_ensureApplicationHasCorrectJsonRepresentation()
Image: "asd",
Internal: true,
}
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Aasdasfgeeg","name":"myapp","description":"mydesc", "image": "asd", "internal":true}`)
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Aasdasfgeeg","name":"myapp","description":"mydesc", "image": "asd", "internal":true, "defaultPriority":0}`)
}
func (s *ApplicationSuite) Test_CreateApplication_expectBadRequestOnEmptyName() {
@@ -527,6 +527,29 @@ func (s *ApplicationSuite) Test_UpdateApplicationName_expectSuccess() {
}
}
func (s *ApplicationSuite) Test_UpdateApplicationDefaultPriority_expectSuccess() {
s.db.User(5).NewAppWithToken(2, "app-2")
test.WithUser(s.ctx, 5)
s.withFormData("name=name&description=&defaultPriority=4")
s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}
s.a.UpdateApplication(s.ctx)
expected := &model.Application{
ID: 2,
Token: "app-2",
UserID: 5,
Name: "name",
Description: "",
DefaultPriority: 4,
}
assert.Equal(s.T(), 200, s.recorder.Code)
if app, err := s.db.GetApplicationByID(2); assert.NoError(s.T(), err) {
assert.Equal(s.T(), expected, app)
}
}
func (s *ApplicationSuite) Test_UpdateApplication_preservesImage() {
app := s.db.User(5).NewAppWithToken(2, "app-2")
app.Image = "existing.png"

View File

@@ -371,6 +371,11 @@ func (a *MessageAPI) CreateMessage(ctx *gin.Context) {
if strings.TrimSpace(message.Title) == "" {
message.Title = application.Name
}
if message.Priority == nil {
message.Priority = &application.DefaultPriority
}
message.Date = timeNow()
message.ID = 0
msgInternal := toInternalMessage(&message)
@@ -388,9 +393,12 @@ func toInternalMessage(msg *model.MessageExternal) *model.Message {
ApplicationID: msg.ApplicationID,
Message: msg.Message,
Title: msg.Title,
Priority: msg.Priority,
Date: msg.Date,
}
if msg.Priority != nil {
res.Priority = *msg.Priority
}
if msg.Extras != nil {
res.Extras, _ = json.Marshal(msg.Extras)
}
@@ -403,7 +411,7 @@ func toExternalMessage(msg *model.Message) *model.MessageExternal {
ApplicationID: msg.ApplicationID,
Message: msg.Message,
Title: msg.Title,
Priority: msg.Priority,
Priority: &msg.Priority,
Date: msg.Date,
}
if len(msg.Extras) != 0 {

View File

@@ -53,7 +53,7 @@ func (s *MessageSuite) Test_ensureCorrectJsonRepresentation() {
actual := &model.PagedMessages{
Paging: model.Paging{Limit: 5, Since: 122, Size: 5, Next: "http://example.com/message?limit=5&since=122"},
Messages: []*model.MessageExternal{{ID: 55, ApplicationID: 2, Message: "hi", Title: "hi", Date: t, Priority: 4, Extras: map[string]interface{}{
Messages: []*model.MessageExternal{{ID: 55, ApplicationID: 2, Message: "hi", Title: "hi", Date: t, Priority: intPtr(4), Extras: map[string]interface{}{
"test::string": "string",
"test::array": []interface{}{1, 2, 3},
"test::int": 1,
@@ -331,7 +331,29 @@ func (s *MessageSuite) Test_CreateMessage_onJson_allParams() {
msgs, err := s.db.GetMessagesByApplication(7)
assert.NoError(s.T(), err)
expected := &model.MessageExternal{ID: 1, ApplicationID: 7, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t}
expected := &model.MessageExternal{ID: 1, ApplicationID: 7, Title: "mytitle", Message: "mymessage", Priority: intPtr(1), Date: t}
assert.Len(s.T(), msgs, 1)
assert.Equal(s.T(), expected, toExternalMessage(msgs[0]))
assert.Equal(s.T(), 200, s.recorder.Code)
assert.Equal(s.T(), expected, s.notifiedMessage)
}
func (s *MessageSuite) Test_CreateMessage_WithDefaultPriority() {
t, _ := time.Parse("2006/01/02", "2017/01/02")
timeNow = func() time.Time { return t }
defer func() { timeNow = time.Now }()
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
s.db.User(4).AppWithTokenAndDefaultPriority(8, "app-token", 5)
s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage"}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")
s.a.CreateMessage(s.ctx)
msgs, err := s.db.GetMessagesByApplication(8)
assert.NoError(s.T(), err)
expected := &model.MessageExternal{ID: 1, ApplicationID: 8, Title: "mytitle", Message: "mymessage", Priority: intPtr(5), Date: t}
assert.Len(s.T(), msgs, 1)
assert.Equal(s.T(), expected, toExternalMessage(msgs[0]))
assert.Equal(s.T(), 200, s.recorder.Code)
@@ -352,7 +374,7 @@ func (s *MessageSuite) Test_CreateMessage_WithTitle() {
msgs, err := s.db.GetMessagesByApplication(5)
assert.NoError(s.T(), err)
expected := &model.MessageExternal{ID: 1, ApplicationID: 5, Title: "mytitle", Message: "mymessage", Date: t}
expected := &model.MessageExternal{ID: 1, ApplicationID: 5, Title: "mytitle", Message: "mymessage", Date: t, Priority: intPtr(0)}
assert.Len(s.T(), msgs, 1)
assert.Equal(s.T(), expected, toExternalMessage(msgs[0]))
assert.Equal(s.T(), 200, s.recorder.Code)
@@ -446,6 +468,7 @@ func (s *MessageSuite) Test_CreateMessage_WithExtras() {
Message: "mymessage",
Title: "msg with extras",
Date: t,
Priority: intPtr(0),
Extras: map[string]interface{}{
"gotify::test": map[string]interface{}{
"string": "test",
@@ -492,7 +515,7 @@ func (s *MessageSuite) Test_CreateMessage_onQueryData() {
s.a.CreateMessage(s.ctx)
expected := &model.MessageExternal{ID: 1, ApplicationID: 2, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t}
expected := &model.MessageExternal{ID: 1, ApplicationID: 2, Title: "mytitle", Message: "mymessage", Priority: intPtr(1), Date: t}
msgs, err := s.db.GetMessagesByApplication(2)
assert.NoError(s.T(), err)
@@ -515,7 +538,7 @@ func (s *MessageSuite) Test_CreateMessage_onFormData() {
s.a.CreateMessage(s.ctx)
expected := &model.MessageExternal{ID: 1, ApplicationID: 99, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t}
expected := &model.MessageExternal{ID: 1, ApplicationID: 99, Title: "mytitle", Message: "mymessage", Priority: intPtr(1), Date: t}
msgs, err := s.db.GetMessagesByApplication(99)
assert.NoError(s.T(), err)
assert.Len(s.T(), msgs, 1)
@@ -528,3 +551,7 @@ func (s *MessageSuite) withURL(scheme, host, path, query string) {
s.ctx.Request.URL = &url.URL{Path: path, RawQuery: query}
s.ctx.Set("location", &url.URL{Scheme: scheme, Host: host})
}
func intPtr(x int) *int {
return &x
}

View File

@@ -2063,6 +2063,13 @@
"image"
],
"properties": {
"defaultPriority": {
"description": "The default priority of messages sent by this application. Defaults to 0.",
"type": "integer",
"format": "int64",
"x-go-name": "DefaultPriority",
"example": 4
},
"description": {
"description": "The description of the application.",
"type": "string",
@@ -2115,6 +2122,13 @@
"name"
],
"properties": {
"defaultPriority": {
"description": "The default priority of messages sent by this application. Defaults to 0.",
"type": "integer",
"format": "int64",
"x-go-name": "DefaultPriority",
"example": 5
},
"description": {
"description": "The description of the application.",
"type": "string",
@@ -2326,7 +2340,7 @@
"example": "**Backup** was successfully finished."
},
"priority": {
"description": "The priority of the message.",
"description": "The priority of the message. If unset, then the default priority of the\napplication will be used.",
"type": "integer",
"format": "int64",
"x-go-name": "Priority",

View File

@@ -42,4 +42,9 @@ type Application struct {
// example: image/image.jpeg
Image string `gorm:"type:text" json:"image"`
Messages []MessageExternal `json:"-"`
// The default priority of messages sent by this application. Defaults to 0.
//
// required: false
// example: 4
DefaultPriority int `form:"defaultPriority" query:"defaultPriority" json:"defaultPriority"`
}

View File

@@ -42,10 +42,11 @@ type MessageExternal struct {
//
// example: Backup
Title string `form:"title" query:"title" json:"title"`
// The priority of the message.
// The priority of the message. If unset, then the default priority of the
// application will be used.
//
// example: 2
Priority int `form:"priority" query:"priority" json:"priority"`
Priority *int `form:"priority" query:"priority" json:"priority"`
// The extra data sent along the message.
//
// The extra fields are stored in a key-value scheme. Only accepted in CreateMessage requests with application/json content-type.

View File

@@ -70,7 +70,7 @@ func NewManager(db Database, directory string, mux *gin.RouterGroup, notifier No
internalMsg := &model.Message{
ApplicationID: message.Message.ApplicationID,
Title: message.Message.Title,
Priority: message.Message.Priority,
Priority: *message.Message.Priority,
Date: message.Message.Date,
Message: message.Message.Message,
}

View File

@@ -26,7 +26,7 @@ func (c redirectToChannel) SendMessage(msg compat.Message) error {
ApplicationID: c.ApplicationID,
Message: msg.Message,
Title: msg.Title,
Priority: msg.Priority,
Priority: &msg.Priority,
Date: time.Now(),
Extras: msg.Extras,
},

View File

@@ -138,6 +138,13 @@ func (ab *AppClientBuilder) newAppWithTokenAndName(id uint, token, name string,
return application
}
// AppWithTokenAndDefaultPriority creates an application with a token and defaultPriority and returns a message builder.
func (ab *AppClientBuilder) AppWithTokenAndDefaultPriority(id uint, token string, defaultPriority int) *MessageBuilder {
application := &model.Application{ID: id, UserID: ab.userID, Token: token, DefaultPriority: defaultPriority}
ab.db.CreateApplication(application)
return &MessageBuilder{db: ab.db, appID: id}
}
// Client creates a client and returns itself.
func (ab *AppClientBuilder) Client(id uint) *AppClientBuilder {
return ab.ClientWithToken(id, "client"+fmt.Sprint(id))

View File

@@ -127,6 +127,7 @@ func (s *DatabaseSuite) Test_Apps() {
userBuilder.InternalAppWithTokenAndName(10, "test-tokeni-2", "app name")
userBuilder.AppWithToken(11, "test-token-3")
userBuilder.InternalAppWithToken(12, "test-tokeni-3")
userBuilder.AppWithTokenAndDefaultPriority(13, "test-tokeni-4", 4)
s.db.AssertAppExist(1)
s.db.AssertAppExist(2)
@@ -140,6 +141,7 @@ func (s *DatabaseSuite) Test_Apps() {
s.db.AssertAppExist(10)
s.db.AssertAppExist(11)
s.db.AssertAppExist(12)
s.db.AssertAppExist(13)
s.db.DeleteApplicationByID(2)

View File

@@ -6,27 +6,29 @@ import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import TextField from '@material-ui/core/TextField';
import Tooltip from '@material-ui/core/Tooltip';
import {NumberField} from '../common/NumberField';
import React, {Component} from 'react';
interface IProps {
fClose: VoidFunction;
fOnSubmit: (name: string, description: string) => void;
fOnSubmit: (name: string, description: string, defaultPriority: number) => void;
}
interface IState {
name: string;
description: string;
defaultPriority: number;
}
export default class AddDialog extends Component<IProps, IState> {
public state = {name: '', description: ''};
public state = {name: '', description: '', defaultPriority: 0};
public render() {
const {fClose, fOnSubmit} = this.props;
const {name, description} = this.state;
const {name, description, defaultPriority} = this.state;
const submitEnabled = this.state.name.length !== 0;
const submitAndClose = () => {
fOnSubmit(name, description);
fOnSubmit(name, description, defaultPriority);
fClose();
};
return (
@@ -59,6 +61,14 @@ export default class AddDialog extends Component<IProps, IState> {
fullWidth
multiline
/>
<NumberField
margin="dense"
className="priority"
label="Default Priority"
value={defaultPriority}
onChange={(value) => this.setState({defaultPriority: value})}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={fClose}>Cancel</Button>

View File

@@ -35,15 +35,32 @@ export class AppStore extends BaseStore<IApplication> {
};
@action
public update = async (id: number, name: string, description: string): Promise<void> => {
await axios.put(`${config.get('url')}application/${id}`, {name, description});
public update = async (
id: number,
name: string,
description: string,
defaultPriority: number
): Promise<void> => {
await axios.put(`${config.get('url')}application/${id}`, {
name,
description,
defaultPriority,
});
await this.refresh();
this.snack('Application updated');
};
@action
public create = async (name: string, description: string): Promise<void> => {
await axios.post(`${config.get('url')}application`, {name, description});
public create = async (
name: string,
description: string,
defaultPriority: number
): Promise<void> => {
await axios.post(`${config.get('url')}application`, {
name,
description,
defaultPriority,
});
await this.refresh();
this.snack('Application created');
};

View File

@@ -66,6 +66,7 @@ class Applications extends Component<Stores<'appStore'>> {
<TableCell>Name</TableCell>
<TableCell>Token</TableCell>
<TableCell>Description</TableCell>
<TableCell>Priority</TableCell>
<TableCell />
<TableCell />
</TableRow>
@@ -75,6 +76,7 @@ class Applications extends Component<Stores<'appStore'>> {
<Row
key={app.id}
description={app.description}
defaultPriority={app.defaultPriority}
image={app.image}
name={app.name}
value={app.token}
@@ -103,11 +105,12 @@ class Applications extends Component<Stores<'appStore'>> {
{updateId !== false && (
<UpdateDialog
fClose={() => (this.updateId = false)}
fOnSubmit={(name, description) =>
appStore.update(updateId, name, description)
fOnSubmit={(name, description, defaultPriority) =>
appStore.update(updateId, name, description, defaultPriority)
}
initialDescription={appStore.getByID(updateId).description}
initialName={appStore.getByID(updateId).name}
initialDefaultPriority={appStore.getByID(updateId).defaultPriority}
/>
)}
{deleteId !== false && (
@@ -147,6 +150,7 @@ interface IRowProps {
value: string;
noDelete: boolean;
description: string;
defaultPriority: number;
fUpload: VoidFunction;
image: string;
fDelete: VoidFunction;
@@ -154,7 +158,7 @@ interface IRowProps {
}
const Row: SFC<IRowProps> = observer(
({name, value, noDelete, description, fDelete, fUpload, image, fEdit}) => (
({name, value, noDelete, description, defaultPriority, fDelete, fUpload, image, fEdit}) => (
<TableRow>
<TableCell padding="default">
<div style={{display: 'flex'}}>
@@ -169,6 +173,7 @@ const Row: SFC<IRowProps> = observer(
<CopyableSecret value={value} style={{display: 'flex', alignItems: 'center'}} />
</TableCell>
<TableCell>{description}</TableCell>
<TableCell>{defaultPriority}</TableCell>
<TableCell align="right" padding="none">
<IconButton onClick={fEdit} className="edit">
<Edit />

View File

@@ -6,37 +6,41 @@ import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import TextField from '@material-ui/core/TextField';
import Tooltip from '@material-ui/core/Tooltip';
import {NumberField} from '../common/NumberField';
import React, {Component} from 'react';
interface IProps {
fClose: VoidFunction;
fOnSubmit: (name: string, description: string) => void;
fOnSubmit: (name: string, description: string, defaultPriority: number) => void;
initialName: string;
initialDescription: string;
initialDefaultPriority: number;
}
interface IState {
name: string;
description: string;
defaultPriority: number;
}
export default class UpdateDialog extends Component<IProps, IState> {
public state = {name: '', description: ''};
public state = {name: '', description: '', defaultPriority: 0};
constructor(props: IProps) {
super(props);
this.state = {
name: props.initialName,
description: props.initialDescription,
defaultPriority: props.initialDefaultPriority,
};
}
public render() {
const {fClose, fOnSubmit} = this.props;
const {name, description} = this.state;
const {name, description, defaultPriority} = this.state;
const submitEnabled = this.state.name.length !== 0;
const submitAndClose = () => {
fOnSubmit(name, description);
fOnSubmit(name, description, defaultPriority);
fClose();
};
return (
@@ -69,6 +73,14 @@ export default class UpdateDialog extends Component<IProps, IState> {
fullWidth
multiline
/>
<NumberField
margin="dense"
className="priority"
label="Default Priority"
value={defaultPriority}
onChange={(value) => this.setState({defaultPriority: value})}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={fClose}>Cancel</Button>

View File

@@ -0,0 +1,36 @@
import {TextField, TextFieldProps} from '@material-ui/core';
import React from 'react';
export interface NumberFieldProps {
value: number;
onChange: (value: number) => void;
}
export const NumberField = ({
value,
onChange,
...props
}: NumberFieldProps & Omit<TextFieldProps, 'value' | 'onChange'>) => {
const [stringValue, setStringValue] = React.useState<string>(value.toString());
const [error, setError] = React.useState('');
return (
<TextField
value={stringValue}
type="number"
helperText={error}
error={error !== ''}
onChange={(event) => {
setStringValue(event.target.value);
const i = parseInt(event.target.value, 10);
if (!Number.isNaN(i)) {
onChange(i);
setError('');
} else {
setError('Invalid number');
}
}}
{...props}
/>
);
};

View File

@@ -17,8 +17,9 @@ enum Col {
Name = 2,
Token = 3,
Description = 4,
EditUpdate = 5,
EditDelete = 6,
DefaultPriority = 5,
EditUpdate = 6,
EditDelete = 7,
}
const hiddenToken = '•••••••••••••••';

View File

@@ -5,6 +5,7 @@ export interface IApplication {
description: string;
image: string;
internal: boolean;
defaultPriority: number;
}
export interface IClient {