From b385f73d070c8ee22f3634d942f4ae0f10007dd8 Mon Sep 17 00:00:00 2001 From: liaarbel Date: Sat, 6 Feb 2021 11:02:57 +0200 Subject: [PATCH 01/21] created templates of forms and the list, started with database models and add some functions --- app/database/models.py | 31 ++++++++++++ app/internal/todo_list.py | 88 +++++++++++++++++++++++++++++++++++ app/static/todo_list.css | 78 +++++++++++++++++++++++++++++++ app/templates/tasks_list.html | 39 ++++++++++++++++ app/templates/todo_form.html | 30 ++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 app/internal/todo_list.py create mode 100644 app/static/todo_list.css create mode 100644 app/templates/tasks_list.html create mode 100644 app/templates/todo_form.html diff --git a/app/database/models.py b/app/database/models.py index 3e7c1782..d2091e4e 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -43,11 +43,27 @@ class User(Base): is_active = Column(Boolean, default=False) events = relationship("UserEvent", back_populates="participants") + tasks = relationship( + "Task", cascade="all, delete", back_populates="owner") def __repr__(self): return f'' +class UserTask(Base): + __tablename__ = "user_task" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column('user_id', Integer, ForeignKey('users.id')) + event_id = Column('task_id', Integer, ForeignKey('tasks.id')) + + tasks = relationship("Task", back_populates="participants") + participants = relationship("User", back_populates="tasks") + + def __repr__(self): + return f'' + + class Event(Base): __tablename__ = "events" @@ -176,3 +192,18 @@ def __repr__(self): f'{self.start_day_in_month}/{self.start_month}-' f'{self.end_day_in_month}/{self.end_month}>' ) + + +class Task(Base): + __tablename__ = "tasks" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String) + description = Column(String) + is_done = Column(Boolean, nullable=False) + is_important = Column(Boolean, nullable=False) + date = Column(DateTime, nullable = False) + time = Column(DateTime, nullable = False) + owner_id = Column(Integer, ForeignKey("users.id")) + + owner = relationship("User", back_populates="tasks") \ No newline at end of file diff --git a/app/internal/todo_list.py b/app/internal/todo_list.py new file mode 100644 index 00000000..877c7184 --- /dev/null +++ b/app/internal/todo_list.py @@ -0,0 +1,88 @@ +from datetime import datetime +from operator import attrgetter +from typing import List +from urllib.request import Request + +from fastapi import Depends +from requests import Session +from sqlalchemy.exc import SQLAlchemyError + +from app.database.database import get_db +from app.database.models import Task, UserTask +from app.internal.utils import create_model +from app.main import router +from app.routers.event import by_id + + +def create_task(db, title, description, date, time, owner_id, is_important=None) -> Task: + """Creates and saves a new task.""" + + task = create_model( + db, Task, + title=title, + description=description, + date=date, + time=time, + owner_id=owner_id, + is_important=is_important, + is_done=False + ) + create_model( + db, UserTask, + user_id=owner_id, + task_id=task.id + ) + return task + + +def sort_by_time(tasks: List[Task]) -> List[Task]: + """Sorts the tasks by the start of the task.""" + + temp = tasks.copy() + return sorted(temp, key=attrgetter('time')) + + +def get_tasks(session: Session, **param): + """Returns all tasks filter by param.""" + + try: + tasks = list(session.query(Task).filter_by(**param)) + except SQLAlchemyError: + return [] + else: + return tasks + + +def is_date_before(date: datetime) -> bool: + """Check if the start date is earlier than the end date""" + + return date < datetime.now() + + +@router.delete("/{task_id}") +def delete_task(request: Request, + task_id: int, + db: Session = Depends(get_db)): + + # TODO: Check if the user is the owner of the task. + task = by_id(db, task_id) + participants = get_participants_emails_by_task(db, task_id) + try: + # Delete task + db.delete(task) + + # Delete user_task + db.query(UserTask).filter(UserTask.task_id == task_id).delete() + + db.commit() + + except (SQLAlchemyError, TypeError): + return templates.TemplateResponse( + "task/taskview.html", {"request": request, "task_id": task_id}, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) + if participants and task.start > datetime.now(): + pass + # TODO: Send them a cancellation notice + # if the deletion is successful + return RedirectResponse( + url="/calendar", status_code=status.HTTP_200_OK) \ No newline at end of file diff --git a/app/static/todo_list.css b/app/static/todo_list.css new file mode 100644 index 00000000..8ce59b1b --- /dev/null +++ b/app/static/todo_list.css @@ -0,0 +1,78 @@ +.form-style { + font: 95% Arial, Helvetica, sans-serif; + max-width: 400px; + margin: 10px auto; + padding: 16px; + background: #F7F7F7; +} + +#table { + max-width: 500px; +} + +.form-style h1 { + background: #004d99; + padding: 20px 0; + font-size: 140%; + font-weight: 300; + text-align: center; + color: #fff; + margin: -16px -16px 16px -16px; +} + +.form-style input[type="text"], +.form-style input[type="date"], +.form-style input[type="time"], +.form-style textarea { + -webkit-transition: all 0.30s ease-in-out; + -moz-transition: all 0.30s ease-in-out; + -ms-transition: all 0.30s ease-in-out; + -o-transition: all 0.30s ease-in-out; + outline: none; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + width: 100%; + background: #fff; + margin-bottom: 4%; + border: 1px solid #ccc; + padding: 3%; + color: #555; + font: 95% Arial, Helvetica, sans-serif; +} + +.form-style input[type="text"]:focus, +.form-style input[type="date"]:focus, +.form-style input[type="time"]:focus, +.form-style textarea:focus { + box-shadow: 0 0 5px #43D1AF; + padding: 3%; + border: 1px solid #43D1AF; +} + +th, td { + padding-right: 30px; + padding-top: 20px; + text-align: left; +} + +button { + padding: 2%; + background-color: #80bfff; + text-align: right; + margin-left: 85% +} + +.edit { + text-decoration: none; + background-color: #eafaf6; + border-style: solid; + border-color: #004d99; + border-radius: 10px; +} + +button:hover { + background-color: #004d99; + color: white; + box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); +} diff --git a/app/templates/tasks_list.html b/app/templates/tasks_list.html new file mode 100644 index 00000000..1fdf3920 --- /dev/null +++ b/app/templates/tasks_list.html @@ -0,0 +1,39 @@ + + + + + + Daily tasks + + +
+

Todo-List

+ + + + + + + + {% for task in tasks %} + {% if task.is_important %} + + + + + + + {% endif %} + {% else %} + + + + + + + {% endelse %} + {% endfor %} + +
TimeTaskDescription
{{task.time}}{{task.title}}{{task.description}}Edit
{{task.time}}{{task.title}}{{task.description}}Edit
+ + \ No newline at end of file diff --git a/app/templates/todo_form.html b/app/templates/todo_form.html new file mode 100644 index 00000000..8ea03252 --- /dev/null +++ b/app/templates/todo_form.html @@ -0,0 +1,30 @@ + + + + + + Todo-List + + +
+

Add Task

+ {% if model.any_id is defined %} +
+ +
+ {% endif %} +
+ + + + + + + + + + +
+
+ + From 73d9f378e47a1602a7c3e3263a0d28bd33be208d Mon Sep 17 00:00:00 2001 From: liaarbel Date: Sun, 7 Feb 2021 17:00:30 +0200 Subject: [PATCH 02/21] created to do list functions, templates an routers --- app/database/models.py | 20 +-- app/internal/todo_list.py | 92 ++++++++------ app/routers/dayview.py | 4 +- app/routers/todo_list.py | 53 ++++++++ app/static/dayview.css | 90 ++++++++++++++ app/templates/dayview.html | 221 ++++++++++++++++++++++++---------- app/templates/tasks_list.html | 2 +- app/templates/todo_form.html | 2 +- 8 files changed, 367 insertions(+), 117 deletions(-) create mode 100644 app/routers/todo_list.py diff --git a/app/database/models.py b/app/database/models.py index d2091e4e..79139d6d 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -4,7 +4,7 @@ from typing import Dict, Any from sqlalchemy import (DDL, Boolean, Column, DateTime, ForeignKey, Index, - Integer, String, event, UniqueConstraint) + Integer, String, event, UniqueConstraint, Date, Time) from sqlalchemy.dialects.postgresql import TSVECTOR from sqlalchemy.exc import IntegrityError, SQLAlchemyError from sqlalchemy.orm import relationship, Session @@ -50,20 +50,6 @@ def __repr__(self): return f'' -class UserTask(Base): - __tablename__ = "user_task" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column('user_id', Integer, ForeignKey('users.id')) - event_id = Column('task_id', Integer, ForeignKey('tasks.id')) - - tasks = relationship("Task", back_populates="participants") - participants = relationship("User", back_populates="tasks") - - def __repr__(self): - return f'' - - class Event(Base): __tablename__ = "events" @@ -202,8 +188,8 @@ class Task(Base): description = Column(String) is_done = Column(Boolean, nullable=False) is_important = Column(Boolean, nullable=False) - date = Column(DateTime, nullable = False) - time = Column(DateTime, nullable = False) + date = Column(Date, nullable = False) + time = Column(Time, nullable = False) owner_id = Column(Integer, ForeignKey("users.id")) owner = relationship("User", back_populates="tasks") \ No newline at end of file diff --git a/app/internal/todo_list.py b/app/internal/todo_list.py index 877c7184..73f343a3 100644 --- a/app/internal/todo_list.py +++ b/app/internal/todo_list.py @@ -1,20 +1,19 @@ from datetime import datetime from operator import attrgetter -from typing import List -from urllib.request import Request +from typing import List, Dict, Optional, Any -from fastapi import Depends +from fastapi import HTTPException from requests import Session from sqlalchemy.exc import SQLAlchemyError +from starlette import status -from app.database.database import get_db -from app.database.models import Task, UserTask +from app.database.models import Task +from app.dependencies import logger from app.internal.utils import create_model -from app.main import router -from app.routers.event import by_id -def create_task(db, title, description, date, time, owner_id, is_important=None) -> Task: +def create_task(db, title, description, date, time, owner_id, + is_important) -> Task: """Creates and saves a new task.""" task = create_model( @@ -27,11 +26,6 @@ def create_task(db, title, description, date, time, owner_id, is_important=None) is_important=is_important, is_done=False ) - create_model( - db, UserTask, - user_id=owner_id, - task_id=task.id - ) return task @@ -59,30 +53,56 @@ def is_date_before(date: datetime) -> bool: return date < datetime.now() -@router.delete("/{task_id}") -def delete_task(request: Request, - task_id: int, - db: Session = Depends(get_db)): +def by_id(db, task_id): + task = db.query(Task).filter_by(id=task_id).one() + return task - # TODO: Check if the user is the owner of the task. - task = by_id(db, task_id) - participants = get_participants_emails_by_task(db, task_id) - try: - # Delete task - db.delete(task) - # Delete user_task - db.query(UserTask).filter(UserTask.task_id == task_id).delete() +def is_fields_types_valid(to_check: Dict[str, Any], types: Dict[str, Any]): + """validate dictionary values by dictionary of types""" + errors = [] + for field_name, field_type in to_check.items(): + if types[field_name] and not isinstance(field_type, types[field_name]): + errors.append( + f"{field_name} is '{type(field_type).__name__}' and" + + f"it should be from type '{types[field_name].__name__}'") + logger.warning(errors) + if errors: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=errors) - db.commit() - except (SQLAlchemyError, TypeError): - return templates.TemplateResponse( - "task/taskview.html", {"request": request, "task_id": task_id}, - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - if participants and task.start > datetime.now(): - pass - # TODO: Send them a cancellation notice - # if the deletion is successful - return RedirectResponse( - url="/calendar", status_code=status.HTTP_200_OK) \ No newline at end of file +def get_task_with_editable_fields_only(task: Dict[str, Any] + ) -> Dict[str, Any]: + """Remove all keys that are not allowed to update""" + + return {i: task[i] for i in UPDATE_TASKS_FIELDS if i in task} + + +def _update_task(db: Session, task_id: int, task_to_update: Dict) -> task: + try: + # Update database + db.query(Task).filter(Task.id == task_id).update( + task_to_update, synchronize_session=False) + + db.commit() + return by_id(db, task_id) + except (AttributeError, SQLAlchemyError) as e: + logger.exception(str(e)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal server error") + + +def update_task(task_id: int, task: Dict, db: Session + ) -> Optional[Task]: + # TODO Check if the user is the owner of the task. + old_task = by_id(db, task_id) + task_to_update = get_task_with_editable_fields_only(task) + is_fields_types_valid(task_to_update, UPDATE_TASKS_FIELDS) + check_change_dates_allowed(old_task, task_to_update) + if not task_to_update: + return None + task_updated = _update_task(db, task_id, task_to_update) + # TODO: Send emails to recipients. + return task_updated \ No newline at end of file diff --git a/app/routers/dayview.py b/app/routers/dayview.py index 53fab46f..df584ad1 100644 --- a/app/routers/dayview.py +++ b/app/routers/dayview.py @@ -6,7 +6,7 @@ from sqlalchemy import and_, or_ from app.database.database import get_db -from app.database.models import Event, User +from app.database.models import Event, User, Task from app.dependencies import TEMPLATES_PATH from app.internal import zodiac @@ -106,6 +106,8 @@ async def dayview(request: Request, date: str, db_session=Depends(get_db)): and_(Event.start < day_end, day_end < Event.end))) events_n_attrs = [(event, DivAttributes(event, day)) for event in events] zodiac_obj = zodiac.get_zodiac_of_day(db_session, day) + tasks = db_session.query(Task).filter(Task.owner_id == user.id)\ + .filter(Task.date == day.date()) return templates.TemplateResponse("dayview.html", { "request": request, "events": events_n_attrs, diff --git a/app/routers/todo_list.py b/app/routers/todo_list.py new file mode 100644 index 00000000..6926d388 --- /dev/null +++ b/app/routers/todo_list.py @@ -0,0 +1,53 @@ +from datetime import datetime +from urllib.request import Request + +from fastapi import Depends, APIRouter +from requests import Session +from sqlalchemy.exc import SQLAlchemyError +from starlette import status +from starlette.responses import RedirectResponse + +from app.config import templates +from app.database.database import get_db +from app.database.models import User +from app.internal.todo_list import create_task + +router = APIRouter( + prefix="/task", + tags=["task"], + responses={404: {"description": "Not found"}}, +) + + +@router.delete("/{task_id}") +def delete_task(request: Request, + task_id: int, + db: Session = Depends(get_db)): + # TODO: Check if the user is the owner of the task. + task = by_id(db, task_id) + try: + # Delete task + db.delete(task) + + db.commit() + + except (SQLAlchemyError, TypeError): + return templates.TemplateResponse( + "dayview.html", {"request": request, "task_id": task_id}, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) + # TODO: Send them a cancellation notice + # if the deletion is successful + return RedirectResponse( + url="/calendar", status_code=status.HTTP_200_OK) + + +@router.post("/add") +async def add_task(title: str, description: str, datestr: str, timestr: str, + session=Depends(get_db), is_important: bool = False): + # TODO: add a login session + user = session.query(User).filter_by(username='test1').first() + create_task(session, title, description, + datetime.strptime(datestr, '%Y-%m-%d') + .date(), datetime.strptime(timestr, '%H:%M').time(), + user.id, is_important) + return RedirectResponse(f"/day/{datestr}") diff --git a/app/static/dayview.css b/app/static/dayview.css index 1c50d3e3..638e3bd0 100644 --- a/app/static/dayview.css +++ b/app/static/dayview.css @@ -101,4 +101,94 @@ html { background-color: var(--borders-variant); border-radius:50px; box-shadow: 1px 1px 2px #999; +} + +.todo-style { + font: 95% Arial, Helvetica, sans-serif; + background: #F7F7F7; +} + +#table { + max-width: 500px; +} + +.open-button { + background-color: var(--primary); + color: #fff; + margin-top: 12px; + font-family: "Consolas"; + font-size: 20px; + padding: 14px; + border-radius: 20px; + transition-duration: 0.4s; + box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19); +} + +.open-button:hover { + background-color: #ccddff; + color: #00001a; + box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); +} + +.todo-style h1 { + color: #fff; + text-align: center; +} + +.todo-style input[type="text"], +.todo-style input[type="date"], +.todo-style input[type="time"], +.todo-style textarea { + -webkit-transition: all 0.30s ease-in-out; + -moz-transition: all 0.30s ease-in-out; + -ms-transition: all 0.30s ease-in-out; + -o-transition: all 0.30s ease-in-out; + outline: none; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + width: 100%; + background: #fff; + margin-bottom: 4%; + border: 1px solid #ccc; + padding: 3%; + color: #555; + font: 95% Arial, Helvetica, sans-serif; +} + +.todo-style input[type="text"]:focus, +.todo-style input[type="date"]:focus, +.todo-style input[type="time"]:focus, +.todo-style textarea:focus { + box-shadow: 0 0 5px #43D1AF; + padding: 3%; + border: 1px solid #43D1AF; +} + +.todo-style th, .todo-style td { + padding-right: 30px; + padding-top: 20px; + text-align: left; +} + +.todo-style button { + border-radius: 20px; + padding: 2%; + text-align: right; + background-color: #80bfff; +} + + +.edit { + text-decoration: none; + background-color: #eafaf6; + border-style: solid; + border-color: #004d99; + border-radius: 10px; +} + +.todo-style button:hover { + background-color: var(--primary); + color: white; + box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); } \ No newline at end of file diff --git a/app/templates/dayview.html b/app/templates/dayview.html index 29a804b6..8f8e19f9 100644 --- a/app/templates/dayview.html +++ b/app/templates/dayview.html @@ -1,68 +1,167 @@ - - - - - - - dayview - - -
- - {{month}} - {{day}} - {% if zodiac %} -
- zodiac sign + + + + + + + dayview + + +
+ + {{month}} + {{day}} + {% if zodiac %} +
+ zodiac sign +
+ {% endif %} +
+ + + +
+
@@ -86,18 +101,41 @@