mirror of
https://github.com/pds-nest/nest.git
synced 2024-11-25 14:34:19 +00:00
Merge remote-tracking branch 'origin/main' into main
This commit is contained in:
commit
396311aefd
8 changed files with 170 additions and 81 deletions
|
@ -3,23 +3,6 @@ from flask.testing import Client
|
||||||
'''A file that contains tests of classes and methods for all the requests concerning an user.'''
|
'''A file that contains tests of classes and methods for all the requests concerning an user.'''
|
||||||
|
|
||||||
|
|
||||||
class TestRepositoryGetAll:
|
|
||||||
def test_get_all_user_repositories(self, flask_client: Client, user_headers):
|
|
||||||
r = flask_client.get(f'/api/v1/repositories/', headers=user_headers,
|
|
||||||
json={'owner_id': 'utente_test@nest.com', 'isActive': False})
|
|
||||||
assert r.json["result"] == "success"
|
|
||||||
# assert r.json["data"]["owner"] == "utente_test@nest.com"
|
|
||||||
# assert r.json["data"]["isAdmin"] is not True
|
|
||||||
|
|
||||||
def test_get_all_admin_repositories(self, flask_client: Client, admin_headers):
|
|
||||||
r = flask_client.get(f'/api/v1/repositories/', headers=admin_headers,
|
|
||||||
json={'owner_id': 'admin@admin.com', 'isActive': False})
|
|
||||||
assert r.json["result"] == "success"
|
|
||||||
# assert r.json["data"]["owner"] == "admin@admin.com"
|
|
||||||
# assert r.json["data"]["isAdmin"] is True
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestRepositoryAdd:
|
class TestRepositoryAdd:
|
||||||
def test_for_success(self, flask_client: Client, user_headers):
|
def test_for_success(self, flask_client: Client, user_headers):
|
||||||
r = flask_client.post(f'/api/v1/repositories/', headers=user_headers, json={
|
r = flask_client.post(f'/api/v1/repositories/', headers=user_headers, json={
|
||||||
|
@ -34,12 +17,12 @@ class TestRepositoryAdd:
|
||||||
'name': 'repo_test',
|
'name': 'repo_test',
|
||||||
'is_active': True
|
'is_active': True
|
||||||
})
|
})
|
||||||
|
assert r.status_code == 200
|
||||||
assert r.json["result"] == "success"
|
assert r.json["result"] == "success"
|
||||||
assert r.json["data"]["is_active"] is True
|
assert r.json["data"]["is_active"] is True
|
||||||
|
|
||||||
|
|
||||||
# non vengono passate le condizioni necessarie, in questo caso il nome della repository
|
# non vengono passate le condizioni necessarie, in questo caso il nome della repository
|
||||||
def test_for_failure(self, flask_client: Client, user_headers):
|
def test_no_name(self, flask_client: Client, user_headers):
|
||||||
r = flask_client.post(f'/api/v1/repositories/', headers=user_headers, json={
|
r = flask_client.post(f'/api/v1/repositories/', headers=user_headers, json={
|
||||||
'conditions': [
|
'conditions': [
|
||||||
{
|
{
|
||||||
|
@ -49,12 +32,72 @@ class TestRepositoryAdd:
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'evaluation_mode': 0,
|
'evaluation_mode': 0,
|
||||||
|
|
||||||
'is_active': True
|
'is_active': True
|
||||||
})
|
})
|
||||||
|
assert r.status_code == 400
|
||||||
assert r.json["msg"] == "Missing arguments."
|
assert r.json["msg"] == "Missing arguments."
|
||||||
assert r.json["result"] == "failure"
|
assert r.json["result"] == "failure"
|
||||||
|
|
||||||
|
# viene passato un campo evaluation_mode con valore non previsto dall'enum
|
||||||
|
def test_wrong_evaluation_mode(self, flask_client: Client, user_headers):
|
||||||
|
r = flask_client.post(f'/api/v1/repositories/', headers=user_headers, json={
|
||||||
|
'conditions': [
|
||||||
|
{
|
||||||
|
'content': 'PdS2021',
|
||||||
|
'id': 0,
|
||||||
|
'type': 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'evaluation_mode': 99,
|
||||||
|
'name': 'repo_test',
|
||||||
|
'is_active': True
|
||||||
|
})
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert r.json["result"] == "failure"
|
||||||
|
|
||||||
|
# viene passato un campo type con valore non previsto dall'enum
|
||||||
|
def test_wrong_condition(self, flask_client: Client, user_headers):
|
||||||
|
r = flask_client.post(f'/api/v1/repositories/', headers=user_headers, json={
|
||||||
|
'conditions': [
|
||||||
|
{
|
||||||
|
'content': 'PdS2021',
|
||||||
|
'id': 0,
|
||||||
|
'type': 99
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'evaluation_mode': 2,
|
||||||
|
'name': 'repo_test',
|
||||||
|
'is_active': True
|
||||||
|
})
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert r.json["result"] == "failure"
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepositoryGetAll:
|
||||||
|
def test_get_all_user_repositories(self, flask_client: Client, user_headers):
|
||||||
|
r = flask_client.get(f'/api/v1/repositories/', headers=user_headers,
|
||||||
|
json={'owner_id': 'utente_test@nest.com', 'isActive': False})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json["result"] == "success"
|
||||||
|
|
||||||
|
def test_get_all_admin_repositories(self, flask_client: Client, admin_headers):
|
||||||
|
r = flask_client.get(f'/api/v1/repositories/', headers=admin_headers,
|
||||||
|
json={'owner_id': 'admin@admin.com', 'isActive': False})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json["result"] == "success"
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepositoryGet:
|
||||||
|
def test_get_existing_repository(self, flask_client: Client, user_headers):
|
||||||
|
r = flask_client.get(f'/api/v1/repositories/1', headers=user_headers)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json["result"] == "success"
|
||||||
|
|
||||||
|
def test_get_non_existing_repository(self, flask_client: Client, admin_headers):
|
||||||
|
r = flask_client.get(f'/api/v1/repositories/99', headers=admin_headers)
|
||||||
|
assert r.status_code == 404
|
||||||
|
assert r.json["result"] == "failure"
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
class TestUserDelete:
|
class TestUserDelete:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React from "react"
|
||||||
import Layout from "./components/interactive/Layout"
|
import Layout from "./components/interactive/Layout"
|
||||||
import { BrowserRouter } from "react-router-dom"
|
import { BrowserRouter } from "react-router-dom"
|
||||||
import GlobalTheme from "./components/providers/GlobalTheme"
|
import GlobalTheme from "./components/providers/GlobalTheme"
|
||||||
|
|
|
@ -3,41 +3,50 @@ import BoxFull from "../base/BoxFull"
|
||||||
import SummaryRepository from "./SummaryRepository"
|
import SummaryRepository from "./SummaryRepository"
|
||||||
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons"
|
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons"
|
||||||
import ContextUser from "../../contexts/ContextUser"
|
import ContextUser from "../../contexts/ContextUser"
|
||||||
|
import Loading from "../base/Loading"
|
||||||
|
import BoxFullScrollable from "../base/BoxFullScrollable"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link BoxFull} listing all the user's active repositories.
|
* A {@link BoxFull} listing all the user's active repositories.
|
||||||
*
|
*
|
||||||
* @param repositories - Array of repositories to display in the box.
|
* @param repositories - Array of repositories to display in the box.
|
||||||
* @param refresh - Function that can be called to refresh the repositories list.
|
* @param archiveRepository - Function to be called when archive is pressed on a repository summary.
|
||||||
|
* @param destroyRepository - Function to be called when delete is pressed on a repository summary.
|
||||||
|
* @param running - If an action is currently running.
|
||||||
* @param props - Additional props to pass to the box.
|
* @param props - Additional props to pass to the box.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function BoxRepositoriesActive({ repositories, refresh, ...props }) {
|
export default function BoxRepositoriesActive({ repositories, archiveRepository, destroyRepository, running, ...props }) {
|
||||||
const { user } = useContext(ContextUser)
|
const { user } = useContext(ContextUser)
|
||||||
|
|
||||||
let contents
|
let contents
|
||||||
if(repositories.length > 0) {
|
if(repositories === null) {
|
||||||
|
contents = <Loading/>
|
||||||
|
}
|
||||||
|
else if(repositories.length === 0) {
|
||||||
|
contents = <i>There's nothing here.</i>
|
||||||
|
}
|
||||||
|
else {
|
||||||
contents = repositories.map(repo => (
|
contents = repositories.map(repo => (
|
||||||
<SummaryRepository
|
<SummaryRepository
|
||||||
key={repo["id"]}
|
key={repo["id"]}
|
||||||
repo={repo}
|
repo={repo}
|
||||||
icon={faFolderOpen}
|
icon={faFolderOpen}
|
||||||
refresh={refresh}
|
archiveSelf={() => archiveRepository(repo["id"])}
|
||||||
|
deleteSelf={() => destroyRepository(repo["id"])}
|
||||||
canArchive={true}
|
canArchive={true}
|
||||||
canEdit={true}
|
canEdit={true}
|
||||||
canDelete={repo["owner"]["username"] === user["username"]}
|
canDelete={repo["owner"]["username"] === user["username"]}
|
||||||
|
running={running}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
contents = <i>There's nothing here.</i>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxFull header={"Your active repositories"} {...props}>
|
<BoxFullScrollable header={"Your active repositories"} {...props}>
|
||||||
{contents}
|
{contents}
|
||||||
</BoxFull>
|
</BoxFullScrollable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,52 @@
|
||||||
import React, { useContext } from "react"
|
import React, { useContext } from "react"
|
||||||
import BoxFull from "../base/BoxFull"
|
import BoxFull from "../base/BoxFull"
|
||||||
import ContextUser from "../../contexts/ContextUser"
|
|
||||||
import SummaryRepository from "./SummaryRepository"
|
import SummaryRepository from "./SummaryRepository"
|
||||||
import { faFolder } from "@fortawesome/free-solid-svg-icons"
|
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import ContextUser from "../../contexts/ContextUser"
|
||||||
|
import Loading from "../base/Loading"
|
||||||
|
import BoxFullScrollable from "../base/BoxFullScrollable"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link BoxFull} listing all the user's archived repositories.
|
* A {@link BoxFull} listing all the user's archived repositories.
|
||||||
*
|
*
|
||||||
* @param repositories - Array of repositories to display in the box.
|
* @param repositories - Array of repositories to display in the box.
|
||||||
* @param refresh - Function that can be called to refresh the repositories list.
|
* @param archiveRepository - Function to be called when archive is pressed on a repository summary.
|
||||||
|
* @param destroyRepository - Function to be called when delete is pressed on a repository summary.
|
||||||
|
* @param running - If an action is currently running.
|
||||||
* @param props - Additional props to pass to the box.
|
* @param props - Additional props to pass to the box.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function BoxRepositoriesArchived({ repositories, refresh, ...props }) {
|
export default function BoxRepositoriesArchived({ repositories, archiveRepository, destroyRepository, running, ...props }) {
|
||||||
const { user } = useContext(ContextUser)
|
const { user } = useContext(ContextUser)
|
||||||
|
|
||||||
let contents
|
let contents
|
||||||
if(repositories.length > 0) {
|
if(repositories === null) {
|
||||||
|
contents = <Loading/>
|
||||||
|
}
|
||||||
|
else if(repositories.length === 0) {
|
||||||
|
contents = <i>There's nothing here.</i>
|
||||||
|
}
|
||||||
|
else {
|
||||||
contents = repositories.map(repo => (
|
contents = repositories.map(repo => (
|
||||||
<SummaryRepository
|
<SummaryRepository
|
||||||
key={repo["id"]}
|
key={repo["id"]}
|
||||||
repo={repo}
|
repo={repo}
|
||||||
icon={faFolder}
|
icon={faFolderOpen}
|
||||||
refresh={refresh}
|
archiveSelf={() => archiveRepository(repo["id"])}
|
||||||
|
deleteSelf={() => destroyRepository(repo["id"])}
|
||||||
canArchive={false}
|
canArchive={false}
|
||||||
canEdit={false}
|
canEdit={false}
|
||||||
canDelete={repo["owner"]["username"] === user["username"]}
|
canDelete={repo["owner"]["username"] === user["username"]}
|
||||||
|
running={running}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
contents = <i>There's nothing here.</i>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxFull header={"Your archived repositories"} {...props}>
|
<BoxFullScrollable header={"Your active repositories"} {...props}>
|
||||||
{contents}
|
{contents}
|
||||||
</BoxFull>
|
</BoxFullScrollable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,20 +13,20 @@ import Summary from "../base/Summary"
|
||||||
* @param repo - The repository object.
|
* @param repo - The repository object.
|
||||||
* @param refresh - Function that can be called to refresh the repositories list.
|
* @param refresh - Function that can be called to refresh the repositories list.
|
||||||
* @param canDelete - If the Delete button should be displayed or not.
|
* @param canDelete - If the Delete button should be displayed or not.
|
||||||
|
* @param deleteSelf - Function to call when the Delete button is pressed.
|
||||||
* @param canEdit - If the Edit button should be displayed or not.
|
* @param canEdit - If the Edit button should be displayed or not.
|
||||||
* @param canArchive - If the Archive button should be displayed or not.
|
* @param canArchive - If the Archive button should be displayed or not.
|
||||||
|
* @param archiveSelf - Function to call when the Archive button is pressed.
|
||||||
|
* @param running - If an action is currently running.
|
||||||
* @param className - Additional class(es) to be added to the outer box.
|
* @param className - Additional class(es) to be added to the outer box.
|
||||||
* @param props - Additional props to pass to the outer box.
|
* @param props - Additional props to pass to the outer box.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function SummaryRepository(
|
export default function SummaryRepository(
|
||||||
{ repo, refresh, canDelete, canEdit, canArchive, className, ...props },
|
{ repo, refresh, canDelete, deleteSelf, canEdit, canArchive, archiveSelf, running, className, ...props },
|
||||||
) {
|
) {
|
||||||
const { fetchDataAuth } = useContext(ContextUser)
|
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { fetchNow: archiveThis } = useBackend(fetchDataAuth, "PATCH", `/api/v1/repositories/${repo.id}`, { "close": true })
|
|
||||||
const { fetchNow: deletThis } = useBackend(fetchDataAuth, "DELETE", `/api/v1/repositories/${repo.id}`)
|
|
||||||
|
|
||||||
const onRepoClick = () => {
|
const onRepoClick = () => {
|
||||||
history.push(`/repositories/${repo.id}`)
|
history.push(`/repositories/${repo.id}`)
|
||||||
|
@ -36,22 +36,13 @@ export default function SummaryRepository(
|
||||||
history.push(`/repositories/${repo.id}/edit`)
|
history.push(`/repositories/${repo.id}/edit`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onArchiveClick = async () => {
|
|
||||||
await archiveThis()
|
|
||||||
await refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDeleteClick = async () => {
|
|
||||||
await deletThis()
|
|
||||||
await refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttons = <>
|
const buttons = <>
|
||||||
{canDelete ?
|
{canDelete ?
|
||||||
<Button
|
<Button
|
||||||
color={"Red"}
|
color={"Red"}
|
||||||
icon={faTrash}
|
icon={faTrash}
|
||||||
onClick={onDeleteClick}
|
onClick={deleteSelf}
|
||||||
|
disabled={running}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -61,6 +52,7 @@ export default function SummaryRepository(
|
||||||
color={"Yellow"}
|
color={"Yellow"}
|
||||||
icon={faPencilAlt}
|
icon={faPencilAlt}
|
||||||
onClick={onEditClick}
|
onClick={onEditClick}
|
||||||
|
disabled={running}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -69,7 +61,8 @@ export default function SummaryRepository(
|
||||||
<Button
|
<Button
|
||||||
color={"Grey"}
|
color={"Grey"}
|
||||||
icon={faArchive}
|
icon={faArchive}
|
||||||
onClick={onArchiveClick}
|
onClick={archiveSelf}
|
||||||
|
disabled={running}
|
||||||
>
|
>
|
||||||
{"Archive"}
|
{"Archive"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -127,6 +127,25 @@ export default function useBackendViewset(resourcesPath, pkName) {
|
||||||
[apiList],
|
[apiList],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const refreshResource = useCallback(
|
||||||
|
async (pk) => {
|
||||||
|
try {
|
||||||
|
const refreshedResource = await apiRetrieve(pk)
|
||||||
|
setResources(resources => resources.map(resource => {
|
||||||
|
if(resource[pkName] === pk) {
|
||||||
|
return refreshedResource
|
||||||
|
}
|
||||||
|
return resource
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
return { error: e }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
[apiRetrieve, pkName]
|
||||||
|
)
|
||||||
|
|
||||||
const createResource = useCallback(
|
const createResource = useCallback(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
try {
|
try {
|
||||||
|
@ -191,12 +210,14 @@ export default function useBackendViewset(resourcesPath, pkName) {
|
||||||
resources,
|
resources,
|
||||||
running,
|
running,
|
||||||
loaded,
|
loaded,
|
||||||
|
apiRequest,
|
||||||
apiList,
|
apiList,
|
||||||
apiRetrieve,
|
apiRetrieve,
|
||||||
apiCreate,
|
apiCreate,
|
||||||
apiEdit,
|
apiEdit,
|
||||||
apiDestroy,
|
apiDestroy,
|
||||||
refreshResources,
|
refreshResources,
|
||||||
|
refreshResource,
|
||||||
createResource,
|
createResource,
|
||||||
editResource,
|
editResource,
|
||||||
destroyResource,
|
destroyResource,
|
||||||
|
|
|
@ -1,32 +1,44 @@
|
||||||
import React, { useContext } from "react"
|
import React, { useCallback, useContext } from "react"
|
||||||
import Style from "./PageRepositories.module.css"
|
import Style from "./PageRepositories.module.css"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import BoxRepositoriesActive from "../components/interactive/BoxRepositoriesActive"
|
import BoxRepositoriesActive from "../components/interactive/BoxRepositoriesActive"
|
||||||
import BoxRepositoriesArchived from "../components/interactive/BoxRepositoriesArchived"
|
import BoxRepositoriesArchived from "../components/interactive/BoxRepositoriesArchived"
|
||||||
import useBackendImmediately from "../hooks/useBackendImmediately"
|
import useBackendViewset from "../hooks/useBackendViewset"
|
||||||
import ContextUser from "../contexts/ContextUser"
|
|
||||||
import renderContents from "../utils/renderContents"
|
|
||||||
|
|
||||||
|
|
||||||
export default function PageRepositories({ children, className, ...props }) {
|
export default function PageRepositories({ children, className, ...props }) {
|
||||||
const { fetchDataAuth } = useContext(ContextUser)
|
const bv = useBackendViewset("/api/v1/repositories/", "id")
|
||||||
const repositoryRequest = useBackendImmediately(fetchDataAuth, "GET", "/api/v1/repositories/")
|
|
||||||
const contents = renderContents(
|
const archiveRepository = useCallback(
|
||||||
repositoryRequest,
|
async (pk) => {
|
||||||
data => {
|
try {
|
||||||
const repositories = [...data["owner"], ...data["spectator"]]
|
await bv.apiRequest("PATCH", `/api/v1/repositories/${pk}`, {
|
||||||
const active = repositories.filter(r => r.is_active)
|
"close": true,
|
||||||
const archived = repositories.filter(r => !r.is_active)
|
})
|
||||||
return <>
|
await bv.refreshResource(pk)
|
||||||
<BoxRepositoriesActive repositories={active} refresh={repositoryRequest.fetchNow}/>
|
}
|
||||||
<BoxRepositoriesArchived repositories={archived} refresh={repositoryRequest.fetchNow}/>
|
catch(e) {
|
||||||
</>
|
return { error: e }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
},
|
},
|
||||||
|
[bv.apiRequest, bv.refreshResource]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(Style.PageRepositories, className)} {...props}>
|
<div className={classNames(Style.PageRepositories, className)} {...props}>
|
||||||
{contents}
|
<BoxRepositoriesActive
|
||||||
|
repositories={bv.loaded ? bv.resources.filter(r => r.is_active) : null}
|
||||||
|
archiveRepository={archiveRepository}
|
||||||
|
destroyRepository={bv.destroyResource}
|
||||||
|
running={bv.running}
|
||||||
|
/>
|
||||||
|
<BoxRepositoriesArchived
|
||||||
|
repositories={bv.loaded ? bv.resources.filter(r => !r.is_active) : null}
|
||||||
|
archiveRepository={archiveRepository}
|
||||||
|
destroyRepository={bv.destroyResource}
|
||||||
|
running={bv.running}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React from "react"
|
||||||
import Loading from "../components/base/Loading"
|
import Loading from "../components/base/Loading"
|
||||||
import BoxAlert from "../components/base/BoxAlert"
|
import BoxAlert from "../components/base/BoxAlert"
|
||||||
import Starting from "../components/base/Starting"
|
import Starting from "../components/base/Starting"
|
||||||
|
|
Loading…
Reference in a new issue