Skip to content

Commit

Permalink
Migration: Refactoring
Browse files Browse the repository at this point in the history
Minor stuff, like using the arrow sytax everywhere and moving some
interface definitions around.
Also, added a message provider for unified banners, and implemented a
rule to disallow projects with names that conflict with pages on docat
web.
  • Loading branch information
reglim committed Dec 12, 2022
1 parent f85bc99 commit 04d8069
Show file tree
Hide file tree
Showing 41 changed files with 695 additions and 564 deletions.
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none"
}
9 changes: 9 additions & 0 deletions docat/docat/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
get_all_projects,
get_project_details,
index_all_projects,
is_forbidden_project_name,
remove_docs,
remove_file_index_from_db,
remove_version_from_version_index,
Expand Down Expand Up @@ -317,6 +318,10 @@ def upload(
docat_api_key: Optional[str] = Header(None),
db: TinyDB = Depends(get_db),
):
if is_forbidden_project_name(project):
response.status_code = status.HTTP_400_BAD_REQUEST
return ApiResponse(message=f'Project name "{project}" is forbidden, as it conflicts with pages in docat web.')

project_base_path = DOCAT_UPLOAD_FOLDER / project
base_path = project_base_path / version
target_file = base_path / file.filename
Expand Down Expand Up @@ -396,6 +401,10 @@ def claim(project: str, db: TinyDB = Depends(get_db)):
@app.put("/api/{project}/rename/{new_project_name}", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@app.put("/api/{project}/rename/{new_project_name}/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
def rename(project: str, new_project_name: str, response: Response, docat_api_key: str = Header(None), db: TinyDB = Depends(get_db)):
if is_forbidden_project_name(new_project_name):
response.status_code = status.HTTP_400_BAD_REQUEST
return ApiResponse(message=f'New project name "{new_project_name}" is forbidden, as it conflicts with pages in docat web.')

project_base_path = DOCAT_UPLOAD_FOLDER / project
new_project_base_path = DOCAT_UPLOAD_FOLDER / new_project_name

Expand Down
10 changes: 10 additions & 0 deletions docat/docat/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ def calculate_token(password, salt):
return hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 100000).hex()


def is_forbidden_project_name(name: str) -> bool:
"""
Checks if the given project name is forbidden.
The project name is forbidden if it conflicts with
a page on the docat website.
"""
name = name.lower().strip()
return name in ["upload", "claim", "delete", "search", "help"]


def get_all_projects(upload_folder_path: Path) -> Projects:
"""
Returns all projects in the upload folder.
Expand Down
23 changes: 23 additions & 0 deletions docat/tests/test_rename.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,26 @@ def test_rename_success(client_with_claimed_project):
assert len(claims_with_old_name) == 0
claims_with_new_name = table.search(Project.name == "new-project-name")
assert len(claims_with_new_name) == 1


def test_rename_rejects_forbidden_project_name(client_with_claimed_project):
"""
Names that conflict with pages in docat web are forbidden,
and renaming a project to such a name should fail.
"""

create_response = client_with_claimed_project.post(
"/api/some-project/1.0.0", files={"file": ("index.html", io.BytesIO(b"<h1>Hello World</h1>"), "plain/text")}
)
assert create_response.status_code == 201

with patch("os.rename") as rename_mock:
for project_name in ["upload", "claim", "delete", "Search ", "help"]:

rename_response = client_with_claimed_project.put(f"/api/some-project/rename/{project_name}", headers={"Docat-Api-Key": "1234"})
assert rename_response.status_code == 400
assert rename_response.json() == {
"message": f'New project name "{project_name}" is forbidden, as it conflicts with pages in docat web.'
}

assert rename_mock.mock_calls == []
17 changes: 17 additions & 0 deletions docat/tests/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,20 @@ def test_fails_with_invalid_token(client_with_claimed_project):
assert response_data["message"] == "Docat-Api-Key token is not valid for some-project"

assert remove_mock.mock_calls == []


def test_upload_rejects_forbidden_project_name(client_with_claimed_project):
"""
Names that conflict with pages in docat web are forbidden,
and creating a project with such a name should fail.
"""

with patch("docat.app.remove_docs") as remove_mock:
for project_name in ["upload", "claim", "delete", "Search ", "help"]:
response = client_with_claimed_project.post(
f"/api/{project_name}/1.0.0", files={"file": ("index.html", io.BytesIO(b"<h1>Hello World</h1>"), "plain/text")}
)
assert response.status_code == 400
assert response.json() == {"message": f'Project name "{project_name}" is forbidden, as it conflicts with pages in docat web.'}

assert remove_mock.mock_calls == []
3 changes: 3 additions & 0 deletions web/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"project": [
"./tsconfig.json"
]
},
"rules": {
"@typescript-eslint/space-before-function-paren": "off"
}
},
{
Expand Down
15 changes: 9 additions & 6 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import NotFound from './pages/NotFound'
import Upload from './pages/Upload'
import Search from './pages/Search'
import EscapeSlashForDocsPath from './pages/EscapeSlashForDocsPath'
import { MessageBannerProvider } from './data-providers/MessageBannerProvider'

function App (): JSX.Element {
function App(): JSX.Element {
const router = createHashRouter([
{
path: '/',
Expand Down Expand Up @@ -79,11 +80,13 @@ function App (): JSX.Element {

return (
<div className="App">
<ConfigDataProvider>
<ProjectDataProvider>
<RouterProvider router={router} />
</ProjectDataProvider>
</ConfigDataProvider>
<MessageBannerProvider>
<ConfigDataProvider>
<ProjectDataProvider>
<RouterProvider router={router} />
</ProjectDataProvider>
</ConfigDataProvider>
</MessageBannerProvider>
</div>
)
}
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/ClaimButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import ReactTooltip from 'react-tooltip'

import styles from './../style/components/ControlButtons.module.css'

class ButtonProps {
interface Props {
isSingleButton?: boolean
}

export default function ClaimButton (props: ButtonProps): JSX.Element {
export default function ClaimButton (props: Props): JSX.Element {
return (
<>
<ReactTooltip />
Expand Down
60 changes: 25 additions & 35 deletions web/src/components/DataSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,36 @@ export default function DataSelect (props: Props): JSX.Element {
props.value ?? 'none'
)

function onSelect (e: { target: { value: string } }): void {
const value = e.target.value

setSelectedValue(value)
props.onChange(value)
}

// clear field if selected value is not in options
if (
props.values.length > 0 &&
selectedValue !== 'none' &&
!props.values.includes(selectedValue)
) {
if (selectedValue !== 'none' && !props.values.includes(selectedValue)) {
setSelectedValue('none')
}

return (
<>
<FormGroup>
<TextField
onChange={onSelect}
value={props.values.length > 0 ? selectedValue : 'none'}
label={props.label}
error={props.errorMsg !== undefined && props.errorMsg !== ''}
helperText={props.errorMsg}
select
>
<MenuItem value="none" disabled>
{props.emptyMessage}
</MenuItem>
<FormGroup>
<TextField
onChange={(e: { target: { value: string } }) => {
setSelectedValue(e.target.value)
props.onChange(e.target.value)
}}
value={props.values.length > 0 ? selectedValue : 'none'}
label={props.label}
error={props.errorMsg !== undefined && props.errorMsg !== ''}
helperText={props.errorMsg}
select
>
<MenuItem value="none" disabled>
{props.emptyMessage}
</MenuItem>

{props.values.map((value) => {
return (
<MenuItem key={value} value={value}>
{value}
</MenuItem>
)
})}
</TextField>
</FormGroup>
</>
{props.values.map((value) => {
return (
<MenuItem key={value} value={value}>
{value}
</MenuItem>
)
})}
</TextField>
</FormGroup>
)
}
4 changes: 2 additions & 2 deletions web/src/components/DeleteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import React from 'react'

import styles from './../style/components/ControlButtons.module.css'

class ButtonProps {
interface Props {
isSingleButton?: boolean
}

export default function DeleteButton (props: ButtonProps): JSX.Element {
export default function DeleteButton (props: Props): JSX.Element {
return (
<>
<ReactTooltip />
Expand Down
15 changes: 7 additions & 8 deletions web/src/components/DocumentControlButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ interface Props {
}

export default function DocumentControlButtons (props: Props): JSX.Element {
const buttonStyle = { width: '25px', height: '25px' }

return (
<div className={styles.controls}>
<ReactTooltip />
Expand All @@ -23,31 +25,28 @@ export default function DocumentControlButtons (props: Props): JSX.Element {
className={styles['home-button']}
data-tip="Project Overview"
>
<Home sx={{ width: '25px', height: '25px' }} />
<Home sx={buttonStyle} />
</Link>

<FormControl>
<Select
className={styles['version-select']}
onChange={(e) => props.onVersionChange(e.target.value)}
value={(props.versions.length > 0) ? props.version : ''}
value={props.versions.length > 0 ? props.version : ''}
>
{props.versions.map((v) => (
<MenuItem key={v.name} value={v.name}>
{v.name + ((v.tags.length > 0) ? ` (${v.tags.join(', ')})` : '')}
{v.name + (v.tags.length > 0 ? ` (${v.tags.join(', ')})` : '')}
</MenuItem>
))}
</Select>
</FormControl>
<button
className={styles['hide-controls-button']}
data-tip="Hide Controls"
onClick={() => {
window.history.pushState({}, '', window.location.pathname)
props.onHideUi()
}}
onClick={props.onHideUi}
>
<VisibilityOff sx={{ width: '25px', height: '25px' }} />
<VisibilityOff sx={buttonStyle} />
</button>
</div>
)
Expand Down
8 changes: 5 additions & 3 deletions web/src/components/FavoriteStar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import { Star, StarOutline } from '@mui/icons-material'
import React, { useState } from 'react'
import ProjectRepository from '../repositories/ProjectRepository'

export default function FavoriteStar (props: {
interface Props {
projectName: string
onFavoriteChanged: () => void
}): JSX.Element {
}

export default function FavoriteStar (props: Props): JSX.Element {
const [isFavorite, setIsFavorite] = useState<boolean>(
ProjectRepository.isFavorite(props.projectName)
)

function toggleFavorite (): void {
const toggleFavorite = (): void => {
const newIsFavorite = !isFavorite
ProjectRepository.setFavorite(props.projectName, newIsFavorite)
setIsFavorite(newIsFavorite)
Expand Down
Loading

0 comments on commit 04d8069

Please sign in to comment.