Skip to content

Commit

Permalink
Merge pull request #8 from 3nd3r1/main
Browse files Browse the repository at this point in the history
v1.0.0
  • Loading branch information
3nd3r1 authored Oct 15, 2024
2 parents 0540ded + b2fa5d7 commit 1270d02
Show file tree
Hide file tree
Showing 54 changed files with 2,410 additions and 572 deletions.
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@
</p>

## Key Features:
- **Task Management**: Create, assign, and track tasks with due dates, priorities, and progress statuses.
- **Kanban Board**: Visualize and manage tasks across different stages (To Do, In Progress, Done).
- **Project Organization**: Organize tasks within specific projects for better clarity and tracking.
- **User Roles**: Assign roles (Admin/User) with different access permissions.
- **Comments and Collaboration**: Leave comments on tasks for better communication between team members.
- **Activity Log**: Track task updates and project changes in real time.
- **Visualization**: Easily create charts (E.g. burndown chart) to visualize your project.
- [x] **Task Management**: Create, assign, and track tasks with due dates, priorities, and progress statuses.
- [x] **Kanban Board**: Visualize and manage tasks across different stages (To Do, In Progress, Done).
- [x] **Project Organization**: Organize tasks within specific projects for better clarity and tracking.
- [x] **User Roles**: Assign roles (Admin/User) with different access permissions.
- [x] **Comments and Collaboration**: Leave comments on tasks for better communication between team members.
- [x] **Activity Log**: Track task updates and project changes in real time.
- [ ] **Visualization**: Easily create charts (E.g. burndown chart) to visualize your project.

## Tech Stack:
- **Backend**: Flask (Python)
Expand Down Expand Up @@ -75,6 +75,20 @@ poetry run invoke dev

## Weekly Report

### Loppupalautus - 2024-10-15

The project is now finished. The project has a good code structure and clean code. All features except the visualization feature have been implemented. After adding some unittests for tasks and projects, the test coverage has risen to around 60%.

Addressing some of the feedback in labtool:
- The CSRF vulnerability has been fixed
- After a thorough code review, I believe there are no SQL injection vulnerabilities present. While I am formatting some queries, all user inputs are sanitized and passed as parameters. The formatting applied to table and field names does not involve any user input.
- I added a COUNT query to the dashboard, but I am not sure if that is enough. I couldn't think of any other aggregates that would be useful.
- I removed all HTML5 client-side validation so all the errors are displayed at once. However, I don't think disabling client-side validation is a good idea, since it will add more load to the server.

The project is available in production on [protaskinate-page.host.ender.fi](https://protaskinate-page.host.ender.fi/).

More info about updates in the [changelog](docs/changelog.md).

### Välipalautus 3 - 2024-10-1

The project is almost finished. The project still has good code structure and clean code. Many more features have been implemented, but some are still left. Test coverage is still very poor.
Expand Down
27 changes: 27 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ ProTaskinate changelog

This format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [v1.0.0] - 2024-10-15

### Added

- User can now have one of the following roles for each project: `reader`, `writer`, `admin`
- `reader` can view tasks, view project and comment to a task
- `writer` can also create tasks and update/delete user's own tasks
- `admin` can also update/delete all tasks and delete/update the project
- User can now view updated_at for all tasks user has access to
- User can now update the deadline of a task user has access to
- User can now addditionally view description, creator, role, updated_at and created_at for all projects user has access to
- User can now assign and de-assign roles for projects user has admin access to
- User can now create a project
- User can now view all tasks of project user has access to in a kanban board
- User can now view a project activity log if he has admin access to the project
- Activity log shows task creation, task update, task deletion, task creation and project update
- User can view the amount of tasks assigned to the user in the dashboard

### Fixed

- Important buttons are now purple to fit the color scheme
- Page content has been divided into logical sections
- When a project is selected, the navbar shows navigations for that project
- Forms now show errors all at once instead of one by one
- Added length restrictions to most fields
- Datetimes are now displayed according to the user's timezone

## [v0.2.0-beta] - 2024-10-01

### Added
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 46 additions & 18 deletions protaskinate/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from werkzeug.security import generate_password_hash

from protaskinate.routes import dashboard, login, logout, project, register
from protaskinate.utils.csrf import csrf
from protaskinate.utils.database import db
from protaskinate.utils.login_manager import lm

Expand All @@ -18,15 +19,19 @@


def create_app():
""" Create the Flask app """
"""Create the Flask app"""
app = Flask(__name__)

# If DATABASE_URL or SECRET_KEY not set throw error
if not os.environ.get("DATABASE_URL"):
raise ValueError("DATABASE_URL environment variable is not set")
if not os.environ.get("SECRET_KEY"):
raise ValueError(("Please generate a secret_key with "
"`poetry run invoke generate-secret-key`"))
raise ValueError(
(
"Please generate a secret_key with "
"`poetry run invoke generate-secret-key`"
)
)
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URL")
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
"pool_pre_ping": True,
Expand All @@ -40,6 +45,7 @@ def create_app():

db.init_app(app)
lm.init_app(app)
csrf.init_app(app)

app.cli.add_command(create_schema)
app.cli.add_command(populate_db)
Expand All @@ -61,7 +67,7 @@ def ping():
@click.command("create_schema")
@with_appcontext
def create_schema():
""" Create the database schema """
"""Create the database schema"""
logging.info("Creating schema")
with open("schema.sql", "r", encoding="utf-8") as fp:
sql = fp.read()
Expand All @@ -75,34 +81,56 @@ def create_schema():
@click.command("populate_db")
@with_appcontext
def populate_db():
""" Populate the database with sample data """
"""Populate the database with sample data"""
logging.info("Populating database")
sql_users = """
INSERT INTO users (username, password) VALUES (:username, :password);
INSERT INTO users (username, password) VALUES
('admin', :admin_password),
('user', :user_password);
"""
sql_projects = """
INSERT INTO projects (name, creator_id) VALUES
('Project 1', 1),
('Project 2', 1),
('Project 3', 1);
INSERT INTO projects (name, creator_id, created_at, updated_at, description) VALUES
('Project 1', 1, '2021-01-01', '2021-01-03', 'Project 1 description'),
('Project 2', 1, '2021-01-02', '2021-01-03', 'Project 2 description'),
('Project 3', 1, '2021-01-03', '2021-01-03', 'Project 3 description');
"""
sql_tasks = """
INSERT INTO tasks (title, status, creator_id, created_at, priority, project_id, description) VALUES
('Task 1', 'open', 1, '2021-01-01', 'low', 1, 'Task 1 description'),
('Task 2', 'in_progress', 1, '2021-01-02', 'high', 1, 'Task 2 description'),
('Task 3', 'done', 1, '2021-01-03', 'very_high', 1, 'Task 3 description');
INSERT INTO tasks (title, status, creator_id, created_at, updated_at, priority, project_id, description) VALUES
('Project 1 Task 1', 'open', 1, '2021-01-01', '2021-01-01', 'low', 1, 'Task 1 description'),
('Project 1 Task 2', 'in_progress', 1, '2021-01-02', '2021-01-02', 'high', 1, 'Task 2 description'),
('Project 1 Task 3', 'done', 2, '2021-01-03', '2021-01-03', 'very_high', 1, 'Task 3 description'),
('Project 2 Task 1', 'open', 1, '2021-01-01', '2021-01-01', 'low', 2, 'Task 1 description'),
('Project 2 Task 2', 'in_progress', 1, '2021-01-02', '2021-01-02', 'high', 2, 'Task 2 description'),
('Project 2 Task 3', 'done', 2, '2021-01-03', '2021-01-03', 'very_high', 2, 'Task 3 description'),
('Project 3 Task 1', 'open', 1, '2021-01-01', '2021-01-01', 'low', 3, 'Task 1 description'),
('Project 3 Task 2', 'in_progress', 1, '2021-01-02', '2021-01-02', 'high', 3, 'Task 2 description'),
('Project 3 Task 3', 'done', 2, '2021-01-03', '2021-01-03', 'very_high', 3, 'Task 3 description');
"""
sql_comments = """
INSERT INTO comments (task_id, creator_id, created_at, content) VALUES
(1, 1, '2021-01-01', 'Comment 1'),
(1, 1, '2021-01-02', 'Comment 2'),
(2, 1, '2021-01-03', 'Comment 3');
(1, 2, '2021-01-02', 'Comment 2'),
(1, 1, '2021-01-03', 'Comment 4'),
(2, 1, '2021-01-04', 'Comment 3');
"""
sql_user_projects = """
INSERT INTO user_projects (user_id, project_id, role) VALUES
(2, 2, 'reader'),
(2, 3, 'writer');
"""

with db.engine.connect() as conn:
conn.execute(text(sql_users),
{"username": "admin", "password": generate_password_hash("admin")})
conn.execute(
text(sql_users),
{
"admin_password": generate_password_hash("admin"),
"user_password": generate_password_hash("user"),
},
)
conn.execute(text(sql_projects))
conn.execute(text(sql_tasks))
conn.execute(text(sql_comments))
conn.execute(text(sql_user_projects))
conn.commit()
1 change: 1 addition & 0 deletions protaskinate/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from .task import Task
from .project import Project
from .comment import Comment
from .activity_log import ActivityLog
42 changes: 42 additions & 0 deletions protaskinate/entities/activity_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""protaskinate/entities/activity_log.py"""

from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import TypedDict

from protaskinate.utils.validation import validate_enum, validate_type


class ActivityLogAction(Enum):
"""Enumeration representing the action of an activity log"""

CREATE_TASK = "create_task"
UPDATE_TASK = "update_task"
DELETE_TASK = "delete_task"
UPDATE_PROJECT = "update_project"

class NewActivityLog(TypedDict):
"""Type representing the data required to create a new activity log"""

user_id: int
project_id: int
created_at: datetime
action: ActivityLogAction

@dataclass
class ActivityLog:
"""Class representing an activity log"""

id: int
user_id: int
project_id: int
created_at: datetime
action: ActivityLogAction

def __post_init__(self):
validate_type(self.id, int, "id")
validate_type(self.user_id, int, "user_id")
validate_type(self.project_id, int, "project_id")
validate_type(self.created_at, datetime, "created_at")
self.action = validate_enum(self.action, ActivityLogAction, "action")
1 change: 1 addition & 0 deletions protaskinate/entities/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@dataclass
class Comment:
"""Class representing a comment"""

id: int
task_id: int
creator_id: int
Expand Down
52 changes: 46 additions & 6 deletions protaskinate/entities/project.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,61 @@
"""protaskinate/entities/project.py"""

from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Optional

from protaskinate.entities.user import User
from protaskinate.utils.validation import validate_enum, validate_type


class ProjectRole(Enum):
"""Enumeration representing the role of a user in a project"""

READER = "reader"
WRITER = "writer"
ADMIN = "admin"


@dataclass
class Project:
"""Class representing a project"""

id: int
name: str
creator_id: int
created_at: datetime
updated_at: datetime
description: Optional[str] = None

def __post_init__(self):
if not isinstance(self.id, int):
raise ValueError(f"Invalid id: {self.id}")
validate_type(self.id, int, "id")
validate_type(self.name, str, "name")
validate_type(self.creator_id, int, "creator_id")
validate_type(self.created_at, datetime, "created_at")
validate_type(self.updated_at, datetime, "updated_at")
validate_type(self.description, str, "description", allow_none=True)


@dataclass
class ProjectWithRole:
"""Class representing a project with the current user's role"""

if not isinstance(self.name, str):
raise ValueError(f"Invalid name: {self.name}")
project: Project
role: ProjectRole

if not isinstance(self.creator_id, int):
raise ValueError(f"Invalid creator_id: {self.creator_id}")
def __post_init__(self):
validate_type(self.project, Project, "project")
self.role = validate_enum(self.role, ProjectRole, "role")


@dataclass
class ProjectUser:
"""Class representing a user in a project"""

user: User
role: ProjectRole

def __post_init__(self):
validate_type(self.user, User, "user")
self.role = validate_enum(self.role, ProjectRole, "role")
15 changes: 8 additions & 7 deletions protaskinate/entities/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import List, Optional
from typing import Optional

from protaskinate.entities.comment import Comment
from protaskinate.utils.validation import validate_enum, validate_type


class TaskStatus(Enum):
"""Enumeration representing the status of a task"""

OPEN = "open"
IN_PROGRESS = "in_progress"
DONE = "done"


class TaskPriority(Enum):
"""Enumeration representing the priority of a task"""

LOW = "low"
MEDIUM = "medium"
HIGH = "high"
Expand Down Expand Up @@ -44,20 +46,22 @@ def __gt__(self, other):
def __ge__(self, other):
return self.as_int() >= other.as_int()


@dataclass
class Task:
"""Class representing a task"""

id: int
project_id: int
creator_id: int
title: str
status: TaskStatus
priority: TaskPriority
created_at: datetime
updated_at: datetime
assignee_id: Optional[int] = None
deadline: Optional[datetime] = None
description: Optional[str] = None
comments: Optional[List[Comment]] = None

def __post_init__(self):
validate_type(self.id, int, "id")
Expand All @@ -67,10 +71,7 @@ def __post_init__(self):
self.status = validate_enum(self.status, TaskStatus, "status")
self.priority = validate_enum(self.priority, TaskPriority, "priority")
validate_type(self.created_at, datetime, "created_at")
validate_type(self.updated_at, datetime, "updated_at")
validate_type(self.assignee_id, int, "assignee_id", allow_none=True)
validate_type(self.deadline, datetime, "deadline", allow_none=True)
validate_type(self.description, str, "description", allow_none=True)
validate_type(self.comments, list, "comments", allow_none=True)
if self.comments:
for comment in self.comments:
validate_type(comment, Comment, "comment")
Loading

0 comments on commit 1270d02

Please sign in to comment.