Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement: Integrate the number of versions into projects endpoint #365

Merged
merged 1 commit into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docat/docat/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ class ClaimResponse(ApiResponse):
token: str


class ProjectWithVersionCount(BaseModel):
name: str
versions: int


class Projects(BaseModel):
projects: list[str]
projects: list[ProjectWithVersionCount]


class ProjectVersion(BaseModel):
Expand Down
38 changes: 22 additions & 16 deletions docat/docat/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from bs4.element import Comment
from tinydb import Query, TinyDB

from docat.models import ProjectDetail, Projects, ProjectVersion
from docat.models import ProjectDetail, Projects, ProjectVersion, ProjectWithVersionCount

NGINX_CONFIG_PATH = Path("/etc/nginx/locations.d")
UPLOAD_FOLDER = "doc"
Expand Down Expand Up @@ -114,20 +114,26 @@ def get_all_projects(upload_folder_path: Path) -> Projects:
Returns all projects in the upload folder.
"""

def has_not_hidden_versions(project):
def count_not_hidden_versions(project) -> int:
path = upload_folder_path / project
return any(
(path / version).is_dir() and not (path / version / ".hidden").exists() for version in (upload_folder_path / project).iterdir()
)

return Projects(
projects=list(
filter(
has_not_hidden_versions,
[str(project.relative_to(upload_folder_path)) for project in upload_folder_path.iterdir() if project.is_dir()],
)
)
)
versions = [
version
for version in (upload_folder_path / project).iterdir()
if (path / version).is_dir() and not (path / version).is_symlink() and not (path / version / ".hidden").exists()
]
return len(versions)

projects: list[ProjectWithVersionCount] = []

for project in upload_folder_path.iterdir():
if project.is_dir():
versions = count_not_hidden_versions(project)
if versions < 1:
continue

projects.append(ProjectWithVersionCount(name=str(project.relative_to(upload_folder_path)), versions=versions))

return Projects(projects=projects)


def get_project_details(upload_folder_path: Path, project_name: str) -> ProjectDetail | None:
Expand Down Expand Up @@ -174,8 +180,8 @@ def index_all_projects(
all_projects = get_all_projects(upload_folder_path).projects

for project in all_projects:
update_version_index_for_project(upload_folder_path, index_db, project)
update_file_index_for_project(upload_folder_path, index_db, project)
update_version_index_for_project(upload_folder_path, index_db, project.name)
update_file_index_for_project(upload_folder_path, index_db, project.name)


def update_file_index_for_project(upload_folder_path: Path, index_db: TinyDB, project: str):
Expand Down
2 changes: 1 addition & 1 deletion docat/tests/test_hide_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def test_hide_only_version_not_listed_in_projects(client_with_claimed_project):
projects_response = client_with_claimed_project.get("/api/projects")
assert projects_response.status_code == 200
assert projects_response.json() == {
"projects": ["some-project"],
"projects": [{"name": "some-project", "versions": 1}],
}

# hide the only version
Expand Down
2 changes: 1 addition & 1 deletion docat/tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_project_api(temp_project_version):
response = client.get("/api/projects")

assert response.ok
assert response.json() == {"projects": ["project"]}
assert response.json() == {"projects": [{"name": "project", "versions": 1}]}


def test_project_api_without_any_projects():
Expand Down
23 changes: 23 additions & 0 deletions docat/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import io
from pathlib import Path
from unittest.mock import MagicMock, patch

Expand Down Expand Up @@ -86,3 +87,25 @@ def test_remove_symlink_version(temp_project_version):
remove_docs(project, "latest", docat.DOCAT_UPLOAD_FOLDER)

assert not symlink_to_latest.exists()


def test_get_all_projects_counts_versions_correctly(client_with_claimed_project):
"""
Tests whether get_all_projects returns the correct number of versions.
(Don't count symlinks)
"""

versions = ["1.0.0", "2.0.0", "3.0.0"]
for version in versions:
response = client_with_claimed_project.post(
f"/api/some-project/{version}", files={"file": ("index.html", io.BytesIO(b"<h1>Hello World</h1>"), "plain/text")}
)
assert response.status_code == 201

# tag "3.0.0" as latest
response = client_with_claimed_project.put(f"/api/some-project/{versions[-1]}/tags/latest")
assert response.status_code == 201

response = client_with_claimed_project.get("/api/projects")
assert response.status_code == 200
assert response.json() == {"projects": [{"name": "some-project", "versions": len(versions)}]}
45 changes: 15 additions & 30 deletions web/src/components/Project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,22 @@ import ReactTooltip from 'react-tooltip'
import { Link } from 'react-router-dom'
import ProjectRepository from '../repositories/ProjectRepository'
import styles from './../style/components/Project.module.css'
import { Project as ProjectType } from '../models/ProjectsResponse'

import ProjectDetails from '../models/ProjectDetails'
import FavoriteStar from './FavoriteStar'

interface Props {
projectName: string
project: ProjectType
onFavoriteChanged: () => void
}

export default function Project (props: Props): JSX.Element {
const [versions, setVersions] = useState<ProjectDetails[]>([])
export default function Project(props: Props): JSX.Element {
const [logo, setLogo] = useState<Blob | null>(null)

// try to load image to prevent image flashing
useEffect(() => {
void (async () => {
const logoURL = ProjectRepository.getProjectLogoURL(props.projectName)
const logoURL = ProjectRepository.getProjectLogoURL(props.project.name)
try {
const response = await fetch(logoURL)
if (response.status === 200) {
Expand All @@ -30,62 +29,48 @@ export default function Project (props: Props): JSX.Element {
setLogo(null)
}
})()
}, [props.projectName])

// reload versions on project name change
useEffect(() => {
void (async () => {
try {
const versionResponse = await ProjectRepository.getVersions(
props.projectName
)
setVersions(versionResponse)
} catch (e) {
setVersions([])
}
})()
}, [props.projectName])
}, [props.project])

return (
<div className={styles['project-card']}>
<ReactTooltip />
<div className={styles['project-card-header']}>
<Link to={`/${props.projectName}/latest`}>
<Link to={`/${props.project.name}/latest`}>
{logo == null
? (
<div
className={styles['project-card-title']}
data-tip={props.projectName}
data-tip={props.project.name}
>
{props.projectName}
{props.project.name}
</div>
)
: (
<>
<img
className={styles['project-logo']}
src={URL.createObjectURL(logo)}
alt={`${props.projectName} project Logo`}
alt={`${props.project.name} project Logo`}
/>

<div
className={styles['project-card-title-with-logo']}
data-tip={props.projectName}
data-tip={props.project.name}
>
{props.projectName}
{props.project.name}
</div>
</>
)}
</Link>
<FavoriteStar
projectName={props.projectName}
projectName={props.project.name}
onFavoriteChanged={props.onFavoriteChanged}
/>
</div>
<div className={styles.subhead}>
{versions.length === 1
? `${versions.length} version`
: `${versions.length} versions`}
{props.project.versions === 1
? `${props.project.versions} version`
: `${props.project.versions} versions`}
</div>
</div>
)
Expand Down
7 changes: 4 additions & 3 deletions web/src/components/ProjectList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import Project from './Project'
import React from 'react'

import styles from './../style/components/ProjectList.module.css'
import { Project as ProjectType } from '../models/ProjectsResponse'

interface Props {
projects: string[]
projects: ProjectType[]
onFavoriteChanged: () => void
}

Expand All @@ -17,8 +18,8 @@ export default function ProjectList (props: Props): JSX.Element {
<div className={styles['project-list']}>
{props.projects.map((project) => (
<Project
projectName={project}
key={project}
project={project}
key={project.name}
onFavoriteChanged={() => props.onFavoriteChanged()}
/>
))}
Expand Down
5 changes: 3 additions & 2 deletions web/src/data-providers/ProjectDataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
*/

import React, { createContext, useContext, useEffect, useState } from 'react'
import ProjectsResponse, { Project } from '../models/ProjectsResponse'
import { useMessageBanner } from './MessageBannerProvider'

interface ProjectState {
projects: string[] | null
projects: Project[] | null
loadingFailed: boolean
reload: () => void
}
Expand Down Expand Up @@ -42,7 +43,7 @@ export function ProjectDataProvider({ children }: any): JSX.Element {
)
}

const data: { projects: string[] } = await response.json()
const data: ProjectsResponse = await response.json()
setProjects({
projects: data.projects,
loadingFailed: false,
Expand Down
8 changes: 8 additions & 0 deletions web/src/models/ProjectsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface Project {
name: string
versions: number
}

export default interface ProjectsResponse {
projects: Project[]
}
6 changes: 3 additions & 3 deletions web/src/pages/Claim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ export default function Claim(): JSX.Element {
}

/**
* Returns loaded Projects for DataSelect
* @returns projects as string[] or an empty array
* Returns loaded project names for DataSelect
* @returns project names as string[] or an empty array
*/
const getProjects = (): string[] => {
if (loadingFailed || projects == null) {
return []
}

return projects
return projects.map((project) => project.name)
}

const onProjectSelect = (p: string): void => {
Expand Down
4 changes: 2 additions & 2 deletions web/src/pages/Delete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,15 @@ export default function Delete(): JSX.Element {
}

/**
* Returns loaded Projects for DataSelect
* Returns loaded project names for DataSelect
* @returns string[] or an empty array
*/
const getProjects = (): string[] => {
if (loadingFailed || projects == null) {
return []
}

return projects
return projects.map((project) => project.name)
}

/**
Expand Down
9 changes: 5 additions & 4 deletions web/src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,23 @@ import LoadingPage from './LoadingPage'

import styles from './../style/pages/Home.module.css'
import { ErrorOutline } from '@mui/icons-material'
import { Project } from '../models/ProjectsResponse'

export default function Home (): JSX.Element {
const { projects, loadingFailed } = useProjects()
const [nonFavoriteProjects, setNonFavoriteProjects] = useState<string[]>([])
const [favoriteProjects, setFavoriteProjects] = useState<string[]>([])
const [nonFavoriteProjects, setNonFavoriteProjects] = useState<Project[]>([])
const [favoriteProjects, setFavoriteProjects] = useState<Project[]>([])

document.title = 'Home | docat'

const updateFavorites = (): void => {
if (projects == null) return

setFavoriteProjects(
projects.filter((project) => ProjectRepository.isFavorite(project))
projects.filter((project) => ProjectRepository.isFavorite(project.name))
)
setNonFavoriteProjects(
projects.filter((project) => !ProjectRepository.isFavorite(project))
projects.filter((project) => !ProjectRepository.isFavorite(project.name))
)
}

Expand Down